Strange variable scoping behavior in Jenkinsfile

haridsv picture haridsv · May 28, 2018 · Viewed 12.3k times · Source

When I run the below Jenkins pipeline script:

def some_var = "some value"

def pr() {
    def another_var = "another " + some_var
    echo "${another_var}"
}

pipeline {
    agent any

    stages {
        stage ("Run") {
            steps {
                pr()
            }
        }
    }
}

I get this error:

groovy.lang.MissingPropertyException: No such property: some_var for class: groovy.lang.Binding

If the def is removed from some_var, it works fine. Could someone explain the scoping rules that cause this behavior?

Answer

Vitalii Vitrenko picture Vitalii Vitrenko · May 28, 2018

TL;DR

  • variables defined with def in the main script body cannot be accessed from other methods.
  • variables defined without def can be accessed directly by any method even from different scripts. It's a bad practice.
  • variables defined with def and @Field annotation can be accessed directly from methods defined in the same script.

Explanation

When groovy compiles that script it actually moves everything to a class that roughly looks something like this

class Script1 {
    def pr() {
        def another_var = "another " + some_var
        echo "${another_var}"
    }
    def run() {
        def some_var = "some value"
        pipeline {
            agent any
            stages {
                stage ("Run") {
                    steps {
                        pr()
                    }
                }
            }
        }
    }
}

You can see that some_var is clearly out of scope for pr() becuse it's a local variable in a different method.

When you define a variable without def you actually put that variable into a Binding of the script (so-called binding variables). So when groovy executes pr() method firstly it tries to find a local variable with a name some_var and if it doesn't exist it then tries to find that variable in a Binding (which exists because you defined it without def).

Binding variables are considered bad practice because if you load multiple scripts (load step) binding variables will be accessible in all those scripts because Jenkins shares the same Binding for all scripts. A much better alternative is to use @Field annotation. This way you can make a variable accessible in all methods inside one script without exposing it to other scripts.

import groovy.transform.Field

@Field 
def some_var = "some value"

def pr() {
    def another_var = "another " + some_var
    echo "${another_var}"
}
//your pipeline

When groovy compiles this script into a class it will look something like this

class Script1 {
    def some_var = "some value"

    def pr() {
        def another_var = "another " + some_var
        echo "${another_var}"
    }
    def run() {
        //your pipeline
    }
}