Jenkins Course
Mini Project — End-to-End CI/CD Pipeline
Every concept in this course has been building toward this lesson. No new syntax. No new plugins. Just everything you have learned, assembled into one production-grade pipeline for a real microservice — from Git push to running in Kubernetes, with security, notifications, and failure handling built in.
What you'll build
A complete CI/CD pipeline for the payment-api microservice — Git trigger → test → Docker build → push to registry → deploy to staging → smoke test → manual approval → deploy to production → notify team. With timeouts, log rotation, credentials via the store, and rollback on failure.
From Section I–II
- Declarative pipeline structure
- Stages and steps
- Environment variables
- Credentials management
- Pipeline triggers
- Post-build actions
From Section III
- Failure handling and retry
- Security — no hardcoded secrets
- Log rotation
- Slack notifications
- Timeout protection
- Build discarder
From Section IV–V
- Kubernetes pod agent
- Docker build and push
- Shared library import
- Manual approval gate
- Rollback on failure
- Branch-specific behaviour
The Pipeline Flow
trigger
unit+integration
Docker image
staging
manual gate
production
Slack
Feature branches run Test and Build only — no deploy. Only main deploys to staging and beyond.
The Complete Jenkinsfile
Every comment in this file references the lesson where that concept was taught. Read it as a map of the entire course.
// payment-api/Jenkinsfile
// Mini Project — End-to-End CI/CD Pipeline
// Lesson 36: Shared library import — pinned version for production safety
@Library('acme-pipelines@v3.0.0') _
pipeline {
// Lesson 39: Kubernetes pod agent — clean ephemeral build environment
agent {
kubernetes {
label 'java-builder'
defaultContainer 'maven'
}
}
// Lesson 16: Environment variables — declared once, used everywhere
environment {
APP_NAME = 'payment-api'
DOCKER_REGISTRY = 'registry.acmecorp.com'
STAGING_NS = 'staging'
PROD_NS = 'production'
HEALTH_PATH = '/actuator/health'
// Lesson 18: Credentials — injected from Jenkins store, masked in output
// credentials() for usernamePassword creates _USR and _PSW variables
DOCKER_CREDS = credentials('docker-registry-credentials')
}
options {
// Lesson 44 (AP-04): Always set a pipeline timeout
timeout(time: 45, unit: 'MINUTES')
// Lesson 44 (AP-10): Log rotation — never accumulate unlimited builds
buildDiscarder(logRotator(numToKeepStr: '20', artifactNumToKeepStr: '5'))
timestamps()
skipDefaultCheckout()
}
// Lesson 19: Webhook trigger — fires on every push to GitHub
triggers {
githubPush()
}
stages {
stage('Checkout') {
steps {
checkout scm
script {
// Lesson 16: Capture git metadata for image tag and notifications
env.GIT_SHORT = sh(script: 'git rev-parse --short HEAD',
returnStdout: true).trim()
env.GIT_AUTHOR = sh(script: 'git log -1 --format="%an"',
returnStdout: true).trim()
env.GIT_MESSAGE = sh(script: 'git log -1 --format="%s"',
returnStdout: true).trim()
// Build-number + short SHA = unique, traceable image tag
env.IMAGE_TAG = "${APP_NAME}:${BUILD_NUMBER}-${env.GIT_SHORT}"
env.FULL_IMAGE = "${DOCKER_REGISTRY}/${env.IMAGE_TAG}"
echo "Building ${env.FULL_IMAGE}"
echo "'${env.GIT_MESSAGE}' by ${env.GIT_AUTHOR}"
}
}
}
stage('Test') {
steps {
// Lesson 15: Dedicated test stage — failures caught before any image is built
sh './gradlew clean test integrationTest --continue'
}
post {
always {
// Lesson 9: Publish test results regardless of pass/fail
junit allowEmptyResults: true,
testResults: 'build/test-results/**/*.xml'
archiveArtifacts artifacts: 'build/reports/**',
allowEmptyArchive: true
}
}
}
stage('Code Quality') {
steps {
sh './gradlew checkstyleMain spotbugsMain'
}
post {
always {
archiveArtifacts artifacts: 'build/reports/checkstyle/**,build/reports/spotbugs/**',
allowEmptyArchive: true
}
}
}
stage('Build Docker Image') {
steps {
// Lesson 23/39: Switch to the docker container in the build pod
container('docker') {
sh """
docker login -u ${DOCKER_CREDS_USR} -p ${DOCKER_CREDS_PSW} ${DOCKER_REGISTRY}
docker build \
--build-arg BUILD_NUMBER=${BUILD_NUMBER} \
--build-arg GIT_COMMIT=${env.GIT_SHORT} \
-t ${FULL_IMAGE} \
-t ${DOCKER_REGISTRY}/${APP_NAME}:latest \
.
docker push ${FULL_IMAGE}
docker push ${DOCKER_REGISTRY}/${APP_NAME}:latest
"""
}
}
}
stage('Deploy to Staging') {
// Lesson 15/21: Only deploy from main — feature branches stop after Build
when { branch 'main' }
steps {
script {
// Lesson 24: Capture current image for rollback if deploy fails
def previousImage = sh(
script: "kubectl get deployment ${APP_NAME} -n ${STAGING_NS} " +
"-o jsonpath='{.spec.template.spec.containers[0].image}'",
returnStdout: true
).trim()
try {
sh """
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${FULL_IMAGE} \
--namespace=${STAGING_NS}
kubectl rollout status deployment/${APP_NAME} \
--namespace=${STAGING_NS} --timeout=3m
"""
} catch (err) {
echo "Staging deploy failed — rolling back to ${previousImage}"
sh "kubectl rollout undo deployment/${APP_NAME} --namespace=${STAGING_NS}"
error("Deploy to staging failed: ${err.message}")
}
}
}
}
stage('Smoke Test — Staging') {
when { branch 'main' }
steps {
// Lesson 24: timeout + retry for transient startup delay
timeout(time: 2, unit: 'MINUTES') {
retry(3) {
sh """
sleep 5
curl --fail --silent \
https://${APP_NAME}.${STAGING_NS}.acmecorp.com${HEALTH_PATH} \
| grep -q '"status":"UP"'
"""
}
}
echo "Staging smoke test passed — ${env.FULL_IMAGE} is healthy"
}
}
stage('Production Approval') {
when { branch 'main' }
steps {
// Lesson 13: Manual gate — release engineers approve before production
// Outer timeout covers the waiting period — abandons after 24 hrs
timeout(time: 24, unit: 'HOURS') {
input(
message: "Deploy ${env.IMAGE_TAG} to PRODUCTION?\n" +
"Commit: ${env.GIT_MESSAGE}\n" +
"Author: ${env.GIT_AUTHOR}\n" +
"Staging: PASSED",
ok: 'Deploy to Production',
submitter: 'release-engineers,payments-leads'
)
}
}
}
stage('Deploy to Production') {
when { branch 'main' }
steps {
script {
def previousImage = sh(
script: "kubectl get deployment ${APP_NAME} -n ${PROD_NS} " +
"-o jsonpath='{.spec.template.spec.containers[0].image}'",
returnStdout: true
).trim()
withCredentials([file(credentialsId: 'kubeconfig-production',
variable: 'KUBECONFIG')]) {
try {
sh """
kubectl set image deployment/${APP_NAME} \
${APP_NAME}=${FULL_IMAGE} \
--namespace=${PROD_NS}
kubectl rollout status deployment/${APP_NAME} \
--namespace=${PROD_NS} --timeout=5m
"""
} catch (err) {
echo "Production deploy failed — rolling back to ${previousImage}"
sh "kubectl rollout undo deployment/${APP_NAME} --namespace=${PROD_NS}"
error("Production deploy failed: ${err.message}")
}
}
}
}
}
stage('Smoke Test — Production') {
when { branch 'main' }
steps {
timeout(time: 2, unit: 'MINUTES') {
retry(3) {
sh """
sleep 5
curl --fail --silent \
https://${APP_NAME}.acmecorp.com${HEALTH_PATH} \
| grep -q '"status":"UP"'
"""
}
}
echo "Production smoke test passed — ${env.FULL_IMAGE} is live"
}
}
} // end stages
post {
success {
script {
// Lesson 20 / Lesson 44 (AP-07): Notify on success only for main
// Feature branch green builds stay quiet — no alert fatigue
if (env.BRANCH_NAME == 'main') {
slackSend(
channel: '#payments-deployments',
color: 'good',
message: ":white_check_mark: *${APP_NAME}* deployed to production\n" +
"Build: <${BUILD_URL}|#${BUILD_NUMBER}>\n" +
"Image: `${env.IMAGE_TAG}`\n" +
"Commit: _${env.GIT_MESSAGE}_ by ${env.GIT_AUTHOR}"
)
}
}
}
failure {
// Notify on every failure regardless of branch
slackSend(
channel: '#payments-deployments',
color: 'danger',
message: ":x: *${APP_NAME}* build failed — " +
"<${BUILD_URL}|Build #${BUILD_NUMBER}> — ${env.GIT_MESSAGE}"
)
}
// Lesson 25: Always clean workspace — no stale files between builds
always { cleanWs() }
}
}
Where to run this: Fork any Java/Gradle repository on GitHub. Add this Jenkinsfile to the root. In Jenkins, create a Multibranch Pipeline pointing at the repo. Push a commit — Test and Build run automatically on every branch. Merge to main — the full pipeline through to the Production Approval gate runs. Everything configurable is in the environment { } block at the top.
Started by GitHub push — main (priya@acmecorp.com)
Loading library acme-pipelines@v3.0.0
Creating pod: java-builder-p8x3k in namespace jenkins
[Checkout]
+ git rev-parse --short HEAD → e5f6g7h
+ git log -1 --format="%an" → Priya Sharma
+ git log -1 --format="%s" → Add 3DS2 authentication support
Building registry.acmecorp.com/payment-api:88-e5f6g7h
[Test]
+ ./gradlew clean test integrationTest
63 unit tests passed, 0 failed
12 integration tests passed, 0 failed
JUnit results published.
[Code Quality]
+ ./gradlew checkstyleMain spotbugsMain
No critical issues found.
[Build Docker Image]
+ docker login -u jenkins-bot -p **** registry.acmecorp.com → Login Succeeded
+ docker build -t registry.acmecorp.com/payment-api:88-e5f6g7h .
Successfully built 3a4b5c6d
+ docker push registry.acmecorp.com/payment-api:88-e5f6g7h ✓
[Deploy to Staging] branch=main
Previous image captured: payment-api:87-d4e5f6a
kubectl set image → payment-api:88-e5f6g7h --namespace=staging
Waiting for rollout... Deployment rolled out. ✓
[Smoke Test — Staging]
{"status":"UP","service":"payment-api","version":"88-e5f6g7h"}
Staging smoke test passed — payment-api:88-e5f6g7h is healthy
[Production Approval]
Waiting for approval (submitters: release-engineers, payments-leads)
Approved by: carlos (release-engineer) at 14:32:07 UTC
[Deploy to Production]
Previous image captured: payment-api:86-c3d4e5b
kubectl set image → payment-api:88-e5f6g7h --namespace=production
Waiting for rollout... Deployment rolled out. ✓
[Smoke Test — Production]
{"status":"UP","service":"payment-api","version":"88-e5f6g7h"}
Production smoke test passed — payment-api:88-e5f6g7h is live
[post/success]
Slack: ✅ payment-api deployed to production — Build #88
Pod java-builder-p8x3k deleted.
Workspace cleaned.
Finished: SUCCESS (total: 18m 42s)
What just happened — the full course in one build
- Shared library loaded at startup (L36) — pinned to
v3.0.0for production safety. All shared helper functions are available without duplication in the Jenkinsfile. - Ephemeral Kubernetes pod (L39) — created fresh for this build, deleted after. No state leaks between builds, no disk accumulates on a static agent.
- Git metadata as env vars (L16) — commit message, author, and short SHA captured with
returnStdout: true. Used in the image tag, approval message, and Slack notification — one pipeline, complete context everywhere. - Credentials masked (L18) — Docker password appears as
****. Thecredentials()helper registers the value with Jenkins' secret masker — it cannot be accidentally printed in plain text. - Rollback in both deploys (L24) — previous image captured before each deploy. If the rollout failed,
kubectl rollout undoruns automatically before re-throwing the error. The cluster is never left in an unknown state. - Manual approval gate with context (L13) — Carlos saw the commit message, author, and staging health before approving. Everything in the approval message came from environment variables the pipeline already held.
- Feature branches stayed quiet (L44 AP-07) — only the
mainbranch success notifies Slack. Developer feature branch pushes don't spam the deployments channel. - 18m 42s total — every minute accounted for — tests 5m, Docker build 4m, staging deploy + smoke 3m, approval wait 6m, production deploy + smoke 2m. Every stage is visible, timed, and attributable.
Adapting This Pipeline for Your Service
| Your stack | What to change |
|---|---|
| Node.js / npm | Agent label → node-builder, Test → npm ci && npm test, Quality → npm run lint |
| Python / pytest | Agent image → python:3.12, Test → pytest --junitxml=results.xml, Quality → ruff check . |
| Non-Kubernetes deploy | Replace kubectl set image with helm upgrade, ansible-playbook, or ssh deploy@server ./deploy.sh |
| No staging environment | Remove Deploy Staging and Smoke Test Staging — keep the approval gate before production |
| No manual approval | Remove the Production Approval stage — production deploys automatically after staging smoke test passes |
Course Complete
You now know Jenkins end to end.
From what Jenkins is and why it exists, through Freestyle jobs, Declarative pipelines, security, plugins, scaling, Kubernetes, IaC, microservices, migration, troubleshooting, and anti-patterns — you have built the complete mental model. The pipeline in this lesson is not an exercise. It is what production CI/CD looks like at a company that has Jenkins working well.
Where to go next
- Run this pipeline on a real service
- Write and test a shared library
- Set up JCasC on a staging Jenkins
- Add Prometheus + Grafana monitoring
Official resources
- jenkins.io/doc — official docs
- plugins.jenkins.io — plugin index
- community.jenkins.io — forums
- github.com/jenkinsci — source
Teacher's Note
The best way to consolidate everything in this course is to run a real pipeline — not a tutorial exercise. Pick an actual service you work on, build this pipeline for it, and break it deliberately. Fix the OOM. Hit the timeout. Let the smoke test catch a bad deploy. The failures teach more than the successes.
Final Practice Questions
1. What condition ensures the Deploy to Staging, Approval, and Deploy to Production stages only run on the main branch — not on feature branches?
2. What rollback mechanism does this pipeline use if a Kubernetes deployment fails in staging or production?
3. Under what conditions does this pipeline send a Slack notification?
Final Quiz
1. Why does this pipeline deploy the same Docker image tag to both staging and production rather than rebuilding for production?
2. Which two options { } directives in this pipeline directly address the anti-patterns from Lesson 44?
3. What does a Kubernetes pod agent give you over a static VM agent for CI builds?
🎉
Course Complete
You have completed the Jenkins course from first principles to production-grade CI/CD. You have the knowledge to build, secure, scale, troubleshoot, and maintain Jenkins at any team size. Now go build something real.