在Jenkins声明性流水线中确定失败阶段

14

我应该如何报告声明性流水线失败的阶段?在失败块中,我想获取failedStage.name并报告它(最终到slack)。

pipeline {
    agent { label 'master'}
    stages {
        stage('Ok') {
            steps {
                echo 'do thing'
            }
        }
        stage('NotOK') {
            steps {
                sh 'make fail'
            }
        }
    }
    post {
        always {
            echo 'ok'
        }
        failure {
            echo 'Failed during Which Stage?'
        }
    }
}
6个回答

12

概述

使用Blue Ocean插件API可以通用地实现此操作。 可以使用类PipelineNodeGraphVisitor 遍历所有管道节点(如阶段、并行分支和步骤)。只需检查FlowNodeWrappertype属性是否等于FlowNodeWrapper.NodeType.STAGE

此外,我们可以从节点中存储的ErrorAction中获取失败原因。

代码

通常,您会将以下代码放入共享库中,因为如果直接插入管道代码中,它将防止在沙箱环境中运行管道。

import io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeGraphVisitor
import io.jenkins.blueocean.rest.impl.pipeline.FlowNodeWrapper
import org.jenkinsci.plugins.workflow.support.steps.build.RunWrapper
import org.jenkinsci.plugins.workflow.actions.ErrorAction

// Get information about all stages, including the failure causes.
//
// Returns a list of maps: [[id, displayName, result, errors]]
// The 'errors' member is a list of unique exceptions.

@NonCPS
List<Map> getStageResults( RunWrapper build ) {

    // Get all pipeline nodes that represent stages
    def visitor = new PipelineNodeGraphVisitor( build.rawBuild )
    def stages = visitor.pipelineNodes.findAll{ it.type == FlowNodeWrapper.NodeType.STAGE }

    return stages.collect{ stage ->

        // Get all the errors from the stage
        def errorActions = stage.getPipelineActions( ErrorAction )
        def errors = errorActions?.collect{ it.error }.unique()

        return [ 
            id: stage.id, 
            displayName: stage.displayName, 
            result: "${stage.status.result}",
            errors: errors
        ]
    }
}

// Get information of all failed stages
@NonCPS
List<Map> getFailedStages( RunWrapper build ) {
    return getStageResults( build ).findAll{ it.result == 'FAILURE' }
}

演示流程线

pipeline{
    agent any

    stages {
        stage('SuccessStage') {
            steps {
                echo 'Success'
            }
        }
        stage('FailedStage') {
            steps {
                readFile 'dfgkjsdffj'
            }
        }
        stage('SkippedStage') {
            steps {
                echo 'Skipped because of error in FailedStage'
            }
        }
    }
    post {
        failure {
            script {              
                // Print information about all failed stages
                def failedStages = getFailedStages( currentBuild )
                echo "Failed stages:\n" + failedStages.join('\n')

                // To get a list of just the stage names:
                //echo "Failed stage names: " + failedStages.displayName
            }
        }
    }
}

蓝色海洋视角

BlueOcean 截图

注释

如果您想获取除 FAILURE 以外的其他结果阶段,可以查看我的函数 getFailedStages()。您可以简单地更改条件,例如:

  • it.result in ['FAILURE','UNSTABLE']
    • 获取不稳定阶段
  • it.result != 'SUCCESS'
    • 获取所有失败阶段,包括跳过的阶段

可能的备选实现:

严格来说,没有必要使用 Blue Ocean API。它只是大大简化了代码。您可以仅使用基本的 Jenkins 管道 API 来完成相同的任务。作为起点,在迭代管道节点时,请查找 FlowGraphWalker。查看 Blue Ocean 的 PipelineNodeGraphVisitor 代码,以找出他们如何确定“Stage”节点类型。


1
我见过的最漂亮的代码。我有包含9k步骤的构建,这使我能够很好地收集/报告用户的失败。谢谢。在共享库中使用这个太棒了。 - Petra Kahn
我需要指出的是,您可能需要管理员批准在您的实例上使用这些签名。我不得不批准一堆签名,以便在我正在开发的共享库中使用它们。“脚本不允许使用新的io.jenkins.blueocean.rest.impl.pipeline.PipelineNodeGraphVisitor org.jenkinsci.plugins.workflow.job.WorkflowRun。管理员可以决定批准或拒绝此签名。” - Rezkin
@PeterKahn 你有九千步的管道吗?? - Max Cascone
2
是的,@max-cascone。我们为分布在7个或8个发行版中的1500多个内核构建Linux内核模块。我们的内核检测系统通常会发现1-5个新的内核,然后只构建这些内核,但当我们更改源代码时,必须重新构建它们全部。这就是Linux内核驱动程序的乐趣所在。 - Petra Kahn

