The Blog

Feb 23
0

Automatic Deploy Notifications

by Ola Mork

Categories

When we make a deploy to our staging or production environments there are about 5 people who need to know and about 25 who want to know. Often someone is forgotten or the email doesn’t go out at all and we get a phone call (which no one likes).

The solution? Capistrano recipes to the rescue!

before 'deploy', 'deploy:notify'

No matter what happens, if we’re deploying people are going to know about it.

Here’s that task:

namespace :deploy do
  desc "sends a notification email letting everyone know we're deploying"
  task :notify do
    deployment_name = application.to_s.gsub(/(-.*)?/, '').upcase
    stage_name = stage.to_s.upcase
    action_mailer_with_temporary_delivery_method do
      mail = TMail::Mail.new
      mail.subject = "Super Fantastic Project Pending Deploy [#{deployment_name} #{stage}]"
      mail.to      = "superproject-deploy@lists.yourcompany.com"
      mail.from    = ActionMailer::Base.smtp_settings[:user_name]
      mail.date    = Time.now
      mail.body    = ActionMailer::Utils.normalize_new_lines(%Q(
Hello everyone,

We're making a deploy to the #{stage} environment. The #{stage} site may (or may not) be
unavailable for a few minutes sometime within the next 30 minutes (depending on how long
it takes to upload the changes).

This message is automated.

---------------------- debugging information ----------------------
#{variables}
))
      ActionMailer::Base.deliver(mail)
      # puts mail.body
    end
  end
end

Now, what’s going on here? First, apologies to DHH but I’m not smart enough to figure out how to make the Rails email system work so we just use TMail directly. Which is fine, but we use ARMailer which puts all of our email in a queue and it gets delivered by a cron job every 5 minutes or so. This usually works pretty well but in this case we have two problems though. The first is this is run on the deployer’s machine, which may not have access to the database where it needs to queue up the email. Plus, when we deploy we disable the crons and we don’t turn them back on until we’re satisfied everything is working. If we used ARMailer for this particular email, the site may be down and no one would know; defeats the purpose of the notification.

action_mailer_with_temporary_delivery_method addresses this problem:

def action_mailer_with_temporary_delivery_method
  existing_delivery_method = ActionMailer::Base.delivery_method
  existing_smtp_settings = ActionMailer::Base.smtp_settings
  begin
    ActionMailer::Base.delivery_method = :smtp
    ActionMailer::Base.smtp_settings = {
      :address => "smtp.gmail.com",
      :port => "587",
      :domain => "your.domain.com",
      :authentication => :plain,
      :user_name => "do-not-reply@your.domain.com",
      :password => "PASSword"
    }
    yield
  ensure
    ActionMailer::Base.delivery_method = existing_delivery_method
    ActionMailer::Base.smtp_settings = existing_smtp_settings
  end
end

action_mailer_with_temporary_delivery_method temporarily sets up ActionMailer to use SMTP (with gmail) to send out our notification. When we’re done, it sets it back to whatever it was set to before.

We’ve hurdled the actual message delivery, but how can we make that notification message itself more useful to the internal group? We do two things. The first is to sort out exactly what we’re deploying and where we’re deploying it to:

deployment_name = application.to_s.gsub(/(-.*)?/, '').upcase
stage_name      = stage.to_s.upcase

application and stage are set in our regular deploy script with:

set :application, 'superproject-staging'

:stage is more complicated but essentially the same (in this example it would be ‘staging’)

The second pieces is variables. Capistrano 2.3 has an instance variable available to all tasks named @variables. We were just inspecting that for a while but it has a lot of procs for determining some of the values. Now we do this:

def variables
  result = []
  @variables.reject { |k,v| k.to_s.downcase.eql?('password') }.each_pair do |key, value|
    # since we're going to be printing this out, we don't want to see garbage like
    # "#<proc :0xb77fef88@/usr/lib/ruby/gems/1.8/gems/capistrano-2.3.0/lib/capistrano/cli/options.rb:128>"
    value = value.is_a?(Proc) ? value.call : value
    value = value.is_a?(Array) ? value.join(", ") : value
    result < < "#{key} : #{value}"
  end
  result.sort.join("\n")
end

Which gives you some better output:

---------------------- debugging information ----------------------
  application : superproject-staging
  available_stages : staging, production, qa
  cron_file : config/cron/staging_cron.conf
  cron_role : utility
  cron_role_only_clause :
  current_dir : current
  current_path : /home/www-data/superproject-staging/current
  current_release : /home/www-data/superproject-staging/releases/20090220155729
  current_revision : 32024
  default_environment :
  default_run_options : ptytrue
  deploy_to : /home/www-data/superproject-staging
  deploy_via : checkout
  keep_releases : 7
  latest_release : /home/www-data/superproject-staging/releases/20090220162352
  latest_revision : 32024
  logger : #<Capistrano::Logger:0x1214e30>
  memcache_extra_options :
  memcache_memory : 64
  memcache_pid_file : /home/www-data/superproject-staging/shared/log/memcache.pid
  memcache_port : 20109
  memcache_role : app
  memcache_user : root
  migrate_plugin_role : db
  migrate_plugin_role_only_clause : primarytrue
  mongrel_address : 0.0.0.0
  mongrel_environment : staging
  mongrel_instances : 8
  .
  .
  .
  .
  etc

Prettier examples are available at pastie. Drop that in config/recipes/notification.rb and in your normal config/deploy.rb include this:
load 'config/recipes/notification'

  • Reddit
  • Digg
  • del.icio.us
  • Facebook
  • Tumblr
  • Twitter

Leave a Reply

*