Environment Promotion
Environment Promotion
Environment promotion is the controlled process of moving a verified artifact — a container image at a specific digest — from a lower-trust environment (dev) through a staging environment and finally into production, with a Git commit as the immutable audit record at each step. In GitOps, promotion is not a button in a UI or a flag in a shell script. It is a deliberate change to the desired state stored in Git. This distinction is the entire point.
The promotion model used at companies like Shopify, Stripe, and Weaveworks treats environments as separate folders (or branches) in the config repo, each reconciled independently. You never promote "code" — you promote a specific image tag or digest that was already built, scanned, and tested by CI. The only question Git has to answer is: which version of that artifact is each environment allowed to run?
The Anatomy of a Promotion Flow
A three-environment promotion path looks like this in Git terms:
- dev: image tag updated automatically by CI on every merge to
main. The GitOps agent reconciles within seconds. No human approval. - staging: image tag updated by a promotion script or a human PR. May require automated smoke tests to pass first. Gated by a branch protection rule or a required status check.
- production: image tag updated only after a human approves a PR (or a policy engine like OPA Gatekeeper approves automatically). Change-freeze windows enforced via branch protection rules on the config repo.
Repository Layout for Multi-Environment GitOps
The canonical structure that scales to large orgs uses environment folders inside a single config repo (or separate repos per environment for strictest isolation). Here is a real Kustomize-based layout:
v1.2.3 can be overwritten by a docker push. A digest like sha256:a3f7... is cryptographically immutable. Use kustomize edit set image with the full digest in your promotion script. ArgoCD Image Updater and Flux's ImagePolicy can automate digest pinning in dev/staging while leaving production pinned to a human-approved digest.
Automating Dev Promotion from CI
After a successful CI build, the pipeline updates the dev overlay automatically. This is the only environment where CI writes directly to the config repo. The pattern uses a bot token with write access scoped only to the config repo:
Promoting to Staging: Script-Driven PR
Staging promotion is typically triggered manually (a developer runs a script or clicks a UI button after monitoring dev) or automatically after a time-based soak period in dev. The script opens a pull request; human merge is required. Required status checks on the PR (smoke tests, security scans) act as the gate.
ImageUpdateAutomation CRD that can watch a container registry and commit updated tags to Git automatically, with policies controlling which semver ranges are eligible (e.g. semver: range: '>=1.0.0 <2.0.0'). This is excellent for dev and staging. For production, disable automation and keep the human PR gate. ArgoCD delegates to external tools like Renovate or the ArgoCD Image Updater add-on for similar automation.
Promoting to Production: The Human Gate
Production promotion follows the same pattern as staging, but the PR requires explicit human review. At big-tech scale this is enforced structurally, not by convention:
- CODEOWNERS file:
apps/api-service/overlays/production/ @myorg/oncall-approvers— GitHub/GitLab automatically requests review from this team and blocks merge until one of them approves. - Branch protection rules: require 2 approvals, dismiss stale reviews on new commits, require all status checks to pass (including a custom "change-freeze" check that fails during declared freeze windows).
- Policy engines: OPA Conftest or Kyverno policies run in the CI check on the config repo PR, validating that the image has passed the required security scan, the digest matches what was promoted through staging, and no resource limits were accidentally removed.
Production Failure Modes and How to Avoid Them
Promotions fail in predictable ways. Knowing these patterns lets you build guardrails before they hit production:
- Promoting a mutable tag: CI builds
:latest, staging passes, but by the time production is promoted,:latestpoints to a different build. Always promote image digests (sha256:...), never mutable tags. - Config drift between overlays: a developer manually edits the staging overlay to test something, forgets to remove it, and the staging-to-prod promotion carries the change silently. Use
kustomize buildin CI to diff the rendered output of staging vs prod before merging. - Race conditions in multi-service promotions: when service A depends on service B's new API, promoting A before B causes errors. Use ArgoCD sync waves (
argocd.argoproj.io/sync-waveannotation) or FluxdependsOnto sequence deployments. - Forgetting to update environment-specific secrets: a new environment variable is added in dev but the Secret or ExternalSecret is not added to the prod overlay. The app deploys but crashes. Always validate rendered manifests against a schema.
Tracking Promotion State
At the end of a promotion, you should be able to answer from Git alone: which image digest is currently running in each environment? A simple convention is a VERSIONS.md or a structured versions.json in the repo root updated by the promotion script. Better yet, query ArgoCD or Flux directly:
Environment promotion done well is the difference between a deployment process you can trust at 2 AM and one you dread every Friday afternoon. The discipline of "every change to every environment is a Git commit with a human approval chain" is what makes GitOps the foundation of safe, auditable delivery at scale.