5
您可以在每个阶段使用post指令,以特定的操作和通知来处理失败。
但如果您想在所有阶段中都使用它,那么您需要重复编写代码,而且我认为您无法动态访问阶段名称,因此它非常冗长和硬编码。不过,您可能可以重构代码以使用库。
pipeline {
    agent { label 'master'}
    stages {
        stage('Ok') {
            steps {
                echo 'do thing'
            }
            post {
                failure {
                    echo 'FAILED (in stage OK - should not happen :))'
                }
            }
        }
        stage('NotOK') {
            steps {
                sh 'make fail'
            }
            post {
                failure {
                    echo 'FAILED (in stage NotOK)'
                }
            }
        }
    }
    post {
        always {
            echo 'COMPLETED (global)'
        }
        failure {
            echo 'FAILED (global)'
        }
    }
}

3
根据 Jenkins 的版本不同,您可以访问 env.STAGE_NAME 来获取当前阶段的名称,并使用 currentBuild.currentResult 来获取当前阶段的结果。 - m0j0hn
@m0j0hn,我之前不知道有env.STAGE_NAME,谢谢!但是currentBuild.currentResult往往会欺骗你(特别是如果构建在早期阶段就失败了,例如在声明式检出或管道初始化期间)。 - haylem

2

PipelineVisitor是一种不错的方法。然而,如果你只想看到错误信息,那么利用FlowGraphTable可能会更好。

以下提供了每个失败步骤的映射列表,并遍历下游作业。我发现这非常有用。

为了避免安全沙盒警告/批准,您需要使用共享库。

List<Map> getStepResults() {
    def result = []
    WorkflowRun build = currentBuild()
    FlowGraphTable t = new FlowGraphTable(build.execution)
    t.build()
    for (def row in t.rows) {
        if (row.node.error) {
            def nodeInfo = [
                    'name': "${row.node.displayName}",
                    'url': "${env.JENKINS_URL}${row.node.url}",
                    'error': "${row.node.error.error}",
                    'downstream': [:]

            ]
            if (row.node.getAction(LogStorageAction)) {
                nodeInfo.url += 'log/'
            }

            for (def entry in getDownStreamJobAndBuildNumber(row.node)) {
                nodeInfo.downstream["${entry.key}-${entry.value}"] = getStepResults(entry.key, entry.value)
            }
            result << nodeInfo
        }
    }
    log(result)
    return result
}

