Jenkins - abort running build if new one is started

GitJenkinsGroovyMultibranch Pipeline

Git Problem Overview


I use Jenkins and Multibranch Pipeline. I have a job for each active git branch. New build is triggered by push in git repository. What I want is to abort running builds in current branch if new one appears in same branch.

For example: I commit and push to branch feature1. Then BUILD_1 started in Jenkins. I make another commit and push to branch feature1 while BUILD_1 is still running. I want BUILD_1 to be aborted and to start BUILD_2.

I tried to use stage concurrency=x option, and stage-lock-milestone feature, but didn't manage to solve my problem.

Also I've read this thread https://stackoverflow.com/questions/10280846/stopping-jenkins-job-in-case-newer-one-is-started, but there is no solution for my problem.

Do you know any solution to this?

Git Solutions


Solution 1 - Git

With Jenkins script security many of the solutions here become difficult since they are using non-whitelisted methods.

With these milestone steps at the start of the Jenkinsfile, this is working for me:

def buildNumber = env.BUILD_NUMBER as int
if (buildNumber > 1) milestone(buildNumber - 1)
milestone(buildNumber)

The result here would be:

  • Build 1 runs and creates milestone 1
  • While build 1 is running, build 2 fires. It has milestone 1 and milestone 2. It passes milestone 1, which causes build #1 to abort.

Solution 2 - Git

enable job parallel run for your project with Execute concurrent builds if necessary

use execute system groovy script as a first build step:

import hudson.model.Result
import jenkins.model.CauseOfInterruption

//iterate through current project runs
build.getProject()._getRuns().iterator().each{ run ->
  def exec = run.getExecutor()
  //if the run is not a current build and it has executor (running) then stop it
  if( run!=build && exec!=null ){
    //prepare the cause of interruption
    def cause = { "interrupted by build #${build.getId()}" as String } as CauseOfInterruption 
    exec.interrupt(Result.ABORTED, cause)
  }
}

and in the interrupted job(s) there will be a log:

Build was aborted
interrupted by build #12
Finished: ABORTED 

Solution 3 - Git

If anybody needs it in Jenkins Pipeline Multibranch, it can be done in Jenkinsfile like this:

def abortPreviousRunningBuilds() {
  def hi = Hudson.instance
  def pname = env.JOB_NAME.split('/')[0]

  hi.getItem(pname).getItem(env.JOB_BASE_NAME).getBuilds().each{ build ->
    def exec = build.getExecutor()

    if (build.number != currentBuild.number && exec != null) {
      exec.interrupt(
        Result.ABORTED,
        new CauseOfInterruption.UserInterruption(
          "Aborted by #${currentBuild.number}"
        )
      )
      println("Aborted previous running build #${build.number}")
    } else {
      println("Build is not running or is current build, not aborting - #${build.number}")
    }
  }
}

Solution 4 - Git

Based on the idea by @C4stor I have made this improved version... I find it more readable from @daggett 's version

import hudson.model.Result
import hudson.model.Run
import jenkins.model.CauseOfInterruption.UserInterruption

def abortPreviousBuilds() {
    Run previousBuild = currentBuild.rawBuild.getPreviousBuildInProgress()

    while (previousBuild != null) {
        if (previousBuild.isInProgress()) {
            def executor = previousBuild.getExecutor()
            if (executor != null) {
                echo ">> Aborting older build #${previousBuild.number}"
                executor.interrupt(Result.ABORTED, new UserInterruption(
                    "Aborted by newer build #${currentBuild.number}"
                ))
            }
        }

        previousBuild = previousBuild.getPreviousBuildInProgress()
    }
}

Solution 5 - Git

Got it to work by having the following script in the Global Shared Library :

import hudson.model.Result
import jenkins.model.CauseOfInterruption.UserInterruption
 
def killOldBuilds() {
  while(currentBuild.rawBuild.getPreviousBuildInProgress() != null) {
    currentBuild.rawBuild.getPreviousBuildInProgress().doKill()
  }
}

And calling it in my pipeline :

@Library('librayName')
def pipeline = new killOldBuilds()
[...] 
stage 'purge'
pipeline.killOldBuilds()

Edit : Depending on how strongly you want to kill the oldBuild, you can use doStop(), doTerm() or doKill() !

Solution 6 - Git

