Overview

Push to deploy is a mechanism to automate building and deploying code within a version control system (VCS) to a staging or production server. In this case, we’ll be using Git for our VCS, Git’s post-receive hooks for automating the builds and Hugo to build the blog itself.

This assumes you want two remotes for Git, one for your VCS purposes and the other a production server for deployments. You can expand the logic here to include a staging server or a more elaborate deployment to multiple servers. Of course, there’s other tools more suited to proper deployment processes.

Alternatively this design can be adjusted to support pushing to a continuous integration (CI) server that can be tasked to build environments, run tests deploy code to multiple servers.

Although this processes contains references to Hugo, this process can be used to deploy any code, however, as this is only designed to deploy a simple blog on the master branch, your mileage may vary.

Steps

  1. Start with an existing repository with a remote called origin pointing to your favourite VCS (eg personal server, GitLab, GitHub / BitBucket etc).
  2. On your production server, create a repo that will be used for hosting your live content.
  3. Add post receive hook to the live remote which checks out master, runs Hugo and cuts over blog.
  4. Add the new live remote to existing repo.
  5. Push to live remote.

Starting Point

For this example, we’re starting with a straight forward git repo with only one remote.

$ git remote -v
origin  git@bitbucket.org:bradleyfalzon/bradleyf-blog.git (fetch)
origin  git@bitbucket.org:bradleyfalzon/bradleyf-blog.git (push)
$ git branch
* master

You can see I store my blog content on Atlassian’s BitBucket service. I’ve checked it out locally and I’m on the master branch. From now on, I’ll assume you’re not looking at tracking remote branches, but this process would only require minor modifications in that case.

Configure Production Remote

Here we’ll need to:

First we’ll too create a new bare repo on the server. An alternative technique is available in git-scm book.

$ cd /data/git
$ mkdir bradleyf-blog.git
$ cd bradleyf-blog.git
$ git init --bare
Initialized empty Git repository in /data/git/bradleyf-blog.git/

This has created an empty git repo with no working tree (thanks to --bare). A simple git clone would’ve create a working tree and checked out the master branch, which would stop a client from pushing to it whilst that branch is checked out.

Configure Post Receive Hooks

With our new (empty) git repo on the server, we need to configure our post-receive hook that will be executed after a push is successful. Whilst this hook is running, it will block the client from disconnecting, so don’t run anything too slow or background slow running scripts.

The post receive hook is stored in hooks/post-receive, and must be executable. Use hashbangs (#!) to specify which interpreter to execute for your script, in this case it’s BASH.

We’re using post-receive hooks, alternatively you could use a pre-receive hook which will give you the ability to exit with a non-zero status to indicate a failure and reject the push. This could be used to execute simple tests before accepting a push from a client.

Our post-receive hook looks like:

#!/bin/bash

# SYMDIR will be a symlink pointing to the current version
# of the content. In my case nginx has this directories public
# dir as the document_root.
SYMDIR=/data/www/bradleyf.id.au

# When there's failures, send emails to this address
EMAIL=user@example.com

# Store all logs here, being overriden each deploy
LOG=/tmp/blog-deploy.log

# Tell check that this other directory is the working tree, so
# checkout the content to this directory
GIT_WORK_TREE=$SYMDIR-`date +"%s"`

export GIT_WORK_TREE

# Simple checkErrors function, the first argument is a string to write to log
# if something happens.
function checkErrors() {
        if [ "$?" != "0" ]; then
                echo $1 >> $LOG
                cat $LOG | mail -s "Git deploy problems" $EMAIL
                exit 1
        fi
}

date > $LOG

# Create our working tree
rm -rf $GIT_WORK_TREE &> /dev/null
mkdir $GIT_WORK_TREE &>> $LOG
checkErrors "Could not mkdir $GIT_WORK_TREE"

# Checkout master to the working tree directory
git checkout -f master &>> $LOG
checkErrors "Could not checkout master to $GIT_WORK_TREE"

cd $GIT_WORK_TREE &>> $LOG
checkErrors "Could not change directory to $GIT_WORK_TREE"

# Run hugo to build the content and store in public/
hugo &>> $LOG
checkErrors "Could not use hugo to build blog"

# Atomically cut over the old working tree to the new, note
# using ln may not be the best method and mv should be considered.
ln -sfn $GIT_WORK_TREE $SYMDIR &>> $LOG
checkErrors "Could not create sym link from $GIT_WORK_TREE to $SYMDIR"

# Remove old versions (to revert use git-revert)
find $SYMDIR-* -maxdepth 0 -type d | grep -v $GIT_WORK_TREE | xargs rm -rf

Note, this is the first revision of this script your environment may require different options or flows depending on your use case.

Configure Clients

Once the server’s been configured, all clients will need to add this server as a remote. In my case, I’ve left BitBucket as my origin and added the production server, bradleyf.id.au, as a remote called live.

$ git remote add live root@bradleyf.id.au:/data/git/bradleyf-blog.git

Push to Production

Now a quick git push live would push my content to the production server and git’s post-receive hook will build the content and deploy for me (or email me if there’s a problem).

$ git push live

I wanted to manually push to live so I could control when I’m pushing as to whether it’s going into my VCS/origin (default) or live production server.

You must remember to push to both remotes when you’re ready, one for VCS, one for live. To make this two step process one, I manually created a third remote called all, so I could git push all which would push to the origin remote and live remote in one command.

$ tail -n 3 .git/config
[remote "all"]
    url = git@bitbucket.org:bradleyfalzon/bradleyf-blog.git
    url = root@bradleyf.id.au:/data/git/bradleyf-blog.git

In total, I have three options when I push:

$ git push
    - Push to origin remote only
$ git push live
    - Push to live production server only
$ git push all
    - Push to both BitBucket and production server

Additional Tips

Show the committed differences between current master and the remote live.

git diff master..live/master