Theory only takes you so far. In this final lesson we assemble everything covered across the tutorial — Declarative syntax, agents, shared libraries, credentials, and branch-aware triggers — into a single production-grade Jenkinsfile that you can drop into a real microservice repository today. We will also walk through the decisions behind every block so you understand why it is written the way it is, not just what it does.
The Target Architecture
Our example service is a Java Spring Boot API. The pipeline must:
Run unit and integration tests in parallel on every commit.
Build a Docker image tagged with the Git commit SHA.
Push the image to an internal container registry (AWS ECR in this example).
Deploy to a staging Kubernetes cluster automatically when commits land on main.
Require a manual approval gate before promoting to production.
Send a Slack notification on success and failure.
Pull all credentials from Jenkins Credentials Store — zero secrets in the Jenkinsfile.
Enterprise pipeline: parallel tests, image build and security scan, branch-gated staging deploy, manual approval gate before production.
The Shared Library Setup
Before writing the Jenkinsfile we configure the shared library (covered in Lesson 6) that provides three reusable functions: dockerBuild(), helmDeploy(), and notifySlack(). These live in the org's jenkins-shared-lib repository and are registered globally in Manage Jenkins → Configure System → Global Pipeline Libraries under the name acme-shared.
Why a shared library for three functions? In a real org with dozens of microservices, every team would otherwise copy-paste the same Docker login, Helm upgrade, and Slack call. When the Slack API endpoint or the ECR login command changes, you fix it in one place and every pipeline picks it up on the next run — without touching 40 Jenkinsfiles.
The Complete Enterprise Jenkinsfile
@Library('acme-shared@main') _ // import shared library, pin to main branch
pipeline {
agent none // no global agent — each stage declares its own to save executor slots
options {
buildDiscarder(logRotator(numToKeepStr: '30')) // keep 30 builds of history
timestamps() // prefix every log line with time
timeout(time: 45, unit: 'MINUTES') // kill runaway builds
disableConcurrentBuilds(abortPrevious: true) // one build per branch at a time
}
environment {
APP_NAME = 'payments-service'
ECR_REGISTRY = '123456789012.dkr.ecr.us-east-1.amazonaws.com'
IMAGE_NAME = "${ECR_REGISTRY}/${APP_NAME}"
IMAGE_TAG = "${GIT_COMMIT[0..7]}" // first 8 chars of commit SHA
K8S_NS_STG = 'staging'
K8S_NS_PROD = 'production'
}
stages {
// ── 1. CHECKOUT ────────────────────────────────────────────────────────
stage('Checkout') {
agent { label 'builder' }
steps {
checkout scm
script {
// expose full SHA for downstream stages (IMAGE_TAG uses short form above)
env.GIT_COMMIT_FULL = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()
env.GIT_BRANCH_SAFE = env.BRANCH_NAME.replaceAll('[^a-zA-Z0-9_.-]', '-')
}
stash name: 'source', includes: '**' // stash so other agents can use it
}
}
// ── 2. PARALLEL TESTS ──────────────────────────────────────────────────
stage('Tests') {
parallel {
stage('Unit Tests') {
agent { label 'builder' }
steps {
unstash 'source'
sh './mvnw test -Dgroups=unit -T 4' // 4 threads, unit group only
}
post {
always {
junit 'target/surefire-reports/**/*.xml'
publishCoverage adapters: [jacocoAdapter('target/site/jacoco/jacoco.xml')]
}
}
}
stage('Integration Tests') {
agent { label 'builder' }
steps {
unstash 'source'
sh './mvnw verify -Dgroups=integration -DskipUnitTests'
}
post {
always { junit 'target/failsafe-reports/**/*.xml' }
}
}
stage('Static Analysis') {
agent { label 'builder' }
steps {
unstash 'source'
sh './mvnw checkstyle:check spotbugs:check pmd:check'
}
}
}
}
// ── 3. BUILD DOCKER IMAGE ──────────────────────────────────────────────
stage('Build & Scan Image') {
agent { label 'docker' } // dedicated agent with Docker daemon
steps {
unstash 'source'
script {
// shared library call — handles ECR login internally
dockerBuild(
image: env.IMAGE_NAME,
tag: env.IMAGE_TAG,
args: "--build-arg APP_VERSION=${env.IMAGE_TAG} --cache-from ${env.IMAGE_NAME}:latest"
)
}
// Trivy CVE scan — fail build on HIGH or CRITICAL findings
sh """
trivy image \\
--exit-code 1 \\
--severity HIGH,CRITICAL \\
--ignore-unfixed \\
${IMAGE_NAME}:${IMAGE_TAG}
"""
}
}
// ── 4. PUSH TO ECR ────────────────────────────────────────────────────
stage('Push Image') {
agent { label 'docker' }
steps {
withCredentials([[$class: 'AmazonWebServicesCredentialsBinding',
credentialsId: 'aws-ecr-push']]) {
sh """
aws ecr get-login-password --region us-east-1 \\
| docker login --username AWS --password-stdin ${ECR_REGISTRY}
docker push ${IMAGE_NAME}:${IMAGE_TAG}
docker tag ${IMAGE_NAME}:${IMAGE_TAG} ${IMAGE_NAME}:latest
docker push ${IMAGE_NAME}:latest
"""
}
}
}
// ── 5. DEPLOY TO STAGING (main branch only) ───────────────────────────
stage('Deploy → Staging') {
when {
branch 'main'
not { changeRequest() }
}
agent { label 'k8s-deployer' }
steps {
script {
helmDeploy(
release: env.APP_NAME,
chart: 'charts/payments-service',
namespace: env.K8S_NS_STG,
values: "deploy/values-staging.yaml",
set: "image.tag=${env.IMAGE_TAG}"
)
}
// smoke test the staging endpoint
sh "curl --retry 5 --retry-delay 5 --fail https://staging.acme.internal/payments/health"
}
}
// ── 6. MANUAL GATE → PRODUCTION ───────────────────────────────────────
stage('Approve Production') {
when { branch 'main' }
steps {
timeout(time: 24, unit: 'HOURS') {
input message: "Deploy ${env.APP_NAME}:${env.IMAGE_TAG} to PRODUCTION?",
ok: 'Ship it',
submitter: 'release-team' // only members of the release-team group
}
}
}
// ── 7. DEPLOY TO PRODUCTION ───────────────────────────────────────────
stage('Deploy → Production') {
when { branch 'main' }
agent { label 'k8s-deployer' }
steps {
script {
helmDeploy(
release: env.APP_NAME,
chart: 'charts/payments-service',
namespace: env.K8S_NS_PROD,
values: "deploy/values-production.yaml",
set: "image.tag=${env.IMAGE_TAG}",
atomic: true, // rolls back automatically on failure
timeout: '5m'
)
}
}
}
}
// ── POST: NOTIFICATIONS ───────────────────────────────────────────────────
post {
success {
script {
notifySlack(
channel: '#deployments',
color: 'good',
message: ":white_check_mark: *${env.APP_NAME}* `${env.IMAGE_TAG}` deployed to *${currentBuild.result}*\n<${env.BUILD_URL}|Build #${env.BUILD_NUMBER}>"
)
}
}
failure {
script {
notifySlack(
channel: '#deployments',
color: 'danger',
message: ":x: *${env.APP_NAME}* `${env.IMAGE_TAG}` FAILED on `${env.BRANCH_NAME}`\n<${env.BUILD_URL}console|View logs>"
)
}
}
always {
cleanWs() // wipe workspace to prevent artifact bleed between builds
}
}
}
Key Design Decisions Explained
agent none at the top level. Declaring no global agent forces each stage to request an executor only while it is running. With a global agent, Jenkins holds an executor for the entire build — including the multi-hour input wait. That would exhaust your executor pool. Stage-level agents release the slot the moment the stage finishes.
disableConcurrentBuilds(abortPrevious: true). On a busy feature branch, a developer may push two commits in quick succession. Without this option both builds run simultaneously, the older one may finish last and deploy stale code. abortPrevious: true cancels the in-flight build the moment a newer one is queued for the same branch.
Stash / unstash for source code. Because we use agent none, each stage runs on a potentially different agent host. stash archives the workspace to the Jenkins controller after checkout; each stage that needs the source calls unstash to restore it. For large repos, combine stash with a shared NFS mount or use the Artifact Caching Plugin to avoid sending gigabytes over the network on every stage.
Trivy inside the pipeline, not as a pre-commit hook. Pre-commit hooks run on developer laptops and can be bypassed. Running Trivy inside Jenkins means every image that reaches the registry has been scanned against the current CVE database — not a database that was cached six months ago on someone's laptop.
atomic: true on the Helm production deploy. This flag tells Helm to roll back the release automatically if the new pods do not reach Running state within the timeout window. Without it, a bad image leaves the release in a deployed state that Helm considers successful even though your pods are crash-looping.
Tag images with the full Git SHA, not a branch name or latest. Branch names and latest are mutable — pulling the same tag tomorrow may give a different image. The commit SHA is immutable and ties every running container back to a specific line of source history. Production on-call engineers and incident reviewers will thank you.
The Shared Library Counterparts
The three library calls above live in vars/dockerBuild.groovy, vars/helmDeploy.groovy, and vars/notifySlack.groovy in the jenkins-shared-lib repository. A minimal helmDeploy.groovy looks like this:
The input step blocks a build executor if you are not using agent none. A 24-hour approval window with a global agent { label 'builder' } will hold one builder slot hostage for a full day. This is one of the most common Jenkins resource leaks in corporate deployments. Always declare agent none at the pipeline level and assign agents only at the stage level.
Running It for the First Time
Place this Jenkinsfile at the root of the repository, then create a Multibranch Pipeline pointing at the repo (covered in Lesson 8). Jenkins discovers every branch that has the file, creates a job, and runs the pipeline. The first run on main will hit the input gate — click Approve Production in the build UI or leave it and let the 24-hour timeout abort it safely. Every subsequent push to any branch runs tests and builds an image; only pushes to main go further.
This Jenkinsfile represents the production standard at most large technology companies. The exact tool choices — ECR vs GCR vs Harbor, Helm vs raw kubectl, Slack vs Teams — vary by organisation, but the structural patterns are universal: parallel tests, immutable image tags, shared credential management, branch-gated deploys, manual production gates, and clean-up on every exit. Master this structure and you can adapt it to any stack in any company.