CI/CD Artifact Management: Build, Store, and Promote
Learn how to manage build artifacts in CI/CD pipelines including versioning, storage, promotion strategies, and cleanup policies.
What you'll learn
- ✓Build and version artifacts consistently in CI/CD pipelines
- ✓Store artifacts in registries with proper tagging strategies
- ✓Promote artifacts across environments without rebuilding
- ✓Implement cleanup policies to manage storage costs
Prerequisites
- •Basic CI/CD pipeline experience
- •Docker fundamentals
- •Familiarity with package registries
Every CI/CD pipeline produces artifacts: compiled binaries, Docker images, npm packages, JAR files, or deployment bundles. How you build, store, version, and promote these artifacts determines whether your deployments are reliable and reproducible or chaotic and unpredictable.
A well-designed artifact management strategy ensures that the exact same artifact tested in staging is the one deployed to production. No rebuilding, no recompiling, no surprises. This guide covers everything from building artifacts with proper versioning to promoting them through your deployment pipeline.
What Are Build Artifacts
Build artifacts are the outputs of your CI/CD pipeline. They include compiled application binaries, Docker container images, npm or Python packages, Terraform plan files, static website bundles, and Helm charts. The key principle is that artifacts should be immutable. Once built, they should never be modified. When you need a change, you build a new artifact with a new version.
Versioning Strategies
Consistent versioning is the foundation of artifact management. Every artifact needs a unique identifier that tells you exactly what code it contains.
Semantic Versioning
Semantic versioning (semver) uses the format MAJOR.MINOR.PATCH:
# Determine version based on commit messages
# patch: fix typo in login form
# minor: add user profile page
# major: redesign authentication API
# Automate with semantic-release
npm install --save-dev semantic-release @semantic-release/changelog @semantic-release/git
{
"release": {
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
["@semantic-release/npm", {
"npmPublish": false
}],
["@semantic-release/git", {
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version}"
}],
"@semantic-release/github"
]
}
}
Git-Based Versioning
For container images, combining semver with Git metadata gives you traceability:
# Generate version tags
VERSION="1.2.3"
GIT_SHA=$(git rev-parse --short HEAD)
BRANCH=$(git rev-parse --abbrev-ref HEAD)
BUILD_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Tag examples:
# 1.2.3
# 1.2.3-abc1234
# main-abc1234
# latest (only for main branch)
Building Docker Artifacts
Docker images are the most common artifact type in modern CI/CD pipelines. Building them efficiently and consistently requires attention to layering, caching, and metadata.
Optimized Dockerfile
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency files first for better caching
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm build
# Production stage
FROM node:20-alpine AS production
WORKDIR /app
# Add labels for traceability
ARG VERSION
ARG GIT_SHA
ARG BUILD_DATE
LABEL org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${GIT_SHA}" \
org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.source="https://github.com/my-org/myapp"
# Copy only production artifacts
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
# Use non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "dist/main.js"]
Build and Push Workflow
name: Build and Publish
on:
push:
branches: [main]
tags: ['v*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
VERSION=${{ steps.meta.outputs.version }}
GIT_SHA=${{ github.sha }}
BUILD_DATE=${{ github.event.head_commit.timestamp }}
Storing Artifacts
Different artifact types need different storage solutions.
GitHub Actions Artifacts
For pipeline-internal artifacts like test reports and build outputs that need to be shared between jobs:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build application
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 14
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
coverage/
test-results/
retention-days: 30
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy
run: |
# Deploy the exact artifact that was built and tested
aws s3 sync dist/ s3://my-bucket/ --delete
Container Registry Organization
Organize your container registry to support promotion across environments:
# Registry structure
ghcr.io/my-org/myapp:sha-abc1234 # Immutable, built from commit
ghcr.io/my-org/myapp:1.2.3 # Release version
ghcr.io/my-org/myapp:staging # Currently deployed to staging
ghcr.io/my-org/myapp:production # Currently deployed to production
npm Package Registry
For shared libraries in a monorepo, publish to a private npm registry:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: 'https://npm.pkg.github.com'
- run: npm ci
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Artifact Promotion
Promotion is the process of moving an artifact from one environment to the next without rebuilding it. This guarantees that the exact binary tested in staging is deployed to production.
Tag-Based Promotion
The simplest promotion strategy uses registry tags:
name: Promote to Production
on:
workflow_dispatch:
inputs:
image_tag:
description: 'Image tag to promote (e.g., sha-abc1234)'
required: true
jobs:
promote:
runs-on: ubuntu-latest
environment: production # Requires approval
permissions:
packages: write
steps:
- name: Log in to Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Promote image
run: |
SOURCE="ghcr.io/my-org/myapp:${{ inputs.image_tag }}"
TARGET="ghcr.io/my-org/myapp:production"
# Pull the staging-tested image
docker pull "$SOURCE"
# Tag it for production
docker tag "$SOURCE" "$TARGET"
# Push the production tag
docker push "$TARGET"
echo "Promoted $SOURCE to $TARGET"
- name: Deploy to production
run: |
kubectl set image deployment/myapp \
myapp="ghcr.io/my-org/myapp:${{ inputs.image_tag }}"
kubectl rollout status deployment/myapp --timeout=300s
Promotion Pipeline
A complete promotion pipeline that moves artifacts through environments:
name: Release Pipeline
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Build and push image
id: meta
run: |
TAG="sha-$(git rev-parse --short HEAD)"
echo "version=$TAG" >> "$GITHUB_OUTPUT"
docker build -t ghcr.io/my-org/myapp:$TAG .
docker push ghcr.io/my-org/myapp:$TAG
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- name: Deploy to staging
run: |
kubectl config use-context staging
kubectl set image deployment/myapp \
myapp="ghcr.io/my-org/myapp:${{ needs.build.outputs.image_tag }}"
- name: Run smoke tests
run: |
curl --fail https://staging.myapp.example.com/health
deploy-production:
needs: [build, deploy-staging]
runs-on: ubuntu-latest
environment: production # Manual approval gate
steps:
- name: Deploy to production
run: |
kubectl config use-context production
kubectl set image deployment/myapp \
myapp="ghcr.io/my-org/myapp:${{ needs.build.outputs.image_tag }}"
Artifact Cleanup
Artifacts accumulate over time and increase storage costs. Implement automated cleanup policies.
Container Image Cleanup
name: Cleanup Old Images
on:
schedule:
- cron: '0 2 * * 0' # Weekly on Sunday at 2 AM
jobs:
cleanup:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Delete old container images
uses: actions/delete-package-versions@v5
with:
package-name: myapp
package-type: container
min-versions-to-keep: 20
delete-only-untagged-versions: true
- name: Delete old pre-release images
uses: actions/delete-package-versions@v5
with:
package-name: myapp
package-type: container
min-versions-to-keep: 5
delete-only-pre-release-versions: true
S3 Artifact Lifecycle Policy
For artifacts stored in S3, use lifecycle rules:
{
"Rules": [
{
"ID": "Delete old build artifacts",
"Status": "Enabled",
"Filter": {
"Prefix": "artifacts/builds/"
},
"Expiration": {
"Days": 90
}
},
{
"ID": "Move old releases to cheaper storage",
"Status": "Enabled",
"Filter": {
"Prefix": "artifacts/releases/"
},
"Transitions": [
{
"Days": 30,
"StorageClass": "STANDARD_IA"
},
{
"Days": 180,
"StorageClass": "GLACIER"
}
]
}
]
}
aws s3api put-bucket-lifecycle-configuration \
--bucket my-artifacts-bucket \
--lifecycle-configuration file://lifecycle-policy.json
Wrapping Up
Artifact management is the connective tissue of your CI/CD pipeline. Build artifacts once with proper versioning and metadata. Store them in appropriate registries with consistent tagging. Promote the same immutable artifact across environments rather than rebuilding. And clean up old artifacts automatically to control storage costs. Following these practices ensures your deployments are reproducible, traceable, and efficient. Start by adding proper image tagging and a promotion workflow to your existing pipeline, then layer in cleanup policies as your artifact volume grows.
Related articles
- CI/CD Optimize Docker Builds in CI/CD Pipelines
Speed up Docker builds in CI/CD — multi-stage builds, layer caching, BuildKit, registry caching, and slim base images for faster deployments.
- CI/CD GitHub Actions vs Jenkins: CI/CD Compared
Compare GitHub Actions and Jenkins for CI/CD pipelines. Covers setup, configuration, plugins, pricing, and when to choose each tool for your team.
- CI/CD GitHub Actions: Complete CI/CD Guide for Any Project
Set up GitHub Actions CI/CD from scratch — workflows, jobs, caching, secrets, matrix builds, deployment, and reusable patterns for any language or framework.
- CI/CD CI/CD for Monorepos: Turborepo and Nx Pipelines
Build efficient CI/CD pipelines for monorepos using Turborepo and Nx with smart caching, affected-only builds, and parallel execution.