conditional execution in Jenkins nested stages pipelines - jenkins

I've seen Conditional execution based on result of previous stage in declarative pipeline and I got a flavour of the question: how the things would look if we got a simplified workflow like
A-BC-{B1-B2-B3}-D in sequential mode?
What I'm trying to achieve is to have stage B2 executed only if B1 succeeds and B3 should be executed on condition B1 fails and finally proceed with D.
Thanks for the input.

The following did work for me - define an extra map stageResultMap.ObjectdircreationFailed = true in the exception phase and call it in the stage which should be invoked upon failure. Example below.
stage ('Build'){
stages {
stage ('Objectdircreation'){
steps {
script {
try {
sh'''#!/usr/bin/env bash
PATH=/sdev/user/bin:/opt/rational/clearcase/bin:$PATH
export PATH
mkdir -p $OBJECTDIR
'''
stageResultMap.didObjectdircreationSucceed = true
}
catch (Exception e) {
unstable("${STAGE_NAME} failed!")
currentBuild.result = 'FAILURE'
stageResultMap.didObjectdircreationSucceed = false
//set an extra map to true and test it
stageResultMap.ObjectdircreationFailed = true
}
}
}
} // end of Objectdircreation stage
stage ('Inject objects under an objdir'){
//
// execute only if Objectdircreation succeeded
//
when {
expression {
return stageResultMap.find{ it.key == "didObjectdircreationcreationSucceed" }?.value
}
}
steps {
catchError(buildResult: 'FAILURE', stageResult: 'FAILURE')
{
sh """#!/usr/bin/env bash
make ${ARGS} $OBJDIRPREFIX=$OBJDIR build
"""
}
}
} // end of build under an object tree
stage ('Inject build under current objdir') {
//
// execute only if object dir creation failed
//
when {
expression {
return stageResultMap.find{ it.key == "didProjecttreecreationFailed" }?.value
}
}
steps {
catchError(buildResult: 'FAILURE', stageResult: 'FAILURE') {
sh """#!/usr/bin/env bash
make ${ARGS} build
"""
}
}
} // end of build under root
} // end of internal build stages
//
} // end of build nested stages
} // end of main stages section
//
//
} //pipeline end

Related

Jenkins Declarative Pipeline - Running multiple things paralellel but skip "branch" if earlier failure occured

I wanna build one Jenkins pipeline that builds and runs tests on multiple versions of a program (e.g. Different databases)
But when any step fails, I want to skip the following steps only for that "branch" so to speak..
This is my example Code where Stage 1 is run first, with possible parallel steps (1.a, 1.b). The code does not work and is only some sort of example of how I would like it to work:
pipeline {
agent any
environment {
stageOneFailed = "false"
stageTwoFailed = "false"
}
stages {
stage ("Stage 1") {
parallel {
stage("Stage 1.a") {
// Something like this maybe?
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
// Do stuff here..
}
}
post {
unsuccessful {
// When stage did not succeed..
// Set stageOneFailed = "true"
}
}
}
stage("Stage 1.b") {
// Do Stuff..
// If Stage 1.b fails, set stageTwoFailed="true"
}
}
}
stage("Stage 2") {
parallel {
// Only run stages if earlier steps didn't fail
stage("Stage 2.a") {
when {
environment(name: "stageOneFailed", value: "false")
}
steps {
// Do stuff..
// If Stage 2.a fails, set stageOneFailed="true"
}
}
stage("Stage 2.b") {
when {
environment(name: "stageTwoFailed", value: "false")
}
steps {
// Do stuff..
// If Stage 2.b fails, set stageTwoFailed="true"
}
}
}
}
// stage()
}
}
Can anyone give any advice on how to do this the proper way?
Thanks in advance
EDIT: Changed code example. The example runs now!
pipeline {
agent any
environment {
stageOneFailed = "false"
stageTwoFailed = "false"
}
stages {
stage ("Stage 1") {
parallel {
stage("Stage 1.a") {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
bat "ech Stage 1.a" // Should fail because ech is no valid command
}
}
post {
failure {
script {
env.stageOneFailed = "true"
}
}
}
}
stage("Stage 1.b") {
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
bat "echo Stage 1.b" // Should not fail
}
}
post {
failure {
script {
env.stageTwoFailed = "true"
}
}
}
}
}
}
stage("Stage 2") {
parallel {
// Only run stages if earlier steps didn't fail
stage("Stage 2.a") {
when {
environment(name: "stageOneFailed", value: "false")
}
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
bat "echo Stage 2.a"
}
}
post {
failure {
script {
env.stageOneFailed = "true"
}
}
}
}
stage("Stage 2.b") {
when {
environment(name: "stageTwoFailed", value: "false")
}
steps {
catchError(buildResult: 'UNSTABLE', stageResult: 'FAILURE') {
bat "echo Stage 2.b"
}
}
post {
failure {
script {
env.stageTwoFailed = "true"
}
}
}
}
}
}
}
}
But when running the example, the Stage 1.a fails but Stage 2.a is still run, maybe anyone could help out here..
EDIT: I added output to see, what value stageNFailed is set to. Even after calling env.stageOneFailed, when going into next stage, it takes the old value false..
My assumption is, that when calling script env.stageNFailed = "true", the value is only set temporarily for that stage..
The example you have used is a perfectly acceptable way to do it. You have introduced 2 env variables that get used to determine if the previous step failed. You have used catchError to ensure that the pipeline doesn't fail when the stage fails. You have to use catchError in every stage to prevent the pipeline from failing (but I guess you already know that). In the post part of the stage, you have set the appropriate env variable to true, which is also correct.
post {
failure {
script {
env.stageOneFailed = true
}
}
}
Then when the next relevant stage starts, you have used the when condition to check if the stage should be run (you could also do something like this):
when {
expression { stageOneFailed == false }
}
So basically you have done everything right.

