The OIDC lesson scoped an IAM role to a specific branch. That answers "which workflow can deploy" — it doesn't answer "does a human need to sign off before this specific deploy proceeds," or "how does staging get a different database URL than production without a developer manually swapping values." That's what a CI platform's own Environments feature is actually for.
3 min read
Scoping OIDC trust to refs/heads/main (the OIDC lesson) controls which
code can assume a deploy role. It says nothing about when — a push to
main that passes CI still deploys immediately and automatically unless
something else inserts a pause. Lesson 1's Continuous Delivery vs.
Deployment distinction named this as a real choice; Environments are
the concrete feature that implements the "delivery" side of that choice.
jobs:
deploy-production:
environment: production
runs-on: ubuntu-latest
steps:
- name: Deploy
run: ./deploy.shConfiguring the production environment (in repository settings) can
attach:
Environments also scope secrets — DATABASE_URL in the staging
environment and DATABASE_URL in the production environment are two
independently-configured values, referenced identically in workflow code:
jobs:
deploy:
environment: ${{ github.event.inputs.target_env }}
steps:
- name: Deploy
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
run: ./deploy.shThe workflow code never branches on which environment it's targeting —
secrets.DATABASE_URL automatically resolves to whichever environment the
job is actually running against. This is what prevents the exact failure
mode the artifact-versioning lesson's whole "build once, promote" argument
is protecting against from creeping back in through configuration instead
of code: without environment-scoped secrets, teams often fall back to
manually editing a config file per environment, or worse, hardcoding a
staging value that accidentally ships to production.
It's tempting to think branch-scoped OIDC trust alone is "enough" protection for production deploys, but a required-reviewer gate and a branch condition protect against different things: the branch condition stops the wrong code from deploying; a required reviewer stops the right code, at the wrong moment — a change that's technically ready but shouldn't go out five minutes before a major event, or one that needs a second set of eyes regardless of how correct the code itself is. Relying on only one of the two leaves a real gap the other was specifically designed to close.
A required-reviewer gate is a manual step by design, and manual steps are exactly what a heavily-automated pipeline otherwise tries to eliminate. This is a reasonable, deliberate exception rather than a contradiction: the value of "continuous deployment with zero human gates" only holds once a team has enough confidence (test coverage, canary/rollback tooling from later lessons) that skipping the gate is actually safe — for a change with meaningfully higher blast radius, keeping one human checkpoint is a rational trade, not a failure to fully automate.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What gap does a required-reviewer gate close that a branch-scoped OIDC trust policy alone doesn't?
2. How do environment-scoped secrets prevent a configuration-level version of the artifact-versioning lesson's core problem?
3. What does a GitHub Environment's wait timer provide?
4. Why is a required-reviewer gate considered a reasonable exception rather than a failure to fully automate a pipeline?
CI/CD & Deployment Pipelines