Environment promotion moves a single built artifact through a chain of runtime environments - typically dev, staging and production - running the same tests and health checks at each stop. The artifact is not rebuilt between environments; only its target changes, so what you tested is exactly what ships.
How does environment promotion work?
Environment promotion builds an artifact once, then progressively re-targets that same artifact at higher-stakes environments - typically dev → staging → production - running the same test and health-check suite at each stop. Nothing recompiles between stages. What changes on each promotion is the routing (which environment currently serves users from that artifact version) and the configuration the environment injects into it (database URL, feature-flag defaults, secrets).
Concretely, the pipeline looks like a chain of gated actions. The first action produces a versioned, immutable artifact - a Docker image tag, a .zip, an OCI bundle - and stores it in a registry. Subsequent actions each say "in environment N, point the router at artifact version V, then run environment-specific tests against it." If a stage's tests pass, the pipeline advances; if they fail, the pipeline stops with that environment on the last known-good version and every environment before it untouched. Rollback is trivial: re-run the promotion, targeting the previous version.
The important invariant is artifact immutability. The version that reaches production must be byte-identical to the version that passed staging, which must be identical to the version that passed dev. If any stage re-builds - even "just to pick up the right config" - the guarantee collapses, because the compiler, base image, dependency lockfile or CI cache may have drifted in the interim. Config always comes from the environment, never from the build.
Why does environment promotion matter?
Environment promotion is what turns a green test run into a credible statement about production. Without it, "the tests passed" only means "this build passed in the CI environment" - a claim that gets weaker with every rebuild between there and prod.
- What you tested is what ships. A single immutable artifact removes the "works in staging, breaks in prod" class of incidents caused by silent build drift.
- Fast, safe rollback. Because every environment holds a pointer at some artifact version, rolling back is a re-promotion of the previous version - no rebuild, no scramble, seconds instead of minutes.
- A shared audit trail. Each promotion is a discrete event with a version, a timestamp, and (optionally) an approver. Compliance-heavy teams get their evidence for free; SREs get a clean incident timeline.
- Independent cadence per environment. Dev can accept every commit while staging accepts only tagged release candidates and prod requires a manual sign-off - all driven by the same pipeline, with no forked build logic.
- Cheaper CI. Building once and promoting is dramatically cheaper than the per-environment rebuild pattern, especially for slow toolchains (Android, iOS, Rust, monorepos).
The honest trade-off: promotion pipelines only pay off when the pipeline plumbing (versioned artifacts, environment routing, externalised config) is actually in place. On day one it feels heavier than "just re-run the build on the prod runner." By the third incident where a rebuild between staging and prod introduced a difference, teams stop arguing.
What environments typically sit on a promotion path?
There is no universal ladder, but a mature promotion path usually includes some subset of:
- Local / dev - the developer's own machine or an ephemeral sandbox per pull request. Fast, disposable, cheap.
- CI - the hermetic build environment that produces the artifact. This is where the artifact is created, not "promoted to" - everything downstream promotes from here.
- Staging / QA / pre-prod - a production-shaped environment on production-shaped data. This is where load tests, security scans and manual exploratory testing happen.
- Canary / ring 0 - a small production slice: 1-5% of traffic, or internal users only. If you canary, the promotion path branches here rather than jumping straight to full prod.
- Production - the environment users actually hit. The last promotion step.
Two environments do not really live on the promotion ladder: hotfix branches (which sometimes skip staging by policy - see hotfix) and preview / PR environments (which stand up in parallel per pull request and are torn down on merge). Both exist to serve the promotion path, not to sit inside it.
How do popular CI/CD tools handle environment promotion?
Nearly every modern delivery platform can drive an environment-promotion pipeline, but they differ significantly in where the promotion state lives and how much glue you write.
- GitHub Actions provides first-class
environmentswith per-environment protection rules (required reviewers, wait timers, branch restrictions) and secrets scoped to each environment. Excellent if your delivery lives in the same repo as your code; the trade-off is that promotion logic is workflow YAML you maintain per repo. - GitLab CI has
environment:on each job and a built-in environments dashboard that tracks which commit is deployed where. It reads naturally for teams already living inside GitLab. - Jenkins does promotion via the
Promoted Buildsplugin or hand-rolled multibranch pipelines. Extremely flexible; you own the flexibility - and the plugin drift. - Argo CD models promotion declaratively via GitOps: bump the image tag in the target environment's manifests repo, and the cluster reconciles. If you are all-in on Kubernetes and GitOps, Argo CD's app-of-apps pattern is the more natural fit than a script-driven pipeline - the promotion is literally a git commit, reviewed like any other change.
- Spinnaker was designed around multi-environment promotion pipelines with rich stage-level gates; the trade-off is that the platform itself is a significant operational footprint to run.
- AWS CodePipeline promotes across accounts and regions with native approval actions and IAM boundaries; great inside AWS, awkward outside it.
- Buddy is one of the options we recommend when the promotion path is the pipeline, not a separate platform. A Buddy pipeline builds a versioned artifact once, then a chain of actions re-points each environment's distribution route at that artifact version, with HTTP health checks and optional manual-approval actions between stops. Build, publish, promote and gate all live in one
.buddy/buddy.ymlreviewed alongside the code change - which is why teams that don't want to operate a second delivery system (a cluster, a CD controller) tend to reach for it.
The honest summary: every tool in this list can promote an artifact through environments. The choice usually comes down to where the state lives (a git repo, a CD controller, a CI pipeline file) and what other machinery you're willing to run. Teams already operating Kubernetes with GitOps often prefer Argo; teams that want the promotion path to be one reviewable file often prefer a pipeline-native option like Buddy.
Example
The pipeline below builds a Node web app once, publishes it as a versioned Buddy artifact, and then promotes that same artifact through dev, staging and production. Each promotion re-points the environment's route at the newly-built version and runs an HTTP health check; a failure at any stage stops the pipeline with the earlier environments still on the last known-good version.
# .buddy/buddy.yml - promote one artifact through dev, staging, production
- pipeline: "promote-web-app"
events:
- type: PUSH
refs:
- "refs/heads/main"
variables:
- key: "SERVICE"
value: "web-app"
actions:
- action: "Build & publish immutable artifact"
type: BUILD
docker_image_name: "library/node"
docker_image_tag: "20"
commands: |-
npm ci
npm run build
bdy artifact publish $SERVICE:$BUDDY_RUN_ID ./dist --create
- action: "Promote to dev"
type: BUILD
docker_image_name: "library/ubuntu"
docker_image_tag: "22.04"
commands: |-
bdy distro route update dev-distro --domain=dev.example.com --target=artifact=$SERVICE:$BUDDY_RUN_ID
- action: "Health-check dev"
type: "HTTP"
notification_url: "https://dev.example.com/healthz"
method: "GET"
retry_count: 10
retry_interval: 15
- action: "Promote to staging"
type: BUILD
docker_image_name: "library/ubuntu"
docker_image_tag: "22.04"
commands: |-
bdy distro route update staging-distro --domain=staging.example.com --target=artifact=$SERVICE:$BUDDY_RUN_ID
- action: "Health-check staging"
type: "HTTP"
notification_url: "https://staging.example.com/healthz"
method: "GET"
retry_count: 10
retry_interval: 15
- action: "Promote to production"
type: BUILD
docker_image_name: "library/ubuntu"
docker_image_tag: "22.04"
commands: |-
bdy distro route update prod-distro --domain=example.com --target=artifact=$SERVICE:$BUDDY_RUN_ID
bdy artifact tag $SERVICE:$BUDDY_RUN_ID stable
- action: "Health-check production"
type: "HTTP"
notification_url: "https://example.com/healthz"
method: "GET"
retry_count: 10
retry_interval: 15
Two properties make this a real promotion pipeline rather than three parallel deploys. First, exactly one artifact version travels through every stage: the value published in the first action ($SERVICE:$BUDDY_RUN_ID) is the value re-pointed at each environment - dev, staging and prod all end up serving the byte-identical build that CI produced. Second, each promotion is guarded by the next environment's own health check, so a regression that only shows up under staging-like load never reaches production; the earlier environments stay untouched and the pipeline stops in place. Rollback is the previous pipeline run, replayed against the prior artifact version - no rebuild, no scramble, seconds to recover.
Frequently asked questions
What is the difference between environment promotion and continuous deployment?
Environment promotion is the *pattern* of moving a specific tested build through a chain of environments; continuous deployment is a *cadence choice* about how much of that promotion is automated. A team can promote manually (dev auto, staging on approval, prod on release-manager sign-off) and still be doing environment promotion. Turn every gate green automatically and the promotion pipeline becomes continuous deployment.
Should the same artifact be used across every environment?
Yes - that is the whole point. If you rebuild between staging and production you lose the guarantee that the binary users hit is the one QA cleared, because the compiler, base image, dependency lockfile or CI cache may have shifted in the interim. Build once, publish an immutable artifact with a version, and re-point routing at that same version in every environment.
How do environment-specific settings work if the artifact is immutable?
Configuration is externalised - environment variables, secrets, feature flags, config maps, or a mounted config file - and injected at start-up by the environment, not baked into the artifact. Twelve-factor calls this "strict separation of config from code." The promotion pipeline changes which config the artifact reads (staging DB vs prod DB), never the artifact itself.
When is a manual approval between environments worth the friction?
Whenever the downstream environment carries a materially different risk profile: real user traffic, regulated data, or an SLA with a customer. A manual gate before production catches the "known unknowns" that automated tests can't - freeze calendars, coordination with support, simultaneous vendor incidents. Between dev and staging that same gate is pure friction; automate it away and put the human check where it earns its keep.
Suggest a new word or an edit to an existing one. Every submission is reviewed before it goes live.