Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why an each loop in a Jenkinsfile stops at first iteration

Here is the content of my Jenkinsfile :

node {
    // prints only the first element 'a'
    [ 'a', 'b', 'c' ].each {
        echo it
    }
}

When executing the job in Jenkins (with the Pipeline plugin), only the first item in the list is printed.

Can someone explain me this strange behavior? Is it a bug? or is it just me not understanding the Groovy syntax?

Edit : the for (i in items) works as expected :

node {
    // prints 'a', 'b' and 'c'
    for (i in [ 'a', 'b', 'c' ]) {
        echo i
    }
}
like image 812
Eric Citaire Avatar asked Jun 02 '16 14:06

Eric Citaire


4 Answers

The accepted answer here states that it's a known bug, and uses a workaround that didn't work for me, so I'll offer an update with what I've found lately.

Despite the resolution of JENKINS-26481 (fairly recent, as of this writing) many people may be stuck with an older version of Jenkins where the fix is not available. For-loop iteration over a literal list might work sometimes but related issues like JENKINS-46749 and JENKINS-46747 seem to continue to bedevil many users. Also, depending on the exact context in your Jenkinsfile, possibly echo will work whereas sh fails, and things might fail silently or they might crash the build with serialization failures.

If you dislike surprises (skipped loops and silent failures) and if you want your Jenkinsfiles to be the most portable across multiple versions of Jenkins, the main idea seems to be that you should always use classic counters in your for-loops and ignore other groovy features.

This gist is the best reference I've seen and spells out many cases that you'd think should work the same but have surprisingly different behaviour. It's a good starting place to establish sanity checks and debug your setup, regardless of what kind of iteration you're looking at and regardless of whether you're trying to use @NonCPS, do your iteration directly inside node{}, or call a separate function.

Again, I take no credit for the work itself but I'm embedding the gist of iteration test cases below for posterity:

abcs = ['a', 'b', 'c']

node('master') {
    stage('Test 1: loop of echo statements') {
        echo_all(abcs)
    }
    stage('Test 2: loop of sh commands') {
        loop_of_sh(abcs)
    }
    stage('Test 3: loop with preceding SH') {
        loop_with_preceding_sh(abcs)
    }
    stage('Test 4: traditional for loop') {
        traditional_int_for_loop(abcs)
    }
}

@NonCPS // has to be NonCPS or the build breaks on the call to .each
def echo_all(list) {
    list.each { item ->
        echo "Hello ${item}"
    }
}
// outputs all items as expected

@NonCPS
def loop_of_sh(list) {
    list.each { item ->
        sh "echo Hello ${item}"
    }
}
// outputs only the first item

@NonCPS
def loop_with_preceding_sh(list) {
    sh "echo Going to echo a list"
    list.each { item ->
        sh "echo Hello ${item}"
    }
}
// outputs only the "Going to echo a list" bit

//No NonCPS required
def traditional_int_for_loop(list) {
    sh "echo Going to echo a list"
    for (int i = 0; i < list.size(); i++) {
        sh "echo Hello ${list[i]}"
    }
}
// echoes everything as expected
like image 73
mvr Avatar answered Oct 07 '22 16:10

mvr


Thanks to @batmat on #jenkins IRC channel for answering this question!

It's actually a known bug : JENKINS-26481.

like image 24
Eric Citaire Avatar answered Oct 07 '22 16:10

Eric Citaire


A workaround for this issue is to expand all the commands to a flat text file as a groovy script. Then use load step to load the file and execute.

For example:

@NonCPS
def createScript(){
    def cmd=""
    for (i in [ 'a', 'b', 'c' ]) {
        cmd = cmd+ "echo $i"
    }
    writeFile file: 'steps.groovy', text: cmd
}

Then call the function like

createScript()
load 'steps.groovy'
like image 2
Shengyue Avatar answered Oct 07 '22 15:10

Shengyue


Here is example loop example with curl without NonCPS :

#!/usr/bin/env groovy

node('master') {
    stagesWithTry([
        'https://google.com/',
        'https://github.com',
        'https://releases.hashicorp.com/',
        'https://kubernetes-charts.storage.googleapis.com',
        'https://gcsweb.istio.io/gcs/istio-release/releases'
    ])
    stage ('ALlinOneStage'){
        stepsWithTry([
            'https://google.com/',
            'https://github.com',
            'https://releases.hashicorp.com/',
            'https://kubernetes-charts.storage.googleapis.com',
            'https://gcsweb.istio.io/gcs/istio-release/releases'
        ])
    }
}
//loop in one stage
def stepsWithTry(list){
    for (int i = 0; i < list.size(); i++) {
        try {
        sh "curl --connect-timeout 15 -v -L ${list[i]}"
        } catch (Exception e) {
            echo "Stage failed, but we continue"
        }
    }
}
//loop in multiple stage
def stagesWithTry(list){
    for (int i = 0; i < list.size(); i++) {
        try {
            stage(list[i]){
          sh "curl --connect-timeout 15 -v -L ${list[i]}"
            }
        } catch (Exception e) {
            echo "Stage failed, but we continue"
        }
    }
}

like image 1
airdata Avatar answered Oct 07 '22 14:10

airdata