├── .env-example ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── config.ru └── template_server.rb /.env-example: -------------------------------------------------------------------------------- 1 | GITHUB_PRIVATE_KEY="" 2 | GITHUB_APP_IDENTIFIER= 3 | GITHUB_WEBHOOK_SECRET= 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'sinatra', '~> 2.0' 4 | gem 'jwt', '~> 2.1' 5 | gem 'octokit', '~> 4.0' 6 | gem 'dotenv' 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | addressable (2.8.0) 5 | public_suffix (>= 2.0.2, < 5.0) 6 | dotenv (2.5.0) 7 | faraday (1.5.1) 8 | faraday-em_http (~> 1.0) 9 | faraday-em_synchrony (~> 1.0) 10 | faraday-excon (~> 1.1) 11 | faraday-httpclient (~> 1.0.1) 12 | faraday-net_http (~> 1.0) 13 | faraday-net_http_persistent (~> 1.1) 14 | faraday-patron (~> 1.0) 15 | multipart-post (>= 1.2, < 3) 16 | ruby2_keywords (>= 0.0.4) 17 | faraday-em_http (1.0.0) 18 | faraday-em_synchrony (1.0.0) 19 | faraday-excon (1.1.0) 20 | faraday-httpclient (1.0.1) 21 | faraday-net_http (1.0.1) 22 | faraday-net_http_persistent (1.2.0) 23 | faraday-patron (1.0.0) 24 | jwt (2.1.0) 25 | multipart-post (2.1.1) 26 | mustermann (1.0.3) 27 | octokit (4.13.0) 28 | sawyer (~> 0.8.0, >= 0.5.3) 29 | public_suffix (4.0.6) 30 | rack (2.2.3) 31 | rack-protection (2.0.4) 32 | rack 33 | ruby2_keywords (0.0.4) 34 | sawyer (0.8.2) 35 | addressable (>= 2.3.5) 36 | faraday (> 0.8, < 2.0) 37 | sinatra (2.0.4) 38 | mustermann (~> 1.0) 39 | rack (~> 2.0) 40 | rack-protection (= 2.0.4) 41 | tilt (~> 2.0) 42 | tilt (2.0.8) 43 | 44 | PLATFORMS 45 | ruby 46 | 47 | DEPENDENCIES 48 | dotenv 49 | jwt (~> 2.1) 50 | octokit (~> 4.0) 51 | sinatra (~> 2.0) 52 | 53 | BUNDLED WITH 54 | 1.17.1 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ⚠️ Note: This repository is not maintained. For more recent guides about how to build GitHub Apps, see "[About writing code for a GitHub App](https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/about-writing-code-for-a-github-app)." 2 | 3 | You can use this GitHub App template code as a foundation to create any GitHub App you'd like. You can learn how to configure a template GitHub App by following the archived "[Setting up your development environment](https://web.archive.org/web/20230604175646/https://docs.github.com/en/apps/creating-github-apps/writing-code-for-a-github-app/setting-up-your-development-environment-to-create-a-github-app)" guide. 4 | 5 | ## Install 6 | 7 | To run the code, make sure you have [Bundler](http://gembundler.com/) installed; then enter `bundle install` on the command line. 8 | 9 | ## Set environment variables 10 | 11 | 1. Create a copy of the `.env-example` file called `.env`. 12 | 2. Add your GitHub App's private key, app ID, and webhook secret to the `.env` file. 13 | 14 | ## Run the server 15 | 16 | 1. Run `ruby template_server.rb` on the command line. 17 | 1. View the default Sinatra app at `localhost:3000`. 18 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | require "./server" 2 | run GHAapp 3 | -------------------------------------------------------------------------------- /template_server.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'octokit' 3 | require 'dotenv/load' # Manages environment variables 4 | require 'json' 5 | require 'openssl' # Verifies the webhook signature 6 | require 'jwt' # Authenticates a GitHub App 7 | require 'time' # Gets ISO 8601 representation of a Time object 8 | require 'logger' # Logs debug statements 9 | 10 | set :port, 3000 11 | set :bind, '0.0.0.0' 12 | 13 | 14 | # This is template code to create a GitHub App server. 15 | # You can read more about GitHub Apps here: # https://developer.github.com/apps/ 16 | # 17 | # On its own, this app does absolutely nothing, except that it can be installed. 18 | # It's up to you to add functionality! 19 | # You can check out one example in advanced_server.rb. 20 | # 21 | # This code is a Sinatra app, for two reasons: 22 | # 1. Because the app will require a landing page for installation. 23 | # 2. To easily handle webhook events. 24 | # 25 | # Of course, not all apps need to receive and process events! 26 | # Feel free to rip out the event handling code if you don't need it. 27 | # 28 | # Have fun! 29 | # 30 | 31 | class GHAapp < Sinatra::Application 32 | 33 | # Expects that the private key in PEM format. Converts the newlines 34 | PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) 35 | 36 | # Your registered app must have a secret set. The secret is used to verify 37 | # that webhooks are sent by GitHub. 38 | WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET'] 39 | 40 | # The GitHub App's identifier (type integer) set when registering an app. 41 | APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER'] 42 | 43 | # Turn on Sinatra's verbose logging during development 44 | configure :development do 45 | set :logging, Logger::DEBUG 46 | end 47 | 48 | 49 | # Before each request to the `/event_handler` route 50 | before '/event_handler' do 51 | get_payload_request(request) 52 | verify_webhook_signature 53 | authenticate_app 54 | # Authenticate the app installation in order to run API operations 55 | authenticate_installation(@payload) 56 | end 57 | 58 | 59 | post '/event_handler' do 60 | 61 | # # # # # # # # # # # # 62 | # ADD YOUR CODE HERE # 63 | # # # # # # # # # # # # 64 | 65 | 200 # success status 66 | end 67 | 68 | 69 | helpers do 70 | 71 | # # # # # # # # # # # # # # # # # 72 | # ADD YOUR HELPER METHODS HERE # 73 | # # # # # # # # # # # # # # # # # 74 | 75 | # Saves the raw payload and converts the payload to JSON format 76 | def get_payload_request(request) 77 | # request.body is an IO or StringIO object 78 | # Rewind in case someone already read it 79 | request.body.rewind 80 | # The raw text of the body is required for webhook signature verification 81 | @payload_raw = request.body.read 82 | begin 83 | @payload = JSON.parse @payload_raw 84 | rescue => e 85 | fail "Invalid JSON (#{e}): #{@payload_raw}" 86 | end 87 | end 88 | 89 | # Instantiate an Octokit client authenticated as a GitHub App. 90 | # GitHub App authentication requires that you construct a 91 | # JWT (https://jwt.io/introduction/) signed with the app's private key, 92 | # so GitHub can be sure that it came from the app an not altererd by 93 | # a malicious third party. 94 | def authenticate_app 95 | payload = { 96 | # The time that this JWT was issued, _i.e._ now. 97 | iat: Time.now.to_i, 98 | 99 | # JWT expiration time (10 minute maximum) 100 | exp: Time.now.to_i + (10 * 60), 101 | 102 | # Your GitHub App's identifier number 103 | iss: APP_IDENTIFIER 104 | } 105 | 106 | # Cryptographically sign the JWT. 107 | jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') 108 | 109 | # Create the Octokit client, using the JWT as the auth token. 110 | @app_client ||= Octokit::Client.new(bearer_token: jwt) 111 | end 112 | 113 | # Instantiate an Octokit client, authenticated as an installation of a 114 | # GitHub App, to run API operations. 115 | def authenticate_installation(payload) 116 | @installation_id = payload['installation']['id'] 117 | @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] 118 | @installation_client = Octokit::Client.new(bearer_token: @installation_token) 119 | end 120 | 121 | # Check X-Hub-Signature-256 to confirm that this webhook was generated by 122 | # GitHub, and not a malicious third party. 123 | # 124 | # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to 125 | # create the hash signature sent in the `X-HUB-Signature-256` header of each 126 | # webhook. This code computes the expected hash signature and compares it to 127 | # the signature sent in the `X-HUB-Signature-256` header. If they don't match, 128 | # this request is an attack, and you should reject it. GitHub uses the HMAC 129 | # hexdigest to compute the signature. The `X-HUB-Signature-256` looks something 130 | # like this: "sha256=123456". 131 | # See https://docs.github.com/en/developers/webhooks-and-events/webhooks/securing-your-webhooks for details. 132 | def verify_webhook_signature 133 | their_signature_header = request.env['HTTP_X_HUB_SIGNATURE_256'] || 'sha256=' 134 | method, their_digest = their_signature_header.split('=') 135 | our_digest = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), WEBHOOK_SECRET, @payload_raw) 136 | halt 401 unless Rack::Utils.secure_compare(their_digest, our_digest) 137 | 138 | # The X-GITHUB-EVENT header provides the name of the event. 139 | # The action value indicates the which action triggered the event. 140 | logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" 141 | logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? 142 | end 143 | end 144 | 145 | # Finally some logic to let us run this server directly from the command line, 146 | # or with Rack. Don't worry too much about this code. But, for the curious: 147 | # $0 is the executed file 148 | # __FILE__ is the current file 149 | # If they are the same—that is, we are running this file directly, call the 150 | # Sinatra run method 151 | run! if __FILE__ == $0 152 | end 153 | --------------------------------------------------------------------------------