By

Automate All the Things With Hubot

Deployment, tooling, workflow… in a word: “process”. These things suck and take time away from what you really enjoy doing, like writing code.
But have no fear, with Hubot, you can do more of the stuff you love while Hubot deals with all the annoying automated things that computers are good at.

Automate All the Things

In case you haven't heard of it, hubot is a chat bot written by Github. It’s fairly simple to install, there are a ton of community developed scripts, and it has adapters for just about any chat service you can think of.

Now, while adding a robot to your chat room that you can command to fetch funny images from the Internet is cool, it also allows for some incredibly powerful tools and workflow optimizations.

Expanding Hubot's capabilities is a simple matter of adding a new coffee file to your repo and then telling the robot object what to do when it hears a particular regex pattern. Here's a simple example.

1 module.exports = (robot) ->
2   robot.respond /(sudo)(.*)/i, (msg) ->
3     msg.send "Alright. I'll #{msg.match?[2] || 'do whatever it is you wanted.'}"

As I said earlier, this can lead to some fun little scripts between you and your teammates. But consider what you can do when you get restful services and npm modules into the mix. For example, we use a pull request based workflow here at reviewed.com. So being able to convert a branch/pull request into a running version of our app for stakeholders to verify is a critical piece of our workflow.

With hubot's ability to interact with http endpoints and Heroku's excellent API all we need to do to get a new fork of our production app is to say “hubot fork app_name identifier” and BAM. Our helpful little robot comes back with “Luke Bergen: New app_name staging fork available at https://app_name-identifier.herokuapp.com/!!! Git URL is git@heroku.com:app_name-identifier.git”

I've edited out some of the details that are specific to our team. But the important parts of our fork script look like this.

First, some meta-data that hubot uses to inform users who ask things like “hubot help” or “hubot help fork”.

 1 # Description:
 2 #   Spin up a new fork with a unique identifier from one of our apps
 3 #
 4 # Dependencies:
 5 #   heroku-client
 6 #
 7 # Configuration:
 8 #   HEROKU_API_KEY environment variable
 9 #
10 # Commands:
11 #   hubot fork <app name> <identifier> - create a fork
12 #
13 # Author:
14 #   Tim Raymond and Luke Bergen

Next, we set up the Heroku client with our Heroku API key.

1 Heroku = require 'heroku-client'
2 heroku = new Heroku({token: process.env.HEROKU_API_KEY})

Now we're going to tell the robot what to listen for and set some variables based off of the key pieces of the message. We're also going to grab the current user based off of who it was that invoked hubot's mighty strength.

1 module.exports = (robot) ->
2   robot.respond /fork\s([\w-]+)\s([\w\d-]+)/i, (msg) ->
3     app = msg.match[1]
4     forkname = msg.match[2]
5 
6     currentUser = ""
7     for own key, user of robot.brain.data.users
8       if msg.envelope.user.id == user.id
9         currentUser = user

Now here's where the good stuff happens.

1 heroku.apps().create {name: "#{app}-#{forkname}"}, (err, new_app) ->
2       heroku.apps(app).configVars().info (err, vars) ->
3         fork.configVars().update(vars)
4         fork.features("user-env-compile").update()
5         if currentUser.heroku_email
6           fork.collaborators().create({user: currentUser.heroku_email})
7         else
8           msg.reply "I don't have your Heroku login, so I can't add you as a collaborator to your new fork"

This piece was a little heavy so let's deconstruct it a bit. First, we’re calling into the Heroku module's apps().create function. This function takes a hash with information about the app (we're just passing the name) and a callback function. The callback function takes an error object and an object representing the new app. For simplicity I'm not showing any of our error handling here. This callback function only gets executed once Heroku has either come back with an error or has successfully created the app.

Next, we want to configure our app. All of our apps generally use ENV variables to configure behavior, so it would be nice if our new forked app had the same settings. heroku.apps(app).configVars().info is a function that calls out to Heroku asking for the config vars for our base app and takes a callback to handle the results.

Inside this callback, we take the variables from the base app and set them on the fork app on line 3. We also turn on the user-env-compile feature.

Finally, remember that currentUser that we set up earlier? This is where we make use of it. Through another hubot script we keep a link between all chat room users and their Heroku email addresses. Using this, we add the user who asked us to create the fork as a collaborator to the app so that they can push changes and administrate it as necessary (I'll include this script at the end in case you're interested). And in case the user hasn't specified a Heroku account, we simply reply with a warning message.

Finally, we notify the user that everything is done.

1 msg.reply "New #{app} staging fork available at #{new_app.web_url}!!! Git URL is #{git_url}"

And that's it.

As a separate script, but as an explanation for where that currentUser variable came from, here is our Hubot script to help us keep track of the relationship between chat room users and Heroku logins:

1 robot.respond /my Heroku login is (.+)/i, (msg) ->
2     for own key, user of robot.brain.data.users
3       if msg.envelope.user.id == user.id
4         user.heroku_email = msg.match[1]
5         msg.reply "Okay, you are #{msg.match[1]} on Heroku"

And with hubot's brain being stored in redis your team will only need to “hubot my Heroku login is user@organization.com” once per user.

We have a number of other scripts and integrations that make our day to day tasks easier including Github, Pivotal Tracker, and Travis webhooks to automate our CI process. But for now, I hope you can draw some inspiration from our fork script to improve your workflows as well.