Simple Git Push Deploys

I recently got interested in supporting Heroku style push deployments in the AWS stack at Fullscreen, Inc. There are a few solutions for this in the wild like Dokku, but I wanted to use something flexible enough to grow with our deployment infrastructure that wasn’t dependent on container technology. The solution I came up with involves a deploy server, a Git hook and a bit of Bash.

Configuring the Server

Before I could have engineers start pushing code, I needed to setup a deployment server. The most basic setup involved a new instance with a “git” user for authenticated pushes over ssh. Within the users home directory, I created bare repositories for each project so that remotes could be setup and pushed to:

$ mkdir -p /home/git/project.git
$ cd !$ && git init —bare
To make sure engineers could push code as the git user I also added some lines to the git user’s authorized_keys file:

command=”export GIT_SSH_USER=datguy && git-shell -c \”$SSH_ORIGINAL_COMMAND\”” ssh-rsa dat-guys-pubkey
This line sets an environment variable with the users name and makes a call to the original git push-receive command so that I can identify who is doing the pushing. Now pushing to the deploy server was as simple as:

$ git remote add deploy git@my-build-server:project.git
$ git push deploy
Lastly, I created a custom pre-receive hook to run a script when new code arrives on the build server. This hook needs to be added for each project that is configured on the server.

$ touch /home/git/project.git/hooks/pre-receive

Writing the Hook

In it’s most basic form, a Git hook is a simple script that is called when a particular event on the repository occurs. In the case of a pre-recieve hook, git-receive-pack calls the hook when new code is pushed to the remote. The script is invoked with three arguments on stdin: the old revision, the new incoming revision and the name of the ref being pushed to (i.e. the branch). If the script exits successfully, the code is accepted and merged into the repo. If the script exits with a non-zero exit code, then the code is rejected and the push fails. A simple example of a functional pre-receive hook might look like:

#!/bin/bash -l
# handle a simple code push
while read oldrev newrev refname; do
  # only accept pushes to master branch
  if [[ $refname != "refs/head/master" ]]; then
    echo "Error: You must push to master! Try 'git push <remote> ${refname/refs\/heads\/}:master'"
    exit 1
  fi
  echo “$GIT_SSH_USER pushed $newrev to $refname!”
  # do some stuff..
done

Cleanup the Output

If you run the sample hook above you’ll notice that the push returns stdout prefixed with “remote:“ which is ugly, obviously. There’s a bit of a hack I employed to remove that prefix to make it look as though the hook was being run on the local machine. By prefixing each line of stdout with a control code, I reset the cursor to the begginning of the line in the local terminal before writing to stdout, effectively removing the remote prefix that’s added by the local git command:

exec 3> >(sed -u -e “s/^/”$’\e[1G\e[K’”/”)
echo “ohai” >&3
exec 3>&-

The lines above create a new “transform” file descriptor 3 that passes all its stdout through a sed command that prefixes each line with the proper control character before writing to the hooks stdout. The second line writes some text to fd 3, and the final line closes the file descriptor. I used this method to ensure that all the output from my hook comes through cleanly without that pesky remote prefix.

Making It Useful

Obviously this example hook is completely useless, but it has all the potential to enable a useful deployment pipeline. For my particular use case I ended up creating a temporary build workspace, building the code with the Heroku Ruby Buildpack, shipping a tarball of the resulting build to S3 and finally triggering a deploy on our application servers. The possibilities for what you can do here are endless.

This post originally appeared on Medium.

#engineering