Map getDownStreamJobAndBuildNumber(def node) {
    Map downStreamJobsAndBuilds = [:]
    for (def action in node.getActions(NodeDownstreamBuildAction)) {
        def result = (action.link =~ /.*\/(?!\/)(.*)\/runs\/(.*)\//).findAll()
        if (result) {
            downStreamJobsAndBuilds[result[0][1]] = result[0][2]
        }
    }
    return downStreamJobsAndBuilds
}

1
我为自己的需求构建了一些更简单的东西,尽管它仍然需要每个阶段中的代码片段。
我在这里寻找一种不重复在每个阶段的post部分中获取错误的方法,并且我喜欢上面的解决方案。我可能会将其他答案中的部分内容合并到我的库中。
我有一个env.informationalMessages变量,它会随着每次运行结束时的通知电子邮件一起发送。
在每个阶段中,您都要进行一个不成功的post部分。这应该捕获failedabortedunstable结果。清理在任何其他后置条件之后运行。
stage ('some stage name') {
  steps { ... }  
  post {
    unsuccessful {
      addStageResultToInfoMessages()
    }
    cleanup  {
    // whatever else you want to do
    }
  }
}

vars/addStageResultToInfoMessages.groovy:

// Adds a message with the stage result into the informationalMessages env var.
def call() {
  addToInfoMessages("${currentBuild.result} in stage '${STAGE_NAME}'.")
}

vars/addToInfoMessages.groovy

// Adds the passed-in string to the informationalMessages env var
// that gets sent with notification emails.
// env.informationalMessages is initialized to '' at the start of each run.
def call(String message) {
  env.informationalMessages += "${message}\n"
}

然后在管道的末端,另一个 post 部分:

post {
  unsuccessful {
    addToInfoMessages(getErrorMessage())
  }
  cleanup {
    notifyEmail()
  }
}

vars/getErrorMessage.groovy 从 Jenkins API 中获取原始控制台文本,并查找文本 'ERROR:'。Jenkins 中过时的 Groovy 不支持对列表进行空安全导航,因此您必须使用老式方法。

// gets the Error message from the run's console text
// uses the jenkins api
def call() {
  // get the raw text of the build's console output
  response = httpRequest ignoreSslErrors: true, url: "${BUILD_URL}consoleText"
  
  // find lines with 'ERROR:'
  err = response.getContent().findAll(/.*ERROR:.*/)

  // if null-safe, split off the time code
  if (err) { return err[-1].split('Z] ')[1] }
  else { return 'Error not found' }
}

只是另一种做法。


1

不需要在每个阶段中添加 post 部分,我找到了一些解决方案,从我的角度来看,在声明式流水线中不应该起作用,但它确实起作用。你所需要做的就是覆盖 stage

def stage(String name, Closure cl) {
    echo "Stage: ${name}"
    try {
        cl()
    } catch (Exception e) {
        // I needed to save failed stage and message for parent pipeline job
        // so I saved them in environment variables, otherwise it can be saved
        // in global variables
        if (!env.FAILED_STAGE) {
            env.FAILED_STAGE = name
            env.FAILED_MESSAGE = e.getMessage()
        }
    }
}

pipeline {

    options { timestamps() }
    agent { label 'master' }
    stages {
        stage('First stage') {
            steps {
                //Any steps are working
                script {
                    sh "echo first"
                }
            }
        }
        stage('Second stage') {
            steps {
                echo "second"
            }
        }
        stage('Fail stage') {
            steps {
                error "failed"
            }
        }
        stage('Final stage') {
            steps {
                build "Other job"
            }
        }
    }
    post {
        failure {
            echo "Failed stage: ${env.FAILED_STAGE}"
            echo "Error message: ${env.FAILED_MESSAGE}"
        }
    }
}

我觉得最奇怪的是,在阶段失败后,其他阶段会按照应有的方式被跳过。 以下是输出内容:
14:05:14 Stage: First stage
[Pipeline] script
[Pipeline] {
[Pipeline] sh
14:05:14 + echo first
14:05:14 first
[Pipeline] }
[Pipeline] // script
[Pipeline] echo
14:05:14 Stage: Second stage
[Pipeline] echo
14:05:14 second
[Pipeline] echo
14:05:14 Stage: Fail stage
[Pipeline] error
[Pipeline] error
[Pipeline] echo
14:05:14 Stage: Final stage
Stage "Final stage" skipped due to earlier failure(s)
[Pipeline] echo
14:05:14 Stage: Declarative: Post Actions
[Pipeline] echo
14:05:14 Failed stage: Fail stage
[Pipeline] echo
14:05:14 Error message: failed
[Pipeline] }
[Pipeline] // timestamps
[Pipeline] }
[Pipeline] // node
[Pipeline] End of Pipeline
ERROR: failed
Finished: FAILURE

编辑:请注意,您将失去舞台视图,因为在Jenkins的角度上,没有正常的阶段。


0
为什么不使用全局变量failedStage来存储阶段的名称,并在post步骤中使用它来输出失败的阶段名称呢? (请注意,只有当管道失败时才应使用failedStage的内容;否则它包含最后一个阶段的名称。)
示例代码:
def failedStage = ""
 
pipeline {
    agent {label 'master'}
   
    stages {
        stage('Prepare') {
            steps {
                script { failedStage = env.STAGE_NAME }
                sh """
                echo "pass:" > Makefile
                echo "\texit 0" >> Makefile
                echo "fail:" >> Makefile
                echo "\texit 1" >> Makefile
                """
            }
        }
        stage('Build1') {
            steps {
                script { failedStage = env.STAGE_NAME }
                sh """
                make pass
                """
            }
        }
        stage('Build2') {
            steps {
                script { failedStage = env.STAGE_NAME }
                sh """
                make fail
                """
            }
        }
    }
    post {
        success {
            echo "SUCCESS"
        }
        failure {
            echo "failed stage: ${failedStage}"
        }
    }
}

我的方法比较复杂,但是它允许我遍历顶级作业、所有触发的作业,并收集失败步骤的链接。这使得跟踪松弛消息变得更加方便。我还发现可以在共享库中创建一个全局线程安全问题列表作为单例,并通过这种方式报告问题。我有一个名为support.withCatchAndReport的方法,它接受名称、是否失败构建的布尔值和闭包。然后我也可以防止错误被隐藏。所有这些都带来了更多的复杂性,但对于我们的工作来说,这是值得的。 - Petra Kahn
如果你有并行阶段,这种方法是行不通的。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接