Texas outline

August 2017

Continuous delivery with git hooks

1 August 2017

One of the tricks that helps me move super fast on Quail and related projects is a continuous delivery setup that I use to iterate quickly (on the browser-based portions, anyway). And what’s great about this workflow is that once you’ve got it set up then publishing a release fits exactly into your already-existing workflow, and setting it up requires tools you’re already using.

The trick, of course, is to rely on git.

This first bit is optional, but I like to maintain a master branch that always reflects what’s actually deployed in production. Pushing commits to this master branch logically means that those commits should be immediately deployed, but programmers are lazy and I realised long ago that if I could simply automate away the devops-y step of “ssh into the host, build a distribution of static files, and update nginx” then I’d never forget to deploy a finished release again.

So. Let’s automate this puppy.

First, you’ll need a git host running on your production server (I like Gitolite because it also relies on a git-push-to-deploy model, but your mileage may vary). In the project you want to auto-deploy with git, add this host as a new remote:

git remote add production git@<your production host>:<repo>.git

In your remote repository (e.g. <repo>.git/ wherever you’ve set up Gitolite on your host) you’ll find a directory called hooks. Git hooks are an incredibly powerful tool that I won’t go into the details of, but basically they’re scripts that are triggered by events happening in your repository. Create a file called post-receive in your <repo>.git/hooks directory with the following (obviously replacing <repo> with a more helpful name!):

#!/bin/bash
sudo -u integration /home/integration/<repo>.sh

Because we’re sudoing, we’ll need to add an exception to /etc/sudoers to allow our git user to execute this script (and only this script!) as the integration user. Add the following to the end of /etc/sudoers:

git ALL=(integration) NOPASSWD: /home/integration/<repo>.sh

I like to keep my git hooks super-simple, so the above simply runs another script as a lower-privilege user that we’ll use to actually do the work of deploying our pushed application. Go ahead and create that user (sudo useradd integration) , then become them (su integration; cd ~). Clone the repository you just created (git clone git@localhost:<repo>.git — you may need to set up key authentication for the integration user in Gitolite), then create the script we referenced in your post-receive hook at <repo>.sh:

#!/bin/bash

REPO=<repo>
INTEGRATION=/home/integration
PUBLISH_DIR=/srv/www/<deploy directory>/public_html

# Publish master branch to <deploy directory>
cd $INTEGRATION/$REPO

# Fetch the latest production push
git checkout master
git pull origin master

# Compile a distribution
yarn install
yarn compile

# Remove the existing distribution
rm -rf $PUBLISH_DIR/*

# Copy the compiled static files into a place where nginx can serve them
cp -R dist/* $PUBLISH_DIR

# Update permissions on the published files
chgrp -R nginx $PUBLISH_DIR/*
chmod -R 775 $PUBLISH_DIR/*

Obviously your commands to turn your uncompiled code into static assets will differ.

And that’s it! Work happily on your develop branch, and when it comes time to push code to production just commit on master and git push production master. You’ll see the forwarded output from your <repo>.sh above (in my case, the output of yarn install and yarn compile, and — assuming everything compiles properly — your production-ready assets will be pushed out all the way to where your web server can serve ‘em.