Category: A CI/CD pipeline can be completely green and still be badly designed.
The application compiles. Tests pass. Jenkins creates a WAR file. A Docker image reaches a registry. Production gets deployed. From the dashboard, everything appears healthy.
Then someone asks a deceptively simple question:
What exact artifact is running in production?
If the answer involves rebuilding the application, checking timestamps manually, searching a Jenkins workspace, or hoping that latest still points to the right image, the pipeline has an artifact-management problem.
A production pipeline should follow a stronger rule:
Build an artifact once, give it an immutable identity, store it safely, and promote that exact artifact through every environment.
That principle sounds simple. Implementing it correctly changes how you think about builds, releases, rollbacks, security, and CI/CD itself.
The artifact is the output of your build
Suppose Jenkins builds a Java application:
mvn clean package
The result might be:
target/vprofile-v2.war
That WAR file is a build artifact.
If the application is containerized, the Docker image produced from it becomes another deployable artifact:
docker build -t vprofile:1.4.7 .
The important distinction is that source code and artifacts are not the same thing.
Git stores the instructions required to build software. The artifact is the concrete output created from those instructions.
A mature delivery process therefore looks roughly like this:
Source Code
|
v
Build
|
v
Test
|
v
Versioned Artifact
|
v
Artifact Repository
|
+------> Development
|
+------> Staging
|
+------> Production
Notice what is missing: rebuilding between environments.
That omission is deliberate.
Why rebuilding for every environment is dangerous
Imagine commit a91fd42 passes testing and generates:
vprofile-1.4.7.war
You deploy it to staging and verify it.
Later, instead of promoting that file to production, another pipeline runs:
git checkout a91fd42
mvn clean package
It feels equivalent. It is not necessarily equivalent.
The second build may encounter:
- a different dependency resolution
- an updated base image
- a changed build tool
- modified environment variables
- different compiler settings
- an altered external package repository
You tested Build A, but production received Build B.
Even when both originated from the same Git commit, they are separate build outputs.
Rebuild the same commit for production.
Promote the artifact that already passed testing.
This is the core meaning of build once, promote everywhere.
Give every artifact a real identity
A common beginner solution is timestamp-based versioning:
TIMESTAMP=$(date +%Y%m%d%H%M%S)
cp target/vprofile.war versions/vprofile-$TIMESTAMP.war
This is better than repeatedly overwriting vprofile.war, but timestamps alone provide weak traceability.
If production is running:
vprofile-20260905184530.war
you still need to determine which commit produced it.
A useful artifact identity should connect the binary back to the CI/CD execution and source revision that created it.
For example:
vprofile-142-a91fd42.war
where:
142is the Jenkins build numbera91fd42is the short Git commit SHA
A Jenkins pipeline can derive this information directly:
stage('Version Artifact') {
steps {
script {
env.GIT_SHA = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.ARTIFACT_NAME =
"vprofile-${BUILD_NUMBER}-${env.GIT_SHA}.war"
}
}
}
Then copy the build output using the generated identity:
stage('Prepare Artifact') {
steps {
sh '''
mkdir -p versions
cp target/*.war versions/$ARTIFACT_NAME
'''
}
}
Now the artifact itself carries useful traceability.
Jenkins workspaces are not artifact repositories
This distinction prevents a surprisingly common architectural mistake.
A Jenkins workspace exists primarily to execute builds:
/var/lib/jenkins/workspace/vprofile-pipeline/
It should not become your permanent release archive.
Workspaces can disappear because of:
- cleanup policies
- job deletion
- node replacement
- ephemeral build agents
- disk failures
- autoscaling
- manual cleanup
Storing months of production binaries inside build workspaces also ties release history to Jenkins infrastructure unnecessarily.
For small pipelines, Jenkins can archive artifacts:
post {
success {
archiveArtifacts(
artifacts: 'versions/*.war',
fingerprint: true
)
}
}
That is useful for build-level retention and traceability.
For durable delivery architecture, artifacts should normally move into a dedicated storage system such as Amazon S3, an artifact repository, or a container registry.
For example:
aws s3 cp \
versions/vprofile-142-a91fd42.war \
s3://company-artifacts/vprofile/releases/
Containerized applications follow the same principle with registries such as Amazon ECR.
The storage technology changes. The lifecycle principle does not.
Version Docker images properly
Container pipelines frequently recreate the same problem through careless tagging.
Consider:
docker build -t vprofile:latest .
docker push vprofile:latest
latest is only a tag. It is not a trustworthy release identity.
Today it may reference one image. Tomorrow somebody pushes another image under the same tag.
A better pipeline creates immutable version tags:
docker build \
-t vprofile:${BUILD_NUMBER}-${GIT_SHA} .
For Amazon ECR, that could become:
docker tag \
vprofile:${BUILD_NUMBER}-${GIT_SHA} \
123456789012.dkr.ecr.us-east-1.amazonaws.com/vprofile:${BUILD_NUMBER}-${GIT_SHA}
docker push \
123456789012.dkr.ecr.us-east-1.amazonaws.com/vprofile:${BUILD_NUMBER}-${GIT_SHA}
Production can then reference the exact version that passed previous stages.
For even stronger guarantees, container platforms can deploy by image digest, which identifies image content rather than relying only on a human-readable tag.
Promotion is different from rebuilding
This distinction is central to reliable CI/CD.
| Approach | Rebuild per environment | Same artifact tested | Traceability | Rollback |
|---|---|---|---|---|
| Rebuild everywhere | Yes | No | Weaker | Harder |
| Promote versioned artifact | No | Yes | Strong | Easier |
| Promote by image digest | No | Yes | Very strong | Precise |
A release pipeline might therefore operate like this:
Commit a91fd42
|
v
Jenkins Build #142
|
v
vprofile-142-a91fd42
|
v
Unit / Security Tests
|
v
Artifact Repository
|
v
Staging
|
v
Approval
|
v
Production
Production receives the object that already survived the pipeline.
That gives the artifact a history instead of merely a filename.
Rollback becomes a deployment operation
Bad artifact management turns rollback into another build.
Suppose release 142-a91fd42 introduces a production issue.
If the old binary disappeared, the team may need to find an older Git revision and rebuild it.
That adds uncertainty during an incident, precisely when uncertainty is least welcome.
With retained immutable artifacts, previous versions remain available:
vprofile-139-72bf812.war
vprofile-140-c923a11.war
vprofile-141-f834ce2.war
vprofile-142-a91fd42.war
If build 141 was the last known-good release, rollback means redeploying:
vprofile-141-f834ce2.war
No compilation. No dependency resolution. No new artifact.
Rollback should usually select a previously validated artifact, not manufacture another one.
Artifact integrity matters too
Versioning solves identity, but identity alone does not prove that an artifact remained unchanged.
Checksums provide another useful control.
For example:
sha256sum versions/vprofile-142-a91fd42.war
Output might resemble:
8f4c...91ae versions/vprofile-142-a91fd42.war
If the file changes, its SHA-256 digest changes.
This enables a deployment system to verify that the artifact being released is the artifact originally produced.
Modern supply-chain security goes further with signed artifacts, provenance information, software bills of materials, and restricted repository permissions. The underlying principle remains straightforward:
Once a release artifact has been approved, nobody should quietly modify it.
Common mistakes that weaken pipelines
Several patterns should immediately trigger suspicion:
- Reusing filenames such as
app.warfor every release - Using only
latestfor production container images - Keeping the only artifact copy inside a Jenkins workspace
- Rebuilding an application separately for staging and production
- Allowing published release artifacts to be overwritten
- Having no mapping between an artifact and its Git commit
- Deleting previous production artifacts before their rollback window expires
- Giving build systems unnecessary write or delete permissions
These mistakes often survive in small projects because nothing breaks immediately.
That does not make them good architecture. It merely means the bill has not arrived yet.
A practical Jenkins artifact flow
A simplified pipeline can implement the essential pattern:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/example/vprofile.git'
}
}
stage('Build and Test') {
steps {
sh 'mvn clean verify'
}
}
stage('Version') {
steps {
script {
env.GIT_SHA = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
env.ARTIFACT_NAME =
"vprofile-${BUILD_NUMBER}-${env.GIT_SHA}.war"
}
sh '''
mkdir -p versions
cp target/*.war versions/$ARTIFACT_NAME
'''
}
}
stage('Archive') {
steps {
archiveArtifacts(
artifacts: 'versions/*.war',
fingerprint: true
)
}
}
}
}
A production implementation should additionally define retention policies, centralized artifact storage, credentials management, security scanning, deployment controls, and repository permissions.
The important architectural boundary is already present:
Build -> Verify -> Identify -> Store -> Promote
Each stage has a clear responsibility.
Treat artifacts as release records
Artifact management may initially look like housekeeping: rename a WAR file, keep a few versions, archive some Jenkins output.
It is much more important than that.
A well-designed artifact lifecycle answers critical operational questions:
- What exactly is running?
- Which commit created it?
- Which CI build produced it?
- Did this exact artifact pass testing?
- Has it changed since the build?
- Where is the previous version?
- Can we roll back without rebuilding?
If your delivery system cannot answer those questions quickly, adding more deployment automation will not fix the underlying weakness. It will simply automate uncertainty faster, which is a remarkably efficient way to create production incidents.
Jenkins provides artifact archiving and fingerprinting for build traceability. AWS documents durable object storage through Amazon S3 and container image management through Amazon ECR. These tools solve different parts of the problem, but the engineering rule connecting them is the same.
Build once. Identify it. Verify it. Store it immutably. Promote that exact artifact.
When production fails at 2 a.m., you should be choosing a known-good version to redeploy, not reopening Maven and hoping history recompiles itself.
Enjoyed this? I write about what I'm learning.
DevOps, cloud, Linux, and automation - one honest post at a time. Follow along on my learning journey.