How to print out a map in an email body?

Hi, I have a declarative pipeline that executes parallel stages. The exit code of each stage is assigned to map results and printed to the log console:

def p = [:]         // map for parallel stages
def results = [:]   // map of exit code for each stage

pipeline {

    stages {
        stage('run') {
            steps {
                script {
                    for (file in files_list) {
                        p[file] = {
                            node(myagent) {
                                results[file] = sh returnStatus: true, script:'./myApp $file'
                            }
                        }
                    }

                    parallel p

                    // Print the results
                    for (def key in results.keySet()) {
                        println "file = ${key}, exit code = ${results[key]}"
                    }
                }
            }
        }  
    }

    post {
        success {
            script { emailext (subject: "${env.JOB_NAME}: Build #${env.BUILD_NUMBER} - Successful!",
                                body: '${DEFAULT_CONTENT}',
                                recipientProviders: [buildUser()])
            }
        }
    }
}

How might I include the map printout in the body of the notification email sent by the success post-stage?

There are several common pitfalls in the above example:

1 - Avoid for loops in EVERYWHERE in Jenkins, they are very likely to not do what you expect, instead use closure methods - i.e. for (file in files_list) becomes files_list.each { String file -> ... } - the logic is essentially like a loop, but it is far more reliable. You can also do more advanced things like Map tasks = files_list.collectEntries { String file -> return (file, { sh ... }} }

Example that demos this:

List files_list = ['one', 'two', 'three']
Map tasks_for = [:]
// Wrong way
for (file in files_list) {
    tasks_for[file] = { echo "With 'for' we got: ${file}" }
}
// Right way
Map tasks_each = [:]
files_list.each { String file ->
    tasks_each[file] = { echo "With 'each' we got: ${file}" }
}

parallel tasks_for
parallel tasks_each

you will get some very different results with these:

[one]   With 'for' we got: three
[two]   With 'for' we got: three
[three] With 'for' we got: three
[one]   With 'each' we got: one
[two]   With 'each' we got: two
[three] With 'each' we got: three

What happens here is that the context within a closure is shared with the closure owner, so with for loop, all closures use the same instance of the variable file, using its value at the time the closure is executed which is AFTER the loop is over, so they all get the last value provided

2 - Even with collection iteration methods like each {} you can very easily get into trouble.

Consider this variant:

List files_list = ['one', 'two', 'three']
Map tasks_each = [:]

files_list.each { String filename ->
    file = "${filename}.tmp"
    echo "Converted ${filename} into ${file} "
    tasks_each[file] = { 
        echo "We got: ${file} from ${filename}" 
    }
}

parallel tasks_each

You get:

            Converted one into one.tmp 
            Converted two into two.tmp 
            Converted three into three.tmp 
[one.tmp]   We got: three.tmp from one
[two.tmp]   We got: three.tmp from two
[three.tmp] We got: three.tmp from three

This is the same context problem as before because in this case file is shared with all. While you can manually ensure that every variable is isolated, much easier way to do this is to generate the closure in an external method and call the method to create the task. Each call creates a new context and it will force you to isolate each run.

3 - Now, your actual question is similar - short answer is - use Collection methods:

Same as what you have (assuming the value is an int):

results.each { String branch, int exitCode ->
  echo "file = ${branch}, exit code = ${exitCode}"
}

Or you can output it all as a single echo statement:

// Generate list of strings
List resultStrings = results.collect { String branch, int exitCode ->
  "file = ${branch}, exit code = ${exitCode}"
}
// Join them into a newline delimited string
String resultInfoText = resultStrings.join('\n')

// Print results
echo "Results are:\n${resultInfoText}"

// or use it anywhere there is a need for text, including email

HTH

3 Likes

Thanks very much for your answer. It’s very helpful.
TBH I know very little Groovy. I have gotten away with using just declarative pipeline until this application. I guess I will have to learn some Groovy now.

1 Like