Dashboard > Pulse v2.1 > ... > Remote API Examples > Ruby Example - Email Committers
  Pulse v2.1 Log In | Sign Up   View a printable version of the current page.  
  Ruby Example - Email Committers
Added by Jason Sankey, last edited by Jason Sankey on Aug 17, 2010  (view change)
Labels: 
(None)

Pulse Manual Index

Overview

This example is a sanitised version of a real script used as an alternative to the built-in email committers hook task. It illustrates how the remote API can be used to take more control of, and tweak, email notifications.

An important aspect of this script is the use of a fork and detach to make it run asynchronously. By default Pulse waits for hooks to complete, and because a hook can fail a build, the final result build is not saved until after the hooks are done. By detaching, this script allows pulse™ to continue the build, in particular to the point where the build is marked as complete. After forking, the script waits for the build to complete before continuing.

Attribution

This example was kindly donated by Kate Ebneter.

Code

#!/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://pulse"
    
    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)

Zutubi wiki is Powered by Atlassian Confluence, the Enterprise Wiki. (Version: 2.2.10 Build:#528 Nov 29, 2006) - Bug/feature request - Contact Administrators