Understanding Artifact Promotion
What Is Artifact Promotion in Simple Terms
Artifact promotion is the discipline of building once and shipping the same thing everywhere. You build a Docker image in CI, test it thoroughly, and then that exact image — identified by its immutable SHA256 digest — is what gets deployed to staging and then to production. No rebuilding. No repackaging. The exact artifact that passed every test is what your users run.
The alternative — rebuilding the image for each environment — is a common mistake that invalidates the entire purpose of CI testing. If the production image is a fresh build from the same source code, environmental differences in the build process (different base image layers, different dependency versions pulled at build time) mean the production image is technically untested.
How It Works
+------------------------------------------+| Build Job || Git commit: abc123 || Docker image: payment-api:abc123 || Image digest: sha256:f8a2b3... |+------------------------------------------+ | push to ECR, run tests on image | v+------------------------------------------+| Test Job || Pulls image: sha256:f8a2b3... || Runs 847 tests -- all pass || Security scan: 0 HIGH/CRITICAL findings |+------------------------------------------+ | promote SAME image to staging | v+------------------------------------------+| Deploy Staging || Deploys: sha256:f8a2b3... to staging || Smoke tests pass |+------------------------------------------+ | promote SAME image to production | v+------------------------------------------+| Deploy Production || Deploys: sha256:f8a2b3... to production || Same image tested in CI and staging |+------------------------------------------+Implementation with image digest pinning:
jobs: build: runs-on: ubuntu-latest outputs: image-digest: ${{ steps.push.outputs.digest }} steps: - name: Build and push image id: push uses: docker/build-push-action@v5 with: push: true ## Tag with git SHA for immutability tags: | ${{ env.ECR_REGISTRY }}/payment-api:${{ github.sha }} ${{ env.ECR_REGISTRY }}/payment-api:latest deploy-staging: needs: build runs-on: ubuntu-latest steps: - name: Deploy to staging env: ## Use the exact digest from build, not a tag IMAGE_DIGEST: ${{ needs.build.outputs.image-digest }} run: | ## Deploy using digest -- immune to tag mutation helm upgrade payment-api ./charts/payment-api \ --set image.repository=$ECR_REGISTRY/payment-api \ --set image.digest=$IMAGE_DIGEST \ -n payment-api-staging deploy-production: needs: [build, deploy-staging] runs-on: ubuntu-latest environment: production steps: - name: Deploy to production env: ## SAME digest as staging -- not a new build IMAGE_DIGEST: ${{ needs.build.outputs.image-digest }} run: | helm upgrade payment-api ./charts/payment-api \ --set image.digest=$IMAGE_DIGEST \ -n payment-api-productionTroubleshooting
| Symptom | Check | What to Look For |
|---|---|---|
| Different behaviour in staging vs prod | Image tags vs digests | Tags are mutable -- use digests |
| Cannot reproduce a production issue | Image traceability | Tag images with git SHA always |
| Latest tag causing inconsistency | Tag strategy | Never deploy using :latest tag |
COMMON MISTAKE / WARNING**Common Mistake:** Tagging images with :latest and deploying that tag to production. The :latest tag is mutable — it can point to a different image between your test run and your deployment. Always tag with the immutable Git commit SHA (payment-api:abc1234) and optionally add :latest as an alias. Deploy using the SHA tag or the image digest.
REMEMBER THIS**Remember:** An image digest (sha256:...) is the only truly immutable image reference. A tag like :v1.2.3 can be moved to point to a different image. A digest is a cryptographic hash of the image content — it cannot be changed or faked. Use digests for production deployments where audit trail and reproducibility matter.
COMMON MISTAKE / WARNING**Security:** The artifact promotion pattern is also a supply chain security control. When you build once and promote the digest through environments, you have a complete chain of custody: this exact image was built from this commit, scanned at this time, tested by these jobs, and approved by these engineers. Rebuilding per environment breaks this chain and makes supply chain attestation impossible.
PLACEMENT PRO TIP**Tip:** Add the Git commit SHA and pipeline run ID to your Docker image labels during build. Use `--label git.commit=${{ github.sha }} --label ci.run=${{ github.run_id }}`. This metadata is stored inside the image and is readable with docker inspect — providing a complete audit trail from the running container back to the exact commit and pipeline run that produced it.