Jenkins ‘rerun failed tests’

We needed a way to reproduce GitHub Actions’ ability to only rerun those tests that failed in Jenkins. This is to speed up the re-testing in addition to reducing costs. Looking around no one really had a solution for this. So we wrote our own. This is the declarative pipeline that calls multiple jobs on a pull request from GitHub using the GHPRB (GitHub Pull Request Builder) plugin:

// declare our vars outside the pipeline
def tests = [:]
def jobRuns = [
     ['GroupName',[
                 'Test-Name'
                ,'Test2-Name'
    ]]
]

def cleanupName(name) {
    return name.replaceAll("/","_").replaceAll("-","_").replaceAll(" ","_")
}

def getJobResultName(stepName) {
    return "RESULT_" + cleanupName(env.JOB_NAME) + cleanupName(stepName)
}

@NonCPS
def commitHashForBuild(build) {
  return build.rawBuild.getEnvironment().ghprbActualCommit
}

@NonCPS
def getLastBuild(curBuild, curHash) {
  def lastBuild = curBuild.getPreviousBuild()
  if ( lastBuild ) {
      def lastHash = commitHashForBuild(lastBuild)
      if ( lastHash == curHash ) {
          return lastBuild
      } else return getLastBuild(lastBuild, curHash)
  }
  return null
}

def checkIfPassed(lastBuild,jobName) {
    if ( lastBuild ) {
        def buildResults = lastBuild.getBuildVariables()
        if ( (buildResults[getJobResultName(jobName)] != null) && ( buildResults[getJobResultName(jobName)] == "SUCCESS" ) ) {
            return true
        }
    }
    return false
}

pipeline {
    agent { label 'agent_name_or_group' }
//    options {
//        timeout(time: 30, unit: 'MINUTES')
//    }
    stages {
        stage('Run Tests') {
            steps {
                echo "Start check on "+currentBuild.getDisplayName()
                script {
                    def lastBuild = getLastBuild(currentBuild, commitHashForBuild(currentBuild))
                    echo "Commit: " + env.ghprbActualCommit
                    echo "Commit2: " + currentBuild.buildVariableResolver.resolve("ghprbActualCommit")
                    if ( lastBuild ) {
                        echo "Found build "+lastBuild.getDisplayName()
                    } else {
                        echo "No previous build"
                    }

                    jobRuns.each { f ->
                        tests[f[0]] = {
                            // when running parallel build jobs, it is unnecessary to put in a 'node' block since the job itself will specify a node
                            f[1].each { j ->
                                echo "Has passed "+j+":"+checkIfPassed(lastBuild,j)
                                if (checkIfPassed(lastBuild,j)) { // preserve previous passed state
                                    env[getJobResultName(j)] = "SUCCESS"
                                } else { // run last failed job
                                    final buildJob = build job: j, parameters: [
                                         string(name: 'sha1', value: env.sha1)
                                        ,string(name: 'ghprbActualCommit', value: env.ghprbActualCommit)
                                        ,string(name: 'ghprbActualCommitAuthor', value: env.ghprbActualCommitAuthor)
                                        ,string(name: 'ghprbActualCommitAuthorEmail', value: env.ghprbActualCommitAuthorEmail)
                                        ,string(name: 'ghprbAuthorRepoGitUrl', value: env.ghprbAuthorRepoGitUrl)
                                        ,string(name: 'ghprbTriggerAuthor', value: env.ghprbTriggerAuthor)
                                        ,string(name: 'ghprbTriggerAuthorEmail', value: env.ghprbTriggerAuthorEmail)
                                        ,string(name: 'ghprbTriggerAuthorLogin', value: env.ghprbTriggerAuthorLogin)
                                        ,string(name: 'ghprbTriggerAuthorLoginMention', value: env.ghprbTriggerAuthorLoginMention)
                                        ,string(name: 'ghprbPullId', value: env.ghprbPullId)
                                        ,string(name: 'ghprbTargetBranch', value: env.ghprbTargetBranch)
                                        ,string(name: 'ghprbSourceBranch', value: env.ghprbSourceBranch)
                                        ,string(name: 'ghprbPullAuthorEmail', value: env.ghprbPullAuthorEmail)
                                        ,string(name: 'ghprbPullAuthorLogin', value: env.ghprbPullAuthorLogin)
                                        ,string(name: 'ghprbPullAuthorLoginMention', value: env.ghprbPullAuthorLoginMention)
                                        ,string(name: 'ghprbPullDescription', value: env.ghprbPullDescription)
                                        ,string(name: 'ghprbPullTitle', value: env.ghprbPullTitle)
                                        ,string(name: 'ghprbPullLink', value: env.ghprbPullLink)
                                        ,string(name: 'ghprbPullLongDescription', value: env.ghprbPullLongDescription)
                                        ,string(name: 'ghprbCommentBody', value: env.ghprbCommentBody)
                                        ,string(name: 'ghprbGhRepository', value: env.ghprbGhRepository)
                                        ,string(name: 'ghprbCredentialsId', value: env.ghprbCredentialsId)
                                        ,string(name: 'random_string', value: env.random_string)
                                    ]
                                    env[getJobResultName(j)] = buildJob.getResult()
                                }
                            }
                        }
                    }
                    // Still within the 'Script' block, run the parallel array object
                    parallel tests
                }
            }
        }
    }
}


The major drawback here is that you have to give in-script process approvals for the following things:

  • method hudson.model.Run getEnvironment
  • method org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper getRawBuild
  • staticMethod org.codehaus.groovy.runtime.DefaultGroovyMethods putAt java.lang.Object java.lang.String java.lang.Object

It shouldn’t be a terrible thing, considering your Jenkins should only have things in it that are approved by multiple sets of eyes, but still an important thing to note.

If you have questions on any of the above, please contact us at facts@wolfssl.com, call us at +1 425 245 8247 , or visit FAQ page.