Skip to content

GitHub Actions Deployment Approvals

Lesson 11 of 11Intermediate4 min readGitHub Actions & CI/CD · Advanced ActionsVerified: GitHub environment protection rules, August 2026

An approval gate is a person deciding whether a change should reach production. Whether it adds safety or just latency depends almost entirely on whether the approver is given enough information to make a real decision — and most approval gates are not.

  1. Settings → Environments → the environment.

  2. Enable Required reviewers and add up to six users or teams. Any one of them can approve.

  3. Optionally set a wait timer — a delay before the job may start.

  4. Optionally restrict deployment branches and tags to the refs allowed to deploy here.

jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- run: ./deploy.sh

The workflow file does not change. The gate is a property of the environment, which is what stops someone bypassing it by editing the workflow.

The job enters a Waiting state before its first step. It has claimed no runner and executed nothing. Reviewers are notified, the pull request and run page show the pending deployment, and approving or rejecting requires a comment field that becomes part of the deployment record.

A rejected deployment fails the job. An unapproved one eventually expires — by default after 30 days, and the run then shows as failed rather than as still waiting.

By default a user with access can approve their own deployment. There is a setting to prevent it, and whether it is available depends on the plan.

Whether to enable it is a genuine trade-off rather than an obvious win. Requiring a second person is the standard separation-of-duties control and it is what most compliance regimes expect. It also means that at 3am, the person fixing an outage cannot ship the fix alone. Teams that enable it generally pair it with a documented break-glass path — a separate environment with different reviewers, or a process for temporarily relaxing the rule that is itself logged.

Decide deliberately, and write down what happens during an incident.

This is the part that determines whether the gate does anything. “Deploy to production?” with no context gets approved reflexively within a week of being introduced.

Write the decision-relevant facts into the job summary of the job before the gate:

prepare:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Summarise what will deploy
run: |
{
echo "## Deployment candidate"
echo ""
echo "**Commit:** \`${GITHUB_SHA}\`"
echo ""
echo "### Commits since the last production deployment"
echo ""
git log --oneline "$(git describe --tags --abbrev=0 --match 'prod-*' 2>/dev/null || echo HEAD~10)..HEAD"
echo ""
echo "### Migrations in this change"
echo ""
git diff --name-only "$(git describe --tags --abbrev=0 --match 'prod-*' 2>/dev/null || echo HEAD~10)..HEAD" -- migrations/ || echo "none"
} >> "$GITHUB_STEP_SUMMARY"
deploy:
needs: prepare
environment: production
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh

The two things an approver most needs are what changed and whether anything is irreversible. For an infrastructure pipeline that means the Terraform destroy count — see deploying Terraform. For an application it means whether there is a database migration, because that is the part a rollback will not undo.

fetch-depth: 0 is required for git log to see history; the default checkout is shallow.

Being honest about this matters, because approval gates are often treated as a stronger control than they are:

  • It does not verify the code. Approval happens after merge; code review happened earlier, or did not.
  • It does not verify the artifact matches the commit unless you have provenance attestation.
  • It does not prevent a compromised workflow from doing something else. The approver approves a deployment, not a specific set of commands. A malicious change to the deploy script is approved along with everything else.
  • It does not make the deployment reversible.

The control approval genuinely provides is a timing gate with a named human and an audit record. That is valuable — it is what stops an automated pipeline from shipping at 5pm on a Friday, and it gives an auditor a name — but it is not code review and should not be relied on as such.

Wait timer: 10 minutes

The job holds for the configured period before it may start, or before reviewers can approve. It is useful in two shapes:

A cancellation window after an automatic staging deployment, so metrics and alerts have time to say something before production follows.

A cooling-off period on a sensitive environment, which prevents a mistake being compounded immediately by a hurried second attempt.

Terminal window
gh run view <run-id>
gh api -X POST repos/OWNER/REPO/actions/runs/<run-id>/deployment_protection_rule \
-f environment_name=production -F state=approved -f comment="Reviewed migrations, none present"

Useful for on-call. Note that scripting approvals is in tension with the point of having them — an approval issued by automation is not a human decision, however it is recorded.

  1. Add required reviewers to a production environment and deploy. Watch the job sit in Waiting with no runner claimed.

  2. Reject it with a comment. Confirm the job fails and the comment is recorded.

  3. Add the prepare job above so the approver sees the commit list and any migrations before deciding.

  4. Add a 5-minute wait timer and confirm approval is not offered until it elapses.

  5. Restrict the environment to main. Try deploying from a branch and confirm it is refused before the gate.

  6. If your plan offers it, enable “prevent self-review” — then write down what your team does at 3am when only one person is awake.

You have finished the advanced cluster. Security is next, and several pages here pointed into it.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Want production-ready workflow templates? The Professional Toolkit has five, with permissions set correctly.