Jenkinsfile/Groovy: why is result of dictionary AND integer an integer? - jenkins

In Groovy/Jenkinsfile declarative syntax, why is the result of the boolean AND operation on dictionary, dictionary, and integer objects an integer instead of boolean true/false?
pipeline {
agent any
stages {
stage( "1" ) {
steps {
script {
a = [:]
a.a = [:]
a.a["a"] = "1"
a.a["b"] = "2"
echo "${a}"
echo "${a.a}"
echo "${a.a.size()}"
def my_bool = (a && a.a && a.a.size())
echo "my_bool ${my_bool}"
}
}
}
stage( "2" ) {
when {
expression { true == (a && a.a && a.a.size()) } // Fails because result is integer "2", not boolean "true"
}
steps {
script {
echo "hello, world!"
}
}
}
}
}
My biases from other programming languages led me to think that a && a.a && a.a.size() should implicitly be converted to a boolean value. The echo reveals that the value is integer 2.
What is the Jenkins/Groovy idiomatic way to deal with this? I.e. if a stage is conditional on "dictionaries being non-null and having nonzero size", what is the idiomatically correct/preferred way to write that conditional?
Update:
Note: the echo "my_bool ${my_bool}" statement prints "my_bool 2". This is with Jenkins version 2.222.3.

expression { a?.a?.size() }
or even
expression { a?.a }

Related

Sort a Map with a custom comparator in Groovy

final_map = ["/7amd64-Aug2022.1":"2022-08-09","/7amd64-Oct2022.1":"2022-10-12","/7":"2022-11-08","/7amd64-Jul2022.1":"2022-07-12","/7amd64":"2022-11-08","/7amd64-June2022.1":"2022-06-14","/7amd64-beta":"2022-11-08","/7amd64-Sep2022.1":"2022-09-14","/7amd64-Nov2022.1":"2022-11-08","/_uploads":"2022-11-08"]
Jenkins Pipeline (below is the code I have, which is not working)
result = final_map.sort { a,b -> a.value <=> b.value }
echo "Output: ${result}"
Expecting to sort the map with date (value).
You can use a custom comparator for this. Check the following Groovy code.
final_map = ["/7amd64-Aug2022.1":"2022-08-09","/7amd64-Oct2022.1":"2022-10-12","/7":"2022-11-08","/7amd64-Jul2022.1":"2022-07-12","/7amd64":"2022-11-08","/7amd64-June2022.1":"2022-06-14","/7amd64-beta":"2022-11-08","/7amd64-Sep2022.1":"2022-09-14","/7amd64-Nov2022.1":"2022-11-08","/_uploads":"2022-11-08"]
final_map.sort { s1, s2 ->
def s1Date = new Date(s1.value.replace('-', '/'))
def s2Date = new Date(s2.value.replace('-', '/'))
if( s1Date.before(s2Date)) {
return -1
} else {
return 1
}
}
println final_map

How to write script to perform sum of integers in jenkins , by taking input from parameters

I want to perform sum of integers (c = a+b) in jenkins, initially i have defined the value of a by giving def a = 5, but I want to take values of b from parameters . So i added string parameter but this is not considering as integer ,instead it is just attaching the 2 values , is there any way so that i can take inputs of b from parameter and perform addition
the pipeline is as follows
pipeline {
agent any
stages {
stage('Stage 1') {
steps {
script{
def a = 5;
//def b = "${params.inputvalue}";
c = "${a + b}" ;
echo "value of c is ${c}"
}
}
}
}
}
in parameter if i give value of b as 2 the output it's giving as 25 but the expected output is 7 i.e 2+5
I could solve this by converting string parameter to integer
int a = 10;
stage('arithmetic stage') {
int b = params.Value;
c = a + b;
echo "${c}"
}
here "Value" is String parameter name .

Groovy multiline string interpolation whitespace