Show a Jenkins pipeline build job stage as failed without failing the whole job

I have a Jenkins pipeline with some parallel stages that should not fail the job if they fail.
Those stages start a build job.
I started from https://stackoverflow.com/a/56975220/1817610.
The original sample works, but not if my stage builds another pipeline.
pipeline {
agent any
stages {
stage('1') {
steps {
sh 'exit 0'
}
}
stage('2') {
parallel {
stage('2.1') {
steps {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
build job: 'Failing pipeline'
}
}
}
stage('2.2') {
steps {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
build job: 'Succesful pipeline'
}
}
}
}
}
stage('3') {
steps {
sh 'exit 0'
}
}
}
}
See build 7 in screenshot
If I changed the stage to
stage('2.1') {
steps {
build job: 'Failing pipeline', propagate: false
}
}
The job does not fail, but also the stage does not fail, see build 8.
I'd like to have the global state as successful but still showing that one of the builds failed.
you could make use of pure groovy try..catch block and control your SUCCESS and FAILURE with some condition.
Below is an example:
pipeline {
agent any;
stages {
stage('01') {
steps {
sh "echo Hello"
}
}
stage('02') {
parallel {
stage('02.1') {
steps {
script {
try {
def v = 10/0
println v
}catch(Exception e) {
println e
}
}
}
}
stage('02.2') {
steps {
script {
try {
def v = 10 % 2
println v
}catch(Exception e) {
println e
}
}
}
}
}
}
}
}
In my example, my parallel stage 02.1 will fail with java.lang.ArithmeticException: Division by zero but catch block will handle it by catching the exception.
if you want to fail the build with some condition, you can put if..else condition inside catch {} and fail by throwing back the exception to Jenkins like
...
stage('02.1') {
steps {
script {
try {
def v = 10/0
println v
}catch(Exception e) {
if(someCondition) {
println e
} else {
throw e;
}
}
}
}

Jenkins Pipeline Get Current Stage Status After Using catchError

This is a follow-up to my earlier question:
Set a stage status in Jenkins Pipelines
It turns out I can keep a pipeline as SUCCESS but can mark an individual stage as UNSTABLE if I want via catchError like this:
node()
{
stage("Stage 1")
{
catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE')
{
sh 'exit 1'
}
}
}
If I want to get the current status of the pipeline itself, I can use currentBuild.getCurrentResult() but I don't see a currentStage analog to this.
I'm interested in trying out a pattern that might look something like this in my stages:
stage("1") {
catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
// do stuff
}
// perhaps more catchError() blocks
if(currentStage.getCurrentResult() == "UNSTABLE") {
// do something special if we're unstable
}
}
but that would fail because there's no currentStage available.
So basically, catchError() is nice but I'd like to know how I can catch the status change to my stage if it gets changed... Does anyone know how you access the status of the current stage you're in from a pipeline?
I did it like this (to keep the catchError):
def boolean test_results = false
pipeline {
...
stage( 'x' ) {
steps{
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
<Do some dangerous stuff here>
//
// If we reached here the above step hasn't failed
//
script { test_results = true }
}
}
}
stage( 'y' ) {
steps{
script{
if( test_results == true ) {
} else {
}
}
}
}
}
Though there is no direct method for accessing the result of a stage in a pipeline as of now, you can work around it. This is considering you are only interested in either SUCCESS or UNSTABLE stage results as per the question and not in FAILURE.
The workaround is to initialize an empty map at the top of your pipeline to store the result of each stage. Now, instead of the catchError() method, use the unstable() method in combination with a try-catch block. This is because the latter not only lets you set the result as unstable but also perform other operations such as add the result to the map in the except block. Then you can read this stored result from the map in your if statement.
Example
stageResults = [:]
...
stage("1") {
try {
// do stuff
// Add to map as SUCCESS on successful execution
stageResults."{STAGE_NAME}" = "SUCCESS"
} catch (Exception e) {
// Set the result and add to map as UNSTABLE on failure
unstable("[ERROR]: ${STAGE_NAME} failed!")
currentBuild.result = "SUCCESS"
stageResult."{STAGE_NAME}" = "UNSTABLE"
}
if(stageResults.find{ it.key == "{STAGE_NAME}" }?.value == "UNSTABLE") {
// do something special if we're unstable
}
}
As for me the most elegant way to do it, it's a post section.
In your example, you mark the stage as UNSTABLE so after that you can catch it using post->unstable.
stage("1") {
steps {
catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') {
error 'Something goes wrong'
}
}
post {
always { echo 'Executed on every build'}
unstable { echo 'Executed only if build is unstable (marked by catchError)'}
}
}
As an alternative to add to Dibakar Aditya's answer, it is possible to wrap everything in a function which resembles regular steps. I.e.:
stage("1") {
def localSuccess = catchLocalError {
// do stuff
}
if(!localSuccess) {
// do something special if we're unstable
}
}
boolean catchLocalError(Closure c) {
try {
c()
return true
} catch (Exception e) {
return false
}
}