Adding to Brandon Squizzato's answer. If builds are sometimes skipped the milestone mechanism as mentioned will fail. Setting older milestones in a for-loop solves this.

Also make sure you don't have disableConcurrentBuilds in your options. Otherwise the pipeline doesn't get to the milestone step and this won't work.

def buildNumber = env.BUILD_NUMBER as int
for (int i = 1; i < buildNumber; i++)
{
    milestone(i)
}
milestone(buildNumber)

Solution 7 - Git

From Jenkins 2.42 you can simply do

// as a step in a scripted pipeline    
properties([disableConcurrentBuilds(abortPrevious: true)]) 
// as a directive in a declarative pipeline
options { disableConcurrentBuilds abortPrevious: true } 

Found solution in comments here https://issues.jenkins.io/browse/JENKINS-43353

Solution 8 - Git

Based on @daggett method. If you want to abort running build when the new push is coming and before fetch updates.

  1. Enable Execute concurrent builds if necessary

  2. Enable Prepare an environment for the run

  3. Running bellow code in Groovy Script or Evaluated Groovy script

    import hudson.model.Result import hudson.model.Run import jenkins.model.CauseOfInterruption

    //def abortPreviousBuilds() { Run previousBuild = currentBuild.getPreviousBuildInProgress()

     while (previousBuild != null) {
         if (previousBuild.isInProgress()) {
             def executor = previousBuild.getExecutor()
             if (executor != null) {
                 println ">> Aborting older build #${previousBuild.number}"
                 def cause = { "interrupted by build #${currentBuild.getId()}" as String } as CauseOfInterruption 
                 executor.interrupt(Result.ABORTED, cause)
             }
         }
         previousBuild = previousBuild.getPreviousBuildInProgress()
     }
    

    //}

Solution 9 - Git

Before each build task, first determine whether all the tasks currently under construction are the same as the branch of this build. If they are the same, keep the latest task.

    stage('Setup') {
        steps {
            script {
                JOB_NAME = env.JOB_NAME
                branch_name = "${env.gitlabTargetBranch}"
                def job = Jenkins.instance.getItemByFullName( JOB_NAME )
                def builds = job.getBuilds()
                for( build in job.getBuilds()) {
                    if (build.isBuilding()) {
                      String parameters = build?.actions.find{ it instanceof ParametersAction }?.parameters?.collectEntries {
                            [ it.name, it.value ]
                            }.collect { k, v -> "${v}" }.join('\n')
                            
                             if (branch_name == "${parameters}") {
                                 if (env.BUILD_NUMBER > "${build.getId()}") {
                                     build.doKill()  
                                 }
                             }
                    }
                }
                
                
            }
           ......

Solution 10 - Git

I also compiled a version from the previously given ones with a few minor tweaks:

  • the while() loop generated multiple outputs for each build
  • the UserInterruption currently expects a userId instead of a reasoning string, and will not show a reasoning string anywhere. Therefore this just provides the userId
def killOldBuilds(userAborting) {
    def killedBuilds = []
    while(currentBuild.rawBuild.getPreviousBuildInProgress() != null) {
        def build = currentBuild.rawBuild.getPreviousBuildInProgress()
        def exec = build.getExecutor()

        if (build.number != currentBuild.number && exec != null && !killedBuilds.contains(build.number)) {
            exec.interrupt(
                    Result.ABORTED,
                    // The line below actually requires a userId, and doesn't output this text anywhere
                    new CauseOfInterruption.UserInterruption(
                            "${userAborting}"
                    )
            )
            println("Aborted previous running build #${build.number}")
            killedBuilds.add(build.number)
        }
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questionkakty3View Question on Stackoverflow
Solution 1 - GitBrandon SquizzatoView Answer on Stackoverflow
Solution 2 - GitdaggettView Answer on Stackoverflow
Solution 3 - GitmidNView Answer on Stackoverflow
Solution 4 - GitStefanos KalantzisView Answer on Stackoverflow
Solution 5 - GitC4storView Answer on Stackoverflow
Solution 6 - GitCodeMonkeyView Answer on Stackoverflow
Solution 7 - GitMaxence GuichonView Answer on Stackoverflow
Solution 8 - GitIan KwokView Answer on Stackoverflow
Solution 9 - Gituser16590667View Answer on Stackoverflow
Solution 10 - GitJanis PeisenieksView Answer on Stackoverflow