0

I'm building a scripted Jenkinsfile (written in Groovy, which I do not know all that well -- nor do I know Java for that matter)

I have a particular string variable that for some reason doesn't want to behave properly and I'm perplexed.

import groovy.transform.Field
// THIS WORKS
@Field def FOO = "Mary had a little lamb"
@Field def BAR = "whose fleece was white as snow"
@Field def FOOBAR = "${FOO} ${BAR}"
echo "FOOBAR: ${FOOBAR}"
echo "FOO & BAR: ${FOO} ${BAR}"

results as you would expect

[Pipeline] echo
FOOBAR: Mary had a little lamb whose fleece was white as snow
[Pipeline] echo
FOO & BAR: Mary had a little lamb whose fleece was white as snow

However, the following:

// THIS DOES NOT WORK AS EXPECTED
@Field def BAR = "and she murdered it"
@Field def FOO = "Mary had a little lamb"
if (FOO) {
    BAR = "whose fleece was white as snow"
}
@Field def FOOBAR = "${FOO} ${BAR}"
echo "FOOBAR: ${FOOBAR}"
echo "FOO & BAR: ${FOO} ${BAR}"

results

[Pipeline] echo
FOOBAR: Mary had a little lamb and she murdered it
[Pipeline] echo
FOO & BAR: Mary had a little lamb whose fleece was white as snow

Is there some nuance to order of events in Java/Groovy that I'm not understanding? (I'm a python guy) BAR is overwritten but it would appear when defining the variable FOOBAR, it uses the original value for BAR whereas when simply building the string in the echo statement it uses the new value for BAR. What?!
Or is this a nuance with how Jenkins is working in conjunction with Groovy?

Marcel Wilson
  • 3,842
  • 1
  • 26
  • 55

1 Answers1

3

When you write a Groovy script, groovy effectively wraps your script in a standard java class, inside a method run()

So if we remove the @Field annotations, the script:

def FOO = "Mary had a little lamb"
def BAR = "whose fleece was white as snow"
def FOOBAR = "${FOO} ${BAR}"
echo "FOOBAR: ${FOOBAR}"
echo "FOO & BAR: ${FOO} ${BAR}"

Becomes effectively:

public class script1582564680906 extends groovy.lang.Script { 

    public java.lang.Object run() {
        java.lang.Object FOO = 'Mary had a little lamb'
        java.lang.Object BAR = 'whose fleece was white as snow'
        java.lang.Object FOOBAR = "$FOO $BAR"
        this.echo("FOOBAR: $FOOBAR")
        this.echo("FOO & BAR: $FOO $BAR")
    }

}

Plus some other stuff that isn't important for this question... This is fine, but if you add a method to your script, like so:

def something() {
    FOO = 'tim'
}

def FOO = "Mary had a little lamb"
def BAR = "whose fleece was white as snow"
def FOOBAR = "${FOO} ${BAR}"
echo "FOOBAR: ${FOOBAR}"
echo "FOO & BAR: ${FOO} ${BAR}"

The class becomes:

public class script1582564855552 extends groovy.lang.Script { 

    public java.lang.Object run() {
        java.lang.Object FOO = 'Mary had a little lamb'
        java.lang.Object BAR = 'whose fleece was white as snow'
        java.lang.Object FOOBAR = "$FOO $BAR"
        this.echo("FOOBAR: $FOOBAR")
        this.echo("FOO & BAR: $FOO $BAR")
    }

    public java.lang.Object something() {
        FOO = 'tim'
    }

}

As you can see, in my something() method, I'm trying to access FOO, but this is only valid inside the run() method.

This is why the @Field annotation exists. It tells Groovy to move the definition up to class level instead of inside run(), so this:

import groovy.transform.Field

@Field def FOO = "Mary had a little lamb"
@Field def BAR = "whose fleece was white as snow"
@Field def FOOBAR = "${FOO} ${BAR}"
echo "FOOBAR: ${FOOBAR}"
echo "FOO & BAR: ${FOO} ${BAR}"

Becomes this:

public class script1582565001609 extends groovy.lang.Script { 

    java.lang.Object FOO = 'Mary had a little lamb'
    java.lang.Object BAR = 'whose fleece was white as snow'
    java.lang.Object FOOBAR = "$FOO $BAR"

    public java.lang.Object run() {
        this.echo("FOOBAR: $FOOBAR")
        this.echo("FOO & BAR: $FOO $BAR")
    }

}

So now we get to your question... Taking this:

@Field def BAR = "and she murdered it"
@Field def FOO = "Mary had a little lamb"
if (FOO) {
    BAR = "whose fleece was white as snow"
}
@Field def FOOBAR = "${FOO} ${BAR}"
echo "FOOBAR: ${FOOBAR}"
echo "FOO & BAR: ${FOO} ${BAR}"

And converting it following the above rules gives us:

public class script1582565140864 extends groovy.lang.Script { 

    java.lang.Object BAR = 'and she murdered it'
    java.lang.Object FOO = 'Mary had a little lamb'
    java.lang.Object FOOBAR = "$FOO $BAR"

    public java.lang.Object run() {
        if ( FOO ) {
            BAR = 'whose fleece was white as snow'
        }
        this.echo("FOOBAR: $FOOBAR")
        return this.echo("FOO & BAR: $FOO $BAR")
    }
}

As you can see... FOOBAR is set up as a field, so it initialised way before you modify BAR to be 'whose fleece was white as snow'

But FOO and BAR are as you'd expect when you debug them...

Avoid @Field if you can get away with it, as it makes things hard to reason about

tim_yates
  • 167,322
  • 27
  • 342
  • 338
  • Excellent explanation! I have been using `@Field` based on this answer https://stackoverflow.com/a/50573082/2532408. I wanted the variables to be 'global' but only at the script level. I see now why I was confused about how this is actually working. I think my solution will be to 'initialize' FOOBAR to an empty string followed by reassigning the variable. – Marcel Wilson Feb 24 '20 at 17:33