Created
September 9, 2011 13:05
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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