Permalink
Browse files

Generalize GitHub webhook listener and add PullRequest support.

  • Loading branch information...
spladug committed Sep 30, 2012
1 parent 8e0e22f commit 2681ba99a7df7bc1e15328025b3f59c5b9f4d5ea
Showing with 159 additions and 28 deletions.
  1. +12 −15 README.md
  2. +49 −13 harold/plugins/{postreceive.py → github.py}
  3. +98 −0 register-github-webhooks.py
View
@@ -66,21 +66,20 @@ Server password to use when connecting to IRC.
A comma-delimited list of channels to join automatically. Additional channels
may be added by other plugins.
-## postreceive
+## github
-The postreceive plugin implements an endpoint for GitHub-style post-receive
-notifications. It depends on the IRC plugin and will notify users via IRC
-when code is pushed to repositories under its purview. The plugin itself does
-not have any configuration (though the section header must exist in the config
-file for it to be activated.) Instead, each repository that will send
-notifications should have its own section of the format
-`[harold:repository:owner/repository]`.
+The github plugin implements an endpoint for GitHub webhook notifications. It
+depends on the IRC plugin and will notify users via IRC when code is pushed to
+repositories under its purview. The plugin itself does not have any
+configuration (though the section header must exist in the config file for it
+to be activated.) Instead, each repository that will send notifications should
+have its own section of the format `[harold:repository:owner/repository]`.
-## postreceive repository configuration
+## github repository configuration
### channel
-IRC channel to send commit notifications to.
+IRC channel to send notifications to.
### branches
@@ -131,11 +130,9 @@ Defaults to
### Configuring GitHub for postreceive
-Follow the [GitHub post-receive hooks
-instructions](http://help.github.com/post-receive-hooks/) and set the
-post-receive URL to the following:
-
- http://HOST/harold/post-receive/SECRET
+Use the register-github-webhooks.py script in this directory. Set the webhook
+host to a publicly accessible name for the harold HTTP server, e.g.
+123.45.67.89:8888.
## ident
@@ -7,7 +7,7 @@
REPOSITORY_PREFIX = 'harold:repository:'
-class PostReceiveConfig(object):
+class GitHubConfig(object):
def __init__(self, config, channels):
self.repositories_by_name = {}
@@ -38,11 +38,11 @@ def _get_commit_author(commit):
return author_info.get('username', author_info['name'])
-class PostReceiveDispatcher(object):
- def __init__(self, config, bot):
+class PushDispatcher(object):
+ def __init__(self, config, bot, shortener):
self.config = config
self.bot = bot
- self.shortener = UrlShortener()
+ self.shortener = shortener
def _dispatch_commit(self, repository, branch, commit):
d = self.shortener.make_short_url(commit['url'])
@@ -85,8 +85,7 @@ def onUrlShortened(short_url):
})
d.addCallback(onUrlShortened)
- def dispatch(self, payload):
- parsed = json.loads(payload)
+ def dispatch(self, parsed):
repository_name = (parsed['repository']['owner']['name'] + '/' +
parsed['repository']['name'])
repository = self.config.repositories_by_name[repository_name]
@@ -101,20 +100,57 @@ def dispatch(self, payload):
self._dispatch_bundle(parsed, repository, branch, commits)
-class PostReceiveListener(ProtectedResource):
+class PullRequestDispatcher(object):
+ def __init__(self, config, bot, shortener):
+ self.config = config
+ self.bot = bot
+ self.shortener = shortener
+
+ def dispatch(self, parsed):
+ action = parsed["action"]
+ if action != "opened":
+ return
+
+ repository_name = parsed["repository"]["full_name"]
+ repository = self.config.repositories_by_name[repository_name]
+
+ html_link = parsed["pull_request"]["_links"]["html"]["href"]
+ d = self.shortener.make_short_url(html_link)
+ def onUrlShortened(short_url):
+ message = ("%(user)s opened pull request #%(id)d (%(short_url)s) "
+ "on %(repo)s: %(title)s")
+ self.bot.send_message(repository.channel, message % dict(
+ user=parsed["sender"]["login"],
+ id=parsed["number"],
+ short_url=short_url,
+ repo=repository_name,
+ title=parsed["pull_request"]["title"][:72],
+ ))
+ d.addCallback(onUrlShortened)
+
+
+class GitHubListener(ProtectedResource):
isLeaf = True
def __init__(self, config, http, bot):
ProtectedResource.__init__(self, http)
- self.dispatcher = PostReceiveDispatcher(config, bot)
+ shortener = UrlShortener()
+ self.dispatchers = {
+ "push": PushDispatcher(config, bot, shortener),
+ "pull_request": PullRequestDispatcher(config, bot, shortener),
+ }
def _handle_request(self, request):
- post_data = request.args['payload'][0]
- self.dispatcher.dispatch(post_data)
+ event = request.requestHeaders.getRawHeaders("X-Github-Event")[-1]
+ dispatcher = self.dispatchers.get(event)
+
+ if dispatcher:
+ post_data = request.args['payload'][0]
+ parsed = json.loads(post_data)
+ dispatcher.dispatch(parsed)
def make_plugin(config, http, irc):
- pr_config = PostReceiveConfig(config, irc.channels)
+ gh_config = GitHubConfig(config, irc.channels)
- http.root.putChild('post-receive',
- PostReceiveListener(pr_config, http, irc.bot))
+ http.root.putChild('github', GitHubListener(gh_config, http, irc.bot))
@@ -0,0 +1,98 @@
+#!/usr/bin/python
+
+import ConfigParser
+import getpass
+import json
+import requests
+from requests.auth import HTTPBasicAuth
+import urlparse
+
+from harold.plugins.github import REPOSITORY_PREFIX
+
+
+def _make_hooks_url(repo, endpoint=None):
+ path_fragments = ["/repos", repo, "hooks"]
+ if endpoint:
+ path_fragments.append(endpoint)
+
+ return urlparse.urlunsplit((
+ "https",
+ "api.github.com",
+ "/".join(path_fragments),
+ None,
+ None
+ ))
+
+# figure out which repos we care about
+repositories = []
+parser = ConfigParser.ConfigParser()
+with open("harold.ini", "r") as f:
+ parser.readfp(f)
+
+for section in parser.sections():
+ if not section.startswith(REPOSITORY_PREFIX):
+ continue
+ repositories.append(section[len(REPOSITORY_PREFIX):])
+
+print "I will ensure webhooks are registered for:"
+for repo in repositories:
+ print " - " + repo
+print
+
+netloc = raw_input("Harold GitHub Webhook Host: ")
+username = raw_input("GitHub Username: ")
+password = getpass.getpass("GitHub Password: ")
+session = requests.session(auth=HTTPBasicAuth(username, password),
+ verify=True
+ )
+
+http_secret = parser.get("harold:plugin:http", "secret")
+webhook_url = urlparse.urlunsplit((
+ "http",
+ netloc,
+ "/harold/github/" + http_secret,
+ None,
+ None
+))
+
+DESIRED_EVENTS = ["push", "pull_request"]
+for repo in repositories:
+ print repo
+
+ # list existing hooks
+ hooks_response = session.get(_make_hooks_url(repo))
+ hooks_response.raise_for_status()
+ hooks = hooks_response.json
+
+ # determine if we're already configured / destroy non-conforming hooks
+ found_valid_hook = False
+ for hook in hooks:
+ if (hook["config"]["url"] != webhook_url or
+ hook["events"] != DESIRED_EVENTS or
+ found_valid_hook):
+ print " Deleting non-conforming hook %d" % hook["id"]
+ response = session.delete(_make_hooks_url(repo, str(hook["id"])))
+ response.raise_for_status()
+ else:
+ print " Found existing valid hook (%d)" % hook["id"]
+ found_valid_hook = True
+
+ if found_valid_hook:
+ continue
+
+ print " Registering hook"
+ response = session.post(
+ _make_hooks_url(repo),
+ data=json.dumps(dict(
+ name="web",
+ config=dict(
+ url=webhook_url,
+ ),
+ events=[
+ "push",
+ "pull_request",
+ ],
+ active=True,
+ )),
+ )
+ response.raise_for_status()

0 comments on commit 2681ba9

Please sign in to comment.