Jenkins pipelines can be surprisingly tricky when it comes to environment variables, often leading to confusion about where they’re defined, how they’re scoped, and why they sometimes disappear mid-pipeline.
Let’s see how this actually looks in a pipeline. Imagine a Jenkinsfile that tries to set a variable and then use it, not just in a sh step, but also in a plugin configuration.
pipeline {
agent any
environment {
MY_GLOBAL_VAR = 'initial_value'
ANOTHER_VAR = "${MY_GLOBAL_VAR}_extended" // This works
}
stages {
stage('Show Global Vars') {
steps {
echo "MY_GLOBAL_VAR is: ${env.MY_GLOBAL_VAR}" // Accessing via env object
echo "ANOTHER_VAR is: ${env.ANOTHER_VAR}"
}
}
stage('Override and Use') {
environment {
MY_GLOBAL_VAR = 'overridden_value' // Overrides the global one for this stage
}
steps {
echo "MY_GLOBAL_VAR in this stage: ${env.MY_GLOBAL_VAR}"
sh 'echo "MY_GLOBAL_VAR from shell: $MY_GLOBAL_VAR"' // Shell access
}
}
stage('Scoped to Step') {
steps {
script {
def buildSpecificVar = 'build_specific_value'
sh "echo 'Variable scoped to script block: ${buildSpecificVar}'"
// echo "${buildSpecificVar}" // This would fail outside the script block
}
// echo "${buildSpecificVar}" // This would also fail here
}
}
stage('Using in Plugin') {
steps {
// Example using a hypothetical plugin that reads env vars
// Many plugins, like Docker, Kubernetes, or credential plugins,
// can directly reference environment variables.
// For example, a Docker build step might look like:
// docker.build("my-image:${env.BUILD_NUMBER}")
echo "Using BUILD_NUMBER: ${env.BUILD_NUMBER}" // BUILD_NUMBER is always available
}
}
}
}
This pipeline demonstrates a few key things:
- Global
environmentblock: Variables defined here are available to all stages and steps within the pipeline. - Nested
environmentblocks: Variables defined within astageorstep’senvironmentblock override global variables of the same name for that specific scope. - Accessing variables: You can access them using
env.VARIABLE_NAMEwithin Groovy code and directly as$VARIABLE_NAMEwithin shell steps. scriptblock scope: Variables declared withdefinside ascriptblock are only accessible within that block.
The core problem this solves is providing dynamic configuration and sensitive information to your build jobs without hardcoding them. Think API keys, database credentials, deployment targets, or build parameters. Environment variables are the standard mechanism for this.
Internally, Jenkins injects these variables into the execution environment of each step. For sh or bat steps, this means they are exported as actual shell environment variables. For Groovy steps, they are accessible via the env object, which is a thread-local map.
The most surprising thing about Jenkins environment variables is that order of definition matters more than you’d think, and scope is king. You can define a variable globally, then override it at the stage level, and then override it again within a specific steps block or even a single script block. Each level of nesting can redefine a variable, and the innermost definition wins. This allows for very granular control, but also means a variable you think is set globally might be subtly changed by a stage-specific environment block you forgot about.
The one thing most people don’t realize is how effectively you can use other environment variables to construct new ones within the environment block itself, as shown with ANOTHER_VAR = "${MY_GLOBAL_VAR}_extended". This allows for complex configurations where variables are built upon each other, and Jenkins resolves these interpolations before the pipeline step actually executes. This isn’t just string concatenation; it’s a resolution phase that happens within Jenkins’ pipeline engine.
Understanding how these variables cascade and override is crucial for debugging. The next concept to grapple with is how to securely inject sensitive credentials as environment variables.