Environment, parameters & when
Configure and branch a pipeline.
One Jenkinsfile has to ship code to a staging server where testers poke at it, and to production where real customers live. You could copy the file into two separate jobs. Six months later those two jobs disagree about everything and nobody remembers why. A coffee machine solves the same problem with a dial rather than two machines: same hardware, different setting. Three declarative blocks give your pipeline that dial. environment sets values the whole pipeline shares. parameters lets a person or another job feed values in at the moment a build starts. when gates a stage so it runs only under conditions you spell out. A Jenkinsfile is the text file, written in Groovy (a scripting language that runs on Java), that describes your pipeline. Declarative is the structured, block-based style Jenkins recommends over free-form scripting.
environment: set a value once, use it everywhere
A recipe card lists the oven temperature once at the top, and every step below refers back to it. The environment block does that for your pipeline. It holds key/value pairs, and Jenkins hands them to every stage as environment variables (the named values, like PATH or HOME, that any shell command in the build can read). Declare REGISTRY once at the top and you change it in one place, instead of hunting for a hardcoded copy buried in the third stage. Jenkins also injects its own built-in variables that you can reference here. BUILD_NUMBER is the one you will reach for most: a counter that ticks up by one on every run of the job, which makes it ideal for stamping a unique version onto an artifact. You can also nest an environment block inside a single stage when you want a variable scoped to that stage alone. Now the part that bites. Two different kinds of substitution are in play. ${BUILD_NUMBER} inside double quotes is Groovy interpolation, resolved while Jenkins parses the file on the controller (the Jenkins brain machine that schedules work). $IMAGE inside a single-quoted sh step is expanded much later, by the shell on the agent (the worker machine that actually runs your commands). Mixing the two up is almost everyone's first bug.
pipeline {agent { label 'linux' }environment {REGISTRY = 'registry.acme.internal'IMAGE = "${REGISTRY}/payments-api"VERSION = "1.4.${BUILD_NUMBER}" // BUILD_NUMBER is provided by JenkinsREGISTRY_CREDS = credentials('acme-registry') // username/password from the store, masked}stages {stage('Build') {steps {sh 'docker build -t $IMAGE:$VERSION .'}}stage('Push') {steps {// _USR and _PSW are created automatically from a username/password credentialsh 'echo "$REGISTRY_CREDS_PSW" | docker login $REGISTRY -u "$REGISTRY_CREDS_USR" --password-stdin'sh 'docker push $IMAGE:$VERSION'}}}}
Secrets get their own helper inside environment. credentials('id') looks up an entry in Jenkins' encrypted credential store by its ID (the short name you gave it when you saved it) and binds it to your variable, masked as **** anywhere it would otherwise print in the log. A hotel key card works the same way. The card opens your room, and nobody at the front desk ever reads the master key out loud. For a username/password credential Jenkins quietly creates three variables rather than one. REGISTRY_CREDS holds username:password. REGISTRY_CREDS_USR and REGISTRY_CREDS_PSW hold the two halves separately, so each command can take whichever shape it wants. Only the ID ever appears in the Jenkinsfile. The secret itself is decrypted on the controller and injected into the build, which is one more reason to treat your agents as machines you have to trust. When you need a secret in one step rather than across the whole pipeline, the withCredentials wrapper is the tighter-scoped option. Credentials get a full lesson of their own later, so here you wire one in and keep moving.
parameters: let the operator fill in the blanks
Parameters are the blanks on a form. Someone fills them in when the build starts, and one pipeline suddenly covers a dozen situations. Three types carry most of the work. string takes free text, like a release tag. choice gives a fixed dropdown, and the first entry in the list is the default. booleanParam is a checkbox for a yes/no switch, such as whether to run the slow end-to-end tests. Inside your stages you read each one as params.NAME. Once a job knows it has parameters, the sidebar button changes from plain Build Now to Build with Parameters, and clicking that opens a form holding exactly these fields for the operator to fill in before the run. Every value chosen gets recorded on the build as well. The console output opens with a 'with parameters' line, so months later you can open build #217 and see precisely what it was told to do.
pipeline {agent { label 'linux' }parameters {choice(name: 'TARGET', choices: ['staging', 'production'], description: 'Where to deploy')booleanParam(name: 'RUN_E2E', defaultValue: false, description: 'Run slow end-to-end tests')string(name: 'RELEASE_TAG', defaultValue: '', description: 'Image tag to deploy')}stages {stage('Show inputs') {steps {echo "TARGET=${params.TARGET} RUN_E2E=${params.RUN_E2E} RELEASE_TAG=${params.RELEASE_TAG}"}}}}
There is a catch here, and it surprises every engineer exactly once. Jenkins registers your parameter definitions on the job only after a build has actually run and read the Jenkinsfile, because the parameters block is applied as a job property during execution, not before it. So the very first build of a brand-new pipeline shows no Build with Parameters form at all. You click Build Now, the pipeline runs, and params.TARGET quietly falls back to whatever default you declared, or comes out empty if you declared none. From the second build onward the form appears and the operator's choices take effect. Nothing is broken. The job had not read its own instructions yet. That is why a first run can behave differently from every run after it.
Started by user Sachin Chaurasiya[Pipeline] Start of Pipeline[Pipeline] nodeRunning on agent-linux-1 in /home/jenkins/workspace/payments-deploy[Pipeline] {[Pipeline] stage[Pipeline] { (Show inputs)[Pipeline] echoTARGET=staging RUN_E2E=false RELEASE_TAG=[Pipeline] }[Pipeline] // stage[Pipeline] }[Pipeline] // node[Pipeline] End of PipelineFinished: SUCCESS>> No "with parameters" line: build #1 fell back to the defaults.>> The sidebar now offers "Build with Parameters" for build #2 onward.
when: the gate in front of a stage
A bouncer at a door checks a list before letting anyone through. The when directive is that bouncer. It sits inside a stage and decides whether the stage's steps run at all. branch 'main' is true only when the branch being built is called main, which matters in a multibranch pipeline (a job type where Jenkins finds every branch in the repository, creates a sub-job for each one, and sets BRANCH_NAME as it goes). expression { } runs any snippet of Groovy you like and treats whatever comes back as a true or false answer. Here is the rule that trips people up. List several conditions directly inside when { } and every single one has to be true for the stage to run. It is a logical AND, never an OR. If one condition comes back false, Jenkins marks the stage skipped, prints the reason, and carries on. The build still finishes green.
stage('Deploy') {when {branch 'main' // condition 1expression { params.TARGET == 'production' } // condition 2 (both true = AND)beforeAgent true // evaluate before allocating an agent}steps {sh './deploy.sh "$TARGET" "$RELEASE_TAG"' // params are also exposed as env vars}}
So the Deploy stage above fires only when the build is on main AND the operator picked production. A production build launched from a feature branch gets skipped, because the branch check fails. A main build aimed at staging gets skipped, because the expression fails. When you genuinely want OR, wrap the conditions in anyOf { }. allOf { } spells the AND out when you want it obvious to whoever reads the file next, and not { } flips a condition around. One internals detail is worth your attention. By default Jenkins evaluates when after it has already allocated an agent for the stage, so a worker machine spins up, gets asked one question, and is thrown away. Add beforeAgent true and the condition is checked first, so you never pay for a worker you were always going to skip.
Started by user Sachin Chaurasiya with parameters: [TARGET=production, RUN_E2E=true, RELEASE_TAG=1.4.7][Pipeline] nodeRunning on agent-linux-3 in /home/jenkins/workspace/payments-deploy[Pipeline] {[Pipeline] stage[Pipeline] { (Show inputs)[Pipeline] echoTARGET=production RUN_E2E=true RELEASE_TAG=1.4.7[Pipeline] stage[Pipeline] { (Deploy)[Pipeline] sh+ ./deploy.sh production 1.4.7Deploying payments-api:1.4.7 to production ...[Pipeline] }Finished: SUCCESS>> Same job run on branch "feature/login" with TARGET=production:Stage "Deploy" skipped due to when conditional
Parameters are named inputs and nothing more, so a script can fill them in as easily as a person can. The Jenkins CLI (command-line interface, a small client program that talks to the controller's remote API, the machine-readable door into Jenkins that other programs knock on instead of clicking buttons) starts a parameterized build with one -p NAME=value per parameter. That is how an upstream job or a chat bot kicks off a deploy without anyone opening the web page. Authenticate with an API token, a long random string tied to an account that you can revoke on its own, rather than with a password. Then give that account only the Build permission it needs. A leaked token can start jobs and do nothing else.
$ java -jar jenkins-cli.jar -s https://jenkins.acme.internal/ \-auth sachin:$JENKINS_API_TOKEN \build payments-deploy \-p TARGET=production -p RUN_E2E=true -p RELEASE_TAG=1.4.7 \-f -vStarted payments-deploy #7Started from command line by sachin+ ./deploy.sh production 1.4.7Deploying payments-api:1.4.7 to production ...Finished: SUCCESS
When a stage misbehaves, three checks catch most of it. params.X reading empty is usually a first build that had not registered its parameters yet. A when that never fires is often a branch condition sitting in a single-branch job, where BRANCH_NAME is never set in the first place. An expression that always passes is usually handing back a truthy string rather than a real true or false. What decides when the pipeline runs at all, a push, a schedule, or a webhook, is the next lesson.
Try this
Work through “when: the gate in front of a stage” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: use single quotes in any sh step that touches a secret. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.