Search code examples
jenkinsgroovyjenkins-pipelinejenkins-declarative-pipeline

Groovy/Jenkins: when are variables null, when are they empty strings, and when are they missing?


I'm trying to grok the rules surrounding variables in Groovy/Jenkinsfiles/declarative syntax.

The generic webhook trigger captures HTTP POST content and makes them available as variables available to your Jenkinsfile. E.g.:

pipeline {
  agent any
  triggers {
    GenericTrigger (
      genericVariables: [
        [ key: "POST_actor_name", value: "\$.actor.name" ]
      ],

      token: "foo"
    )
  }

  stages {

    stage( "Set up" ) {
      steps {
        script {
          echo "env var ${env.actor_name}"
          echo "global var ${actor_name}"
        }
    }
  }
}

If the HTTP POST content contains a JSON object with an actor_name field valued "foo", then this prints:

env var foo
global var foo

If the HTTP POST content does not contain the JSON field actor_name, then this prints

env var null

...then asserts/aborts with a No such property error.

Jenkins jobs also have a "this project is parameterized" setting, which seems to introduce yet another way to inject variables into your Jenkinsfile. The following Jenkinsfile prints a populated, parameterized build variable, an unpopulated one, and an intentionally nonexistent variable:

pipeline {
  agent any

  stages {

    stage( "Set up" ) {
      steps {
        script {
          echo "1 [${env.populated_var}]"
          echo "2 [${env.unpopulated_var}]"
          echo "3 [${env.dontexist}]"
          echo "4 [${params.populated_var}]"
          echo "5 [${params.unpopulated_var}]"
          echo "6 [${params.dontexist}]"
          echo "7 [${populated_var}]"
          echo "8 [${unpopulated_var}]"
          echo "9 [${dontexist}]"
        }
      }
    }
  }
}

The result is:

1 [foo]
2 []
3 [null]
4 [foo]
5 []
6 [null]
7 [foo]
8 []

...then asserts/aborts with a No such property error.

The pattern I can ascertain is:

  1. env.-scoped variables will be NULL if they come from unpopulated HTTP POST content.
  2. env.-scoped variables will be empty strings if they come from unpopulated parameterized build variables.
  3. env.-scoped variables will be NULL if are nonexistent among parameterized build variables.
  4. Referencing global-scoped variables will assert if they come from unpopulated HTTP POST content.
  5. Referencing global-scoped variables will be be empty strings if they come from unpopulated parameterized build variables.
  6. params.-scoped variables will be NULL if they if are nonexistent among parameterized build variables.
  7. params.-scoped variables will be empty strings if they come from unpopulated parameterized build variables.

I have a few questions about this - I believe they are reasonably related, so am including them in this one post:

  1. What is the underlying pattern/logic behind when a variable is NULL and when it is an empty string?
  2. Why are variables available in different "scopes": env., params., and globally, and what is their relationship (why are they not always 1:1)?
  3. Is there a way for unpopulated values in parameterized builds to be null-valued variables in the Jenkinsfile instead of empty strings?

Context: in my first Jenkinsfile project, I made use of variables populated by HTTP POST content. Through this, I came to associate a value's absence with the corresponding .env variable's null-ness. Now, I'm working with variables coming from parameterized build values, and when a value is not populated, the corresponding .env variable isn't null -- it's an empty string. Therefore, I want to understand the pattern behind when and why these variables are null versus empty, so that I can write solid and simple code to handle absence/non-population of values from both HTTP POST content and parameterized build values.


Solution

  • The answer is a bit complicated.

    For 1 and 2:

    First of all pipeline, stage, steps... are groovy classes. Everything in there is defined as object/variable.

    env is an object that holds pretty much everything,

    params holds all parameter ;)

    They are both a Map, if you access an empty value it's empty, if you access an non existing one it's null.

    The globals are variables itself and if you try to access a non existing the compiler complains.

    For 3:

    You can define "default" parameter:

    pipeline {
      agent any
    
      stages {
    
        stage( "Set up" ) {
          steps {
            script {
              params = setConfig(params);
            }
          }
        }
      }
    }
    
    def merge(Map lhs, Map rhs) {
        return rhs.inject(lhs.clone()) { map, entry ->
            if (map[entry.key] instanceof Map && entry.value instanceof Map) {
                map[entry.key] = merge(map[entry.key], entry.value)
            } else {
                map[entry.key] = entry.value
            }
            return map
        }
    }
    
    def setConfig(givenConfig = [:]) {
      def defaultConfig = [
        "populated_var": "",
        "unpopulated_var": "",
        "dontexist": ""
      ];
     
      effectiveConfig = merge(defaultConfig, givenConfig);
       
      return effectiveConfig
    }