Options, Environment, and Tools
Scenario: A job silently changes after a globally configured latest tool is upgraded.
Two different questions: "how should this run?" and "what values does it need?"
A Jenkinsfile has to answer two separate questions that beginners often blur together: how should this pipeline behave (should it time out? should concurrent runs be blocked?) and what data does it need while running (a target environment name, a secret token, a version number). Declarative Pipeline has one directive for each: options answers the "how should it behave" question, and environment answers the "what values does it need" question.
Analogy: Think ofoptionsas the rules printed on a parking garage sign - "maximum stay 2 hours," "no re-entry after closing" - rules that govern the whole visit, regardless of which car pulls in. Think ofenvironmentas the ticket the car itself carries - specific information ("spot A14," "paid until 3pm") relevant to that one particular visit. You wouldn't print "spot A14" on the garage's rule sign, and you wouldn't print "no re-entry after closing" on one individual car's ticket - each piece of information belongs at the scope where it actually applies.
Keeping this distinction straight - global rules versus per-run values - makes a Jenkinsfile far easier to read, because a reviewer immediately knows where to look for either kind of information.
Pipeline example
pipeline { agent any; options { timeout(time: 20, unit: 'MINUTES'); timestamps() }; environment { TARGET = 'staging' }; stages { stage('Verify') { steps { sh './verify.sh' } } } }
Scope policy deliberately, at the right level
Top-level options can bound an entire run with things like timeout, timestamps(), buildDiscarder (retention policy), disableConcurrentBuilds, skipDefaultCheckout, or disableResume. Stage-level options apply more narrowly, and may or may not include the time spent just allocating that particular stage's agent, depending on the option. A ten-minute test budget should not accidentally also swallow a forty-minute human approval step sitting in a different stage - and a single overall deadline is what actually prevents an abandoned, half-finished run from lingering indefinitely and quietly holding an executor hostage. Put each constraint at the scope where its meaning is actually clear to a future reader.
environment directives create string values, either globally for the whole Pipeline or narrowly scoped to one stage, with a narrower scope overriding a broader one of the same name. Validate any required value early, because environment variables in Jenkins are just strings - there's no real type system checking that TARGET is actually one of the environments you expect. Credential-related helpers (like credentials('my-id') inside environment) create bound values and related variables automatically; never use Groovy string interpolation (double quotes with ${...}) to place a secret into a shell command argument, since that can leak the literal secret value into the Jenkins log or process list. Use single-quoted shell strings so the shell itself expands the variable at execution time, rather than Jenkins baking the literal value into the command text beforehand.
[Pipeline] withCredentials
[Pipeline] {
[Pipeline] sh
+ curl -H 'Authorization: Bearer ****' https://api.example.com/deploy
{"status":"deployed"}
[Pipeline] }
Notice **** in place of the actual token - that's Jenkins' log masking recognizing the bound credential value and redacting it automatically, which only works reliably when the credential is referenced the safe way shown above.
Remove invisible tool drift
The tools directive requests a named, controller-managed installation (like nodejs 'node-latest') and adjusts PATH to use it. If an administrator later repoints what "node-latest" actually resolves to, an unchanged Jenkinsfile can start producing different build results next week, with nobody having touched a single line of the repository - a genuinely confusing debugging experience the first time it happens. Use documented, explicitly versioned tool names (like nodejs 'node-20.11.0'), wrapper scripts, or immutable pre-built agent images instead, and print non-secret provenance information at the start of a build - the exact Java version, Node version, build-tool version, scanner version, container image digest, source revision, selected agent label, and parameters used - so a future investigator doesn't have to guess what actually ran.
A pitfall: a "temporary" global tool change that never got reverted
A real incident shape: someone changes the globally-configured "latest" Node.js version to test something, forgets to revert it, and every job across the whole Jenkins instance that references node-latest silently starts using a different runtime the next time it builds - with zero commits to any of those repositories. Some pass fine; one starts failing in a way that looks like a code regression but is actually a runtime-version change. The only way to have caught this immediately is exactly what pinned, versioned tool names and printed provenance are for: test policy by deliberately letting a timeout expire, launching two concurrent builds to check disableConcurrentBuilds, and restarting with disableResume set to confirm retention removes only what you intended - reproducibility means a future investigator can identify exactly which tool versions and inputs were used, not merely confirm that a symbolic name like "latest" was requested.
Goal: Practice the concrete behavior in jenkins-declarative-rescue. Learn sections have no Jenkins terminal; the Jenkins lab opens the real shared service in a new full-screen tab inside your private folder.