Skip to content

Commit 9ad0922

Browse files
author
Jared Murrell
authored
Merge pull request #179 from github/primetheus/python-webhooks
Flask webhook samples
2 parents 306bf3c + 0f13e79 commit 9ad0922

File tree

6 files changed

+491
-0
lines changed

6 files changed

+491
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
FROM python:2.7
2+
LABEL version="1.0"
3+
LABEL description="Flask-based webhook receiver"
4+
5+
WORKDIR /app
6+
7+
COPY webhooks.py /app/webhooks.py
8+
COPY config.json.sample /app/config.json
9+
COPY requirements.txt /app/requirements.txt
10+
COPY hooks /app/hooks
11+
RUN pip install -r requirements.txt
12+
13+
EXPOSE 5000
14+
CMD ["python", "webhooks.py"]
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
GitHub webhooks test
2+
====================
3+
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4+
5+
This is a simple WSGI application written in Flask to allow you to register and run your own Git hooks. Python is a
6+
favorite programming language of many, so this might be familiar to work with.
7+
8+
9+
Install
10+
=======
11+
```bash
12+
git clone https://github.com/github/platform-samples.git
13+
cd platform-samples/hooks/python/flask-github-webhooks
14+
```
15+
16+
Dependencies
17+
============
18+
There are a few dependencies to install before this can run
19+
* Flask
20+
* ipaddress
21+
* requests
22+
* pyOpenSSL==16.2.0 (required if you have issues with SSL libraries and the GitHub API)
23+
```bash
24+
sudo pip install -r requirements.txt
25+
```
26+
27+
Setup
28+
=====
29+
30+
You can configure what the application does by copying the sample config file
31+
``config.json.sample`` to ``config.json`` and adapting it to your needs:
32+
33+
```json
34+
{
35+
"github_ips_only": true,
36+
"enforce_secret": "",
37+
"return_scripts_info": true,
38+
"hooks_path": "/<other>/<path>/<to>/hooks/"
39+
}
40+
```
41+
42+
| Setting | Description |
43+
|---------|-------------|
44+
| github_ips_only | Restrict application to be called only by GitHub IPs. IPs whitelist is obtained from [GitHub Meta](https://developer.github.com/v3/meta/) ([endpoint](https://api.github.com/meta)). _Default_: ``true``. |
45+
| enforce_secret | Enforce body signature with HTTP header ``X-Hub-Signature``. See ``secret`` at [GitHub WebHooks Documentation](https://developer.github.com/v3/repos/hooks/). _Default_: ``''`` (do not enforce). |
46+
| return_scripts_info | Return a JSON with the ``stdout``, ``stderr`` and exit code for each executed hook using the hook name as key. If this option is set you will be able to see the result of your hooks from within your GitHub hooks configuration page (see "Recent Deliveries"). _*Default*_: ``true``. |
47+
| hooks_path | Configures a path to import the hooks. If not set, it'll import the hooks from the default location (/.../python-github-webhooks/hooks) |
48+
49+
50+
Adding hooks
51+
============
52+
53+
This application uses the following precedence for executing hooks:
54+
55+
```bash
56+
hooks/{event}-{reponame}-{branch}
57+
hooks/{event}-{reponame}
58+
hooks/{event}
59+
hooks/all
60+
```
61+
Hooks are passed to the path to a JSON file holding the
62+
payload for the request as first argument. The event type will be passed
63+
as second argument. For example:
64+
65+
```
66+
hooks/reposotory-mygithubrepo-master /tmp/ksAHXk8 push
67+
```
68+
69+
Webhooks can be written in any language. Simply add a ``shebang`` and enable the execute bit (_chmod 755_)
70+
The following example is a Python webhook receiver that will create an issue when a repository in an organization is deleted:
71+
72+
```python
73+
#!/usr/bin/env python
74+
75+
import sys
76+
import json
77+
import requests
78+
79+
# Authentication for the user who is filing the issue. Username/API_KEY
80+
USERNAME = '<api_username>'
81+
API_KEY = '<github-api-key>'
82+
83+
# The repository to add this issue to
84+
REPO_OWNER = '<repository-owner>'
85+
REPO_NAME = '<repository-name>'
86+
87+
88+
def create_github_issue(title, body=None, labels=None):
89+
"""
90+
Create an issue on github.com using the given parameters.
91+
:param title: This is the title of the GitHub Issue
92+
:param body: Optional - This is the body of the issue, or the main text
93+
:param labels: Optional - What type of issue are we creating
94+
:return:
95+
"""
96+
# Our url to create issues via POST
97+
url = 'https://api.github.com/repos/%s/%s/issues' % (REPO_OWNER, REPO_NAME)
98+
# Create an authenticated session to create the issue
99+
session = requests.Session()
100+
session.auth = (USERNAME, API_KEY)
101+
# Create the issue
102+
issue = {'title': title,
103+
'body': body,
104+
'labels': labels}
105+
# Add the issue to our repository
106+
r = session.post(url, json.dumps(issue))
107+
if r.status_code == 201:
108+
print 'Successfully created Issue "%s"' % title
109+
else:
110+
print 'Failed to create Issue "%s"' % title
111+
print 'Response:', r.content
112+
113+
114+
if __name__ == '__main__':
115+
with open(sys.argv[1], 'r') as jsp:
116+
payload = json.loads(jsp.read())
117+
# What was done to the repo
118+
action = payload['action']
119+
# What is the repo name
120+
repo = payload['repository']['full_name']
121+
# Create an issue if the repository was deleted
122+
if action == 'deleted':
123+
create_github_issue('%s was deleted' % repo, 'Seems we\'ve got ourselves a bit of an issue here.\n\n@<repository-owner>',
124+
['deleted'])
125+
# Log the payload to a file
126+
outfile = '/tmp/webhook-{}.log'.format(repo)
127+
with open(outfile, 'w') as f:
128+
f.write(json.dumps(payload))
129+
```
130+
131+
Not all events have an associated branch, so a branch-specific hook cannot
132+
fire for such events. For events that contain a pull_request object, the
133+
base branch (target for the pull request) is used, not the head branch.
134+
135+
The payload structure depends on the event type. Please review:
136+
137+
https://developer.github.com/v3/activity/events/types/
138+
139+
140+
Deploy
141+
======
142+
143+
Apache
144+
------
145+
146+
To deploy in Apache, just add a ``WSGIScriptAlias`` directive to your
147+
VirtualHost file:
148+
149+
```bash
150+
<VirtualHost *:80>
151+
ServerAdmin [email protected]
152+
ServerName my.site.com
153+
DocumentRoot /var/www/site.com/my/htdocs/
154+
155+
# Handle Github webhook
156+
<Directory "/var/www/site.com/my/flask-github-webhooks">
157+
Order deny,allow
158+
Allow from all
159+
</Directory>
160+
WSGIScriptAlias /webhooks /var/www/site.com/my/flas-github-webhooks/webhooks.py
161+
</VirtualHost>
162+
```
163+
You can now register the hook in your Github repository settings:
164+
165+
https://github.com/youruser/myrepo/settings/hooks
166+
167+
To register the webhook select Content type: ``application/json`` and set the URL to the URL
168+
of your WSGI script:
169+
170+
http://my.site.com/webhooks
171+
172+
Docker
173+
------
174+
175+
To deploy in a Docker container you have to expose the port 5000, for example
176+
with the following command:
177+
178+
```bash
179+
git clone https://github.com/github/platform-samples.git
180+
docker build -t flask-github-webhooks platform-samples/hooks/python/flask-github-webhooks
181+
docker run -ditp 5000:5000 --restart=unless-stopped --name webhooks flask-github-webhooks
182+
```
183+
You can also mount volume to setup the ``hooks/`` directory, and the file
184+
``config.json``:
185+
186+
```bash
187+
docker run -ditp 5000:5000 --name webhooks \
188+
--restart=unless-stopped \
189+
-v /path/to/my/hooks:/app/hooks \
190+
-v /path/to/my/config.json:/app/config.json \
191+
flask-github-webhooks
192+
```
193+
194+
Test your deployment
195+
====================
196+
197+
To test your hook you may use the GitHub REST API with ``curl``:
198+
199+
https://developer.github.com/v3/
200+
201+
```bash
202+
curl --user "<youruser>" https://api.github.com/repos/<youruser>/<myrepo>/hooks
203+
```
204+
Take note of the test_url.
205+
206+
```bash
207+
curl --user "<youruser>" -i -X POST <test_url>
208+
```
209+
You should be able to see any log error in your web app.
210+
211+
212+
Debug
213+
=====
214+
215+
When running in Apache, the ``stderr`` of the hooks that return non-zero will
216+
be logged in Apache's error logs. For example:
217+
218+
```bash
219+
sudo tail -f /var/log/apache2/error.log
220+
```
221+
Will log errors in your scripts if printed to ``stderr``.
222+
223+
You can also launch the Flask web server in debug mode at port ``5000``.
224+
225+
```bash
226+
python webhooks.py
227+
```
228+
This can help debug problem with the WSGI application itself.
229+
230+
231+
Credits
232+
=======
233+
234+
This project is just the reinterpretation and merge of two approaches and a modification of Carlos Jenkins' work:
235+
236+
- [github-webhook-wrapper](https://github.com/datafolklabs/github-webhook-wrapper)
237+
- [flask-github-webhook](https://github.com/razius/flask-github-webhook)
238+
- [python-github-webhooks](https://github.com/carlos-jenkins/python-github-webhooks)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"github_ips_only": true,
3+
"enforce_secret": "",
4+
"return_scripts_info": true
5+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env python
2+
# Rename this file to "repository" in order to
3+
# have it process repo deletions and create issues.
4+
# You will also need to change the content of lines
5+
# 12, 13, 16, 17 and the <user> mention on line 52
6+
7+
import sys
8+
import json
9+
import requests
10+
11+
# Authentication for the user who is filing the issue. Username/API_KEY
12+
USERNAME = ''
13+
API_KEY = ''
14+
15+
# The repository to add this issue to
16+
REPO_OWNER = 'my-github-org'
17+
REPO_NAME = 'github-test-admin'
18+
19+
20+
def create_github_issue(title, body=None, labels=None):
21+
"""
22+
Create an issue on github.com using the given parameters.
23+
:param title: This is the title of the GitHub Issue
24+
:param body: Optional - This is the body of the issue, or the main text
25+
:param labels: Optional - What type of issue are we creating
26+
:return:
27+
"""
28+
# Our url to create issues via POST
29+
url = 'https://api.github.com/repos/%s/%s/issues' % (REPO_OWNER, REPO_NAME)
30+
# Create an authenticated session to create the issue
31+
session = requests.Session()
32+
session.auth = (USERNAME, API_KEY)
33+
# Create the issue
34+
issue = {'title': title,
35+
'body': body,
36+
'labels': labels}
37+
# Add the issue to our repository
38+
r = session.post(url, json.dumps(issue))
39+
if r.status_code == 201:
40+
print 'Successfully created Issue "%s"' % title
41+
else:
42+
print 'Failed to create Issue "%s"' % title
43+
print 'Response:', r.content
44+
45+
46+
if __name__ == '__main__':
47+
with open(sys.argv[1], 'r') as jsp:
48+
payload = json.loads(jsp.read())
49+
action = payload['action']
50+
repo = payload['repository']['full_name']
51+
if action == 'deleted':
52+
create_github_issue('%s was deleted' % repo, 'Seems we\'ve got ourselves a bit of an issue here.\n\n@<user>',
53+
['deleted'])
54+
55+
outfile = '/tmp/webhook-{}.log'.format(repo)
56+
with open(outfile, 'w') as f:
57+
f.write(json.dumps(payload))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Flask
2+
ipaddress
3+
requests
4+
pyOpenSSL==16.2.0

0 commit comments

Comments
 (0)