Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Setting Jenkins multijob build result with Groovy script based on % pass/fail child jobs

I have a Jenkins Multijob project with a very simple structure:

  • Multijob
    • childjob 1
    • childjob 2
    • childjob 3
    • childjob 4 etc...

I want to set the Multijob status as follows:

  • I want a green ball if all child jobs pass
  • I want a yellow ball if any are skipped OR < 25% fail
  • I want a red ball if >= 25% fail

I know I can use a Groovy post build action with a script such as that below, but I don't know how to set the required threshold levels:

void log(msg) {
manager.listener.logger.println(msg)
}

threshold = Result.SUCCESS

void aggregate_results() {
    failed = false

    mainJob = manager.build.getProject().getName()
    job = hudson.model.Hudson.instance.getItem(mainJob)

    log '-------------------------------------------------------------------------------------'
    log 'Aggregated status report'
    log '-------------------------------------------------------------------------------------'

log('${mainJob}    #${manager.build.getNumber()} - ${manager.build.getResult()}')

job.getLastBuild().getSubBuilds().each { subBuild->
  subJob = subBuild.getJobName() 
  subJobNumber = subBuild.getBuildNumber()
  job = hudson.model.Hudson.instance.getItem(subBuild.getJobName())
  log '${subJob}   #${subJobNumber} - ${job.getLastCompletedBuild().getResult()}'
  log job.getLastCompletedBuild().getLog()

  //println subBuild
  dePhaseJob = hudson.model.Hudson.instance.getItem(subBuild.getJobName())
  dePhaseJobBuild = dePhaseJob.getBuildByNumber(subBuild.getBuildNumber())
  dePhaseJobBuild.getSubBuilds().each { childSubBuild ->
    try {
        log '   ${childSubBuild.jobName}'

        job = hudson.model.Hudson.instance.getItem(childSubBuild.getJobName())
        build = job.getBuildByNumber(childSubBuild.getBuildNumber())

        indent = '  '    
        log '${indent} #${build.getNumber()}  - ${build.getResult()}' 
        log build.getLog()

        if(!failed && build.getResult().isWorseThan(threshold) ) {
          failed = true
        }
    } catch (Exception e) {    
        log('ERROR: ${e.getMessage()}')
        failed = true
    }
  }
}

if(failed) {manager.build.setResult(hudson.model.Result.FAILURE)}
}

try {
  aggregate_results()
} catch(Exception e) {
  log('ERROR: ${e.message}')
  log('ERROR: Failed Status report aggregation')
  manager.build.setResult(hudson.model.Result.FAILURE)
}

Can anyone help tweak the script to achieve what I need?

like image 682
Steerpike Avatar asked Mar 13 '18 15:03

Steerpike


1 Answers

Not sure if this really qualifies as an answer. Might be more of a comment but comments do not really lend themselves to long code snippets so here goes.

To make your code a tad more readable and easier to grok I did the following:

  • replaced all instances of getXX and setYY with groovy property access, eg build.getResult() becomes build.result
  • removed unnecessary use of parens in function calls, e.g. log('ERROR: ${e.getMessage()}') becomes log 'ERROR: ${e.getMessage()}'
  • replaced use of string interpolation in single quotes with double quotes since string interpolation does not work in single quotes. E.g. log 'ERROR: ${e.message}' becomes log "ERROR: ${e.message}"
  • switched from declaring all variables in the script global binding scope to local, e.g. subJob = ... becomes def subJob = .... Declaring everything in the global scope leads to hard-to-find issues, especially if you are re-using variable names like job.

I also cleaned out some reduncancies, an example:

job.getLastBuild().getSubBuilds().each { subBuild->
  subJob = subBuild.getJobName() 
  subJobNumber = subBuild.getBuildNumber()
  job = hudson.model.Hudson.instance.getItem(subBuild.getJobName())  // <---
  ...
  //println subBuild
  dePhaseJob = hudson.model.Hudson.instance.getItem(subBuild.getJobName()) // <---
  dePhaseJobBuild = dePhaseJob.getBuildByNumber(subBuild.getBuildNumber())

so here we set both job and dePhaseJob to the same value. Assigning the same value to two separate variables like this is redundant and only makes the code harder to read.

Furthermore (and I'm not intimately familiar with the jenkins internal apis so I might be wrong here) the following flow in the above code seems off:

  • we have the subBuild instance
  • we then retrieve the corresponding job instance into both job and dePhaseJob
  • we then retrieve the build into dePhaseJobBuild using dePHaseJob.getBuildByNumber(subBuild.buildNumer)

but doesn't that leave us with subBuild == dePhaseJobBuild? I.e. we spent all this code just to retrieve a value we already had. We go from build to job and back to build. Unless I'm missing something esoteric in the jenkins apis this seems redunant as well.

With all those changes and a few other minor ones we und up with the following code:

def job(name) {
  hudson.model.Hudson.instance.getItem(name) 
}

def aggregateResults() {
  def mainJobName = manager.build.project.name
  log '-------------------------------------------------------------------------------------'
  log 'Aggregated status report'
  log '-------------------------------------------------------------------------------------'
  log "${mainJobName}    #${manager.build.number} - ${manager.build.result}"

  def failed = false
  job(mainJobName).lastBuild.subBuilds.each { subBuild ->
    log "${subBuild.jobName}   #${subBuild.buildNumber} - ${subBuild.result}"
    log subBuild.log

    subBuild.subBuilds.each { subSubBuild ->
      try {
        log "   ${subSubBuild.jobName}   #${subSubBuild.buildNumber} - ${subSubBuild.result}"
        log "   " + subSubBuild.getLog(Integer.MAX_VALUE).join("\n   ") //indent the log lines

        if(!failed && subSubBuild.result.isWorseThan(threshold)) {
          failed = true
        }
      } catch (Exception e) {
        log "ERROR: ${e.message}"
        failed = true
      }
    }
  }

  if(failed) {
    manager.build.result = hudson.model.Result.FAILURE
  }
}

and again, I don't have a jenkins instance to test this on so I'm flying in the dark here and apologize in advance for misspellings, syntax fumbles or other abuse of the code and the jenkins apis.

The issues in your code (like the string interpolation one which I can't see ever having worked) makes me think that the original code was not working but rather an example pattern.

This makes me further wonder if you really need to do two levels of nesting here, i.e. is the following:

job(mainJobName).lastBuild.subBuilds.each { subBuild ->
  subBuild.subBuilds.each { subSubBuild ->
    ...
  }
}

really necessary or would one level be enough? From the quick graph in your question it would seem that we only need to care about the main job and its sub jobs, not sub-sub jobs.

If this is the case, you could get away with logic along the lines of:

def aggregateResults() {
  def mainJob = job(manager.build.project.name)
  def subs    = mainJob.lastBuild.subBuilds
  def total   = subs.size()
  def failed  = subs.findAll { sub -> sub.result.isWorseThan(threshold) }.size()

  if(failed > 0) {
    manager.build.result = hudson.model.Result.FAILURE
  }

  failed == 0 ? "green" : (failed/total < 0.25 ? "yellow" : "red")
}
like image 107
Matias Bjarland Avatar answered Nov 07 '22 07:11

Matias Bjarland