open atlas
↑ Back to track
CI/CD pipelines CICD · 03 · 02

Environments, approvals, and rollback

A GitHub environment is a deploy gate. Required reviewers, wait timers, and branch rules pause the prod job; env-scoped secrets stay locked until approval. And when a deploy goes bad, roll-forward often beats rollback once a migration has run.

CICD Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior

A Friday-afternoon hotfix went green in CI, deployed to staging, and a teammate clicked “Approve” on the production gate without reading the diff — the eleventh approval that week, all routine. This one shipped a config that pointed prod at the staging database. The deploy itself “succeeded”: the job was green, the approval logged. The pager went off four minutes later when writes started landing in the wrong place. The gate had done its job — it paused and asked a human — but the human had been trained by ten boring approvals to click without looking. The control existed; the attention had been spent.

An environment is a named gate, not a server

In GitHub Actions, an environmentstaging, production — is not a machine. It is a named object on the repo that carries two things: protection rules that decide when a job targeting it may run, and environment-scoped secrets and variables that only resolve inside a job that names that environment. A job opts in with one line:

jobs:
  deploy-prod:
    runs-on: ubuntu-latest
    environment: production        # ← binds this job to the prod gate
    steps:
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}   # resolves to the *production* secret

The instant a job declares environment: production, three things become true. It will not start until the environment’s protection rules pass. Its secrets.* references resolve to the environment’s secrets, shadowing repo-level secrets of the same name. And if a reviewer is required, the job sits in a Waiting state — visibly paused in the run UI — until someone with access approves it. Crucially, a job that is waiting on approval cannot read the environment’s secrets yet: the production API key is unreachable until the gate clears. That is the property you are buying. A leaked workflow that never reaches an approved prod job never sees prod credentials.

Protection rules: reviewers, timers, and who may deploy

When you configure a production environment, the question is not “how do we add a gate?” but “what does this gate actually stop?” GitHub gives an environment a small, blunt set of protection rules, and a senior reads them as blast-radius controls.

RuleWhat it doesHard limit / gotcha
Required reviewersJob waits for a human (up to 6 reviewers/teams) to approve before it runsAuto-fails after 30 days if not approved; only one listed reviewer need click
Prevent self-reviewThe user who triggered the deploy cannot approve their ownOff by default — the direct fix for rubber-stamping your own push
Wait timerDelays the job N minutes after it’s triggered (a soak / cancel window)1–43,200 min (30 days); doesn’t count toward billable time
Deployment branches/tagsRestricts which branches/tags may deploy to this environmente.g. only main or v* tags reach production

On Free/Pro/Team plans these rules apply to public repos only; private repos need GitHub Team or Enterprise. And one design wrinkle bites: protection rules attach to the job, not the workflow. If three jobs each target production, a reviewer approves three times — the fastest road to approval fatigue there is.

The promotion model: staging auto, production paused

The standard shape is a single pipeline that flows build → staging → production, where staging is automatic and production is a gated promotion of the same artifact. You build once, deploy that exact build to staging with no gate, and the production job — depending on staging via needs: — pauses at its environment gate until a reviewer promotes it. Add concurrency: so two pushes can’t race into the same environment.

concurrency:
  group: deploy-${{ github.ref }}   # one prod deploy at a time per ref
  cancel-in-progress: false         # let the in-flight deploy finish, queue the next

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment: staging            # no required reviewer -> runs automatically
    steps:
      - run: ./deploy.sh staging

  deploy-production:
    needs: deploy-staging           # promote the same build
    runs-on: ubuntu-latest
    environment:
      name: production              # required reviewers configured in repo settings
      url: https://app.example.com
    steps:
      - run: ./deploy.sh production

concurrency is the quiet hero here. Without it, two merges to main minutes apart launch two prod deploys that interleave — the older artifact can land after the newer one and silently win. The group key is any string; setting cancel-in-progress: false gives you the safe default of “finish the current deploy, then run the next,” at most one in progress and one pending.

Why this works

Why does environment lock secrets, when you could just store the prod key as a repo secret? Because a repo secret resolves in any job, including one triggered from a fork PR or a less-trusted workflow. An environment secret resolves only in a job that names the environment and clears its protection rules — so the prod credential is unreachable until a human has approved a deploy from an allowed branch. The gate and the secret scope are one mechanism: approval is also the moment the secret becomes readable.

When it breaks: rollback vs roll-forward

The bad deploy ships. Now what? Two moves. Rollback: redeploy the previous known-good artifact — fast, and trivial when your pipeline can re-run a prior build’s deploy job. Roll-forward (a.k.a. fix-forward): write the fix, push it through the same gated pipeline, ship a new version on top.

Rollback is the instinct, but it has a sharp edge: once a deploy has run a database migration, redeploying old code does not roll the schema back. The old binary now meets a schema it was never written for — a dropped column, a renamed table, a backfilled enum. GitLab’s own guidance is blunt: there is no guarantee app and database roll back together, so the safest strategy is usually to roll forward. Roll back code, roll forward data. If migrations are additive and backward-compatible (the expand/contract discipline), a code rollback is safe because the old code still works against the new schema. If they’re destructive, rollback can corrupt or lose data, and a forward fix — a compensating migration plus the corrected code — is the only honest move.

Quiz

A job that declares `environment: production` with a required reviewer is triggered. Before anyone approves, what is true?

Pick the best fit

A v2 deploy ran a migration that renamed a column, then a bug surfaces in the new code. How do you recover?

Recall before you leave
  1. 01
    What three things change the moment a job declares `environment: production`, and why does that make environment secrets safer than repo secrets?
  2. 02
    After a deploy that ran a destructive migration, why is roll-forward usually safer than rolling back to the previous artifact, and when is rollback actually fine?
Recap

A GitHub Actions environment is a named gate, not a server: it carries protection rules that decide when a job targeting it may run, and secrets that resolve only inside a job naming that environment. Declaring environment: production holds the job in a Waiting state until its required reviewer approves, and the prod secrets stay unreadable until that approval — which is exactly why environment secrets beat repo secrets, since a repo secret would resolve in any job including an untrusted one. The protection rules are blunt blast-radius controls: required reviewers (auto-failing after 30 days, only one need approve), Prevent self-review to stop someone rubber-stamping their own push, wait timers from 1 to 43,200 minutes as a soak window, and branch/tag restrictions on who may deploy. They attach to the job, not the workflow, so three prod jobs mean three approvals — the road to approval fatigue, where a reviewer trained by ten routine clicks waves through the eleventh that pointed prod at the wrong database. The standard pipeline builds once and promotes that same artifact staging→production, with staging automatic and production gated, and concurrency keyed on the ref so two pushes can’t race a stale artifact in on top of a newer one. When a deploy goes bad, rollback — redeploying the prior artifact — is fast and correct only when migrations are additive and backward-compatible; once a destructive migration has run, the old code no longer matches the schema, so you roll forward with a compensating migration through the same gate rather than gambling on a rollback or doing live DB surgery. Now when you see a prod incident and someone reaches for “just rollback” — ask whether a migration ran first. That one question determines whether the rollback will help or create a second incident.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 5 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.