Promoting a release from staging to production Jump to heading
Promotion is the moment where a workflow either keeps its guarantee or quietly discards it. The guarantee is that what runs in production is what was tested in staging; the usual way of losing it is a pipeline that rebuilds from source at each stage, producing an artefact that differs in ways nobody chose β a newer base image, a dependency that moved, a build cache that was warm the first time. This recipe promotes by moving references rather than by rebuilding, within environment and deployment branches.
When to use this approach Jump to heading
- You have at least two environments and a promotion step between them.
- Production incidents have been traced to differences between builds.
- You need to state exactly what is running, with evidence.
- Your deployment system can deploy a specific artefact by digest.
- If you deploy directly from the default branch with no staging step, there is nothing to promote β the relevant material is in the parent topic.
Step 1 β Establish what staging is actually running Jump to heading
Not what the branch says; what is deployed.
# The commit the staging reference points at
git rev-parse --short origin/staging
# The artefact actually running, by digest
kubectl get deploy app -n staging -o jsonpath='{.spec.template.spec.containers[0].image}' # Verification: they must agree
img=$(kubectl get deploy app -n staging -o jsonpath='{.spec.template.spec.containers[0].image}')
echo "$img" | grep -q "$(git rev-parse --short origin/staging)" \
&& echo "staging matches its reference" \
|| echo "MISMATCH β resolve before promoting" A mismatch here means someone deployed out of band. Promoting on top of it propagates an unknown state to production, so it is worth resolving before anything else.
Step 2 β Check the promotion is a fast-forward Jump to heading
git fetch origin
git merge-base --is-ancestor origin/production origin/staging \
&& echo "clean promotion" \
|| { echo "production has commits staging does not β reconcile first"; exit 1; } # What exactly is being promoted?
git log --oneline origin/production..origin/staging
git diff --stat origin/production origin/staging Step 3 β Re-tag the artefact rather than rebuilding it Jump to heading
sha=$(git rev-parse --short origin/staging)
digest=$(crane digest "registry.example.com/app:$sha")
echo "promoting digest: $digest"
# Re-tag the identical image; no build step is involved
crane tag "registry.example.com/app@$digest" production # Verification: the production tag resolves to the same digest as the commit tag
test "$(crane digest registry.example.com/app:production)" = "$digest" \
&& echo "identical artefact promoted" SAFETY WARNING β a promotion pipeline that runs a build step is not promoting, it is releasing again. Even with pinned dependencies, the second build differs in timestamps, layer ordering and whatever the base image resolved to that hour, and the difference is invisible until it is the explanation for an incident. Promote by digest; if your pipeline cannot, treat fixing that as the highest-value change available.
Step 4 β Move the reference and deploy Jump to heading
git switch production
git merge --ff-only origin/staging
git push origin production # Or, in a directory-per-environment layout, one edit in a pull request
yq -i ".images[0].newTag = \"$sha\"" overlays/production/kustomization.yaml
git commit -am "chore(release): promote $sha to production" # Verification: the deployed digest matches what was promoted
kubectl rollout status deploy/app -n production --timeout=5m
kubectl get deploy app -n production -o jsonpath='{.spec.template.spec.containers[0].image}' Step 5 β Record the deployment as evidence Jump to heading
stamp=$(date -u +%Y%m%dT%H%M%SZ)
git tag -a "deploy/production/$stamp" \
-m "digest: $digest
commit: $(git rev-parse origin/production)
promoted-from: staging
by: $USER" origin/production
git push origin "deploy/production/$stamp" # Verification: the last three production deployments, with their digests
git for-each-ref --sort=-creatordate --count=3 \
--format='%(creatordate:short) %(refname:short)%0a%(contents)' 'refs/tags/deploy/production/*' Tags are a convenient store for this because they are immutable, replicated to everyone, and queryable without a database. Signing them, as described in signing and verifying release tags, turns the record into evidence rather than a note.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What if staging and production need different configuration? Jump to heading
Configuration is injected at deployment time and lives in overlays or in the deployment system, not in the artefact. If promoting requires rebuilding because configuration is baked in, that is the thing to change β it is also what makes every incident harder to reason about.
How do we promote several services that must move together? Jump to heading
Promote them in one pull request that updates every reference, so the change is atomic and reviewable. Where the deployment system cannot apply them atomically, promote in dependency order and record each one; the record is what makes a partial promotion diagnosable.
Should promotion require an approval? Jump to heading
For production, usually yes, and the approval belongs on the promotion pull request rather than in a separate system. That keeps the evidence together: what was promoted, who approved it, and when, in one object that outlives the pipeline run.
Related Jump to heading
- Environment & Deployment Branches β the parent topic and the promotion invariant.
- Rolling Back a Deployment With Git β undoing a promotion that went wrong.
- Linking a Container Image to Its Commit β proving the digest came from the commit.