I am trying to generate some generic Groovy code for Jenkins but I seem to have trouble with multi line strings and extra white space. I've tried everything I could find by Googling but I can't seem to get it working.
My issue isn't related to simple multi line strings. I managed to trim white space by using the stripIndent() and stripMargin() methods for simple cases. My issue is caused by having interpolated methods inside my strings.
Groovy info: Groovy Version: 3.0.2 JVM: 13.0.2 Vendor: Oracle Corporation OS: Mac OS X
String method2(String tier, String jobName) {
return """
Map downstreamJobs = [:]
stage ("${jobName}-${tier}-\${region}_${jobName}") {
test
}
""".stripIndent().stripMargin()
}
static String simpleLog() {
return """
script {
def user = env.BUILD_USER_ID
}
""".stripIndent().stripMargin()
}
static String method1() {
return """\
import jenkins.model.Jenkins
currentBuild.displayName = "name"
${simpleLog()}
""".stripIndent().stripMargin()
}
String generateFullDeploymentPipelineCode() {
return """Text here
${method1()}
${method2("test1", "test2")}
""".stripIndent().stripMargin()
}
println(generateFullDeploymentPipelineCode())
This is what it prints(or writes to disk):
Text here
import jenkins.model.Jenkins
currentBuild.displayName = "name"
script {
def user = env.BUILD_USER_ID
}
Map downstreamJobs = [:]
stage ("test2-test1-${region}_test2") {
test
}
Why the extra space around the import lines? I know the indentation method is supposed to trim all white space according to the least number of leading spaces, so that's why we use backslash (example here https://stackoverflow.com/a/19882917/7569335).
That works for simple strings, but it breaks down once use start using interpolation. Not with regular variables, just when you interpolate an entire method.
as variant - use just stripMargin() and only once on a final string
String method2(String tier, String jobName) {
return """\
|Map downstreamJobs = [:]
|stage ("${jobName}-${tier}-\${region}_${jobName}") {
| test
|}
"""
}
static String simpleLog() {
return """\
|script {
| def user = env.BUILD_USER_ID
|}
"""
}
static String method1() {
return """\
|import jenkins.model.Jenkins
|currentBuild.displayName = "name"
${simpleLog()}
"""
}
String generateFullDeploymentPipelineCode() {
return """\
|Text here
${method1()}
${method2("test1", "test2")}
""".stripIndent().stripMargin()
}
println(generateFullDeploymentPipelineCode())
result:
Text here
import jenkins.model.Jenkins
currentBuild.displayName = "name"
script {
def user = env.BUILD_USER_ID
}
Map downstreamJobs = [:]
stage ("test2-test1-${region}_test2") {
test
}
another variant with trim() and stripIndent()
def method2(String tier, String jobName) {
return """
Map downstreamJobs = [:]
stage ("${jobName}-${tier}-\${region}_${jobName}") {
test
}
""".trim()
}
def simpleLog() {
return """
script {
def user = env.BUILD_USER_ID
}
""".trim()
}
def method1() {
return """
import jenkins.model.Jenkins
currentBuild.displayName = "name"
${simpleLog()}
""".trim()
}
def generateFullDeploymentPipelineCode() {
return """\
Text here
${method1()}
${method2("test1", "test2")}
""".stripIndent()
}
println(generateFullDeploymentPipelineCode())
When you insert a string through interpolation you only indent the first line of it. The following lines of the inserted string will be indented differently, which messes everything up.
Using some lesser-known members of GString (namely .strings[] and .values[]), we can align the indentation of all lines of each interpolated value.
String method2(String tier, String jobName) {
indented """
Map downstreamJobs = [:]
stage ("${jobName}-${tier}-\${region}_${jobName}") {
test
}
"""
}
String simpleLog() {
indented """\
script {
def user = env.BUILD_USER_ID
}
"""
}
String method1() {
indented """\
import jenkins.model.Jenkins
currentBuild.displayName = "name"
${simpleLog()}
"""
}
String generateFullDeploymentPipelineCode() {
indented """\
Text here
${method1()}
${method2("test1", "test2")}
"""
}
println generateFullDeploymentPipelineCode()
//---------- Move the following code into its own script ----------
// Function to adjust the indentation of interpolated values so that all lines
// of a value match the indentation of the first line.
// Finally stripIndent() will be called before returning the string.
String indented( GString templ ) {
// Iterate over the interpolated values of the GString template.
templ.values.eachWithIndex{ value, i ->
// Get the string preceding the current value. Always defined, even
// when the value is at the beginning of the template.
def beforeValue = templ.strings[ i ]
// RegEx to match any indent substring before the value.
// Special case for the first string, which doesn't necessarily contain '\n'.
def regexIndent = i == 0
? /(?:^|\n)([ \t]+)$/
: /\n([ \t]+)$/
def matchIndent = ( beforeValue =~ regexIndent )
if( matchIndent ) {
def indent = matchIndent[ 0 ][ 1 ]
def lines = value.readLines()
def linesNew = [ lines.head() ] // The 1st line is already indented.
// Insert the indentation from the 1st line into all subsequent lines.
linesNew += lines.tail().collect{ indent + it }
// Finally replace the value with the reformatted lines.
templ.values[ i ] = linesNew.join('\n')
}
}
return templ.stripIndent()
}
// Fallback in case the input string is not a GString (when it doesn't contain expressions)
String indented( String templ ) {
return templ.stripIndent()
}
Live Demo at codingground
Output:
Text here
import jenkins.model.Jenkins
currentBuild.displayName = "name"
script {
def user = env.BUILD_USER_ID
}
Map downstreamJobs = [:]
stage ("test2-test1-${region}_test2") {
test
}
Conclusion:
Using the indented function, a clean Groovy syntax for generating code from GString templates has been achieved.
This was quite a learning experience. I first tried to do it completely different using the evaluate function, which turned out to be too complicated and not so flexible. Then I randomly browsed through some posts from mrhaki blog (always a good read!) until I discovered "Groovy Goodness: Get to Know More About a GString". This was the key to implementing this solution.

