Skip to content

Quota-exhaustion graceful skip is unreachable: the guard is gated on the step that quota exhaustion always fails #350

Description

@dem-extra1

claude-code-review.yml has a purpose-built path for quota exhaustion: skip the review with a warning instead of failing the check. It cannot fire, because the step that detects quota exhaustion only runs when the step that quota exhaustion always fails has succeeded.

Surfaced by the 2026-07-28 outage, where roughly six review runs across three unrelated branches all went red over about eight hours. Every one of them should have been a warning and a skip.

The chain

check-review-execution.sh has the detector, and it matches the real signature exactly (lines 62-72):

if [[ "$is_error" == "true" || "$subtype" == error_* ]]; then
  # total_cost_usd==0 with num_turns==1 means the API rejected the
  # request before any real processing -- quota exhaustion, auth
  # failure, or an immediate network error. Skip gracefully.
  if [[ "$total_cost" == "0" && "$num_turns" == "1" ]]; then
    echo "quota_exhausted=true" >> "$GITHUB_OUTPUT"

The observed result object:

{"type":"result","subtype":"success","is_error":true,"duration_ms":643,
 "num_turns":1,"total_cost_usd":0,"permission_denials_count":0}

is_error true, total_cost_usd 0, num_turns 1 -- a direct hit.

But the guard wrapping that script is gated on the action step's outcome (line 652):

- name: Fail the check if the review did not complete (attempt 1)
  id: fail-check
  if: steps.claude-review.outcome == 'success'
  continue-on-error: true
  uses: d-morrison/gha/.github/actions/run-review-guard@v2

And on quota exhaustion claude-code-action fails its own step first:

##[error]Claude result reported subtype success with is_error:true (run did not complete successfully)
##[error]Action failed with error: Claude execution failed: result is_error:true
##[error]Process completed with exit code 1.

So steps.claude-review.outcome is failure, the guard is skipped, quota_exhausted is never written, "Resolve final review outcome" sees a failed attempt with no flag, and the job fails for real. In the run logs the resolve step's rendered script shows "skipped" substituted where the guard's outcome should be -- visible confirmation the guard never ran.

Everything downstream that consumes the flag is therefore dead code in this scenario: the notice step at line 791, and the require-review job's if: (around line 1051, whose own comment explains that quota_exhausted=true is what keeps the gate from going red).

Two things that look like fixes but are not

Adding continue-on-error: true to the claude-review step. outcome reports the status before continue-on-error is applied; only conclusion reflects it. The gate would still read failure.

Waiting for the quota to reset. The warning text tells the reader to "re-trigger the review once the quota resets", which is right, but the check has already gone red by then, and require-review stays red on every open PR until someone re-runs each one by hand.

Suggested fix

Gate the guard on the execution file being available rather than on the action's outcome. check-review-execution.sh already handles a missing file with a clear error (lines 46-49), so always() is safe too:

if: always() && steps.exec-file.outcome == 'success'

Note exec-file (line 642) carries the same steps.claude-review.outcome == 'success' gate, so it needs widening in the same way. The execution file does exist in this scenario -- the log shows Log saved to /home/runner/work/_temp/claude-execution-output.json before the action errors -- and upload-review-execution already has a temp-path fallback for resolving it.

Worth checking the retry block's equivalent gates (lines 707, 716, 718) at the same time, since they mirror this shape.

Testing

scripts/tests/fixtures/ already has canned execution-output fixtures, and the quota case may already be among them -- if the script is tested but the workflow's gating is not, that is precisely how this survived. The gap is the same class the run-review-guard end-to-end steps were added for after gha#196: the unit was right and the wiring around it was not.

Found while investigating why PR #348's checks were red; the account was quota-exhausted, which is exactly the condition this path exists to handle.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions