Deploying to production without controls is a recipe for disaster. In this episode we create development, staging, and production environments complete with protection rules: required reviewers, wait timer, and environment secrets. We also write a production deploy workflow with an approval gate that blocks the release until a Lead Engineer gives their approval.

In episode 16 we built quality and security gates: tests, coverage, lint, SonarQube, CodeQL, and Dependabot. All of those protect the code, but not yet the deployment environment. Who can deploy to production? When is it allowed? What if production credentials get used by a workflow from a developer's branch? In this episode we answer those questions with Environment Protection Rules.
In this episode we discuss:
An environment is a named deployment segment: development, staging, production. Each environment can carry its own secrets and protection rules. Think of it like doors in a multi-story building — the lower floors (development) are free to pass through, the upper floors (production) need a special badge and an escort.
Environments are created via repository Settings → Environments. Each environment can be filled with:
This is the heart of the approval gateway. When enabled, GitHub pauses the deployment job and shows a notification to the designated reviewers. The deployment only proceeds after a reviewer approves. The Deployment page in the GitHub UI becomes the control center: view the queue, approve, or reject.
Note
Required reviewers are different from branch protection. Branch protection controls who can merge code; required reviewers control who can deploy. Both can — and should — be used together.
The wait timer delays deployment execution for N minutes after the job is prepared. Its purpose is to give an observation window after staging is deployed, or to give developers time to cancel if something looks suspicious.
Environment secrets override repository secrets for jobs that deploy to that environment. For example, the DEPLOY_TOKEN token at the repository level holds a development token, while in the production environment it holds a token with greater rights. With branch binding, workflows from branches other than main can't deploy to production, no matter what's written in the YAML file.
Warning
Protection rules aren't a replacement for security. If a repository is public and uses a self-hosted runner, anyone who can open a pull request can run code on your runner. Always install protection rules AND branch protection, and never use self-hosted runners on public repositories.
This is the core part. A job declares an environment via the environment key. When GitHub sees a job with environment: production, it automatically checks that environment's protection rules — including waiting for reviewer approval:
name: Deploy to Production
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
concurrency:
group: production
cancel-in-progress: false
steps:
- name: Checkout kode
uses: actions/checkout@v4
- name: Deploy production
run: ./scripts/deploy.sh
env:
PRODUCTION_API_KEY: ${{ secrets.PRODUCTION_API_KEY }}Things to note:
concurrency with cancel-in-progress: false ensures only one production deployment runs at a time — new ones queue rather than cancelling the old one.PRODUCTION_API_KEY secret is taken from the production environment, not the repository level. If it's not in that environment, the job fails with a clear message.deploy job will show a "waiting" status before any step runs.For production-grade, combine all protections into one layer:
| Layer | Controls | Example |
|---|---|---|
| Branch protection | Code merge | Require status checks, 2 approvals |
| Deployment branches | From which branch to deploy | main only |
| Required reviewers | Who approves the deploy | Lead Engineer |
| Wait timer | When the deploy runs | 5 minutes after staging |
| Concurrency | How many deploys run | Max 1 per environment |
| Environment secrets | Credentials per environment | Separate staging vs prod tokens |
If production is an expensive environment (downtime = money), these layers transform the pipeline from "anyone can deploy" into "an audited event with explicit approval".
Tip
Start with a staging environment without required reviewers so the flow stays fast, then enable reviewers only for production. Developers stay productive while production is protected.
In this episode we discussed environment protection rules & approval gateways:
environment: production plus concurrency ensures releases run in a controlled manner, only one at a time.A controlled pipeline is the foundation of stable releases. In episode 18 we automate release management & semantic versioning — naming versions from commit messages and creating GitHub Releases with changelogs, all without manual touch. See you there!