GitHub is a wonderful platform for organizing and collaborating on software development, and if your company uses it to full extent, consider yourself lucky. There are so many ways to integrate Hubot with GitHub, it deserves a separate chapter.
If you happen to work on an active GitHub repository, or belong to organization that does everything via GitHub, you should agree on a few things:
First thing you should do is to turn off email notifications. Forget all those filters, just use web notifications (look for a blue dot on top of the page), they are far better. And to get notified about things in real time without leaving junk in your inbox, we will use the power of Hubot. It’s way better to receive notifications about all those issues and pull requests in your chat, not in your mail.
You may think - hey, GitHub has hundreds of service integrations, and you can even propose your own in . And yes, when it comes to getting GitHub events to appear in your chat, sometimes it may fit your needs. For example, GitHub can post information about commits directly to Campfire. But what if you want to edit something? You can’t change how the message looks, you can’t make it tell you about new Pull Requests. Oh, but if you use IRC, then it will tell you about Pull Requests.
That’s why it’s nice to have Hubot in the middle, so you can leverage the power of GitHub to full extent, and have complete control over what you are getting.
To integrate with GitHub API, you first have to create an Auth Token. To do that, you can either go to and click “Create new token” in “Personal Authentication Tokens” panel, or you can do it in command line:
curl -u 'your-github-username' \ -H "X-GitHub-OTP: 123456" \ # Two-Factor auth code, omit if non-relevant -d '{"scopes":["repo"],"note":"Hubot Auth Token"}' \ https://api.github.com/authorizations Enter host password for user 'your-github-username': { "id": 5415883, "url": "https://api.github.com/authorizations/5415883", "app": { "name": "Hubot Auth Token (API)", "url": "http://developer.github.com/v3/oauth/#oauth-authorizations-api", "client_id": "00000000000000000000" }, "token": "44283ef0f15c645629dd29222410ec9b58507f1b", "note": "Hubot Auth Token", "note_url": null, "created_at": "2014-01-25T17:59:38Z", "updated_at": "2014-01-25T17:59:38Z", "scopes": [ "repo" ] } Store the token somewhere safe, as it is nearly as powerful as your password.
Most of GitHub related monitoring is based on . You create a hook, give it an endpoint, and it will make HTTP requests when events occur.
You can create hooks over web interface - go to repository settings, select “Service Hooks”, then “WebHook URLs”. GitHub has recently renewed their hooks management, it is now much more flexible than it was before. Still, my prefered way of doing this is using the command line.
If you will use web based hook management, make sure you always select “Payload version” that ends with json, or otherwise scripts provided in this book may not work.
There are hooks available for over a dozen events, like push, issues, pull_request, fork, team_add, etc. You can find a full and up to date list in .
Let’s create a very simple Hubot script that will be able to receive HTTP requests and dump their contents, so we can use it as a starting point for our integrations.
scripts/github-hook-test.coffee
module.exports = (robot) -> robot.router.get "/github/test", (req, res) -> dump 'Received GET:', req, res robot.router.post "/github/test", (req, res) -> dump 'Received POST:', req, res robot.router.put "/github/test", (req, res) -> dump 'Received PUT:', req, res dump = (message, req, res) -> console.log message, JSON.strigify(req.body, null, 2) res.end() Keep in mind, that GitHub should be able to access Hubot’s HTTP endpoint to post the data. You should also take actions to secure your Hubot HTTP endpoint to prevent malicious attempts to perform unwanted HTTP requests. A common practice is to use firewall to restrict public access and whitelist GitHub IP range. You can find out GitHub IP range on their page, or even better, using our favorite tool - the command line:
hubot@botserv:~$ curl https://api.github.com/meta { "verifiable_password_authentication": true, "hooks": [ "192.30.252.0/22" ], "git": [ "192.30.252.0/22" ] } Now, let’s create a hook that will tell us about pushes. You will need to know the public endpoint of your Hubot. To test it, just open http://<hubot.server>/hubot/help in your browser and see if it shows the help.
hubot@botserv:~$ curl -H "Authorization: token <your_auth_token>" \ -d '{"name":"web","active":true,"events":["push"], \ "config":{"url":"http://botserv.your.org:8080/github/test", \ "content_type":"json"}}' \ https://api.github.com/repos/spajus/hubot-example/hooks { "url": "https://api.github.com/repos/spajus/hubot-example/hooks/1730429", "test_url": "https://api.github.com/repos/spajus/hubot-example/hooks/173042\ 9/test", "id": 1730429, "name": "web", "active": true, "events": [ "push" ], "config": { "url": "http://botserv.your.org:8080/github/test", "content_type": "json" }, "last_response": { "code": null, "status": "unused", "message": null }, "updated_at": "2014-01-26T06:02:04Z", "created_at": "2014-01-26T06:02:04Z" } After a push is made to the repo, here is what hubot.log shows:
Received POST: { "ref": "refs/heads/master", "after": "80398238f9f2952413278ae7a286a9e7b67af9a9", "before": "e52a9c120c7b6636966aa72cf6c37e5707023c14", "created": false, "deleted": false, "forced": true, "compare": "https://github.com/spajus/hubot-example/compare/e52a9c120c7b...\ 80398238f9f2", "commits": [ { "id": "80398238f9f2952413278ae7a286a9e7b67af9a9", "distinct": true, "message": "Add GitHub hooks test script", "timestamp": "2014-01-25T22:38:52-08:00", "url": "https://github.com/spajus/hubot-example/commit/80398238f9f29524\ 13278ae7a286a9e7b67af9a9", "author": { "name": "Tomas Varaneckas", "email": "[email protected]", "username": "spajus" }, "committer": { "name": "Tomas Varaneckas", "email": "[email protected]", "username": "spajus" }, "added": [ "scripts/github-hook-test.coffee" ], "removed": [], "modified": [] } ], "head_commit": { "id": "80398238f9f2952413278ae7a286a9e7b67af9a9", "distinct": true, "message": "Add GitHub hooks test script", "timestamp": "2014-01-25T22:38:52-08:00", "url": "https://github.com/spajus/hubot-example/commit/80398238f9f2952413\ 278ae7a286a9e7b67af9a9", "author": { "name": "Tomas Varaneckas", "email": "[email protected]", "username": "spajus" }, "committer": { "name": "Tomas Varaneckas", "email": "[email protected]", "username": "spajus" }, "added": [ "scripts/github-hook-test.coffee" ], "removed": [], "modified": [] }, "repository": { "id": 15725904, "name": "hubot-example", "url": "https://github.com/spajus/hubot-example", "description": "Examples for \"Automation and Monitoring with Hubot\" boo\ k.", "homepage": "https://leanpub.com/automation-and-monitoring-with-hubot", "watchers": 0, "stargazers": 0, "forks": 0, "fork": false, "size": 204, "owner": { "name": "spajus", "email": "[email protected]" }, "private": false, "open_issues": 0, "has_issues": true, "has_downloads": true, "has_wiki": true, "language": "CoffeeScript", "created_at": 1389156986, "pushed_at": 1390718321, "master_branch": "master" }, "pusher": { "name": "spajus", "email": "[email protected]" } } There is a load of information here, and we will make ourselves a script that extracts the interesting bits.
Before we start using those hooks, it’s good to know how to list and remove them. To list all hooks on one GitHub repository, run this:
hubot@botserv:~$ curl -H "Authorization: token <your_auth_token>" \ https://api.github.com/repos/spajus/hubot-example/hooks [ { "url": "https://api.github.com/repos/spajus/hubot-example/hooks/1730429", "test_url": "https://api.github.com/repos/spajus/hubot-example/hooks/1730\ 429/test", "id": 1730429, "name": "web", ... } ] To delete the hook, do a DELETE request providing hook id:
hubot@botserv:~$ curl -H "Authorization: token <your_auth_token>" \ -X DELETE \ https://api.github.com/repos/spajus/hubot-example/hooks/1730429 There is a ready-made Hubot script for displaying pushes - , but we will create ourselves a new one that is a little more dynamic and uses hubot-pubsub for routing.
scripts/github-pubsub-pushes.coffee
# Description: # hubot-pubsub based GitHub push notifier # # Dependencies: # "hubot-pubsub": "1.0.0" # # URLS: # POST /github/pushes/pubsub/<pubsub-event> # # Authors: # spajus module.exports = (robot) -> robot.router.post "/github/pushes/pubsub/:event", (req, res) -> res.end('') event = req.params.event try payload = req.body prefix = ">>> " if payload.commits.length > 0 merge_commit = false author = payload.commits[0].author.name for commit in payload.commits if commit.author.name != author merge_commit = true break if merge_commit message = "#{prefix}#{payload.pusher.name} merged #{payload.commits\ .length} " + "commits on #{payload.repository.name}:" + "#{payload.ref.replace('refs/heads/', '')} " + "(compare: #{payload.compare})" robot.emit 'pubsub:publish', event, message if payload.commits.length < 10 for commit in payload.commits robot.emit 'pubsub:publish', event, " * #{commit.author.name}: #{commit.message} (#{com\ mit.url})" else message = "#{prefix}#{payload.commits[0].author.name} " + "(#{payload.commits[0].author.username}) " + "pushed #{payload.commits.length} commits to " + "#{payload.repository.name}:#{payload.ref.replace('refs/h\ eads/', '')}" if payload.commits.length > 1 message += " (compare: #{payload.compare})" robot.emit 'pubsub:publish', event, message for commit in payload.commits robot.emit 'pubsub:publish', event, " * #{commit.message} (#{c\ ommit.url})" else robot.emit 'pubsub:publish', event, message for commit in payload.commits do (commit) -> robot.emit 'pubsub:publish', event, " * #{commit.message} (#\ {commit.url})" else if payload.created if payload.base_ref base_ref = ': ' + payload.base_ref.replace('refs/heads/', '') else base_ref = '' robot.emit 'pubsub:publish', event, "#{prefix}#{payload.pusher.name\ } " + "created: #{payload.ref.replace('refs/heads/', '')}#{bas\ e_ref}" if payload.deleted robot.emit 'pubsub:publish', event, "#{prefix}#{payload.pusher.name\ } " + "deleted: #{payload.ref.replace('refs/heads/', '')}" catch error console.log "github-pubsub-push error: #{error}. Payload: #{req.body}" Script source available at
You will get something like this:
Tomas V. hubot subscribe github.pushes Hubot Subscribed 585164 to github.pushes events Hubot github.pushes: >>> Tomas Varaneckas (spajus) pushed 1 commits to hu\ bot-example:master github.pushes: * Update hubot-pubsub to 1.0.0 (https://github.com/s\ pajus/hubot-example/commit/...) This script will also tell you about tags and branches.
It is possible to receive a Hubot notification whenever somebody opens or closes an issue on GitHub. It is configurable per repo.
First we create a hook for the script. It will be listening on /github/issues/pubsub/:event.
hubot@botserv:~$ curl -H "Authorization: token <your_auth_token>" \ -d '{"name":"web","active":true,"events":["issues"],\ "config":{"url":"http://botserv.your.org:8080/github/issues/pubsub/github.i\ ssues",\ "content_type":"json"}}' \ https://api.github.com/repos/spajus/hubot-example/hooks Now, the script:
scripts/github-pubsub-issues.coffee
# Description: # An HTTP Listener that notifies about new Github issues # # Dependencies: # "hubot-pubsub": "1.0.0" # # URLS: # POST /github/issues/pubsub/<pubsub-event> # # Authors: # spajus module.exports = (robot) -> robot.router.post "/github/issues/pubsub/:event", (req, res) -> res.end("") event = req.params.event announceIssue req.body, (data) -> robot.emit 'pubsub:publish', event, data announceIssue = (data, cb) -> if data.action mentioned = data.issue.body.match(/(^|\s)(@[\w\-\/]+)/g) if mentioned unique = (array) -> output = {} output[array[key]] = array[key] for key in [0...array.length] value for key, value of output mentioned = mentioned.map (nick) -> nick.trim() mentioned = unique mentioned mentioned_line = "\nMentioned: #{mentioned.join(", ")}" else mentioned_line = '' cb "Issue #{data.action}: \"#{data.issue.title}\" " + "by #{data.issue.user.login}: #{data.issue.html_url}#{mentioned_line}" Script source available at
Subscribe your room to github.issues via hubot-pubsub and here’s what you will get:
Tomas V. hubot subscribe github.issues Hubot Subscribed 585164 to github.issues events github.issues: Issue opened: "Test the issue hook" by spajus: https\ ://github.com/spajus/hubot-example/issues/1 Mentioned: @spajus Tomas V. I'll go close this Hubot github.issues: Issue closed: "Test the issue hook" by spajus: https\ ://github.com/spajus/hubot-example/issues/1 Mentioned: @spajus Knowing about new pull requests as soon as they appear is essential for maintaining a healthy workflow. If you do pull requests, you must know how long can it take to wait for somebody to review and merge your pull request. It shouldn’t be that way, and when pull request notifications start appearing in developer chatrooms, average response time drops tenfold.
Let’s start by creating a new hook that will post data on /github/pulls/pubsub/:event.
hubot@botserv:~$ curl -H "Authorization: token <your_auth_token>" \ -d '{"name":"web","active":true,"events":["pull_request"],\ "config":{"url":"http://botserv.your.org:8080/github/issues/pubsub/github.p\ ulls",\ "content_type":"json"}}' \ https://api.github.com/repos/spajus/hubot-example/hooks The script:
scripts/github-pubsub-pulls.coffee
# Description: # hubot-pubsub based GitHub pull request notifier # # Dependencies: # "hubot-pubsub": "1.0.0" # # URLS: # POST /github/pulls/pubsub/<pubsub-event> # # Authors: # spajus module.exports = (robot) -> robot.router.post "/github/pulls/pubsub/:event", (req, res) -> event = req.params.event res.end("") announcePullRequest req.body, (data) -> robot.emit 'pubsub:publish', event, data announcePullRequest = (data, cb) -> if data.action == 'opened' mentioned = data.pull_request.body.match(/(^|\s)(@[\w\-\/]+)/g) if mentioned unique = (array) -> output = {} output[array[key]] = array[key] for key in [0...array.length] value for key, value of output mentioned = mentioned.filter (nick) -> slashes = nick.match(/\//g) slashes is null or slashes.length < 2 mentioned = mentioned.map (nick) -> nick.trim() mentioned = unique mentioned mentioned_line = "\nMentioned: #{mentioned.join(", ")}" else mentioned_line = '' cb "New pull request \"#{data.pull_request.title}\" " + "by #{data.pull_request.user.login}: " + "#{data.pull_request.html_url}#{mentioned_line}" Script source available at
Here’s how it looks in action:
Tomas V. hubot subscribe github.pulls Hubot Subscribed 585163 to github.pulls events Tomas V. I'll go make a pull request now... Hubot github.pulls: New pull request "Add GitHub pull request notificatio\ n script" by spajus: https://github.com/spajus/hubot-example/pull/2 Mentioned: @spajus, @other_responsible_guy When your company grows big, you may start worrying about repository permissions being given to wrong teams or people. Luckily, you Hubot can help. When new team gets added to a repository, team_add event can be fired. Let’s write ourselves a script for that:
script/github-pubsub-team.coffee
# Description: # An HTTP Listener that notifies about repository team changes # # Dependencies: # "hubot-pubsub": "1.0.0" # # URLS: # POST /github/team/pubsub/<pubsub-event> # # Authors: # spajus module.exports = (robot) -> robot.router.post "/github/team/pubsub/:event", (req, res) -> res.end("") event = req.params.event announceTeamChange req.body, (data) -> robot.emit 'pubsub:publish', event, data announceTeamChange = (data, cb) -> team = data.team.name team_perm = data.team.permission org = data.sender.login repo = data.repository.full_name cb "@#{org}/#{team} received #{team_perm} rights on #{repo}" Script source available at
Run this on every repository to enable team_add hooks:
hubot@botserv:~$ curl -H "Authorization: token <your_auth_token>" \ -d '{"name":"web","active":true,"events":["team_add"],\ "config":{"url":"http://botserv.your.org:8080/github/team/pubsub/github.tea\ m",\ "content_type":"json"}}' \ https://api.github.com/repos/<your_org>/<your_repo>/hooks Subcribe a chatroom to github.team events, then add a team to repository and see what Hubot tells you:
Tomas V. hubot subscribe github.team Hubot Subscribed 585163 to github.team events Hubot github.team: @example/devs received pull rights on example/app Tracking repository permissions without organization is also possible. Here’s the script:
script/github-pubsub-member.coffee
# Description: # An HTTP Listener that notifies about repo membership changes # # Dependencies: # "hubot-pubsub": "1.0.0" # # URLS: # POST /github/member/pubsub/<pubsub-event> # # Authors: # spajus module.exports = (robot) -> robot.router.post "/github/member/pubsub/:event", (req, res) -> res.end("") event = req.params.event announceMemberChange req.body, (data) -> robot.emit 'pubsub:publish', event, data announceMemberChange = (data, cb) -> if data.action who = data.member.login by_who = data.sender.login repo = data.repository.full_name action = data.action cb "#{repo} membership change: @#{by_who} #{action} @#{who}" Script source available at
At the moment of writing, only added action was sent from GitHub. Still, it’s pretty useful.
Tomas V. hubot subscribe github.member Hubot Subscribed 585163 to github.member events github.member: spajus/hubot-example membership change: @spajus adde\ d @electrotek To prevent old open issues that nobody bothers to follow up with, we can make Hubot automatically run a daily check and close issues that had no activity for over a month.
The following script will be a little more advanced. It requires additional dependencies and configuration.
scripts/github-old-issues.coffee
# Description # Find and close old issues in GitHub # # Dependencies: # "githubot": "0.5.0" # "moment": "2.5.1" # "hubot-pubsub": "1.0.0" # "cron": "1.0.3" # "time": "0.10.0" # # Configuration: # HUBOT_GITHUB_TOKEN (optional, if you want to search in private repos) # HUBOT_GITHUB_ORG - your GitHub organization # # Commands: # hubot close old issues in <repo> - Close outdated issues in given repo # # Author: # spajus # Override these with your target repos. Keep list empty if using org. target_repos = [ 'spajus/hubot-example', 'spajus/hubot-control' ] # Override with your org. Keep blank if non relevant. target_org = '' # Set your time zone timezone = 'America/Los_Angeles' # Set desired time. 00 00 9 * * 1-5 is monday-friday at 9 AM. cron_expression = '00 00 9 * * 1-5' module.exports = (robot) -> github = require('githubot')(robot, apiVersion: 'preview') cronJob = require('cron').CronJob moment = require('moment') new cronJob(cron_expression, closeOldIssues, null, true, timezone) closeOldIssues = -> org = target_org || process.env.HUBOT_GITHUB_ORG if org robot.emit 'github:org:issues:close', org for repo in target_repos robot.emit 'github:repo:issues:close', repo robot.respond /close old issues (in )?(.+\/[^\s]+)/i, (msg) -> repo = msg.match[2] closeOldIssuesIn repo, (data) -> msg.send data robot.on 'github:org:issues:close', (org) -> github.get "/orgs/#{org}/repos", (data) -> for repo in data closeOldIssuesIn repo.full_name, (data) -> robot.emit 'pubsub:publish', 'github.issue.close', data robot.on 'github:repo:issues:close', (repo) -> closeOldIssuesIn repo, (data) -> robot.emit 'pubsub:publish', 'github.issue.close', data closeOldIssuesIn = (repo, cb) -> github.handleErrors (response) -> cb "Error: #{response.statusCode} #{response.error}. Repo: #{repo}" github.get "repos/#{repo}/issues?state=open", (data) -> reply = '' found = false old_time = moment().subtract 'months', 1 for issue in data issue_time = moment issue.updated_at, 'YYYY-MM-DDTHH:mm:ssZ' if issue_time < old_time found = true post_data = { body: "Closing old issue: updated #{issue_time.fromNo\ w()}" } github.post "repos/#{repo}/issues/#{issue.number}/comments", post_d\ ata, (post_resp) -> console.log "Posted comment: #{post_resp.html_url}" close_data = { state: 'closed' } github.request 'PATCH', "repos/#{repo}/issues/#{issue.number}", clo\ se_data, (close_resp) -> console.log "Closed issue: #{close_resp.html_url}" reply = "#{reply}#{issue.title} (#{issue.html_url}) updated #{issue\ _time.fromNow()}\n" if found cb "Found #{data.length} open issues in #{repo}. Closed old ones:\n#{\ reply}" else cb "No old issues found in #{repo}" Script source available at
Before restarting Hubot with this script, make sure to change configuration in first couple of lines, and install the dependencies. Make sure to use npm install --save so the definitions will get automatically added to package.json. You will also have set use a valid auth token in using HUBOT_GITHUB_TOKEN. You can do that in hubot.conf if you’ve set up Hubot as described in this book.
npm install --save moment time cron githubot To test if it works, you can manually trigger the closing of old issues by saying hubot close old issues in some/repo.
Tomas V. hubot close old issues in spajus/hubot-example Hubot No old issues found in spajus/hubot-example