Skip to content
Codeloom
CI/CD

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.

·7 min read · By Codeloom
Intermediate 13 min read

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.