Skip to content

Commit 2fe7297

Browse files
committed
Add new sample code associated with internal-developer.github.com PR #3577
1 parent cadd1df commit 2fe7297

File tree

6 files changed

+378
-0
lines changed

6 files changed

+378
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
source 'http://rubygems.org'
2+
3+
gem 'sinatra', '~> 2.0'
4+
gem 'jwt', '~> 2.1'
5+
gem 'octokit', '~> 4.0'
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
GEM
2+
remote: http://rubygems.org/
3+
specs:
4+
addressable (2.5.2)
5+
public_suffix (>= 2.0.2, < 4.0)
6+
faraday (0.15.2)
7+
multipart-post (>= 1.2, < 3)
8+
jwt (2.1.0)
9+
multipart-post (2.0.0)
10+
mustermann (1.0.2)
11+
octokit (4.9.0)
12+
sawyer (~> 0.8.0, >= 0.5.3)
13+
public_suffix (3.0.2)
14+
rack (2.0.5)
15+
rack-protection (2.0.3)
16+
rack
17+
sawyer (0.8.1)
18+
addressable (>= 2.3.5, < 2.6)
19+
faraday (~> 0.8, < 1.0)
20+
sinatra (2.0.3)
21+
mustermann (~> 1.0)
22+
rack (~> 2.0)
23+
rack-protection (= 2.0.3)
24+
tilt (~> 2.0)
25+
tilt (2.0.8)
26+
27+
PLATFORMS
28+
ruby
29+
30+
DEPENDENCIES
31+
jwt (~> 2.1)
32+
octokit (~> 4.0)
33+
sinatra (~> 2.0)
34+
35+
BUNDLED WITH
36+
1.14.6
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
This is the sample project built by following the "[Build Your First GitHub App](https://developer.github.com/apps/build-your-first-github-app)" quickstart guide on developer.github.com.
2+
3+
It consists of two different servers: `server.rb` (boilerplate) and `advanced_server.rb` (completed project).
4+
5+
## Install and run
6+
7+
To run the code, make sure you have [Bundler](http://gembundler.com/) installed; then type `bundle install` on the command line.
8+
9+
For the boilerplate project, type `ruby server.rb` on the command line.
10+
11+
For the completed project, enter `ruby advanced_server.rb` on the command line.
12+
13+
Both commands will run the server at `localhost:4567`.
14+
15+
## History
16+
17+
The original author is @DEGoodmanWilson, and the original location is https://github.com/DEGoodmanWilson/app-boilerplate.
18+
19+
The @github/product-docs-ecosystem team ported the code here and added `advanced_server.rb` while editing the guide before publishing it.
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
require 'sinatra'
2+
require 'logger'
3+
require 'json'
4+
require 'openssl'
5+
require 'octokit'
6+
require 'jwt'
7+
require 'time' # This is necessary to get the ISO 8601 representation of a Time object
8+
9+
#
10+
#
11+
# This is a customized server for the GitHub App you can build by following
12+
# https://developer.github.com/build-your-first-github-app.
13+
#
14+
#
15+
16+
class GHAapp < Sinatra::Application
17+
18+
# Never, ever, hardcode app tokens or other secrets in your code!
19+
# Always extract from a runtime source, like an environment variable.
20+
21+
22+
# Notice that the private key must be in PEM format, but the newlines should be stripped and replaced with
23+
# the literal `\n`. This can be done in the terminal as such:
24+
# export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem`
25+
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # convert newlines
26+
27+
# You set the webhook secret when you create your app. This verifies that the webhook is really coming from GH.
28+
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
29+
30+
# Get the app identifier—an integer—from your app page after you create your app. This isn't actually a secret,
31+
# but it is something easier to configure at runtime.
32+
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
33+
34+
# You need to authenticate to the REST API to do much of anything
35+
36+
########## Configure Sinatra
37+
#
38+
# Let's turn on verbose logging during development
39+
#
40+
configure :development do
41+
set :logging, Logger::DEBUG
42+
end
43+
44+
45+
########## Before each request to our app
46+
#
47+
# Before each request to our app, we want to instantiate an Octokit client. Doing so requires that we construct a JWT.
48+
# https://jwt.io/introduction/
49+
# We have to also sign that JWT with our private key, so GitHub can be sure that
50+
# a) it came from us
51+
# b) it hasn't been altered by a malicious third party
52+
#
53+
before do
54+
payload = {
55+
# The time that this JWT was issued, _i.e._ now.
56+
iat: Time.now.to_i,
57+
58+
# How long is the JWT good for (in seconds)?
59+
# Let's say it can be used for 10 minutes before it needs to be refreshed.
60+
# TODO we don't actually cache this token, we regenerate a new one every time!
61+
exp: Time.now.to_i + (10 * 60),
62+
63+
# Your GitHub App's identifier number, so GitHub knows who issued the JWT, and know what permissions
64+
# this token has.
65+
iss: APP_IDENTIFIER
66+
}
67+
68+
# Cryptographically sign the JWT
69+
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
70+
71+
# Create the Octokit client, using the JWT as the auth token.
72+
# Notice that this client will _not_ have sufficient permissions to do many interesting things!
73+
# The helper methods below include one that generates an installation token (using the JWT) and
74+
# instantiates a new client object.
75+
@client ||= Octokit::Client.new(bearer_token: jwt)
76+
77+
end
78+
79+
80+
81+
82+
########## Events
83+
#
84+
# This is the webhook endpoint that GH will call with events, and hence where we will do our event handling
85+
#
86+
87+
post '/event_handler' do
88+
request.body.rewind
89+
payload_raw = request.body.read # We need the raw text of the body to check the webhook signature
90+
begin
91+
payload = JSON.parse payload_raw
92+
rescue
93+
payload = {}
94+
end
95+
96+
# Check X-Hub-Signature to confirm that this webhook was generated by GitHub, and not a malicious third party.
97+
# The way this works is: We have registered with GitHub a secret, and we have stored it locally in WEBHOOK_SECRET.
98+
# GitHub will cryptographically sign the request payload with this secret. We will do the same, and if the results
99+
# match, then we know that the request is from GitHub (or, at least, from someone who knows the secret!)
100+
# If they don't match, this request is an attack, and we should reject it.
101+
# The signature comes in with header x-hub-signature, and looks like "sha1=123456"
102+
# We should take the left hand side as the signature method, and the right hand side as the
103+
# HMAC digest (the signature) itself.
104+
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
105+
method, their_digest = their_signature_header.split('=')
106+
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, payload_raw)
107+
halt 401 unless their_digest == our_digest
108+
109+
# Determine what kind of event this is, and take action as appropriate
110+
# TODO we assume that GitHub will always provide an X-GITHUB-EVENT header in this case, which is a reasonable
111+
# assumption, however we should probably be more careful!
112+
logger.debug "---- received event #{request.env['HTTP_X_GITHUB_EVENT']}"
113+
logger.debug "---- action #{payload['action']}" unless payload['action'].nil?
114+
115+
case request.env['HTTP_X_GITHUB_EVENT']
116+
when 'issues'
117+
authenticate_installation(payload)
118+
if payload['action'] === 'opened'
119+
handle_issue_opened_event(payload)
120+
end
121+
end
122+
123+
'ok' # we have to return _something_ ;)
124+
end
125+
126+
127+
########## Helpers
128+
#
129+
# These functions are going to help us do some tasks that we don't want clogging up the happy paths above, or
130+
# that need to be done repeatedly. You can add anything you like here, really!
131+
#
132+
133+
helpers do
134+
135+
# Authenticate each installation of the app in order to run API operations
136+
def authenticate_installation(payload)
137+
installation_id = payload['installation']['id']
138+
installation_token = @client.create_app_installation_access_token(installation_id)[:token]
139+
@bot_client = Octokit::Client.new(bearer_token: installation_token)
140+
end
141+
142+
# When an issue is opened, add a label
143+
def handle_issue_opened_event(payload)
144+
repo = payload['repository']['full_name']
145+
issue_number = payload['issue']['number']
146+
@bot_client.add_labels_to_an_issue(repo, issue_number, ['needs-response'])
147+
end
148+
149+
end
150+
151+
152+
# Finally some logic to let us run this server directly from the commandline, or with Rack
153+
# Don't worry too much about this code ;) But, for the curious:
154+
# $0 is the executed file
155+
# __FILE__ is the current file
156+
# If they are the same—that is, we are running this file directly, call the Sinatra run method
157+
run! if __FILE__ == $0
158+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
require "./server"
2+
run GHAapp
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
require 'sinatra'
2+
require 'logger'
3+
require 'json'
4+
require 'openssl'
5+
require 'octokit'
6+
require 'jwt'
7+
require 'time' # This is necessary to get the ISO 8601 representation of a Time object
8+
9+
#
10+
#
11+
# This is a boilerplate server for your own GitHub App. You can read more about GitHub Apps here:
12+
# https://developer.github.com/apps/
13+
#
14+
# On its own, this app does absolutely nothing, except that it can be installed.
15+
# It's up to you to add fun functionality!
16+
# You can check out one example in advanced_server.rb.
17+
#
18+
# This code is a Sinatra app, for two reasons.
19+
# First, because the app will require a landing page for installation.
20+
# Second, in anticipation that you will want to receive events over a webhook from GitHub, and respond to those
21+
# in some way. Of course, not all apps need to receive and process events! Feel free to rip out the event handling
22+
# code if you don't need it.
23+
#
24+
# Have fun! Please reach out to us if you have any questions, or just to show off what you've built!
25+
#
26+
27+
class GHAapp < Sinatra::Application
28+
29+
# Never, ever, hardcode app tokens or other secrets in your code!
30+
# Always extract from a runtime source, like an environment variable.
31+
32+
33+
# Notice that the private key must be in PEM format, but the newlines should be stripped and replaced with
34+
# the literal `\n`. This can be done in the terminal as such:
35+
# export GITHUB_PRIVATE_KEY=`awk '{printf "%s\\n", $0}' private-key.pem`
36+
PRIVATE_KEY = OpenSSL::PKey::RSA.new(ENV['GITHUB_PRIVATE_KEY'].gsub('\n', "\n")) # convert newlines
37+
38+
# You set the webhook secret when you create your app. This verifies that the webhook is really coming from GH.
39+
WEBHOOK_SECRET = ENV['GITHUB_WEBHOOK_SECRET']
40+
41+
# Get the app identifier—an integer—from your app page after you create your app. This isn't actually a secret,
42+
# but it is something easier to configure at runtime.
43+
APP_IDENTIFIER = ENV['GITHUB_APP_IDENTIFIER']
44+
45+
46+
########## Configure Sinatra
47+
#
48+
# Let's turn on verbose logging during development
49+
#
50+
configure :development do
51+
set :logging, Logger::DEBUG
52+
end
53+
54+
55+
########## Before each request to our app
56+
#
57+
# Before each request to our app, we want to instantiate an Octokit client. Doing so requires that we construct a JWT.
58+
# https://jwt.io/introduction/
59+
# We have to also sign that JWT with our private key, so GitHub can be sure that
60+
# a) it came from us
61+
# b) it hasn't been altered by a malicious third party
62+
#
63+
before do
64+
payload = {
65+
# The time that this JWT was issued, _i.e._ now.
66+
iat: Time.now.to_i,
67+
68+
# How long is the JWT good for (in seconds)?
69+
# Let's say it can be used for 10 minutes before it needs to be refreshed.
70+
# TODO we don't actually cache this token, we regenerate a new one every time!
71+
exp: Time.now.to_i + (10 * 60),
72+
73+
# Your GitHub App's identifier number, so GitHub knows who issued the JWT, and know what permissions
74+
# this token has.
75+
iss: APP_IDENTIFIER
76+
}
77+
78+
# Cryptographically sign the JWT
79+
jwt = JWT.encode(payload, PRIVATE_KEY, 'RS256')
80+
81+
# Create the Octokit client, using the JWT as the auth token.
82+
# Notice that this client will _not_ have sufficient permissions to do many interesting things!
83+
# We might, for particular endpoints, need to generate an installation token (using the JWT), and instantiate
84+
# a new client object. But we'll cross that bridge when/if we get there!
85+
@client ||= Octokit::Client.new(bearer_token: jwt)
86+
end
87+
88+
89+
90+
91+
########## Events
92+
#
93+
# This is the webhook endpoint that GH will call with events, and hence where we will do our event handling
94+
#
95+
96+
post '/event_handler' do
97+
request.body.rewind
98+
payload_raw = request.body.read # We need the raw text of the body to check the webhook signature
99+
begin
100+
payload = JSON.parse payload_raw
101+
rescue
102+
payload = {}
103+
end
104+
105+
# Check X-Hub-Signature to confirm that this webhook was generated by GitHub, and not a malicious third party.
106+
# The way this works is: We have registered with GitHub a secret, and we have stored it locally in WEBHOOK_SECRET.
107+
# GitHub will cryptographically sign the request payload with this secret. We will do the same, and if the results
108+
# match, then we know that the request is from GitHub (or, at least, from someone who knows the secret!)
109+
# If they don't match, this request is an attack, and we should reject it.
110+
# The signature comes in with header x-hub-signature, and looks like "sha1=123456"
111+
# We should take the left hand side as the signature method, and the right hand side as the
112+
# HMAC digest (the signature) itself.
113+
their_signature_header = request.env['HTTP_X_HUB_SIGNATURE'] || 'sha1='
114+
method, their_digest = their_signature_header.split('=')
115+
our_digest = OpenSSL::HMAC.hexdigest(method, WEBHOOK_SECRET, payload_raw)
116+
halt 401 unless their_digest == our_digest
117+
118+
# Determine what kind of event this is, and take action as appropriate
119+
# TODO we assume that GitHub will always provide an X-GITHUB-EVENT header in this case, which is a reasonable
120+
# assumption, however we should probably be more careful!
121+
logger.debug "---- recevied event #{request.env['HTTP_X_GITHUB_EVENT']}"
122+
logger.debug "---- action #{payload['action']}" unless payload['action'].nil?
123+
124+
case request.env['HTTP_X_GITHUB_EVENT']
125+
when :the_event_that_i_care_about
126+
# Add code here to handle the event that you care about!
127+
handle_the_event_that_i_care_about(payload)
128+
end
129+
130+
'ok' # we have to return _something_ ;)
131+
end
132+
133+
134+
########## Helpers
135+
#
136+
# These functions are going to help us do some tasks that we don't want clogging up the happy paths above, or
137+
# that need to be done repeatedly. You can add anything you like here, really!
138+
#
139+
140+
helpers do
141+
142+
# This is our handler for the event that you care about! Of course, you'll want to change the name to reflect
143+
# the actual event name! But this is where you will add code to process the event.
144+
def handle_the_event_that_i_care_about(payload)
145+
logger.debug 'Handling the event that we care about!'
146+
true
147+
end
148+
149+
end
150+
151+
152+
# Finally some logic to let us run this server directly from the commandline, or with Rack
153+
# Don't worry too much about this code ;) But, for the curious:
154+
# $0 is the executed file
155+
# __FILE__ is the current file
156+
# If they are the same—that is, we are running this file directly, call the Sinatra run method
157+
run! if __FILE__ == $0
158+
end

0 commit comments

Comments
 (0)