This is the final out of three posts where I will explain how to hook up Hubot to Github’s API. The goal of these posts is to get notified in Slack when your pull request becomes unmergeable.

Read “Hubot: get it running locally in Slack” and “Hubot: listen to Github’s pull request events” to get up to speed and get the code that this post builds upon.

Connect to Github’s API to check the merge status of a pull request

Is your pull request event set up and your bot listening to your webhook? Great! Let’s do something with that data.

Here you can find an example of the data you’ll receive on pull request events. As you can see there’s a mergeable key. This is either null, true, or false. You want to send a notification to Slack when the value is false.

Hubot has built-in event listener methods: emit and on. With these methods, you can create your own event that Hubot will listen to. You define an event with emit and you listen to it with on.

You can use these methods to handle errors. Wrap your data assignment in a try/catch block and create an error event if there is one:

  robot.router.post '/hubot/github/:room', (req, res) ->
    room = req.params.room

    try
      data = req.body
    catch error
      robot.emit 'error', error

Then somewhere in your script you can add:

  robot.on 'error', (error) ->
    #process the error

Now that you have that sorted, you can set the values you need to work with. In this case: the Slack room name to post in and the pull request url, id and status. Add the following above the catch error line:

  pull_request =
    {
      room: room,
      url: data.pull_request.url
      pullId: data.pull_request.number
      pullState: data.pull_request.state
    }

The room is needed for the Slack notification. The url and number are needed for the GET request you’ll send to Github’s API in order to check the pull request’s merge status. The pull request’s state is needed to filter events on pull requests that are not open. Let’s do that first, because there’s no point in checking for a possible merge conflict in closed ones.

  if (pull_request.pullState == 'open' || pull_request.pullState == 'reopened')
    # do stuff

Tip: throw a console.log() in your code to see what data you have available and know what you’re working with.

When someone pushes to a branch that has a pull request, the payload is sent immediately. The result is that the initial value of mergeable in the data object is unknown. The main reason is that it takes a few seconds until the merge status is known because the code has to be compared to the master branch. In order to know if the pull request has a merge conflict, therefore unable to merge with the master branch, you have to check its merge status via the Github API.

You can use Hubot’s http call methods, that enable you to send GET requests. In this case to the Github API. Let’s create a method that takes care of this and add it at the top in github.coffee , above the export function:

  getRequest = (robot, data, callback) ->
    url = "#{url}/YourRepoName/#{data.repository}/pulls/#{data.pullId}?access_token=#{token}"

    robot.http(url)
      .headers('Accept': 'application/rubyon')
      .get() (err, res, body) ->
        callback(err, res, body)

  module.exports = (robot) ->

The method accepts 3 arguments: the robot instance, data and a callback function.

Notice that the url variable uses data.repository and data.pullId. These come from the pull_request object that you pass as the data argument. There’s also an access_token variable at the end. This is needed in case of a private repository. You can set one here. If your repo is public, you do not have to include the token in the url. What follows is the GET request to that url.

Create another method underneath called checkMergeStatus:

  checkMergeStatus = (robot, data) ->
    getRequest robot, data, (err, res, body) ->
      try
        response = JSON.parse body
        mergeStatus = response.mergeable
      catch error
        robot.emit 'error', error

This method uses your getRequest method and stores the mergeable value. Now, if mergeStatus is false, you want to send a notification to your Slack channel. If it is true you do not have to do anything. If it is still unknown, you want to re-check. Add the following to the checkMergeStatus method, inside the try block:

  if (mergeStatus == false)
    # send notification
  else if (mergeStatus == 'unknown')
    setTimeout ->
      checkMergeStatus(robot, data)
    , 1000
  else
    # do something?

As you can see, if the second condition evaluates to true, the checkMergeStatus will keep calling itself with 1000ms (1sec) intervals, until it changes.

Send a notification to your Slack channel on a merge conflict

Here’s where the previously mentioned emit method comes in handy again. You can create a merge_conflict event for when the status is false. Make sure it has the values you need in your Slack notification:

  if (mergeStatus == false)
    robot.emit 'merge_conflict', {
      room: data.room,
      pullTitle: response.title,
      author: response.user.login,
      pullUrl: response.html_url,
      pullId: response.number
    }

All that you have to do now, is to get Hubot to take action as soon as the merge_conflict event happens. You can let Hubot ‘listen’ to it by using the on method. I moved the code for this to a separate script called merge_conflict_notifier.coffee in the scripts folder:

  module.exports = (robot) ->

    robot.on 'merge_conflict', (merge_conflict) ->
      room_id = robot.adapter.client.rtm.dataStore.getChannelByName(merge_conflict.room).id
      message =
        {
          "text": ":no_entry_sign: Merge conflict: <#{merge_conflict.pullUrl}|##{merge_conflict.number} #{merge_conflict.pullTitle}> by #{merge_conflict.author}"
        }

      robot.messageRoom room_id, message

In order to post a message to a room via Slack’s API, you need to have the room’s id. robot.adapter.client.rtm.dataStore.getChannelByName(<your room name>).id will get that for you.

The message is formatted according to Slack’s requirements. More information about message formatting can be found on their website.

Keeping track

But what happens if someone is going to commit several fixes to their branch but the merge conflict does not get resolved right away? You do not want to spam your Slack channel because that is super annoying. Therefore it would be a good idea to keep a list of the pull requests that have a merge conflict and remove them if they are resolved.

At the top of the github.coffee, add an empty array called unmergeablePulls. Now you can add some code to keep track of them. Add the following above the checkMergeStatus method:

  unmergeablePulls = []

  addPullIdToList = (pullId) ->
    return false if pullId in unmergeablePulls

    unmergeablePulls.push pullId
    return true

  removePullIdFromList = (pullId) ->
    return if pullId == null

    indexOfPullId = unmergeablePulls.indexOf(pullId)
    unmergeablePulls.splice(indexOfPullId, pullId)

Now you can use these methods in the if/else block within the checkMergeStatus method and add the id of the pull request when there’s a merge conflict. When the id has already been added, the notification is skipped. When there is no longer a merge conflict, the id is removed from the list:

  if (mergeStatus == false)
    if addPullIdToList(robot, data.pullId)
      robot.emit 'merge_conflict', {
        room: data.room,
        pullTitle: response.title,
        author: response.user.login,
        pullUrl: response.html_url,
        pullId: response.number
      }
  else if (mergeStatus == 'unknown')
    setTimeout ->
      checkMergeStatus(robot, data)
    , 1000
  else
    removePullIdFromList(data.pullId)

Hubot has a robot.brain method that lets you store key-values to Redis. This could also be used to keep track of the id’s, but for now this is how I made it work.

You can deploy your Hubot to Heroku. Hubot provides documentation on how to do that.

I hope these 3 posts have helped you get an idea of how Hubot works, the advantages of a chatbot in general and how it can enhance your productivity/workflow.

Happy hacking :)