How do GitHub Actions environments work with deployment protection rules, and how would you set up a staging-to-production promotion workflow?
Quick Answer
GitHub environments are named deployment targets (like staging and production) that can have protection rules including required reviewers, wait timers, and branch restrictions. A promotion workflow uses job dependencies where the staging deployment job must succeed before the production job runs, with the production environment requiring manual approval from designated reviewers.
Detailed Answer
Think of GitHub environments like the checkpoint system in an airport. After checking in (push to main), you go through initial screening (staging deployment and tests). Once screening clears, you reach the boarding gate (production environment) where a gate agent (required reviewer) checks your boarding pass (approval) before you can board the plane (deploy to production). If there is a delay policy (wait timer), you wait in the lounge for a mandatory cooling-off period before boarding begins. Each checkpoint adds a layer of human or automated verification.
GitHub environments are created at the repository level under Settings > Environments. Each environment has a name (like staging, production, or canary), optional protection rules, and its own set of environment-specific secrets and variables. Protection rules include required reviewers (up to six people or teams who must approve before a job targeting that environment can run), wait timers (a mandatory delay in minutes before the job starts, useful for observing staging metrics before auto-promoting), and deployment branch restrictions (limiting which branches can deploy to this environment, typically only main or release/* for production). When a workflow job specifies environment: production, the job pauses and waits for all protection rules to be satisfied before executing.
Internally, when a workflow run reaches a job with an environment declaration, GitHub creates a deployment record and transitions it through states: waiting (for approvals or timer), in_progress (running), success, or failure. The deployment record is linked to a specific commit SHA and environment, creating an auditable deployment history accessible through the Deployments section of the repository. The GitHub API exposes deployment statuses, enabling external systems to query which commit is currently deployed to each environment. If the job specifies a concurrency group with the environment name, GitHub ensures only one deployment to that environment runs at a time, automatically canceling or queuing subsequent attempts.
A production-grade staging-to-production workflow at Acme Corp's payments-api might work as follows: a push to main triggers the CI job that runs tests and builds the container image. The staging deployment job depends on CI and targets the staging environment (no protection rules, deploys automatically). An integration test job depends on staging deployment and runs end-to-end tests against the staging URL. The production deployment job depends on the integration tests passing and targets the production environment, which has three protection rules: required approval from two members of sre-team, a 15-minute wait timer so the team can monitor staging metrics, and a branch restriction to main only. The workflow uses concurrency groups to prevent parallel deployments. Environment secrets store the AWS credentials for each target, keeping staging and production credentials completely isolated.
A common gotcha is that environment protection rules only apply to workflow runs on the default branch (or branches matching the environment's branch policy). If a developer creates a workflow on a feature branch that references environment: production, the protection rules still apply, but only if the branch is in the allowed list. If no branch policy is set, any branch can trigger the production environment, which is a security risk. Always set deployment branch restrictions on production environments. Another pitfall is that the wait timer starts after all required reviewers approve, not after the workflow starts. If your intent is a mandatory observation period after staging deployment, you need to model it as a separate job with a delay, not rely solely on the environment timer.