Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Strange variable scoping behavior in Jenkinsfile

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?

like image 942
haridsv Avatar asked May 28 '18 17:05

haridsv


People also ask

How do you declare a variable in Jenkinsfile?

Jenkins pipeline environment variables: You can define your environment variables in both — global and per-stage — simultaneously. Globally defined variables can be used in all stages but stage defined variables can only be used within that stage. Environment variables can be defined using NAME = VALUE syntax.

Is Jenkinsfile case sensitive?

Basically, params from parametrized build are copied to env of the node... and environment variables are known to be case-insensitive, which is the bug mentioned above. Whereas params are just a snapshot of the input paramters, immutable and case sensitive as you have found.


2 Answers

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     } } 
like image 111
Vitalii Vitrenko Avatar answered Oct 21 '22 11:10

Vitalii Vitrenko


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:

  1. 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

  2. new variable declaration and reassigning needs scripted-pipeline within pipeline.

  3. All the variable declared outside pipeline can be accessed between the stages

  4. 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.

like image 41
Kanagavelu Sugumar Avatar answered Oct 21 '22 12:10

Kanagavelu Sugumar