Determine Failed Stage in Jenkins Declarative Pipeline
You can use a post
directive in each stage, to act on failure with specific actions and notifications.
It's not exactly ideal as if you want that in all stages you'd have to repeat it though, and I don't think you can access your stage name dynamically, so it's really verbos and hard-coded. You could probably refactor that to use a library though.
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)'
}
}
}
Overview
This can be achieved generically using Blue Ocean plugin API. Class PipelineNodeGraphVisitor
can be used to iterate over all pipeline nodes (such as stages, parallel branches and steps). We just have to check if the type
property of FlowNodeWrapper
equals FlowNodeWrapper.NodeType.STAGE
.
Additionally we can get the failure cause from the ErrorAction
s stored in the nodes.
Code
You would typically put the following code into a shared library, because it would prevent the pipeline from running in sandbox environment, if inserted directly into pipeline code.
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' }
}
Demo Pipeline
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
}
}
}
}
Blue Ocean View
Notes
If you want to get stages with other results than FAILURE
, have a look at my function getFailedStages()
. You can simply change the condition, e. g.:
it.result in ['FAILURE','UNSTABLE']
- get unstable stages aswell
it.result != 'SUCCESS'
- get all unsuccesfull stages, which also includes skipped stages
Possible alternative implementation:
Strictly spoken, Blue Ocean API is not necessary. It just simplifies the code alot. You can do the same using only basic Jenkins pipeline API. As a starting point, look for FlowGraphWalker
for iterating over the pipeline nodes. Have a look at the code of Blue Ocean's PipelineNodeGraphVisitor
to find out how they determine the "Stage" node type.
PipelineVisitor is a fine approach. However, if you want to see just the errors, then leveraging FlowGraphTable
might be even better.
The following provides a list of maps for each failed steps, and traverses the downstream jobs as well. I find it to be pretty useful.
You'll want to use a shared library to avoid the security sandbox warnings / approvals
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
}