Strange variable scoping behavior in Jenkinsfile - jenkins

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?

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
}
}

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.

Related

What kind of Groovy object is Jenkins "env"?

In Jenkins pipelines, if I assign any key-value to env, I can access it like normal variables in string interpolation and environment variables in shell script. How does this work?
I see the probable implementation here but can't figure out how it works.
[Update]
In below code snippet, I can access the env properties without accessor -->
node {
stage('Preparation') {
env.foo = "bar"
echo "foo is $foo"
}
}
I haven't delved into the Jenkins code, but you could implement something like this by implementing the propertyMissing() method in a class, which would write to the script binding. The propertyMissing method gets called when the code is trying to access a property that is not declared in the class.
class MyEnv {
groovy.lang.Script script
MyEnv( groovy.lang.Script script ) {
this.script = script
}
def propertyMissing( String name ) {
script.getProperty( name )
}
def propertyMissing( String name, value ) {
script.setProperty( name, value )
}
}
def env = new MyEnv( this ) // pass the script context to the class
env.foo = 42 // actually assigns to the script binding
println "env.foo: $env.foo" // prints 42
println "foo: $foo" // prints 42
// It also works the other way around...
foo = 21 // assign to the script binding (Note: "def foo" would not work!)
println "env.foo: $env.foo" // prints 21

Accessing Jenkins pipeline WorkflowScript during execution

I would like to know if there is a way to access the Jenkins Workflow script object during its execution.
I have a shared library, and I can pass this object to any groovy class as an argument, either directly from the Jenkins file, using 'this' keyword, or from any DSL in the vars folder, also using the 'this' keyword.
But I would like to access it using a method, even if this imply using reflexivity.
Is that possible?
Here example with pipeline, where this is a script object. Some other examples here:
MyClass myClass = new MyClass()
pipeline {
agent any
environment {
VAR1 = "var1"
VAR2 = sh(returnStdout: true, script: "echo var2").trim()
VAR3 = "var3"
}
stages {
stage("Stage 1") {
steps {
script {
myClass.myPrint(this, "${VAR1}", "${VAR2}", "${VAR3}")
}
}
}
}
}
class MyClass implements Serializable {
void myPrint(def script, String var1, String var2, String... vars) {
script.echo "myPrint: ${var1}"
}
}

Jenkinsfile: How to call a groovy function with named arguments

I have a simple Declarative Pipeline with function inside. How to correctly use named arguments for a function?
def getInputParams(param1='a', param2='b') {
echo "param1 is ${param1}, param2 is ${param2}"
}
pipeline {
...
...
stages {
stage('Test') {
steps {
getInputParams(param1='x', param2='y')
}
}
}
}
I cannot understand why named params become null in function?
[Pipeline] echo
param1 is null, param2 is null
...
Well, I'm able to call function like getInputParams('x', 'y'), but it's not human readable (arguments amount may increase in future)
Groovy is executed inside the Jenkinsfile so you have to follow its syntax for named arguments.
foo(name: 'Michael', age: 24)
def foo(Map args) { "${args.name}: ${args.age}" }
Quote from Groovy's named arguments:
Like constructors, normal methods can also be called with named
arguments. They need to receive the parameters as a map. In the method
body, the values can be accessed as in normal maps (map.key).
def getInputParams(Map map) {
echo "param1 is ${map.param1}, param2 is ${map.param2}"
}
pipeline {
...
stages {
stage('Test') {
steps {
getInputParams(param1: 'x', param2: 'y')
}
}
}
}
If you are using groovy, use this.
def getInputParams(def param1, def param2) {
println("param1 is "+ param1 + ", param2 is " + param2)
}
pipeline {
...
...
stages {
stage('Test) {
steps {
getInputParams(x, y)
}
}
}
}

groovy, script:this and jenkinsfile

Based on this:
println in "call" method of "vars/foo.groovy" works, but not in method in class
I am trying to get my head around printing to the console from classes created in a Jenkins pipeline using jenkins shared libraries. I have the following:
MyPipeline.groovy
node("test") {
stage("Test") {
def a = new A(script:this)
echo "Calling A.a()"
a.a()
}
}
A.groovy
class A {
Script script;
public void a() {
script.echo("Hello from A")
def b = new B(script)
echo "Calling B.b()"
b.b()
}
}
B.groovy
class B {
Script script;
public void b() {
script.echo("Hello from B")
}
}
When I run that I get:
"Hello from A"
but then I get the error from B:
groovy.lang.GroovyRuntimeException: Could not find matching constructor for: samples.B(samples.MyPipeline)
How do I make it possible to print to console/build log in my classes also when delegating to other classes - like B in the above case?
As suggested by below answer I have now tried to update A.groovy to:
class A {
Script script;
public void a() {
script.echo("Hello from A")
def b = new B()
b.script = script
//def b = new B(script)
echo "Calling B.b()"
b.b()
}
}
But that just gives a new error:
hudson.remoting.ProxyException: groovy.lang.MissingMethodException: No signature of method: samples.A.echo() is applicable for argument types: (java.lang.String) values: [Calling B.b()]
Possible solutions: each(groovy.lang.Closure), getAt(java.lang.String), wait(), a(), every(), grep()
As per groovy's Initializing beans with named parameters and the default constructor
Just call empty constructor and set the parameter script
def b = new B()
b.script = script
With a bean like:
class Server {
String name
Cluster cluster }
Instead of setting each setter in subsequent statements as follows:
def server = new Server()
server.name = "Obelix"
server.cluster = aCluster
Also replace the following echo
echo "Calling B.b()"
To using script.echo method:
script.echo("Calling B.b()")

How to access variables outside stages in Jenkins file Groovy function?

My jenkins file looks like below:
import groovy.json.*
def manifestFile = "C:\\manifest.yml"
node {
stage('Build') {
}
stage('Deploy') {
checkDeployStatus()
}
}
def boolean checkDeployStatus() {
echo "${manifestFile}"
return true
}
The exception that i am getting is below:
groovy.lang.MissingPropertyException: No such property: manifestFile for class: groovy.lang.Binding
at groovy.lang.Binding.getVariable(Binding.java:63)
How do i access variables outside the node?
Groovy has a different kind of scoping at the script level. I can't ever keep it all sorted in my head. Without trying explain all the reasons for it (and probably not doing it justice), I can tell you that (as you have seen), the manifestFile variable is not in scope in that function. Just don't declare the manifestFile (i.e. don't put def in front of it). That will make it a "global" (not really, but for your purposes here) variable, then it should be accessible in the method call.
try this
import groovy.json.*
manifestFile = "C:\\manifest.yml"
node {
stage('Build') {
}
stage('Deploy') {
checkDeployStatus()
}
}
def boolean checkDeployStatus() {
echo "${manifestFile}"
return true
}

Resources