GitHub Actions job waiting for review on a protected environment
A GitHub Actions job waiting for review has not failed and has not reached a runner. It is being held by a deployment protection rule on the environment it references, and it will stay held until someone with the right access clears the rule or the run hits the thirty-day ceiling.

What this error means
The run sits and one job never starts. There is no annotation, no failed step and no exit code, because nothing has run: GitHub's reference states that "All deployment protection rules must pass before a job referencing the environment is sent to a runner." In the REST API the job carries the status waiting, one of the six documented values alongside queued, in_progress, completed, requested and pending, and it has no conclusion at all. A pipeline that treats anything other than success as a failure reports this as one, which is where the confusion starts. The correct reading is that it is paused, waiting for a person.
Which rule is holding it
An environment can carry four kinds of protection rule. They hold a job for different reasons, need different people, and clear on different timescales, so work out which one you have before you go looking for someone to click a button. Everything in this table is from GitHub's deployments and environments reference and its published limits.
| Rule | What it waits for | Limit GitHub documents | Who clears it |
|---|---|---|---|
| Required reviewers | A person or team to approve | Up to six users or teams; one approval is enough | Any listed reviewer with at least read access |
| Wait timer | Elapsed time | An integer from 1 to 43,200 minutes | Nobody; it clears itself |
| Deployment branches and tags | The run ref to match a pattern | Matched against GITHUB_REF | Nobody; the job fails instead of waiting |
| Custom protection rules | A GitHub App to respond | Configured per environment | The third-party system behind the app |
Common causes
The environment lists required reviewers and nobody has acted
The ordinary case. One approval from any listed reviewer releases the job, so a long wait usually means the reviewers do not know. In our experience the notification is the weak link rather than the rule: it goes to the reviewer, not to whoever is watching the pipeline.
Self-review is prevented and the only reviewer started the run
The environment lists you, you pushed the tag, and the approve control is inactive. The setting is doing what it says: users who initiate a deployment cannot approve it even when they are a required reviewer. On a two-person team it looks like a deploy that works for one person and hangs for the other.
A wait timer is still counting
Nothing is wrong and nobody needs to act. A wait timer delays the job by a set number of minutes, up to 43,200, and GitHub notes the waiting time is not billed. On the run page it looks identical to an approval hold, which is why the table above is worth reading first.
A custom protection rule is waiting on a third-party system
An environment can hand the decision to a GitHub App that checks a change management or observability system. When that system is slow or unreachable the job waits exactly as it would for a reviewer, and no reviewer on the repository can release it, because the repository is not what is being asked.
How to fix it
Confirm it is held rather than broken
- Read the job status on the run. A waiting job has no conclusion; a failed one has.
- Look for the Review deployments notification, which only appears for an approval hold.
- No notification and still waiting means a wait timer or a custom rule, not a reviewer.
Approve or reject, and prefer rejecting over abandoning
Open the review prompt on the run page, pick the environments and choose Approve and deploy or Reject. Rejecting fails the workflow, which is a result. Leaving it alone costs you a run that expires with nothing recorded against it.
Give the environment enough reviewers to be releasable
Six users or teams is the ceiling, one approval is enough, and a team counts as one entry, so listing a team rather than three people is usually the change that ends the waiting. Where self-review is prevented, list at least one person who does not normally trigger the deploy.
Use an environment without protection when you only wanted the secrets
If the environment was added so the job could read a secret rather than because the deploy needs a gate, the reviewers are collateral. Move the secret to a second environment with no protection rules, and keep the gate on the environment that genuinely deploys.
jobs:
publish:
runs-on: ubuntu-latest
environment: production # gated
steps:
- run: ./publish.sh
smoke:
runs-on: ubuntu-latest
environment:
name: production-readonly # no protection rules
deployment: false
steps:
- run: ./smoke.shWho is allowed to approve, and who is not
The reference is specific. "Use required reviewers to require a specific person or team to approve workflow jobs that reference the environment. You can list up to six users or teams as reviewers. The reviewers must have at least read access to the repository. Only one of the required reviewers needs to approve the job for it to proceed."
The rule that surprises people is self-review. "You also have the option to prevent self-reviews for deployments to protected environments. If you enable this setting, users who initiate a deployment cannot approve the deployment job, even if they are a required reviewer." On a small team whose only reviewer is also the person who pushes, that turns every deploy into a wait for a second human. The API tells you which side of it you are on: a pending deployment carries current_user_can_approve, documented as "Whether the currently authenticated user can approve the deployment".
Clearing it from the command line
The button lives on the run page under a notification that reads Review deployments, and the choices are Approve and deploy or Reject. Approving is not the only outcome worth knowing: "If a job is rejected, the workflow will fail", so rejecting is how you turn a held run into a finished one rather than leaving it to expire.
For a pipeline or a bot, both are a REST call. GET /repos/{owner}/{repo}/actions/runs/{run_id}/pending_deployments lists what is waiting, with the environment, the reviewers and whether you can approve. The matching POST takes environment_ids, a state of approved or rejected, and a comment. All three are required by the REST description, so a call that leaves the comment out comes back 422; an empty string is accepted.
# what is waiting, and can I clear it
gh api repos/OWNER/REPO/actions/runs/RUN_ID/pending_deployments \
--jq '.[] | {env: .environment.name, canApprove: .current_user_can_approve}'
# clear it
gh api -X POST repos/OWNER/REPO/actions/runs/RUN_ID/pending_deployments \
-F 'environment_ids[]=ENV_ID' -f state=approved -f comment='release 4.2'The deadline nobody plans for
A held job does not wait forever. GitHub publishes the ceiling in its limits table as a gate approval time of thirty days, with the note that "A workflow may wait for up to 30 days on environment approvals." A release cut before a holiday and approved after it is a release that never deployed.
A second thirty-day clock nearby is easy to confuse with this one. Runs held by the separate fork-approval setting are deleted rather than expired: "Workflow runs that have been awaiting approval for more than 30 days are automatically deleted." That one governs whether a contributor's workflow may run at all, not an environment, so a job waiting on an environment gate is not the one that vanishes.
Why there is no recorded run on this page
There is nothing on a runner to record. The job is held by the Actions service before it is dispatched, so no machine picked it up, no step executed and no log exists beyond the run's own timeline. A recorded run could only show a screenshot of a button the reader already has in front of them. What is worth recording instead is the API shape, quoted above from GitHub's own REST description, and the ceiling, quoted from its published limits table.
How to prevent it
- Reserve required reviewers for environments that really deploy, and leave test environments ungated.
- List a team rather than individuals, so holidays do not become a blocked pipeline.
- Watch the pending deployments endpoint from release tooling instead of watching the run page.
- Treat a
waitingjob as a paused pipeline in your dashboards, not as a failed one.
Frequently asked questions
Is a job waiting for review a failure?
waiting, one of the six documented statuses, and the job has not been sent to a runner, because all deployment protection rules must pass first. A dashboard that treats anything other than success as a failure will mislabel it.Why can I not approve my own deployment?
current_user_can_approve.How long will a deployment wait for approval?
Can I approve a pending deployment from a script?
GET /repos/{owner}/{repo}/actions/runs/{run_id}/pending_deployments, then post back to the same path with environment_ids, a state of approved or rejected, and a comment. The comment is required rather than optional, so omitting it returns a 422, though an empty string is fine. The listing also tells you whether the authenticated user is allowed to approve before you try.