Setting up Push-to-Deploy with git

I first set up a push-to-deploy system with git and puppet for a side project a few years back. It worked so well I transitioned NMC's development process onto it for all of our new projects starting last year. It offers the simplicity of the "push-to-deploy" model Heroku pioneered, with full control and flexibility over the operating system environment.

I've started thinking about my next iteration of this system for Didsum, and using pushing for more than just deployment purposes. The Push-to-______ pattern is powerful and easy to use, once you know how the pieces fit together. In this post, I'll walk through the setting up Push-to-Deploy from the ground up. 

(I'm assuming a working knowledge of: terminal, git, and a scripting language.)

Preparing our Repositories

Let's keep things straightforward by placing our development git repository, remote git repository, and deploy directory under the same local directory.

mkdir push-to-deploy
cd push-to-deploy
mkdir {development,remote,deploy}

Awesome, now let's setup the remote git repository. With a real project, this would be a directory on your production server. There is a special setup for git repositories whose purpose is to receive pushes from developers, they're called "bare" repositories. You can read more about "bare" repositories, but for our purposes their purpose is just to receive pushes.

cd remote
git init --bare
cd ..

Perfect, now let's setup our development directory with git. (You could also copy one of your git project's contents into the development folder.)

cd development
git init
echo "Hello, world." >> file.txt
git add file.txt
git commit -m 'First commit.'

We now have a development repository with its first commit. Our last preparation step is to register the "remote" repository. If you're fuzzy on git remotes, the official git site has you covered.

git remote add production ../remote
git push production master

Boom. You've just pushed your commit from development to your bare, 'remote' repository. We're ready to setup push-to-deploy.

Set up Push-to-Deploy

Now that our remote repository is setup, we're ready to write a script for what it'll do when it receives a push. Let's navigate to the hooks folder of our remote repository. Hooks are scripts that git runs when certain events happen.

cd ../remote/hooks
touch post-receive
chmod +x post-receive

The hook we care about for push-to-deploy is post-receive. It is run after receiving and accepting a push of commits. In the commands above, we're creating the post-receive script file with `touch`, and making sure it's an executable file.

Next, open the post-receive script file in your preferred text editor. Copy the contents below:

#!/usr/bin/env ruby
# post-receive

# 1. Read STDIN (Format: "from_commit to_commit branch_name")
from, to, branch = ARGF.read.split " "

# 2. Only deploy if master branch was pushed
if (branch =~ /master$/) == nil
    puts "Received branch #{branch}, not deploying."
    exit
end

# 3. Copy files to deploy directory
deploy_to_dir = File.expand_path('../deploy')
`GIT_WORK_TREE="#{deploy_to_dir}" git checkout -f master`
puts "DEPLOY: master(#{to}) copied to '#{deploy_to_dir}'"

# 4.TODO: Deployment Tasks
# i.e.: Run Puppet Apply, Restart Daemons, etc

Let's walk through each of the steps:

1. When git runs post-receive, the data about what was received is provided to the script via STDIN. It contains three arguments separated by spaces: the previous HEAD commit ID, the new HEAD commit ID, and the name of the branch being pushed to. We're reading these values and assigning them to from, to, and branch variables, respectively.

2. Our purpose here is to automate push-to-deploy. Assuming a workflow that keeps production on the master branch, we want to exit this script prior to deploying if the branch being pushed is not master.

3. The first deploy step is to "checkout", basically export or copy, files from the master branch to the directory where our project is deployed to in production. (Remember, in this demo it's the fake "deploy" directory, in the real world this might be /var/www, or wherever your project expects to be in production.)

4. Now that our deploy directory is up-to-date, we can run whatever deployment tasks we need to run. This could be applying Puppet scripts (I'll write a post on this scenario soon), restarting a web or application server, clearing cache files, recompiling static assets, etc. Whatever steps you'd normally need to do manually after updating your project's files, automate them here!

Save your post-receive hook, and let's test it out!

Testing with Pushing

We can test our script manually, by creating a new commit in our development directory and pushing:

cd ../../development
echo "New line." >> file.txt
git add file.txt
git commit -m 'Testing push-to-deploy'
git push production master

In the output of the git push command, you should see lines starting with "remote:". These lines are the output of our post-receive script:

Already on 'master'
DEPLOY: master($TO_ID) copied to 'push-to-deploy/deploy'

The first line is noisy output from the git checkout command in step 3, we can ignore it. The second line, is from the puts command, also from step 3 in our post-receive script.

The directory we're deploying to should now be populated and up-to-date:

ls ../deploy
diff file.txt ../deploy/file.txt

Pretty awesome, right?

Testing without Pushing

When you're working on a post-receive hook, it's annoying to muck up your project's commit history and push each time you make a change. Luckily, because it's just a script, we can fake it from the command-line.

cd ../remote
git log -2 --format=oneline --reverse

First, we need to get the IDs of our most recent 2 commits. The git log command, above, will give us these two IDs in the order you'll want to replace the $FROM_ID and $TO_ID variables with, respectively.

echo "$FROM_ID $TO_ID master" | ./hooks/post-receive

This method makes setting up your post-receive hooks enjoyable, enabling you to quickly iterate on your script and execute it repeatedly.

Next Steps

In this post, we've walked through how to setup the push-to-deploy with git. For a real world project, your 'remote' and 'deploy' folders would usually be setup on a server, not locally. The details of doing that and properly configuring SSH is beyond the post of this scope (note to self: I should write on SSH configuration, too!).

From here, it's up to your project to determine what actions to automate! Happy pushing!

(If the push-to-X pattern interests you, you should follow me on Twitter, or subscribe to my RSS feed, because I've got a few more posts on the subject coming up! Also, feel free to tweet feedback / questions / topics you'd like to read.)