Strange variable scoping behavior in Jenkinsfile
Solution 1
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
}
}
Solution 2
Great Answer from @Vitalii Vitrenko!
I tried program to verify that. Also added few more test cases.
import groovy.transform.Field
@Field
def CLASS_VAR = "CLASS"
def METHOD_VAR = "METHOD"
GLOBAL_VAR = "GLOBAL"
def testMethod() {
echo "testMethod starts:"
def testMethodLocalVar = "Test_Method_Local_Var"
testMethodGlobalVar = "Test_Metho_Global_var"
echo "${CLASS_VAR}"
// echo "${METHOD_VAR}" //can be accessed only within pipeline run method
echo "${GLOBAL_VAR}"
echo "${testMethodLocalVar}"
echo "${testMethodGlobalVar}"
echo "testMethod ends:"
}
pipeline {
agent any
stages {
stage('parallel stage') {
parallel {
stage('parallel one') {
agent any
steps {
echo "parallel one"
testMethod()
echo "${CLASS_VAR}"
echo "${METHOD_VAR}"
echo "${GLOBAL_VAR}"
echo "${testMethodGlobalVar}"
script {
pipelineMethodOneGlobalVar = "pipelineMethodOneGlobalVar"
sh_output = sh returnStdout: true, script: 'pwd' //Declared global to access outside the script
}
echo "sh_output ${sh_output}"
}
}
stage('parallel two') {
agent any
steps {
echo "parallel two"
// pipelineGlobalVar = "new" //cannot introduce new variables here
// def pipelineMethodVar = "new" //cannot introduce new variables here
script { //new variable and reassigning needs scripted-pipeline
def pipelineMethodLocalVar = "new";
pipelineMethodLocalVar = "pipelineMethodLocalVar reassigned";
pipelineMethodGlobalVar = "new" //no def keyword
pipelineMethodGlobalVar = "pipelineMethodGlobalVar reassigned"
CLASS_VAR = "CLASS TWO"
METHOD_VAR = "METHOD TWO"
GLOBAL_VAR = "GLOBAL TWO"
}
// echo "${pipelineMethodLocalVar}" only script level scope, cannot be accessed here
echo "${pipelineMethodGlobalVar}"
echo "${pipelineMethodOneGlobalVar}"
testMethod()
}
}
}
}
stage('sequential') {
steps {
script {
echo "sequential"
}
}
}
}
}
Observations:
-
Six cases of variables declarations
a. Three types (with def, without def, with def and with @field) before/above pipeline
b. within scripted-pipeline (with def, without def) within pipeline
c. Local to a method (with def) outside pipeline
new variable declaration and reassigning needs scripted-pipeline within pipeline.
All the variable declared outside pipeline can be accessed between the stages
Variable with def keyword generally specific to a method, if it is declared inside script then will not be available outside of it. So need to declare global variable (without def) within script to access outside of script.
haridsv
Updated on June 07, 2022Comments
-
haridsv almost 2 years
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 fromsome_var
, it works fine. Could someone explain the scoping rules that cause this behavior? -
haridsv almost 6 yearsThank you for the detailed explanation with visualization of the generated script. Also thanks for the suggestion on
@Field
. Your answer goes beyond my expectations! -
Kanagavelu Sugumar over 4 yearsGreat Answer, Will declaring variable (with{out} def and @field) inside parallel stages has any access limitation from one stage to another stage? How the visibility of variables declared inside the stages/steps in pipeline script?
-
Kanagavelu Sugumar over 4 yearsAlso kindly explain the scope/accessibility of variable declared inside the environment block, and how it is equivalent to java class variables ?
-
artegen over 2 yearsThank you, also great addition! In my case Jenkins throws when using @Field. I've passed a variable with
def
from pipeline to an external method.