Skip to content

Instantly share code, notes, and snippets.

@baob
Created September 9, 2011 13:05
Show Gist options
  • Save baob/1206151 to your computer and use it in GitHub Desktop.
Save baob/1206151 to your computer and use it in GitHub Desktop.
script (a very hacky one) to determine which git commits are safe to deploy next, given the state of the related lighthouse tickets.
#!/usr/bin/env ruby
# this simple (hah!) script is intended to find the commit to be deployed to production next. (Next stage deploy is easy: 'master')
LH_TOKEN = "<your lighthouse token>"
LH_ACCOUNT = "<your account name>"
LH_PROJECT_ID = nil # set to project id if account has more than one project (we just get the first by default)
REMOTE_REVISION_FILE = "/var/www/aspire/current/REVISION"
LOCAL_REVISION_FILE = "/tmp/deploy_next_REVISION"
LH_TICKET_REGEX = /(\[#|[Ll][Hh])(\d+)/ # REGEX to pick the lighthouse ticket out of the commit text
puts "Executing script/next_deploy #{ARGV.join(' ')}"
begin
require File.dirname(__FILE__) + "/../config/init" # override rubygems and use aspire's gem bundling
require 'fileutils'
require 'lighthouse'
require 'net/scp'
Lighthouse.token = LH_TOKEN
Lighthouse.account = LH_ACCOUNT
bad_commit_overrides = []
# parse the ARGS
ARGV.each do |arg|
case arg
when '-h','--help'
puts "\nHelp:"
puts 'find the next deployable commit : ruby script/next_deploy'
puts 'find the next deployable commit, '
puts ' designating commits sha-x as okay to deploy: ruby script/next_deploy sha-1, sha-2 ..'
puts 'help : ruby script/next_deploy -h'
puts "\n"
exit
else
bad_commit_overrides << arg
end
end
rails_env = ARGV[0]
git_commit_range = ARGV[1]
# ensure we are in root of the project
Dir.chdir(File.join(File.dirname(__FILE__), ".."))
# find current production revision
# scp user@server:/var/www/aspire/current/REVISION /tmp/REVISION
Net::SCP.start("server", "user") do |scp|
# synchronous (blocking) upload; call blocks until upload completes
scp.download! REMOTE_REVISION_FILE, LOCAL_REVISION_FILE
end
# system("cat #{LOCAL_REVISION_FILE}")
current_rev = File.open(LOCAL_REVISION_FILE,'r').gets.chomp
puts "Current Production Revision is #{current_rev}"
git_commit_range = current_rev + "..master"
# extract ticket numbers from commit messages between previous and current revision
puts "extracting ticket numbers from commit messages between current production revision and master ..."
ticket_numbers = []
messages = `git log #{git_commit_range} --first-parent --pretty=oneline`.split("\n")
messages.each { |message| ticket_numbers << message.match(LH_TICKET_REGEX)[2].to_i if message =~ LH_TICKET_REGEX }
ticket_numbers.uniq!
puts "Found #{messages.size} log messages containing #{ticket_numbers.size} ticket numbers"
puts "Ticket numbers are #{ticket_numbers.inspect}"
# check tickets in lighthouse...
puts "checking tickets in lighthouse..."
project = LH_PROJECT_ID.to_s.match(/^[0-9]+$/) ? Lighthouse::Project.find(LH_PROJECT_ID.to_i) : Lighthouse::Project.find(:first)
ticket_info = {}
counter = 0
project.tickets(:q => "state:open", :limit =>1000).each do |ticket|
counter += 1
next unless ticket_numbers.include? ticket.number
ticket_info[ticket.number] = "#{ticket.title} -> state:#{ticket.state}" + ( ticket.state != 'resolved' ? ", responsible:#{ticket.user_name}" : '')
end
puts "#{counter} open tickets found"
counter = 0
project.tickets(:q => "state:closed updated:\"1 month ago\"", :limit =>1000).each do |ticket|
counter += 1
next unless ticket_numbers.include? ticket.number
ticket_info[ticket.number] = "#{ticket.title} -> state:#{ticket.state}" + ( ticket.state != 'resolved' ? ", responsible:#{ticket.user_name}" : '')
end
puts "#{counter} recently closed tickets found"
puts "Ticket data found: #{ticket_info.keys.inspect}"
# puts "#{messages[0..2].inspect}"
bad_message_index = {}
bad_ticket_index = {}
def commit_not_deployable_because(reason,message_sha,bad_message_index)
bad_message_index[message_sha] ||= {}
bad_message_index[message_sha][:messages] ||= []
bad_message_index[message_sha][:messages] << reason
end
def ticket_not_deployable_because(reason,ticket_number,bad_ticket_index)
bad_ticket_index[ticket_number] ||= {}
bad_ticket_index[ticket_number][:messages] ||= []
bad_ticket_index[ticket_number][:messages] << reason
end
def one_pass(messages,bad_message_index,bad_ticket_index,ticket_info,bad_commit_overrides)
show_stopper_found = false
show_stopper_message = nil
show_stopper_ticket = nil
good_sha = nil
count_sha = 0
messages.reverse_each do |message|
message_sha = message.split(' ').first
if message !~ LH_TICKET_REGEX
if show_stopper_found
commit_not_deployable_because("follows show-stopper commit #{show_stopper_message}",message_sha,bad_message_index)
next
end
else
ticket_number = message.match(LH_TICKET_REGEX)[2].to_i
if ticket_info[ticket_number].nil?
puts "ticket number #{ticket_number} not found, needed to resolve message: #{message}"
exit 1
end
if show_stopper_found
reason = "follows show-stopper commit #{show_stopper_message}"
commit_not_deployable_because(reason,message_sha,bad_message_index)
ticket_not_deployable_because("includes #{message_sha} which #{reason}",ticket_number,bad_ticket_index)
next
else
unless bad_commit_overrides.detect{ |gc| message_sha =~ /^#{gc}/ }
if ticket_info[ticket_number] !~ /state:deploy/ &&
ticket_info[ticket_number] !~ /state:resolved/ &&
( !bad_message_index.keys.include?(message_sha) || !bad_ticket_index.keys.include?(ticket_number) )
reason = "Ticket #{ticket_number} is not in deploy state. Ticket summary: #{ticket_info[ticket_number]}"
commit_not_deployable_because(reason,message_sha,bad_message_index) unless bad_message_index.keys.include?(message_sha)
ticket_not_deployable_because(reason,ticket_number,bad_ticket_index) unless bad_ticket_index.keys.include?(ticket_number)
puts "Show-stopper found #{message_sha}. #{reason}"
show_stopper_found = true
show_stopper_message = message_sha
show_stopper_ticket = ticket_number
next
else
if bad_ticket_index.keys.include?(ticket_number) &&
!bad_message_index.keys.include?(message_sha)
# ASSUMPTION: Multiple commits with the same ticket number represent reworks and MUST be deployed together.
reason = "Commit is part of ticket #{ticket_number} and the related reworks cannot be deployed. Reasons: Ticket #{bad_ticket_index[ticket_number][:messages].join(', ')}"
commit_not_deployable_because(reason,message_sha,bad_message_index)
puts "Show-stopper found #{message_sha}. #{reason}"
show_stopper_found = true
show_stopper_message = message_sha
show_stopper_ticket = ticket_number
next
end
end # if ticket_info[ticket_number] !~ /state:deploy/ && ..
end # bad_commit_overrides.detect{ |gc| message_sha =~ /^#{gc}/ }
end # if show_stopper_found
end
unless show_stopper_found || bad_message_index.keys.include?(message_sha)
count_sha += 1
good_sha = message_sha
end
end # messages.reverse_each do |message|
puts "#{count_sha} good commits remaining" if show_stopper_found
return good_sha unless show_stopper_found
end # def one_pass(messages,bad_message_index,bad_ticket_index)
iterations = 0
puts "\nASSUMPTION: Multiple commits with the same ticket number represent reworks and MUST be deployed together."
puts "\nIterations begin ..."
until ( result = one_pass(messages,bad_message_index,bad_ticket_index,ticket_info,bad_commit_overrides) )
iterations += 1
puts "Iteration #{iterations} complete ..."
if bad_message_index.keys.size >= messages.size
puts "NONE of the ticketed commits can be deployed"
break
end
if iterations > 9
puts "Terminating loop after #{iterations} iterations"
break
end
end
puts "\n"
if result
puts "\nDeployable commits include:\n\n"
system("git log --first-parent --pretty=oneline #{current_rev}..#{result}")
puts "\n\nNext commit to deploy is:\n"
system("git log -n1 #{result}")
else
puts "NO COMMIT is deployable"
end
rescue Exception => e
puts "Exception #{e}..."
puts e.backtrace
exit # note: a non-zero exit code results in capistrano raising an exception, which we don't want
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment