11 June 2008

Changelogs and Deployment Notification for Capistrano and Git

Early warning: this is a hack, which doesn't mean it's bad, just that it's not polished. However, I am documenting my solution for myself thus far, as well as figured others might find it useful...

Update: Added my shell command for doing deploys (see end of this post).

I wanted a way to automate a few things around deployments, and integrate this a bit with my continuous integration server. I use CruiseControl for the CI server, and previously blogged about setting up CC.rb with Git. The goals for this next task, and subject of this blog post are:


  • Tag the code on successful deploys. My CI server already tags the code anytime it does a successful build, but since I didn't cover that previously, I'll mention it here as well.

  • Notify a list of people via email whenever a new deploy happens.

  • Generate a changelog, based on Git commit messages (better make sure they're suitable reading for whoever gets your deploy notices!), and include this changelog in the deploy emails.

  • Have the CI tag I want to deploy as the only required piece of info/parameter when issuing a deploy command.



Tagging


First, I tag the code on any successful CI run. This tag is what I can then use as the Git tag to deploy. Capistrano supports this via the branch variable (set its value to the tag name). As you can guess, you can use pretty much any Git ID/tag/branch name for this. To do this, add a task to your cruise.rake file (or similar - wherever you define your custom CruiseControl command), and then ensure you run that task during a CruiseControl session. Here's my task:

desc "Tag the code on successful CI build"
task :ci_tag do
timestamp = Time.now.strftime("%Y%m%d%H%M%S")
tag_name = "CI_#{timestamp}"
# Create an empty file with our tag name, so we can easily go grab the tagname
# from the CI output page and do deploys, etc.
system("touch #{File.join(ENV['CC_BUILD_ARTIFACTS'], tag_name)}")
system("git tag -a -m 'Successful continuous integration build on #{timestamp}' #{tag_name}")
system("git push --tags")
end


From the above, you can see that I'll get tags of the form: CI_timestamp. Next up, I want to tag a successful deploy to indicate which commit/tag actually got deployed and when. This is handled via an after task in my Capistrano deploy.rb:

after "deploy:restart", "tag_last_deploy"
task :tag_last_deploy do
set :timestamp, Time.now
set :tag_name, "deployed_to_#{rails_env}_#{timestamp.to_i}"
`git tag -a -m "Tagging deploy to #{rails_env} at #{timestamp}" #{tag_name} #{branch}`
`git push --tags`
puts "Tagged release with #{tag_name}."
end


This will create tags like, deployed_to_staging_1213223458, and works for both staging and production (or any environment you're targeting - note the use of the rails_env variable - you may need to use something else). One thing to pay particular attention to, is that this tag is actually tagging another tag, as defined by the branch variable (mentioned above). In order for this to work though, you need to ensure that your tags are up to date locally. Thus, somewhere in your workflow you'll need to do a git pull --tags, if like me, your CI server is elsewhere and is generating those tags.

Ok, we're all tagged up, let's move on...

Notification



It turns out there's a nifty new plugin called Cap Gun that will take care of emailing a list of folks on deploy. Setup is covered in their README, but the one bit they don't mention, is that you can include a comment in the email message that goes out. I wanted to include a changelog in these emails, so I tapped into this comment attribute, setting it to the text of my changelog. To use the comment, you can either set it via -s comment="my lovely comment" on your Capistrano deploy command, or you can set the comment variable in your Capistrano deploy.rb or included script. More on that in a minute.

Changelogs


My changelog, so far, is very simple, it just pulls the comments for the Git commits that occurred since the last deploy (for the appropriate target), up to the tag specified (which in this case will be the CI tag you are about to deploy). To handle this, I use a small Ruby script, combined with the great Grit gem that lets one manipulate Git via a nice Ruby API. The script simply spits out a simple chunk of text that will be what gets put into the comment Capistrano variable for our deployment notifications. This is in particular where the "hack" comes into play. This script is not robust, does essentially no error checking, etc, etc. Use at your own risk! And with that, here it is:

#!/usr/bin/env ruby

require 'rubygems'
require 'mojombo-grit'
include Grit

unless ARGV.length == 2
puts "Usage: changelog.rb staging|production <commit-or-tag>"
puts " where commit-or-tag is the commit ID or tag you are planning to deploy"
exit -1
end

repo_location = File.expand_path(File.dirname(__FILE__) + '/..')
target = ARGV[0]
about_to_deploy_commit = ARGV[1]
repo = Repo.new(repo_location)

# Find the tag for the last deployed
tags = repo.tags.collect {|tag| tag.name }
tags.delete_if {|tag| !(tag =~ /^deployed_to_#{target}_/)}
tags.sort!
last_deployed_tag = tags[-1]

commits_for_changelog = repo.commits_between(last_deployed_tag, about_to_deploy_commit)
commits_for_changelog.reverse!

puts "Changes since last release:"
commits_for_changelog.each do |commit|
puts " "
puts " #{commit.message}"
end


To run through it briefly, it takes two parameters (and clearly, you can change this for your own deployment targets, etc.): a deployment target, and a tag (which can actually be a tag, a commit ID, branch, etc.). It sets up a repo variable for your Git repository using Grit, and then proceeds to find the last deployed tag for that deployment target. After that, it gets all the commits between that last deployed tag and the tag you specified as the second script argument, and prints out the commit messages.

To integrate this, I added this line to my Capistrano deploy.rb:

set :comment, `script/changelog.rb staging #{branch}`

As you can see, that one is specific to my staging environment, and lives inside my "staging" task in deploy.rb. Same, appropriately edited version goes for production.

Deployment Command


Lastly, I define a simple shell function to do my deploys, which ensures I have done a git pull so I have all the tags, and makes the command easier to remember and get right, etc:

stagemyproject () {
git pull
cap -s branch=$1 staging deploy:migrations
}


You would thus have a command line to do a deploy like this:

stagemyproject CI_20080612052417

That's it, and if you've managed to read this far, congrats, and if you've not only managed to read this far, but payed attention and got value out of it, well, cool.

For anyone who uses/adapts this, please do let me know improvements you make, or suggestions, or tweaks/changes, and so on. I've been using this for all of about a half dozen deploys so far. If (more like when) I make improvements, I'll update.

3 comments:

Rob said...

Hi

I have a plugin that cap notifications, though it could probalby stand to pull in some of the stuff you've done here.

Its right here:

http://github.com/relevance/cap_gun/tree/master

- Rob

Chris said...

rob, check my blog entry again, I specifically talk about using Cap Gun for the notifications :) It is what I use as well.

Rob said...

Apologies for my lack of reading comprehension today. Carry on. =)