How to exit from the Jenkins pipeline if a stage sets build fail/unstable status?

I have a declarative Jenkins pipeline with stage1, stage2, stage3 and so on. I want to stop stage2 from running if stage1 sets the build unstable/fail.
I know I can stop the steps in stage1 from running using return when the build is not success but couldn't find a way where I can just exit the pipeline without running the stages below stage1
Here is what I have:
stage('stage1') {
steps {
script{
//somesteps
if ("${stdout}" == "1"){
currentBuild.result = 'UNSTABLE'
return
} //if
//somesteps
} //script
} //steps
} //stage
// run only when stage1 is success
stage('stage2'){
when {
expression {
params.name ==~ /x|y/
}
}
steps {
script{
//stage2 steps
}
}
}
If params.name ==~ /z/ stage 3 will be executed skippping stage2
Note: I cannot include the steps in stage2/3/.. in stage1. It should be that way. Based on the build paramters stage2/3/4... will be called after stage1
The easiest way to skip remaining pipeline stages is to set up a variable which will control if following stages should be skipped or not. Something like this:
def skipRemainingStages = false
pipeline {
agent any
stages {
stage("Stage 1") {
steps {
script {
skipRemainingStages = true
println "skipRemainingStages = ${skipRemainingStages}"
}
}
}
stage("Stage 2") {
when {
expression {
!skipRemainingStages
}
}
steps {
script {
println "This text wont show up...."
}
}
}
stage("Stage 3") {
when {
expression {
!skipRemainingStages
}
}
steps {
script {
println "This text wont show up...."
}
}
}
}
}
This is very simple example that sets skipRemainingStages to true at Stage 1 and Stage 2 and Stage 3 get skipped because expression in the when block does not evaluates to true.
Alternatively you can call error(String message) step to stop the pipeline and set its status to FAILED. For example, if your stage 1 calls error(msg) step like:
stage("Stage 1") {
steps {
script {
error "This pipeline stops here!"
}
}
}
In this case pipeline stops whenever error(msg) step is found and all remaining stages are ignored (when blocks are not even checked).
Of course you can call error(msg) depending on some condition to make it FAILED only if specific conditions are met.
You can use post in a stage to exit as follows:
pipeline {
stages {
stage('stage 1') {
steps {
//step 1
}
}
stage('stage 2') {
steps {
script{
//step 2
}
}
post{
success {
}
failure {
script{
sh "exit 1"
//or
error "Failed, exiting now..."
}
}
aborted {
}
unstable {
script{
sh "exit 1"
//or
error "Unstable, exiting now..."
}
}
}
}
}
}
This will abort the build and job wouldn't run further.
You can also simply throw an Exception. That will abort the build.
In fact simply setting the build status in a catch clause works pretty well.
You can also then add custom logic in the finally block for sending notifications for build status changes (email, Slack message etc)
So perhaps something like the following. NOTE: I have copied some of this from an existing Jenkinsfile. So not 100% sure this is the same syntax as you were using:
pipeline {
try {
stages {
stage("stage1") {
if (something) {
throw new RuntimeException("Something went wrong")
}
}
stage("stage2") {
}
}
} catch (e) {
currentBuild.result = "FAILED"
throw e
}
}
You can try:
stage('Set skipRemainingStages variable which decides, whether to run next stages or not') {
steps {
script {
skipRemainingStages = true
try {
println("In if block")
skipRemainingStages = true
}
catch (Exception exc) {
println("Exception block: ${exc}")
skipRemainingStages = false
}
if (skipRemainingStages) {
currentBuild.result = 'FAILURE'
error("Stopping early!")
}
}
}
}
stage('This will not execute if skipRemainingStages=true')
{.
.
.
}
You can use mark the build as failed and then use sh "exit 1" to interrupt its execution in your Jenkins pipelines like below:
pipeline {
stages {
stage('stage 1') {
steps {
}
}
stage('stage 2') {
steps {
script{
if (something) {
currentBuild.result = "FAILURE"
sh "exit 1"
}
}
}
}
}
}

