6

My pipeline setup is as follows. enter image description here

I need to get it working with adherence to following conditions. Help me defining when blocks and other code to be used and in which stages?

  1. If A fails, no other stage is executed and job run is marked failed.
  2. If any of the B stage fails, then corresponding C stage should not be invoked.
  3. Stage D should get executed when either C1 or C2 have been executed regardless of failure in their execution.
  4. Also, if any of the stages have been failed, then the over all job run status should be fail.

What have I tried & observed? From above conditions defined, 1 and 2 are working as expected but not 3 and 4 with my following attempt.

In C1 and C2, I added catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') referring to Continue Jenkins pipeline past failed stage.

But what I have observed is -

  1. D executes if C1 or C2 fails, but over all job run is marked Success. Expected is failure as one of the C steps have failed. But due to catch it gets success status.
  2. When any of the B stages fail, their corresponding C also doesn't execute (expected) but it doesn't trigger D either. As I need D to be triggered because some other C has been executed.
Nazil Khan
  • 135
  • 2
  • 9
  • I see [JENKINS-57826](https://issues.jenkins-ci.org/browse/JENKINS-57826) but not sure will it solve the issue. – Nazil Khan Sep 07 '19 at 09:13
  • [catchError docs](https://jenkins.io/doc/pipeline/steps/workflow-basic-steps/#catcherror-catch-error-and-set-build-result-to-failure) for reference. – Nazil Khan Sep 07 '19 at 09:18

1 Answers1

9

This is what you need:

stageResultMap = [:]

pipeline {
    agent any
    stages {
        stage('A') {
            steps {
                println("This is stage: ${STAGE_NAME}")
            }
        }
        stage('BC') {
            parallel {
                stage ('1'){
                    stages {
                        stage('B1') {
                            steps {
                                script {
                                    // Catch exceptions, set the stage result as unstable,
                                    // build result as failure, and the variable didB1Succeed to false
                                    try {                                        
                                        sh "exit 1"
                                        stageResultMap.didB1Succeed = true
                                    }
                                    catch (Exception e) {
                                        unstable("${STAGE_NAME} failed!")
                                        currentBuild.result = 'FAILURE'
                                        stageResultMap.didB1Succeed = false                                        
                                    }
                                }
                            }
                        }
                        stage('C1') {
                            // Execute only if B1 succeeded
                            when {
                                expression {
                                    return stageResultMap.find{ it.key == "didB1Succeed" }?.value
                                }
                            }
                            steps {
                                // Mark the stage and build results as failure on error but continue pipeline execution
                                catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
                                    sh "echo Hello"
                                }
                            }
                        }
                    } 
                }
                stage ('2'){
                    stages {
                        stage('B2') {
                            steps {
                                script {
                                    // Catch exceptions, set the stage result as unstable,
                                    // build result as failure, and the variable didB2Succeed to false
                                    try {
                                        sh "echo Hello"
                                        stageResultMap.didB2Succeed = true
                                    }
                                    catch (Exception e) {
                                        unstable("${STAGE_NAME} failed!")
                                        currentBuild.result = 'FAILURE'
                                        stageResultMap.didB2Succeed = false                                        
                                    }
                                }
                            }
                        }
                        stage('C2') {
                            // Execute only if B2 succeeded
                            when {
                                expression {
                                    return stageResultMap.find{ it.key == "didB2Succeed" }?.value
                                }
                            }
                            steps {
                                // Mark the stage and build results as failure on error but continue pipeline execution
                                catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
                                    sh "echo Hello"
                                }
                            }
                        }
                    } 
                }
            }
        }
        stage('D') {
            // Execute only when C1 or C2 have executed, that is B1 or B2 have succeeded
            when {
                expression {
                    return stageResultMap.any {it.value}
                }
            }
            steps {
                println("This is stage: ${STAGE_NAME}")
            }
        }
    }
}
  1. For stages C1 & C2, use catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') to mark both the stage and build results as FAILURE but continue pipeline execution.

enter image description here enter image description here

  1. For stages B1 & B2, first initialize an empty map stageResultMap = [:] at the top of your Jenkinsfile to capture the results of each stage. Now, for every Bn stage, use a try-catch block so that on success a unique key-value pair stageResultsMap.didBnSucceed = true is added to the map, and on exception the stage result is set to UNSTABLE, build result to FAILURE, and stageResultsMap.didBnSucceed = false. We use the method unstable here because none of the other two methods catchError or warnError let us add to the map. Then, in the corresponding Cn stage, evaluate the map and execute only if Bn did succeed. Also, if both B1 and B2 fail, i.e., both C1 and C2 do not execute, D wouldn't execute either because when {expression {return stageResultMap.any {it.value}}} will evaluate to false. But D will execute if either C1 or C2 executed regardless of failures.

enter image description here enter image description here

In both the above scenarios, the overall build status will be marked as FAILURE if any of the stages fail.

Of course, a green build on no failures.

enter image description here

Dibakar Aditya
  • 3,893
  • 1
  • 14
  • 25
  • Also can you refer to some docs on scope of vars defined like `didB1Succeed` and `didB2Succeed`. Because if I use identifier `didBSucceed` everywhere, what I observer is a scope creep in i.e. value set in B1 overrides B2 value based on order of execution. If it is true, then it is important in your solution to highlight that the identifiers for each stage has to be unique. – Nazil Khan Sep 08 '19 at 04:42
  • 1
    I get it now. I have updated the pipeline as per your previous comment. Stage D will now execute only if any of C1 and C2 executes, even if they fail. Regarding the scope of variables, you are correct and in this case, they are propagated to the very end of the pipeline until the `post` block, if any. You can limit their scope to the `script {...}` block by prefixing `def` but that doesn't serve us well here. – Dibakar Aditya Sep 08 '19 at 05:13
  • Yes, you got a (close enough) solution. Assuming, there can be `n` parallel stages, it will require unique identifier names like `didBnSucceed` and similarly in `when` block of stage D `didB1Succeed || .. || didBnSucceed`. Can it be made generic, may be using map to store the flags and then in `when` block of D, `anyOf` can be used to check if any of the values in map is true? – Nazil Khan Sep 08 '19 at 05:14
  • maps are better over list, as you can set the flag in Stage `Bn` and appropriately check the same flag in Stage `Cn` regardless of execution order. Also in Stage `D`, `when { expression { return map.values.any() } }`. Will you please update your response with these changes, so I mark it as accepted answer. – Nazil Khan Sep 08 '19 at 06:19
  • There you go. The answer has been updated to use maps. – Dibakar Aditya Sep 08 '19 at 07:16
  • Instead of using map to store state results, is there something native in Jenkins pipeline to track that? E.g. in case of build, `currentBuild.previousBuild.result` – Nazil Khan Sep 10 '19 at 06:59
  • 1
    Unfortunately not at the moment. In fact, the ability to set individual stage results is a fairly new addition. – Dibakar Aditya Sep 10 '19 at 07:01