Skip to content
Latchkey LogoLatchkey home

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.

Four protection rules on an environment and what each one needs before a job starts
Every rule sits in front of the runner. Quoted from GitHub's deployments reference: "Only one of the required reviewers needs to approve the job for it to proceed."

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.

RuleWhat it waits forLimit GitHub documentsWho clears it
Required reviewersA person or team to approveUp to six users or teams; one approval is enoughAny listed reviewer with at least read access
Wait timerElapsed timeAn integer from 1 to 43,200 minutesNobody; it clears itself
Deployment branches and tagsThe run ref to match a patternMatched against GITHUB_REFNobody; the job fails instead of waiting
Custom protection rulesA GitHub App to respondConfigured per environmentThe 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

  1. Read the job status on the run. A waiting job has no conclusion; a failed one has.
  2. Look for the Review deployments notification, which only appears for an approval hold.
  3. 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.

.github/workflows/release.yml (illustrative)
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.sh

Who 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.

Terminal
# 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 waiting job as a paused pipeline in your dashboards, not as a failed one.

Frequently asked questions

Is a job waiting for review a failure?
No. It has no conclusion at all. In the REST API the job status is 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?
Because the environment has self-review prevention turned on. GitHub documents that with that setting, users who initiate a deployment cannot approve the deployment job even if they are a required reviewer. The pending deployments endpoint reports the same thing per user as current_user_can_approve.
How long will a deployment wait for approval?
Up to thirty days. GitHub publishes a gate approval time limit of 30 days for environment approvals. After that the run is over and nothing deployed, so rejecting a run you have decided against is better than leaving it, because a rejection fails the workflow and leaves a record.
Can I approve a pending deployment from a script?
Yes. List what is waiting with 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.

Related guides

References

A reviewer holds this job, not a runner. Latchkey starts it the moment they approve. Start free → 30-day trial · No credit card