Capistrano 2.0 and Tagged Releases
A few weeks ago Anthony setup http://vintagejesus.net. I was going to setup yet another deployment bash script on the server, but not all of our applications are deployed with a bash script. I stepped back and decided it was time to bring some order to the chaos of our current deployment strategy. Currently I use a lot of ‘organic’ bash scripts that deploy off of trunk, something I’ve never been a fan of. Mark enlightened me to use tagging to deploy apps. We use Capistrano to deploy some rails apps, so I figured I would set it up to deploy using tags. I was surprised I couldn’t find a plugin that I liked so I set off on building my own recipe.
The basic idea was that I wanted to run something like this:
cap deploy
The script should prompt for a tag, and if none is given prompt for a
tag name to apply to trunk and then deploy from the new tag.
As a bonus it makes rolling back a revision pretty easy. You can just select the previous tag, or use the built in:
cap deploy:rollback
To do this in Capistrano I created a set_tag recipe that lists the
recent tags and prompts to use one of them or create a new tag from
trunk. This updates the base_repository variable. I also had to
overwrite the default update_code action to call set_tag. (You may want to copy and paste it out since the formating gets a little hairy)
require 'rexml/document' #gem install rexml
set :application, "yourappname"
set :base_repository, "http://your.repository/#{application}"
set :repository, "#{base_repository}/trunk" # default, but is overwritten by set_tag
role :app, "some.prod.server", :primary => true
set :deploy_to, "/some/server/path"
set :use_sudo, false
namespace :deploy do
task :set_tag do
# get the release path
depend :local,:command,"svn"
show_recent_tags("#{base_repository}/tags")
tag = ::Capistrano::CLI.ui.ask("which tag (blank for trunk): ")
if tag == ""
new_tag = ::Capistrano::CLI.ui.ask("deploy from trunk, but you have to give it a tag: ")
raise Capistrano::Error, "have to tag a new release" if new_tag == ""
#raise error if there are invalid characters
raise Capistrano::Error, "invalid characters in tag" if new_tag =~ /[|]|\?|:|\/|\*|\"|\'||\||(\s)/
cmd = "svn copy quiet message \"cap tagged release\" #{base_repository}/trunk #{base_repository}/tags/#{new_tag}"
puts " * locally executing \"#{cmd}\""
system cmd
tag = new_tag
end
set :repository, "#{base_repository}/tags/#{tag}"
puts "repository: #{repository}"
end
# Overwrite update_code task to add call to set_tag
task :update_code, :except => { :no_release => true } do
set_tag
on_rollback { run "rm -rf #{release_path}; true" }
strategy.deploy!
finalize_update
end
def show_recent_tags(repo)
hsh = Hash.new
cmd = "svn ls --xml #{repo}"
puts " >> Listing tags: "
pipe = IO.popen cmd
result = pipe.read
pipe.close
entries = REXML::Document.new(result).root.elements['list']
entries.elements.each do |e|
# set hash key to release id and value to: name by: commit.author
hsh[e.elements['commit'].attribute('revision').to_s] = e.elements['name'].text + " \tby: " + e.elements['commit'].elements['author'].text
end
sorted_arr = hsh.sort {|a,b| a[0].to_i <=> b[0].to_i}
if sorted_arr.size > 5
sorted_arr.to_a[-5, 5].each {|t| puts "#{t[1]} \trev: #{t[0]} "}
else
sorted_arr.to_a.each {|t| puts "#{t[1]} \trev: #{t[0]} "}
end
end
end
Lets take a look at it in action:
cap deploy
>> Listing tags:
cap_test_0.0 by: sfaxon rev:163
cap_test_0.1 by: sfaxon rev: 167
which tag (blank for trunk): [type a tag listed above or hit return]
by just pressing return it asks:
deploying from trunk, but you have to give it a tag: [cap_test_0.2]
Next time I’ll show how we took this and applied it to other applications.
Update: direct link to the code above.





Codex Content
Great first post Seth, nice work on getting around to it. Looks like we need to add a overflow:auto to the code class
Thanks for great tip, Seth. I work for Douglas County PUD over in East Wenatchee, WA and I just implemented this for our rails deployments. I was wondering where you put your code at for extending your deploy.rb. Right now I just made a separate rake task called deploy.set_tag.rake and include it by adding the line
load “lib/tasks/deploy.set_tag.rake”
to the end of my deploy.rb file.
Thanks,
Kevin
It’d be nice to have a downloadable file for this. I always hate copy/pasting code.
Thanks for the recipe, it will save me hours.
Kevin,
I usually add the code above to the bottom of the config/deploy.rb file. Capistrano uses it’s own domain specific language, so it’s not rake. I think it was meant to look like rake files, and for the most part it does. But since there are some different issues it addresses, Jamis had to do a few things differently. I like keeping it together in the deploy.rb file because it’s overriding default behavior. But, we’re adding some things for monit and mongrel deployment that I’ve thought of including the same way you are, but probably from lib/cap/sometask.rb.
I think either way is readable, so whatever makes sense to you.
Thanks for the feedback. I’m glad it was helpful.
Jon,
Thanks for the feedback, I added an update to the bottom of the post.
Hey, great code…thanks for the article! One small thing. I think this line in your show_recent_tags method: ‘cmd = “svn ls xml #{repo}”‘
should be: ‘cmd = “svn ls –xml #{repo}”‘
(note the dash+dash+xml before the xml argument). This fixed an error I was having with the xml parsing of the svn list. Could be my version of svn, I’m not sure. I am using SVN 1.4.4 (r25188)
Brain,
You’re absolutely right, the dashes must have been lost between copy/paste. Great catch.
I am confused by the line:
sorted_arr = hsh.sort {|a,b| a[0].to_ib[0].to_i}
when trying to run it as-is:
lib/tasks/deploy.set_tag.rb:68:in `show_recent_tags’: undefined method `to_ib’ for “5639″:String (NoMethodError)
I’m assuming it’s missing some kind of separator between to_i and b[…