Jenkinsfile Best Practices for Reliable Production Pipelines
A Jenkinsfile Is Production Code
A Jenkinsfile decides what source was trusted, which tests mattered, what artifact was released, and which credentials reached an agent. Treat it like application code: review it, test changes on a branch, keep behavior deterministic, and make failure states obvious.
The most useful production practices are not clever Groovy. They are boundaries: a deadline, a clean workspace, a named artifact, an isolated credential scope, and an explicit result.
---
1. Run Work on Agents, Not the Controller
Use agent none at the Pipeline level when stages need different environments, then select a label or container for each stage.
pipeline {
agent none
stages {
stage('Test') {
agent { label 'node20-linux' }
steps { sh 'npm ci && npm test' }
}
stage('Package') {
agent { label 'docker-builder' }
steps { sh './ci/package.sh' }
}
}
}
Do not run builds on the controller. Build code is untrusted execution, and a CPU-heavy build or a careless deletion must not compete with scheduling and Pipeline persistence. Prefer ephemeral agents when possible so each build starts from a known image and disappears afterward.
---
2. Bound Time and Concurrency
options {
timestamps()
timeout(time: 45, unit: 'MINUTES')
disableConcurrentBuilds(abortPrevious: true)
buildDiscarder(logRotator(
numToKeepStr: '30',
artifactNumToKeepStr: '10'
))
}
A global timeout catches lost agents and forgotten prompts. Add shorter stage timeouts around network calls. disableConcurrentBuilds is appropriate when two runs would mutate the same environment; abortPrevious: true is useful for branch CI where only the latest commit matters. It is not appropriate when every queued release must execute.
Retention is operational hygiene. Without it, console logs, fingerprints, and archived artifacts eventually fill controller or artifact-store capacity. Practice this option in Control Build History Growth.
---
3. Build Once and Promote the Same Bytes
stage('Build') {
steps {
sh './ci/build.sh'
sh 'sha256sum dist/payments.tgz > dist/payments.tgz.sha256'
stash name: 'release-bundle', includes: 'dist/**'
}
}
stage('Deploy Staging') {
steps {
deleteDir()
unstash 'release-bundle'
sh './ci/deploy.sh staging dist/payments.tgz'
}
}
post {
success {
archiveArtifacts artifacts: 'dist/**', fingerprint: true
}
}
Never rerun the compiler in a deployment stage. Rebuilding can resolve a newer dependency, read a changed workspace, or embed a different timestamp. Use stash for modest files moving between stages in one run. Use an artifact repository for large bundles and for promotion across jobs or Pipeline runs. Promote the Exact Artifact exercises this distinction.
---
4. Publish Evidence Even When Tests Fail
stage('Test') {
steps {
sh 'npm test -- --reporter=junit'
}
post {
always {
junit testResults: 'reports/*.xml', allowEmptyResults: false
}
}
}
Console text is not a test report. Publishing JUnit XML gives Jenkins trend history, failure details, and build health. Put the publisher in stage-level post { always { ... } } so a failing test command does not skip it. Do not set allowEmptyResults: true merely to make red builds green; use it only when “no report†is genuinely valid.
---
5. Keep the Jenkinsfile Thin
steps {
sh './ci/test.sh'
}
is easier to run locally than 80 lines of Groovy that discover packages, parse JSON, and reproduce a build system. Pipeline code should connect tools and enforce delivery policy. Put compilation and test logic in repository scripts; put organization-wide Pipeline conventions in a tested shared library.
Avoid global mutable variables and non-serializable objects held across Pipeline steps. Jenkins may checkpoint a Pipeline between steps so it can survive a controller restart. Complex Groovy state makes resumability and debugging harder.
---
6. Make Cleanup and Failure Semantics Explicit
post {
always {
cleanWs()
}
unsuccessful {
echo "Build ${env.JOB_NAME} #${env.BUILD_NUMBER} did not succeed"
}
}
Use post for lifecycle actions, not a final “Cleanup†stage that will be skipped after an earlier failure. Decide deliberately whether an optional scanner should fail the build, mark its stage unstable, or only warn. catchError(buildResult: 'SUCCESS', stageResult: 'UNSTABLE') documents that policy; appending || true hides it.
---
A Review Checklist
- Can a build wait forever for an executor, command, lock, or approval?
- Can simultaneous runs deploy over one another?
- Is the promoted artifact exactly the tested artifact?
- Are secrets scoped to the smallest block and kept out of arguments and logs?
- Are test results and artifacts retained intentionally?
- Does cleanup run after success, failure, and abort?
- Can a developer run the core build commands outside Jenkins?
- Does every caught error still leave an accurate build and stage result?
Use the Jenkins Warmup for the basic syntax, then apply the checklist across the growing Jenkins lab catalog.