diff --git a/.env-example b/.env-example new file mode 100644 index 0000000..0ea477b --- /dev/null +++ b/.env-example @@ -0,0 +1,3 @@ +GITHUB_PRIVATE_KEY="" +GITHUB_APP_IDENTIFIER= +GITHUB_WEBHOOK_SECRET= diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37d7e73 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +.env diff --git a/Gemfile b/Gemfile index 0799a52..6b23a59 100644 --- a/Gemfile +++ b/Gemfile @@ -3,3 +3,4 @@ source 'http://rubygems.org' gem 'sinatra', '~> 2.0' gem 'jwt', '~> 2.1' gem 'octokit', '~> 4.0' +gem 'dotenv' diff --git a/Gemfile.lock b/Gemfile.lock index 5cbe9b9..6c57b34 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,24 +3,25 @@ GEM specs: addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) - faraday (0.15.2) + dotenv (2.5.0) + faraday (0.15.3) multipart-post (>= 1.2, < 3) jwt (2.1.0) multipart-post (2.0.0) - mustermann (1.0.2) - octokit (4.9.0) + mustermann (1.0.3) + octokit (4.13.0) sawyer (~> 0.8.0, >= 0.5.3) - public_suffix (3.0.2) - rack (2.0.5) - rack-protection (2.0.3) + public_suffix (3.0.3) + rack (2.0.6) + rack-protection (2.0.4) rack sawyer (0.8.1) addressable (>= 2.3.5, < 2.6) faraday (~> 0.8, < 1.0) - sinatra (2.0.3) + sinatra (2.0.4) mustermann (~> 1.0) rack (~> 2.0) - rack-protection (= 2.0.3) + rack-protection (= 2.0.4) tilt (~> 2.0) tilt (2.0.8) @@ -28,9 +29,10 @@ PLATFORMS ruby DEPENDENCIES + dotenv jwt (~> 2.1) octokit (~> 4.0) sinatra (~> 2.0) BUNDLED WITH - 1.14.6 + 1.17.1 diff --git a/README.md b/README.md index 2a8f4f9..81f494b 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,21 @@ -This is the sample project built by following the "[Building Your First GitHub App](https://developer.github.com/apps/building-your-first-github-app)" Quickstart guide on developer.github.com. +This is an example GitHub App that adds a label to all new issues opened in a repository. You can follow the "[Using the GitHub API in your app](https://developer.github.com/apps/quickstart-guides/using-the-github-api-in-your-app/)" quickstart guide on developer.github.com to learn how to build the app code in `server.rb`. -It consists of two different servers: `server.rb` (boilerplate) and `advanced_server.rb` (completed project). +This project listens for webhook events and uses the Octokit.rb library to make REST API calls. This example project consists of two different servers: +* `template_server.rb` (GitHub App template code) +* `server.rb` (completed project) -## Install and run +To learn how to set up a template GitHub App, follow the "[Setting up your development environment](https://developer.github.com/apps/quickstart-guides/setting-up-your-development-environment/)" quickstart guide on developer.github.com. + +## Install To run the code, make sure you have [Bundler](http://gembundler.com/) installed; then enter `bundle install` on the command line. -* For the boilerplate project, enter `ruby server.rb` on the command line. +## Set environment variables + +1. Create a copy of the `.env-example` file called `.env`. +2. Add your GitHub App's private key, app ID, and webhook secret to the `.env` file. -* For the completed project, enter `ruby advanced_server.rb` on the command line. +## Run the server -Both commands will run the server at `localhost:3000`. +1. Run `ruby template_server.rb` or `ruby server.rb` on the command line. +1. View the default Sinatra app at `localhost:3000`. diff --git a/server.rb b/server.rb index 9fd6b7b..8a40db4 100644 --- a/server.rb +++ b/server.rb @@ -1,44 +1,19 @@ require 'sinatra' require 'octokit' +require 'dotenv/load' # Manages environment variables require 'json' -require 'openssl' # Used to verify the webhook signature -require 'jwt' # Used to authenticate a GitHub App -require 'time' # Used to get ISO 8601 representation of a Time object -require 'logger' +require 'openssl' # Verifies the webhook signature +require 'jwt' # Authenticates a GitHub App +require 'time' # Gets ISO 8601 representation of a Time object +require 'logger' # Logs debug statements set :port, 3000 - - -# This is template code to create a GitHub App server. -# You can read more about GitHub Apps here: # https://developer.github.com/apps/ -# -# On its own, this app does absolutely nothing, except that it can be installed. -# It's up to you to add fun functionality! -# You can check out one example in advanced_server.rb. -# -# This code is a Sinatra app, for two reasons: -# 1. Because the app will require a landing page for installation. -# 2. To easily handle webhook events. -# -# -# Of course, not all apps need to receive and process events! -# Feel free to rip out the event handling code if you don't need it. -# -# Have fun! -# +set :bind, '0.0.0.0' class GHAapp < Sinatra::Application - # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!! - # Instead, set and read app tokens or other secrets in your code - # in a runtime source, like an environment variable like below - - # Expects that the private key has been set as an environment variable in - # PEM format using the following command to replace newlines with the - # literal `\n`: - # export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem` - # - # Converts the newlines + # Converts the newlines. Expects that the private key has been set as an + # environment variable in PEM format. PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a secret set. The secret is used to verify @@ -54,31 +29,37 @@ class GHAapp < Sinatra::Application end - # Before each request to the `/event_handler` route + # Executed before each request to the `/event_handler` route before '/event_handler' do get_payload_request(request) verify_webhook_signature authenticate_app - # Authenticate each installation of the app in order to run API operations + # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do - # # # # # # # # # # # # # # # # # # # - # ADD YOUR CODE HERE # - # # # # # # # # # # # # # # # # # # # + case request.env['HTTP_X_GITHUB_EVENT'] + when 'issues' + if @payload['action'] === 'opened' + handle_issue_opened_event(@payload) + end + end - 'ok' # We've got to return _something_. ;) + 200 # success status end helpers do - # # # # # # # # # # # # # # # # # # # - # ADD YOUR HELPERS METHODS HERE # - # # # # # # # # # # # # # # # # # # # + # When an issue is opened, add a label + def handle_issue_opened_event(payload) + repo = payload['repository']['full_name'] + issue_number = payload['issue']['number'] + @installation_client.add_labels_to_an_issue(repo, issue_number, ['needs-response']) + end # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) @@ -95,9 +76,9 @@ def get_payload_request(request) end # Instantiate an Octokit client authenticated as a GitHub App. - # GitHub App authentication equires that we construct a + # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, - # so GitHub can be sure that it came from the app an not altererd by + # so GitHub can be sure that it came from the app and was not altered by # a malicious third party. def authenticate_app payload = { @@ -111,32 +92,32 @@ def authenticate_app iss: APP_IDENTIFIER } - # Cryptographically sign the JWT + # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end - # Instantiate an Octokit client authenticated as an installation of a - # GitHub App to run API operations. + # Instantiate an Octokit client, authenticated as an installation of a + # GitHub App, to run API operations. def authenticate_installation(payload) - installation_id = payload['installation']['id'] - installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] - @installation_client = Octokit::Client.new(bearer_token: installation_token) + @installation_id = payload['installation']['id'] + @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] + @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # - # GitHub will the WEBHOOK_SECRET, registered - # to the GitHub App, to create a hash signature sent in each webhook payload - # in the `X-HUB-Signature` header. This code computes the expected hash - # signature and compares it to the signature sent in the `X-HUB-Signature` - # header. If they don't match, this request is an attack, and we should - # reject it. GitHub uses the HMAC hexdigest to compute the signature. The - # `X-HUB-Signature` looks something like this: "sha1=123456" - # See https://developer.github.com/webhooks/securing/ for details + # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to + # create the hash signature sent in the `X-HUB-Signature` header of each + # webhook. This code computes the expected hash signature and compares it to + # the signature sent in the `X-HUB-Signature` header. If they don't match, + # this request is an attack, and you should reject it. GitHub uses the HMAC + # hexdigest to compute the signature. The `X-HUB-Signature` looks something + # like this: "sha1=123456". + # See https://developer.github.com/webhooks/securing/ for details. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') @@ -145,17 +126,17 @@ def verify_webhook_signature # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. - logger.debug "---- recevied event #{request.env['HTTP_X_GITHUB_EVENT']}" + logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end end - - # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code ;) But, for the curious: + # Finally some logic to let us run this server directly from the command line, + # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file - # If they are the same—that is, we are running this file directly, call the Sinatra run method + # If they are the same—that is, we are running this file directly, call the + # Sinatra run method run! if __FILE__ == $0 end diff --git a/advanced_server.rb b/template_server.rb similarity index 59% rename from advanced_server.rb rename to template_server.rb index d617d5a..f913aa1 100644 --- a/advanced_server.rb +++ b/template_server.rb @@ -1,26 +1,27 @@ require 'sinatra' require 'octokit' +require 'dotenv/load' # Manages environment variables require 'json' -require 'openssl' # Used to verify the webhook signature -require 'jwt' # Used to authenticate a GitHub App -require 'time' # Used to get ISO 8601 representation of a Time object -require 'logger' +require 'openssl' # Verifies the webhook signature +require 'jwt' # Authenticates a GitHub App +require 'time' # Gets ISO 8601 representation of a Time object +require 'logger' # Logs debug statements set :port, 3000 +set :bind, '0.0.0.0' # This is template code to create a GitHub App server. # You can read more about GitHub Apps here: # https://developer.github.com/apps/ # # On its own, this app does absolutely nothing, except that it can be installed. -# It's up to you to add fun functionality! +# It's up to you to add functionality! # You can check out one example in advanced_server.rb. # # This code is a Sinatra app, for two reasons: # 1. Because the app will require a landing page for installation. # 2. To easily handle webhook events. # -# # Of course, not all apps need to receive and process events! # Feel free to rip out the event handling code if you don't need it. # @@ -29,16 +30,7 @@ class GHAapp < Sinatra::Application - # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!! - # Instead, set and read app tokens or other secrets in your code - # in a runtime source, like an environment variable like below - - # Expects that the private key has been set as an environment variable in - # PEM format using the following command to replace newlines with the - # literal `\n`: - # export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem` - # - # Converts the newlines + # Expects that the private key in PEM format. Converts the newlines PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # Your registered app must have a secret set. The secret is used to verify @@ -59,33 +51,26 @@ class GHAapp < Sinatra::Application get_payload_request(request) verify_webhook_signature authenticate_app - # Authenticate each installation of the app in order to run API operations + # Authenticate the app installation in order to run API operations authenticate_installation(@payload) end post '/event_handler' do - case request.env['HTTP_X_GITHUB_EVENT'] - when 'issues' - if @payload['action'] === 'opened' - handle_issue_opened_event(@payload) - end - end - - 'ok' # we have to return _something_ ;) + # # # # # # # # # # # # + # ADD YOUR CODE HERE # + # # # # # # # # # # # # + 200 # success status end helpers do - # When an issue is opened, add a label - def handle_issue_opened_event(payload) - repo = payload['repository']['full_name'] - issue_number = payload['issue']['number'] - @installation_client.add_labels_to_an_issue(repo, issue_number, ['needs-response']) - end + # # # # # # # # # # # # # # # # # + # ADD YOUR HELPER METHODS HERE # + # # # # # # # # # # # # # # # # # # Saves the raw payload and converts the payload to JSON format def get_payload_request(request) @@ -102,9 +87,9 @@ def get_payload_request(request) end # Instantiate an Octokit client authenticated as a GitHub App. - # GitHub App authentication equires that we construct a + # GitHub App authentication requires that you construct a # JWT (https://jwt.io/introduction/) signed with the app's private key, - # so GitHub can be sure that it came from the app an not altererd by + # so GitHub can be sure that it came from the app and wasn't alterered by # a malicious third party. def authenticate_app payload = { @@ -118,32 +103,32 @@ def authenticate_app iss: APP_IDENTIFIER } - # Cryptographically sign the JWT + # Cryptographically sign the JWT. jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256') # Create the Octokit client, using the JWT as the auth token. @app_client ||= Octokit::Client.new(bearer_token: jwt) end - # Instantiate an Octokit client authenticated as an installation of a - # GitHub App to run API operations. + # Instantiate an Octokit client, authenticated as an installation of a + # GitHub App, to run API operations. def authenticate_installation(payload) - installation_id = payload['installation']['id'] - installation_token = @app_client.create_app_installation_access_token(installation_id)[:token] - @installation_client = Octokit::Client.new(bearer_token: installation_token) + @installation_id = payload['installation']['id'] + @installation_token = @app_client.create_app_installation_access_token(@installation_id)[:token] + @installation_client = Octokit::Client.new(bearer_token: @installation_token) end # Check X-Hub-Signature to confirm that this webhook was generated by # GitHub, and not a malicious third party. # - # GitHub will the WEBHOOK_SECRET, registered - # to the GitHub App, to create a hash signature sent in each webhook payload - # in the `X-HUB-Signature` header. This code computes the expected hash - # signature and compares it to the signature sent in the `X-HUB-Signature` - # header. If they don't match, this request is an attack, and we should - # reject it. GitHub uses the HMAC hexdigest to compute the signature. The - # `X-HUB-Signature` looks something like this: "sha1=123456" - # See https://developer.github.com/webhooks/securing/ for details + # GitHub uses the WEBHOOK_SECRET, registered to the GitHub App, to + # create the hash signature sent in the `X-HUB-Signature` header of each + # webhook. This code computes the expected hash signature and compares it to + # the signature sent in the `X-HUB-Signature` header. If they don't match, + # this request is an attack, and you should reject it. GitHub uses the HMAC + # hexdigest to compute the signature. The `X-HUB-Signature` looks something + # like this: "sha1=123456". + # See https://developer.github.com/webhooks/securing/ for details. def verify_webhook_signature their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1=' method, their_digest = their_signature_header.split('=') @@ -152,17 +137,17 @@ def verify_webhook_signature # The X-GITHUB-EVENT header provides the name of the event. # The action value indicates the which action triggered the event. - logger.debug "---- recevied event #{request.env['HTTP_X_GITHUB_EVENT']}" + logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}" logger.debug "---- action #{@payload['action']}" unless @payload['action'].nil? end end - - # Finally some logic to let us run this server directly from the commandline, or with Rack - # Don't worry too much about this code ;) But, for the curious: + # Finally some logic to let us run this server directly from the command line, + # or with Rack. Don't worry too much about this code. But, for the curious: # $0 is the executed file # __FILE__ is the current file - # If they are the same—that is, we are running this file directly, call the Sinatra run method + # If they are the same—that is, we are running this file directly, call the + # Sinatra run method run! if __FILE__ == $0 end diff --git a/using-the-github-api-in-your-app-master.zip b/using-the-github-api-in-your-app-master.zip new file mode 100644 index 0000000..af16319 Binary files /dev/null and b/using-the-github-api-in-your-app-master.zip differ