Checking for null pointer exception in an array in Jenkins scripted pipeline method

I am injecting Active Choices parameter value(s) in the Jenkins scripted pipeline.
PFB sample values passed to active choice parameter block:
return['ABC','DEF','GHI',JKL']
PFB my sample script:
node(){
selectModName()
}
def selectModName(){
stage 'Multi selection'
String[] mods = "${modName}".split(',')
modsz = mods.size()
echo ''+modsz+''
for(mod in mods){
if (modsz == null || modsz == 0){
echo 'There is nothing to be printed'
} else {
echo ''+mod+' is name of the module \n'
}
}
}
The else block is executed when I pass greater than or equal to 1 value(s) (working as intended). But if block is not executing its logic when I don't pass any parameter and press build now.
Funny thing is- size() is returning 1 instead of 0 (echo ''+modsz+'') when values aren't passed.
How to make if block execute its logic when no values are passed?
Your code always jumps to the "else" block, because
"".split(',')
produces an array with a single empty string.
assert "".split(',').size() == 1
assert "".splti(',') == [""] as String[]
When you use active choice parameter with multiple values selection and you don't select anything, your variable name stores an empty string. You should check first if the modName parameter is not an empty string and only otherwise split and display values.
node(){
selectModName()
}
def selectModName(){
stage 'Multi selection'
if (modName) {
String[] mods = modName?.split(',')
for (mod in mods) {
echo " ${mod} is name of the module"
}
} else {
echo 'There is nothing to be printed'
}
}

Trouble using catch in simple tcl proc

I have the following proc which basically looks upp a couple of values in a dictonary and returns them as a list.
proc GetAllow { PID Ply } {
# read a dictonary from a file
catch {
append PlyAndDirXt $Ply "_xt"
append PlyAndDirYt $Ply "_yt"
set x_allow_tens [ dict get $allowables $PID $PlyAndDirXt ]
set y_allow_tens [ dict get $allowables $PID $PlyAndDirYt ]
set allowables [ list $x_allow_tens $y_allow_tens ]
} res
if { $res == 0 } {
return $allowables
}
if { $res != 0 } {
return 999
}
}
As I understand "catch" if everything is ok $res should be 0 = TCL_OK. In that case I would like the proc to return the list $allowables.
In case the values are not found in the dict due to none matching keys. I would like it to return 999. But I always get 999 back. What am I'm doing wrong here ?
As per the manual:
If script raises an error, catch will return a non-zero integer value corresponding to the exceptional return code returned by evaluation of script. Tcl defines the normal return code from script evaluation to be zero (0), or TCL_OK.
If the varName argument is given, then the variable it names is set to the result of the script evaluation. When the return code from the script is 1 (TCL_ERROR), the value stored in varName is an error message. When the return code from the script is 0 (TCL_OK), the value stored in resultVarName is the value returned from script.
As such, $res will not be equal to 0 unless the result of your script returns 0.
You can set catch to a variable like this:
set err [catch {
append PlyAndDirXt $Ply "_xt"
append PlyAndDirYt $Ply "_yt"
set x_allow_tens [ dict get $allowables $PID $PlyAndDirXt ]
set y_allow_tens [ dict get $allowables $PID $PlyAndDirYt ]
set allowables [ list $x_allow_tens $y_allow_tens ]
} res]
Then check
if { $err == 0 } {
return $allowables ;# Or return $res since that's the last evaluated line
}
if { $err != 0 } {
return 999
}

Resources