I've recently been working on updating a set of Ruby applications and converting all of the gem dependencies to use Bundler (gembundler.com)
I've added to my optimized Capistrano Bundler tasks to include handling the prequisite rubygems version and providing a number of customization parameters.
Bundler 1.0 now includes a basic "bundle:install" Capistrano script http://github.com/carlhuda/bundler/blob/master/lib/bundler/capistrano.rb, but since mine covers a bit more ground than the default one I'm continuing to build my scripts around the following Bundler tasks.
Capistrano::Configuration.instance(:must_exist).load do
desc "Add deploy hooks to invoke bundler:install"
task :acts_as_bundled do
after "deploy:rollback:revision", "bundler:install"
after "deploy:update_code", "bundler:install"
after "deploy:setup", "bundler:setup"
end
namespace :bundler do
set :bundler_ver, '1.0.0.rc.5'
set :bundler_opts, %w(--deployment --no-color --quiet)
set(:bundler_exec) { ruby_enterprise_path + "/bin/bundle" }
set(:bundler_dir) { "#{shared_path}/bundle" }
set :bundler_rubygems_ver, '1.3.7'
set(:bundler_user) { apache_run_user }
set :bundler_file, "Gemfile"
desc "Update Rubygems to be compatible with bundler"
task :update_rubygems, :except => { :no_release => true } do
gem_ver = capture("gem --version").chomp
if gem_ver < bundler_rubygems_ver
logger.important "RubyGems needs to be udpated, has gem --version #{gem_ver}"
gem2.update_system
end
end
desc "Setup system to use bundler"
task :setup, :except => { :no_release => true } do
bundler.update_rubygems
gem2.install_only "bundler", bundler_ver
end
desc "bundle the release"
task :install, :except => { :no_release => true } do
bundler.setup
#Don't bother if there's no gemfile.
#optionally do it as a specific user to avoid permissions problems
#do as much as possible in a single 'run' for speed.
args = bundler_opts
args << "--path #{bundler_dir}" unless bundler_dir.to_s.empty? || bundler_opts.include?("--system")
args << "--gemfile=#{bundler_file}" unless bundler_file == "Gemfile"
cmd = "cd #{latest_release}; if [ -f #{bundler_file} ]; then #{bundler_exec} install #{args.join(' ')}; fi"
cmd = "sudo -u #{bundler_user} sh -c '#{cmd}'" if bundler_user and not bundler_user.empty?
run cmd
on_rollback do
if previous_release
cmd = "cd #{previous_release}; if [ -f #{bundler_file} ]; then #{bundler_exec} install #{args.join(' ')}; fi"
cmd = "sudo -u #{bundler_user} sh -c '#{cmd}'" if bundler_user and not bundler_user.empty?
run cmd
else
logger.important "no previous release to rollback to, rollback of bundler:install skipped"
end
end
end
end
end
This code uses some other dependencies that I use throughout my Capistrano scripts, namely the customized Gem2 plugin from vmbuilder_plugins. This following code necessary to monkey-patch the Gem2 plugin, which is bundled with Mike Bailey's deprec project http://github.com/mbailey/deprec/blob/master/lib/vmbuilder_plugins/gem.rb
module Gem
GEM_UNINSTALL= "gem uninstall --ignore-dependencies --executables"
def install_only(package, version=nil)
tries = 3
begin
cmd = "if ! gem list | grep --silent -e '#{package}.*#{version}'; then gem uninstall --ignore-dependencies --executables --all #{package}; #{GEM_INSTALL} #{if version then '-v '+version.to_s end} #{package}; fi"
send(run_method,cmd)
rescue Capistrano::Error
tries -= 1
retry if tries > 0
end
end
# uninstalls the gems detailed in +package+, selecting version +version+ if
# specified, otherwise all.
#
#
def uninstall(package, version=nil)
cmd = "#{GEM_UNINSTALL} #{if version then '-v '+version.to_s else '--all' end} #{package}"
wrapped_cmd = "if gem list | grep --silent -e '#{package}.*#{version}'; then #{cmd}; fi"
send(run_method,wrapped_cmd)
end
end
Unlike many examples of Capistrano deploy scripts which are kept inside the app root of the application they deploy, my deploy scripts are kept in their own repository, and they were built to contain all of the "institutional knowledge" of how to interact with all of our server farms and applications. There is a lot of shared code that would have to be duplicated if I were to try to break the deploy scripts apart, and store them with each application. There would also be a lot of tasks that don't make sense to live in only one application, some wouldn't have a home in any application. I've found a couple of patterns that make it easier to hook and unhook code into the main deploy processes, which I may touch on in other posts.
One of the techniques which is used in the bundler tasks is to separate out the callback hooks into their own top level task. This allows me to not have to remember all of bundlers individual hooks, I can easily add them to any application that needs them, and omit them from applications that don't. A second technique that I use is to control the task chains. I learned early on that you want to be able to call individual tasks for maintenance and not inadvertently trigger a long series of chained tasks.
This is an example of what you might see in one of my application deploy scripts:
on :start, :only => ["deploy","deploy:setup","deploy:migrations","deploy:cold"] do
acts_as_bundled
before "deploy:update_code", "deploy:clear_release_path","deploy:setup_dirs"
after "deploy:update_code", "deploy:authentication", "git:track", "scalr:on_boot_finish", "git:config", "scalr:on_hostup"
after "deploy:symlink", "deploy:cleanup"
end
Notice the acts_as_bundled acts as a nice declarative way to invoke bundlers callback assignments, and that they are only invoked if one of the top level tasks matches the :only clause.
So for example I can do a command like
cap deploy:symlink
Without causing "deploy:cleanup" to be executed. But when I do:
cap deploy
It will.





















Not shown but there's also the problem of masking the wheel sides, after failed attempts to use a larger 3m masking tape, it just did not want to flex in a circle without coming off the wheel, I crafted a piece of corrugated cardboard into a cylinder that fit over the masking tape in the groove between the wheel face and wheel rim. The only downside to this, was the corrugated cardboard with an uneven cut left room for a little bit of the charcoal to over-spray into the void which I later had to touch up with the silver paint. The darker color is Duplicolor WP102 Graphite.
While painting the graphite color, just as I made a the final pass, one of the stupid mask labels popped up, and I covered the tip of one of the spokes in graphite. After some expletives, I decided the wheels had enough coats of the graphite color and called it quits. Once the wheels were dry, I sprayed some of the silver onto a piece of cardboard until it pooled, and I used a craft paintbrush to cover up the graphite over-spray on the spoke that had popped up. When I had ordered the mask kit from Ames I also asked about the red Pontiac insert in the center wheel caps. They didn't have a record of a part number for it. I asked about the entire center cap, and apparently its been discontinued. To make mine red again I used a red sharpie ($0.95), that I spotted as I was checking out after buying some tools at OSH.

When I remounted my wheels and was going on my first test drive, something was rubbing, majorly. It turns out that the new way to balance a tire is to install these flat lead weights that are mounted on double sticky tape on the inside of the wheel. WTF?! And they were hitting my brake calipers on the front. I dismounted the wheels and had to cut them out. I've got to drive it to the shop that did the balancing and make them do it again on all wheels to make sure I can rotate the tires. My last lesson learned so far: The valve stems are too short once the chrome covers go on, I guess I should have known this and made sure the shop put on longer ones. Shouldn't the shop have known this? They are common wheels, they knew exactly what kind of car I had even if they couldn't see it. But the wheels and that BFG rubber, look good; so in the end, it'll have been worth it.