Wednesday, April 29, 2015

Automating Code Deployment with GitHub Webhooks and Node

Do you push a "pull" button?
I've been pushing updates to my webcomic's code repo on GitHub for over a year now. My workflow has gone something like this:
  1. Make code changes.
  2. Test locally.
  3. Push changes to GitHub.
  4. Log on to production server.
  5. Pull changes from GitHub.
I'm trying to automate steps 4 and 5. I'd like to have my production server automatically update itself whenever I push code to the "main" branch of the repository.

This week I've been experimenting with using GitHub's Webhooks to automate my code deployment. Webhooks are a cool idea - set one up on your repo and every time an event occurs (push, pull request, fork, etc.) GitHub will send you some data to a URL of your specification which describes the event. All you need is something to process the data!

I set myself up a simple Node application to process the data using the githubhook package. It was very easy to use.

var githubhook = require("githubhook");
var exec = require("child_process").exec;

var github = githubhook({
    port: 8082,
    secret: "your-secret-code"
});

// listen to push on github on branch master
github.on("push", function(repo, ref, data) {

    var branchName = ref.substring(ref.lastIndexOf("/")+1);
    if (branchName === "master") {

        var repoName = data.repository.name;
        if (repoName === "comic") {

            child = exec("/home/comic/update_comic.sh", function(error, stdout, stderr) {

                if (error !== null) {

                    var e = new Error("exec error: " + error);
                    console.log(e);

                } else {

                    console.log("pulled update for " + repoName);

                }

            });

        }

    }

});

// listen to github push
github.listen();

My application will listen on port 8082 for GitHub events, which will be HTTP POSTed in JSON format (when you select that option later). When setting up, the value for secret (line 6) is important. You'll want that to match the value you'll set up in GitHub. Once you create the object, specify callback functions for the events that interest you and start listening.

I am just looking for the "push" event. When that event happens, I check to make sure the push was to the master branch and that the repo was my comic (I might use this for other repos later). If both of those are true, I exec a script that will pull the updates and restart the app if necessary.

Once you have your app running, you can set up your GitHub repo with the Webhook. Just go to your repository page, and click on the Settings link. Then click on "Webhooks & Services" and then on the "Add webhook" button.

Now specify the URL where you are listening with the app. My URL happens to be http://caseyleonard.com:8082/github/callback. The port is obviously the same one I specified above, but the path of /github/callback is the default used by githubhook (you can change it if you want - see the docs). Set the Content Type to be "application/json" and type in the same value for Secret that you used in the app. If these don't match, GitHub will receive a 403 response code when it sends you data and your listener will not be called. You can also select which events of which you want to be notified. It defaults to just "push" which is fine for my use.

It's simple and it works for me, at least so far. The less time I spend updating the code for the comic, the more time I can spend making the comics themselves. Today is the last of the Canterbury Tales parody, a set which took me a very long time to create!