Artifact management
In this series (10 parts)
Prerequisite: Testing in CI pipelines.
A CI pipeline that passes all tests but produces nothing deployable is just an expensive linter. The real goal is to produce a build artifact: a container image, binary, package, or tarball. That artifact is the unit of deployment.
What is a build artifact?
An artifact is the output of a build process. It is immutable: once created, never modified. The same artifact that passes tests in staging runs in production.
Common artifact types:
| Type | Example | Registry |
|---|---|---|
| Container image | app:v1.4.2 | Docker Hub, GitHub Container Registry, ECR, GCR |
| npm package | @myorg/utils@3.1.0 | npm, GitHub Packages |
| Python wheel | mylib-2.0.0-py3-none-any.whl | PyPI, Artifactory |
| Java JAR/WAR | api-service-1.0.0.jar | Maven Central, Nexus |
| Binary | cli-linux-amd64 | GitHub Releases, S3 |
| Helm chart | myapp-chart-0.5.0.tgz | OCI registry, ChartMuseum |
Artifact flow
graph LR SRC["Source Code"] --> BUILD["Build Stage"] BUILD --> TAG["Tag + Version"] TAG --> SIGN["Sign Artifact"] SIGN --> PUSH["Push to Registry"] PUSH --> SCAN["Vulnerability Scan"] SCAN --> PROMOTE["Promote to Production"] style SRC fill:#64748b,color:#fff style BUILD fill:#3b82f6,color:#fff style TAG fill:#8b5cf6,color:#fff style SIGN fill:#ec4899,color:#fff style PUSH fill:#f59e0b,color:#000 style SCAN fill:#ef4444,color:#fff style PROMOTE fill:#22c55e,color:#000
Artifact lifecycle from source to production.
Every artifact has a version, a signature, a scan result, and a promotion record.
Versioning strategies
Semantic Versioning (SemVer)
The format is MAJOR.MINOR.PATCH. Bump major for breaking changes, minor for features, patch for fixes. Works well for libraries and public APIs.
# Tag a release
git tag v2.3.1
git push origin v2.3.1
Git SHA
Use the short commit SHA as the version. This ties the artifact directly to a specific commit. No ambiguity.
steps:
- name: Build and push image
run: |
IMAGE=ghcr.io/myorg/api:${{ github.sha }}
docker build -t $IMAGE .
docker push $IMAGE
Date-based
Format: YYYY.MM.DD-BUILD_NUMBER. Useful for applications with frequent releases where SemVer would create noise.
VERSION="2026.04.20-${GITHUB_RUN_NUMBER}"
docker build -t "ghcr.io/myorg/api:${VERSION}" .
Hybrid approach
Most teams combine strategies. Container images get SHA tags for traceability and SemVer tags for human readability. The same image has two tags pointing to the same digest.
- name: Tag image
run: |
SHA_TAG="ghcr.io/myorg/api:${{ github.sha }}"
SEMVER_TAG="ghcr.io/myorg/api:v2.3.1"
docker tag $SHA_TAG $SEMVER_TAG
docker push $SHA_TAG
docker push $SEMVER_TAG
Container registries
A container registry stores and serves container images. The major options:
- GitHub Container Registry (ghcr.io): free for public images, integrated with GitHub Actions.
- Amazon ECR: tight IAM integration with AWS services.
- Google Artifact Registry: supports containers, npm, Maven, and Python in one place.
- Docker Hub: the original. Rate limits on free tier can cause pipeline failures.
# GitHub Actions: login and push to GHCR
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:${{ github.sha }}
ghcr.io/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
The cache-from and cache-to directives use GitHub Actions cache for Docker layer caching. This can cut build times by 50% or more on subsequent runs.
Package registries
Not everything is a container. Libraries go to package registries.
# Publish an npm package to GitHub Packages
publish:
needs: test
runs-on: ubuntu-latest
permissions:
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 }}
The registry-url in setup-node configures the .npmrc automatically. No manual token file creation.
Artifact signing and provenance
An unsigned artifact is a trust-me artifact. Anyone with registry write access could replace it. Signing proves who built it and that it has not been modified.
Cosign (part of Sigstore)
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign image
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.sha }}"
cosign sign --yes $IMAGE
env:
COSIGN_EXPERIMENTAL: "true"
Keyless signing with Sigstore uses OpenID Connect tokens from the CI provider. No key management required. The signature is stored in a transparency log (Rekor) that anyone can audit.
Verifying signatures
cosign verify \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--certificate-identity-regexp="https://github.com/myorg/.*" \
ghcr.io/myorg/api:v2.3.1
If verification fails, the image was not built by your pipeline. Do not deploy it.
Build provenance
Provenance goes beyond signing. It records what source code was used, what build commands ran, and what dependencies were included. GitHub Actions can generate SLSA provenance attestations natively.
- name: Generate provenance
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Retention policies
Artifacts accumulate fast. A team pushing 10 builds a day generates 300 images a month. Without cleanup, registry costs grow linearly.
Set retention policies based on environment:
| Environment | Retention |
|---|---|
| Development | 7 days |
| Staging | 30 days |
| Production | 1 year or indefinite |
GitHub Container Registry supports lifecycle policies via the API. ECR has built-in lifecycle rules.
{
"rules": [
{
"rulePriority": 1,
"description": "Keep last 10 production images",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["v"],
"countType": "imageCountMoreThan",
"countNumber": 10
},
"action": { "type": "expire" }
},
{
"rulePriority": 2,
"description": "Expire untagged images after 7 days",
"selection": {
"tagStatus": "untagged",
"countType": "sinceImagePushed",
"countUnit": "days",
"countNumber": 7
},
"action": { "type": "expire" }
}
]
}
Tag your production images with SemVer. Tag development images with SHA or branch name. Let lifecycle rules clean up the rest.
Key principles
- Build once, deploy everywhere: the same artifact moves through staging and production. Never rebuild per environment.
- Immutable artifacts: once published, an artifact is never overwritten. Use unique tags.
- Sign everything: unsigned artifacts are a supply chain risk.
- Automate cleanup: retention policies prevent storage costs from growing without bound.
- Trace from production to source: every running artifact should link back to its commit, build job, and test results.
What comes next
Artifacts flow through your pipeline, but how do you ensure nothing malicious enters that flow? Pipeline security and supply chain covers dependency scanning, SAST, container scanning, SBOM generation, and the SLSA framework.