Deploying the git way

Let’s start with this: Git is mind-blowingly amazing and simple. We’ll get back to this later.

Moving on though, a while back I was combing through my feed reader and found a post that leapt up at me: Deployment Script Spring Cleaning by Chris Wanstrath over at Github. It amazed me that no one had thought of doing this before. That is, using git as the deployment strategy for Capistrano.

A few weeks later I was preparing to get Connect-A-Sketch production ready (it’s in a private beta and you should sign-up if you haven’t yet), and the git deployment strategy jumped into my head. To reiterate what’s so great about using git, instead of rsync with git or any of the other options: it’s fast, lightweight, and can handle all the rollbacks itself with no need for timestamped directories.

The Reflog Rollback

So, I forked defunkt’s gist, and got to work polishing it up for Connect-A-Sketch. The first thing I noticed (as did a few others who commented on the original blog post) his rollback command only rolled back to the previous commit (via the ref name “HEAD^”). What we really want here is to rollback to the commit that we had previously deployed to. As it turns out there’s a ref name for that “HEAD@{1}”. So, we just replace “HEAD^” with that, and we’re all good right? Nope, because we forgot about the edge-case where we want to keep rolling back one after the other, and this can’t handle that. That’s how Capistrano normally works and so our new deployment strategy should work the same.

Well, if you didn’t know, as I didn’t, the “HEAD@{1}” ref name works because git tracks all the commits that HEAD has pointed to in the reflog (this is a really helpful feature that most other version control systems just don’t do). The deploy script uses git reset to force git to set the HEAD to a specific commit not moving through each commit or doing a rebase, or any other modifications to the HEAD reflog. But when we reset to “HEAD@{1}” that is added to the reflog, it doesn’t just pop off the last ref and move HEAD back (and don’t get me wrong this is what git should do, it’s just not what we want to do in our particular case). So we need to pop off what we don’t need from the reflog so “HEAD@{1}” will point to the correct commit now.

Lucky for us git version and later have the ability to remove items from the HEAD reflog and correctly rewrite the reflog so everything still works correctly internally. The command to do this is git reflog delete --rewrite <ref> (see what I mean about mind-blowing!), by using this in our deploy script we can cleanup the reflog so we can continue to rollback one after another as much as we want.

Cap Compatibility

Capistrano and various cap extensions and plugins expect the releases directory and its subdirectories to exist, as well as identifiers for the current and previous versions to be present. The default values for these variables won’t be set correctly given we have no “releases” directory, so we need to make some changes to keep everything compatible.

First, we want to make sure our migrations are run in the correct directory. By default it will run it in the “current_release” directory, which is the last directory in the “releases” directory. We want it to use the “current” directory, to do that we add this line:

set :migrate_target, :current 

Next, we want to make sure our deploy script sets the correct values for the various directory variables. To do this we add the following lines to our deploy script:

set(:latest_release) { fetch(:current_path) } set(:release_path) { fetch(:current_path) } set(:current_release) { fetch(:current_path) } 

Finally, we need to make sure that the correct version identifier is supplied to the various revision variables:

set(:current_revision) { capture("cd #{current_path}; git rev-parse --short HEAD").strip } set(:latest_revision) { capture("cd #{current_path}; git rev-parse --short HEAD").strip } set(:previous_revision) { capture("cd #{current_path}; git rev-parse --short HEAD@{1}").strip } 

With all that in place (along with a few other minor changes and additions), we have a git based deployment strategy that’s totally compatible and works just the way we expect Capistrano to work.

The complete deployment script can be found here:

Brian is a developer in our Boulder, CO, office. He loves making code readable and maintainable for clients such as Time Life and Shure.

More posts by Brian