Show a Jenkins pipeline stage as failed without failing the whole job

Here's the code I'm playing with
node {
stage 'build'
echo 'build'
stage 'tests'
echo 'tests'
stage 'end-to-end-tests'
def e2e = build job:'end-to-end-tests', propagate: false
result = e2e.result
if (result.equals("SUCCESS")) {
stage 'deploy'
build 'deploy'
} else {
?????? I want to just fail this stage
}
}
Is there any way for me to mark the 'end-to-end-tests' stage as failed without failing the whole job? Propagate false just always marks the stage as true, which is not what I want, but Propagate true marks the job as failed which I also don't want.
This is now possible, even with declarative pipelines:
pipeline {
agent any
stages {
stage('1') {
steps {
sh 'exit 0'
}
}
stage('2') {
steps {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
sh "exit 1"
}
}
}
stage('3') {
steps {
sh 'exit 0'
}
}
}
}
In the example above, all stages will execute, the pipeline will be successful, but stage 2 will show as failed:
As you might have guessed, you can freely choose the buildResult and stageResult, in case you want it to be unstable or anything else. You can even fail the build and continue the execution of the pipeline.
Just make sure your Jenkins is up to date, since this is a fairly new feature.
Stage takes a block now, so wrap the stage in try-catch. Try-catch inside the stage makes it succeed.
The new feature mentioned earlier will be more powerful. In the meantime:
try {
stage('end-to-end-tests') {
node {
def e2e = build job:'end-to-end-tests', propagate: false
result = e2e.result
if (result.equals("SUCCESS")) {
} else {
sh "exit 1" // this fails the stage
}
}
}
} catch (e) {
result = "FAIL" // make sure other exceptions are recorded as failure too
}
stage('deploy') {
if (result.equals("SUCCESS")) {
build 'deploy'
} else {
echo "Cannot deploy without successful build" // it is important to have a deploy stage even here for the current visualization
}
}
Sounds like JENKINS-26522. Currently the best you can do is set an overall result:
if (result.equals("SUCCESS")) {
stage 'deploy'
build 'deploy'
} else {
currentBuild.result = e2e.result
// but continue
}
I recently tried to use vaza's answer
Show a Jenkins pipeline stage as failed without failing the whole job as template for writing a function that excutes a job in an own stage named like the job name. Surprisingly it worked, but maybe some groovy experts have a look at it :)
Here is how it looks like if one of the jobs is aborted:
def BuildJob(projectName) {
try {
stage(projectName) {
node {
def e2e = build job:projectName, propagate: false
result = e2e.result
if (result.equals("SUCCESS")) {
} else {
error 'FAIL' //sh "exit 1" // this fails the stage
}
}
}
} catch (e) {
currentBuild.result = 'UNSTABLE'
result = "FAIL" // make sure other exceptions are recorded as failure too
}
}
node {
BuildJob('job1')
BuildJob('job2')
}
In order to show a successful build with a failed stage when a downstream job fails AND support a user being able to cancel a build (including all subsequent stages), I had to use a combination of various solutions, specifically when, try/catch, throw and catchError().
env.GLOBAL_BUILD_ABORTED = false // Set if the user aborts the build
pipeline {
agent any
stages {
stage('First Stage') {
when { expression { env.GLOBAL_BUILD_ABORTED.toBoolean() == false } }
steps {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
myLocalBuildMethod('Stage #1, build #1')
myLocalBuildMethod('Stage #1, build #2')
}
}
}
stage('Second Stage') {
when { expression { env.GLOBAL_BUILD_ABORTED.toBoolean() == false } }
steps {
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
myLocalBuildMethod('Stage #2, build #1')
myLocalBuildMethod('Stage #2, build #2')
myLocalBuildMethod('Stage #2, build #3')
}
}
}
}
}
def myLocalBuildMethod(myString) {
/* Dummy method to show User Aborts vs Build Failures */
echo "My Local Build Method: " + myString
try {
build (
job: "Dummy_Downstream_Job"
)
} catch (e) {
/* Build Aborted by user - Stop All Test Executions */
if (e.getMessage().contains("was cancelled") || e.getMessage().contains("ABORTED")) {
env.GLOBAL_BUILD_ABORTED = true
}
/* Throw the execiption to be caught by catchError() to mark the stage failed. */
throw (e)
}
// Do other stuff...
}
You could add a explicit fail task, such as 'sh "not exist command"' in the stage.
if (result.equals("SUCCESS")) {
stage 'deploy'
build 'deploy'
} else {
try {
sh "not exist command"
}catch(e) {
}
}
Solution steps
You must emit an error in a stage to mark it as an error
Outside the scope of the stage, handle the exception and choose the build status
This makes the effect desired by a couple of users here, including myself, #user3768904, #Sviatlana
Success with failed Step Example
node("node-name") {
try {
stage("Process") {
error("This will fail")
}
} catch(Exception error) {
currentBuild.result = 'SUCCESS'
return
}
stage("Skipped") {
// This stage will never run
}
}
Aborted with failure Step Example
node("node-name") {
try {
stage("Process") {
error("This will fail")
}
} catch(Exception error) {
currentBuild.result = 'ABORTED'
return
}
stage("Skipped") {
// This stage will never run
}
}
You can use the following code in your else statement:
catchError(buildResult: 'SUCCESS', stageResult: 'FAILURE') {
error "some err msg"
}
This could be a general pattern showing how to customize the stage result with nice messages using the built-in functions and propagate the sub-job's result to the stage result. That the overall build is marked unstable if a sub-job is not successful is just a implementation choice for this example.
def run_sub_job() {
def jobBuild = build(job: 'foo', wait: true, propagate: false)
def result = jobBuild.getResult()
def msg = 'sub-job: ' + result
if ('SUCCESS' == result) {
println(msg)
} else if ('UNSTABLE' == result) {
unstable(msg) // will also set buildResult to UNSTABLE
} else { // anything else (FAILURE, ABORTED ...) is considered an error
catchError(
buildResult: 'UNSTABLE',
stageResult: result // propagate sub-job result
) {
error(msg)
}
}
}

Resources