詹金斯 - 如果开始新的构建,则中止运行构建

Posted

技术标签:

【中文标题】詹金斯 - 如果开始新的构建,则中止运行构建【英文标题】:Jenkins - abort running build if new one is started 【发布时间】:2017-04-07 05:47:39 【问题描述】:

我使用 Jenkins 和多分支管道。我为每个活动的 git 分支都有一份工作。 新构建由 git 存储库中的推送触发。如果新版本出现在同一分支中,我想要的是中止当前分支中正在运行的构建。

例如:我提交并推送到分支feature1。然后BUILD_1 在 Jenkins 中开始。我再次提交并推送到分支feature1,而BUILD_1 仍在运行。我希望BUILD_1 被中止并启动BUILD_2

我尝试使用stage concurrency=x 选项和stage-lock-milestone 功能,但未能解决我的问题。

我也读过这个帖子Stopping Jenkins job in case newer one is started,但我的问题没有解决方案。

你知道解决办法吗?

【问题讨论】:

我们让当前的工作完成,在某些情况下,如果我们从来没有工作,我们会让队列中的工作被清理(如引用的问题中所建议的那样。)不喜欢中止已经开始的工作的想法。 @MaTePe 对于 git 分支的自动化测试等情况,如果分支已更新,则在分支上完成测试通常没有什么好处,因为更新也需要进行测试。显而易见的解决方案是中止之前的测试。可能仍需要进行清理,但不会浪费资源来完成不必要的测试。 【参考方案1】:

有了 Jenkins 脚本安全,这里的许多解决方案都变得困难,因为它们使用的是非白名单方法。

通过 Jenkinsfile 开头的这些里程碑步骤,这对我有用:

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

这里的结果是:

Build 1 运行并创建里程碑 1 当构建 1 运行时,构建 2 触发。它具有里程碑 1 和里程碑 2。它通过了里程碑 1,这导致构建 #1 中止。

【讨论】:

里程碑绝对是多分支声明性管道项目的必经之路。 JENKINS-43353 建议正式发布。 里程碑分支是特定的吗? @David 我不能给你任何关于这方面的文档,但从测试和经验来看——是的,它们是特定于分支的(至少在我的设置中不会跨分支相互取消) @LucasCarnevalli 确实如此——确保上述里程碑代码是您的 Jenkinsfile 中定义的第一件事。它不需要“节点”,因此理论上您应该能够在运行其他任何代码之前运行此代码。如果您的工作由于导入失败或类似原因而在工作早期失败,您可能有更大的问题需要解决:)【参考方案2】:

使用Execute concurrent builds if necessary 为您的项目启用作业并行运行

使用execute system groovy script 作为第一个构建步骤:

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)
  

并且在中断的作业中会有一个日志:

Build was aborted
interrupted by build #12
Finished: ABORTED 

【讨论】:

听起来很不错!目前正在寻找一种将其移植到 scm 提交的管道文件的方法 让代码工作,但奇怪的是,_getRuns 只列出当前正在运行的构建:/ 类 org.jenkinsci.plugins.workflow.job.WorkflowRun 对于像我一样得到这个答案并且无法使代码运行的任何人 - 从闭包中删除 id。基本上将线路:build.getProject()._getRuns().eachid,run-> 改为 build.getProject()._getRuns().each run -> 它在沙盒中不起作用。 execute system groovy script【参考方案3】:

如果有人在 Jenkins Pipeline Multibranch 中需要它,可以像这样在 Jenkinsfile 中完成:

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")
    
  

【讨论】:

也许值得检查一下内部版本号是否低于当前版本号。否则,您可能会杀死更新的构建。【参考方案4】:

根据@C4stor 的想法,我制作了这个改进的版本...我发现@daggett 的版本更具可读性

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()
    

【讨论】:

这解决了我的管道脚本中的问题。正在显示“Aborting old build”消息,但没有显示“Aborted by newer build”。也许是因为我的旧版本正在等待输入操作。 @neves 可能是。同样以防万一:“被较新的构建中止”消息显示在另一个(较旧的)构建上。 这种方法是使用静态方法。所以我收到了这个错误: Scripts not allowed to use staticMethod hudson.model.Hudson getInstance @DmitryKuzmenko 也许您正在沙箱中运行脚本?它在那里行不通。另外,这是从 2018 年开始的,可能新版本存在差异。【参考方案5】:

通过在全局共享库中使用以下脚本使其工作:

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

def killOldBuilds() 
  while(currentBuild.rawBuild.getPreviousBuildInProgress() != null) 
    currentBuild.rawBuild.getPreviousBuildInProgress().doKill()
  

并在我的管道中调用它:

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

编辑:根据您想要杀死 oldBuild 的强度,您可以使用 doStop()、doTerm() 或 doKill()!

【讨论】:

有没有办法向终止的构建发送消息?它发送这个硬终止信号,但没有记录谁杀死它。 我不知道,我们暂时生活在这条完整的灰色线条中,对我们来说已经足够了^^' 从优雅到破坏的顺序是doStop() -> doTerm() -> doKill() 这对您有什么帮助?这是错误的 :) 但是感谢您的想法...我有一个工作版本...查看我的答案 嗯,它现在正在我们的生产堆栈中工作,所以我认为它没有错。您无法使用代码的事实可能来自很多因素,包括 jenkins 版本、java 版本、使用的操作系统、使用中的文件权限......【参考方案6】:

添加到 Brandon Squizzato 的答案。如果有时会跳过构建,则上述里程碑机制将失败。在 for 循环中设置较旧的里程碑可以解决这个问题。

还要确保您的选项中没有 disableConcurrentBuilds。否则管道不会到达里程碑步骤,这将不起作用。

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

    milestone(i)

milestone(buildNumber)

【讨论】:

这样做的潜在问题是,当您有大量构建时,创建这么多里程碑可能会占用大量时间。我不知道事情在什么时候发生了变化——创造了许多过去对我来说很快的里程碑。然后最近,创建一个里程碑每次大约需要半秒——如果你在构建 #900 上显然不理想。所以我创建了不使用 for 循环的解决方案。【参考方案7】:

基于@daggett 方法。如果您想在新推送到来时和获取更新之前中止正在运行的构建。 1.启用Execute concurrent builds if necessary 2.启用Prepare an environment for the run 3.在Groovy ScriptEvaluated 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()
    
//

【讨论】:

【参考方案8】:

我还从之前给出的版本中编译了一个版本,并进行了一些小的调整:

while() 循环为每个构建生成多个输出 UserInterruption 当前需要一个 userId 而不是推理字符串,并且不会在任何地方显示推理字符串。因此,这只是提供了 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)
        
    

【讨论】:

【参考方案9】:

从 Jenkins 2.42 你可以简单地做

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

在 cmets here 中找到解决方案 https://issues.jenkins.io/browse/JENKINS-43353

【讨论】:

以上是关于詹金斯 - 如果开始新的构建,则中止运行构建的主要内容,如果未能解决你的问题,请参考以下文章

在詹金斯中设置构建状态

如何将 Jenkins 构建计划更改为不同的时间?

构建步骤“执行 Windows 批处理命令”在詹金斯中将构建标记为失败

詹金斯:如何测试奴隶

如果执行 shell 失败,不要让 jenkins 构建失败

詹金斯从工件加载文件时出错