How to automatically deploy Jekyll sites with Git post-receive hooks

I like the way that GitHub handles personal Jekyll-based sites and wanted to clone that process on my own web host. Here's how I used Git post-receive hooks to make it happen.

By on

I really like the way that GitHub handles personal Jekyll-based sites: Make some changes, push to master, and the site will be built and deployed. You’ll get a simple email once it’s done.

I wanted to use my own host (Dreamhost) but have this same process. Turns out that it’s really simple, thanks to the magic of Git post-receive hooks.

Environment

For this to work you have to have the latest versions of Node and Ruby on the web server. I’m not sure how other web hosts work, but with Dreamhost it real pain in the ass. I’m not complaining though—I’m certainly grateful for shell access and moderate prices. I finally got it set up and ready to go.

Bare git repositories

Example after example showed folks editing their hooks directly in the root of their git repository. It took me a long time to figure out that I needed to create a git repository using --bare. But what is a bare git repository?:

Repositories created with “git init” command contain 2 things: the .git folder repository (or code history) and also real life working copies of your source code files. You can think of this type of repository as a working directory. Its a folder with code history stored in .git folder and all the source files as well. You can work in this directory changing source files and save your changes using the “git add” and “git commit” commands.

Bare repositories contain only the .git folder and no working copies of your source files. If you “cd” into a bare repository you find only the .git folder and nothing else. A bare repository strictly contains the version history of your code.

So you must have a bare repository for this to work. In my case I just cloned the existing GitHub repo using the --bare flag and removed the remotes. OK, now for the fun stuff.

Post-receive hook

Ah, but what’s a post-receive hook? Kris Jordan explained it simply in his post about Setting up Push-to-Deploy with git:

Hooks are scripts that git runs when certain events happen… The hook we care about for push-to-deploy is post-receive. It is run after receiving and accepting a push of commits.

I created a short, but hard to understand post-receive hook. It calls another script so that the main processing can be run as a background task. Why run it as a background task? My ISP will disconnect any long-running process such as SSH. Since my site takes a while to build I tend to get disconnected.

#!/bin/sh
# $HOME/opt/git/example.git/hooks/post-receive
#
# The "post-receive" script is run after receive-pack has accepted a pack
# and the repository has been updated.  It is passed arguments in through
# stdin in the form
#  <oldrev> <newrev> <refname>
# For example:
#  aa453216d1b3e49e7f6f98441fa56946ddcd6a20 68f7abf4e6f922807889f52bc043ecd31b79f814 refs/heads/master
#
# Start a post-receive hook as a background task
# http://stackoverflow.com/a/8132845

read oldrev newrev refname
if [ "${refname}" == "refs/heads/master" ]; then
    nohup $HOME/usr/local/bin/publish >& /dev/null &
fi
exit 0

Oh god what does it do? First of all, it checks to make sure the commit is getting pushed to master. If that’s good, then it calls a script and detatches it from the shell using nohup:

nohup configures a command to ignore SIGHUP signals sent by the shell. So once any command is launched with nohup, it does not matter whether or not the shell sends out SIGHUP. Any nohup-ed command will continue to run.

It supresses all the output from the script by shoving it into the void. Neat.

Publish script

The Jekyll documentation had a simple Git post-receive hook example which I used as the foundation for my publish script. The rest is borrowed heavily from One-click App Deployment with Server-side Git Hooks, which is a great how-to.

The publish script does the actual work. It clones the repository into a temp location, then builds it into the public folder. When everything is done it sends out an email.

I had issues with Ruby, since I was using RVM. The solution was to Properly source Rbenv.

#!/bin/sh
# $HOME/usr/local/bin/publish

TEMP_DIR=$HOME/tmp/example
LOGFILE=$HOME/example.log

source $HOME/.bash_profile

echo -e "Received push request at $( date +%c )" > $LOGFILE

git clone example.git $TEMP_DIR
cd "$TEMP_DIR"
bundle exec jekyll build -s $TEMP_DIR -d example.com
rm -rf $TEMP_DIR

echo "Finished request at $( date +%c )" >> $LOGFILE
cat $LOGFILE | mail -s "Publish completed" you@example.com
rm $LOGFILE
exit 0

And there you have it. Push-to-deploy in two (fairly) simple scripts.