Книга: Automation and Monitoring with Hubot: For DevOps and Developers
Назад: Graphing With Hubot
Дальше: GitLab Integration

GitHub Integration

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.

Putting An End To GitHub Email Notifications

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.

Why Not Just Use GitHub Service Hooks?

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.

Creating Auth Tokens

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.

Creating GitHub API Hooks

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.

Listing And Deleting GitHub API Hooks

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 

Pushes

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.

Issues

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 

Pull Requests

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 

Team Add

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 

Membership

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 

Automatically Closing Old GitHub Issues

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 
Назад: Graphing With Hubot
Дальше: GitLab Integration