#!/usr/bin/env ruby
require 'xmlrpc/client'
require 'net/smtp'
require 'date'
# Yes, this is a monkey patch. Sue me.
class String
def to_quoted_printable(*args)
[self].pack("M").gsub(/\n/,"\r\n")
end
end
# Shamelessly stolen from _The Ruby Way_ except I return
# a formatted string, not an array. The string looks like
# 3 days 5 hrs 10 mins 32 secs
def formatted_sec2dhms(seconds)
str = ""
time = seconds.round
secs = time % 60
time /= 60
mins = time % 60
time /= 60
hrs = time % 24
time /= 24
days = time
if days > 0
str += "#{days} days "
end
if hrs > 0 || days > 0
str += "#{hrs} hrs "
end
if mins > 0 || days > 0 || hrs > 0
str += "#{mins} mins "
end
if secs > 0 || mins > 0 || days > 0 || hrs > 0
str += "#{secs} secs"
end
str
end
# "Features" == error messages and warnings. As in, "that's not a bug, it's a feature!"
def feature_messages(feature_name, feature_list, build_result)
# feature_list is an array of hashes. Each hash has a message, and a set of keys that locate it
# in the hierarchy
recipe = "[default]"
overall_list = []
artifacts_list = {}
commands_list = {}
stages_list = {}
feature_list.each do |feature|
if feature.has_key?('artifact')
if !artifacts_list.has_key?(feature['artifact'] + feature['command'] + feature['stage'])
artifacts_list[feature['artifact'] + feature['command'] + feature['stage']] = []
end
artifacts_list[feature['artifact'] + feature['command'] + feature['stage']].push(feature['message'])
elsif feature.has_key?('command')
if !commands_list.has_key?(feature['command'] + feature['stage'])
commands_list[feature['command'] + feature['stage']] = []
end
commands_list[feature['command'] + feature['stage']].push(feature['message'])
elsif feature.has_key?('stage')
#puts "#{feature['stage']} :: #{feature['message']}"
if !stages_list.has_key?(feature['stage'])
stages_list[feature['stage']] = []
end
stages_list[feature['stage']].push(feature['message'])
else
overall_list.push(feature['message'])
end
end
overall_string = ""
overall_list.each do |m|
overall_string += "#{m}\n"
end
stage_string = ""
stage_agent = "unknown"
stages_list.each_key do |stage|
build_result[0]['stages'].each do |s|
if s['name'] == stage
stage_agent = s['agent']
end
end
stage_header = "stage #{stage} :: #{recipe}@#{stage_agent}\n"
stage_msg = ""
stages_list[stage].each do |m|
stage_msg += "{m}\n"
end
command_string = ""
commands_list.each_key do |command|
if command =~ /#{stage}/
command_header = "command :: #{command}\n"
command_msg = ""
commands_list[command].each do |m|
command_msg += "#{m}\n"
end
artifact_string = ""
artifacts_list.each_key do |artifact|
if artifact =~ /#{command}/ && artifact =~ /#{stage}/
artifact_path = "unknown"
feature_list.each do |i|
if i['stage'] == stage && i['command'] == command && i['artifact'] == artifact
artifact_path = i['path']
break
end
end
artifact_header = "artifact :: #{artifact_path}\n"
artifact_msg = ""
artifacts_list[artifact].each do |m|
artifact_msg += "#{m}\n"
end
end
artifact_string = artifact_header + artifact_msg
end
command_string = command_header + command_msg + artifact_string
end
stage_string += stage_header + stage_msg + command_string
end
end
msg_str = "#{feature_name.downcase}\n#{overall_string + stage_string}"
end
# ------------ main code starts here ------------ #
project = ARGV[0]
build_number = ARGV[1].to_i
# Spawn another process
pid = fork do
date = Time.now
from = 'pulse@acme.com'
cc = 'build@acme.com'
time_format = "%m/%d/%y %I:%M:%S %p %Z"
pulse_url = "http:
server = XMLRPC::Client.new2("#{pulse_url}/xmlrpc")
pulse = server.proxy("RemoteApi")
pulse_user = "user"
pulse_passwd = "secret"
user_map = {}
authors = []
mail_to = []
# get all the pulse information we need: A list of pulse users,
# the changelist for this build, build result information, and lists
# of errors and warnings for this build.
begin
token = pulse.login(pulse_user, pulse_passwd)
users = pulse.getConfigListing(token, "users")
users.each do |user|
user_info = pulse.getConfig(token, "users/#{user}")
user_map[user_info['name']] = user_info['login']
end
build_result = pulse.getBuild(token, project, build_number)
status = build_result[0]['status']
while status == "in progress" do
sleep 5
build_result = pulse.getBuild(token, project, build_number)
status = build_result[0]['status']
end
changes = pulse.getChangesInBuild(token, project, build_number)
warnings = pulse.getWarningMessagesInBuild(token, project, build_number)
errors = pulse.getErrorMessagesInBuild(token, project, build_number)
pulse.logout(token)
rescue XMLRPC::FaultException => e
puts "Error:"
puts e.faultCode
puts e.faultString
end
# changes is an array of hashes
if (changes)
changes.each do |change|
authors.push(change['author'])
end
end
# build_result is a single-item array; the item is a hash
build_status = build_result[0]['status']
build_reason = build_result[0]['reason']
number_of_tests = build_result[0]['tests']['total']
broken_tests = build_result[0]['tests']['failures']
skipped_tests = build_result[0]['tests']['skipped']
# Note: If the build itself failed, the number of tests will be 0.
if number_of_tests == 0
test_summary = "none"
else
test_summary = "#{broken_tests} of #{number_of_tests} broken"
end
if skipped_tests > 0
test_summary += " (#{skipped_tests} skipped)"
end
start_time = build_result[0]['startTime'].to_time.strftime(time_format)
end_time = build_result[0]['endTime'].to_time.strftime(time_format)
elapsed = formatted_sec2dhms(build_result[0]['endTime'].to_time - build_result[0]['startTime'].to_time)
error_count = build_result[0]['errorCount']
warning_count = build_result[0]['warningCount']
authors.each do |author|
user_map.each do |name, email|
if name =~ /#{author}/
mail_to.push(email + "@acme.com")
end
end
end
mail_to.uniq!
to_list = mail_to.join(" ")
recipe = "[default]" # I'm assuming that's the only recipe...
project_link = pulse_url + "/browse/projects/#{project}"
build_link = project_link + "/builds/#{build_number}"
build_summary = build_link + "/summary/"
build_changes = build_link + "/changes/"
build_tests = build_link + "/tests/"
build_artifacts = build_link + "/artifacts/"
# Stage summary
# build_result[0]['stages'] is an array, the elements of which
# are hashes. None of our builds currently has more than one stage
# AFAIK.
stage_summary = ""
stage_logs_summary = "stage logs :: \n"
build_result[0]['stages'].each do |stage|
stage_name = stage['name']
stage_agent = stage['agent']
stage_status = stage['status'].downcase
stage_start_time = stage['startTime'].to_time.strftime(time_format)
stage_end_time = stage['endTime'].to_time.strftime(time_format)
stage_elapsed = formatted_sec2dhms(stage['endTime'].to_time - stage['startTime'].to_time)
stage_tests_url = "#{build_tests}#{stage_name}/"
per_stage_summary = "#{stage_name} : #{recipe} : #{stage_agent} : #{stage_status.downcase} : #{stage_start_time} : #{stage_end_time} : #{stage_elapsed} : <#{stage_tests_url}/>\n"
stage_summary += per_stage_summary
stage_logs = build_link + "/logs/" + stage_name
stage_logs_summary += "#{stage_name} <#{stage_logs}/>\n"
end
# ------------------
# changelist summary
changes_list = ""
if changes.length > 0
changes.each do |change|
revision = change['revision']
author = change['author']
how_long_ago = formatted_sec2dhms(date - change['date'].to_time)
comment = change['comment'][0,60]
per_change_summary = "#{revision} : #{author} : #{how_long_ago} : #{comment} : <#{build_changes}>\n"
changes_list += per_change_summary
end
else
changes_list = "no changes in this build\n"
end
# error and warnings summary
if error_count > 0
error_messages_string = feature_messages("error", errors, build_result)
else
error_messages_string = ""
end
if warning_count > 0
warning_messages_string = feature_messages("warning", warnings, build_result)
else
warning_messages_string = ""
end
if error_count > 0 || warning_count > 0
features_list_plaintext = "#{error_messages_string}\n#{warning_messages_string}\n"
else
features_list = features_list_plaintext = ""
end
# test results summary
# There doesn't seem to be any way to get at the test results with the remote API. So the best
# I can do is provide a link or links to the appropriate page(s).
test_links = {}
build_result[0]['stages'].each do |stage|
test_links[stage['name']] = build_tests + "#{stage['name']}/TestSuite/"
end
test_output_summary = "test output links :: \n"
test_links.each do |s, link|
test_output_summary += "test output for stage #{s}: <#{link}>\n"
end
text = <<END_OF_PLAINTEXT
project :: #{project}<#{project_link}/> :: build #{build_number}<#{build_link}>
jump to ::
summary<#{build_summary}>
changes <#{build_changes}>
tests<#{build_tests}>
artifacts <#{build_artifacts}>
#{stage_logs_summary}
summary ::
id: #{build_number}<#{build_link}/>
status: #{build_status.downcase}
reason: #{build_reason}
tests: #{test_summary}<#{build_tests}>
start time: #{start_time}
end time: #{end_time}
elapsed: #{elapsed}
stages ::
#{stage_summary}
changes ::
#{changes_list}
#{features_list}
#{test_output_summary}
END_OF_PLAINTEXT
msg_str = <<END_OF_MESSAGE
From: #{from}
To: #{to_list}
CC: #{cc}
Reply-to: #{cc}
Subject: [pulse] #{project}: build #{build_number}: #{build_status.downcase}
Date: #{date}
Content-Type: text/plain; charset="us-ascii"
Content-Transfer-Encoding: quoted-printable
#{text.to_quoted_printable}
END_OF_MESSAGE
Net::SMTP.start('localhost', 25) do |smtp|
smtp.send_message msg_str, from, mail_to
end
end
Process.detach(pid)