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.
claude-code-review.ymlhas 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.shhas the detector, and it matches the real signature exactly (lines 62-72):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_errortrue,total_cost_usd0,num_turns1 -- a direct hit.But the guard wrapping that script is gated on the action step's outcome (line 652):
And on quota exhaustion
claude-code-actionfails its own step first:So
steps.claude-review.outcomeisfailure, the guard is skipped,quota_exhaustedis 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-reviewjob'sif:(around line 1051, whose own comment explains thatquota_exhausted=trueis what keeps the gate from going red).Two things that look like fixes but are not
Adding
continue-on-error: trueto theclaude-reviewstep.outcomereports the status beforecontinue-on-erroris applied; onlyconclusionreflects it. The gate would still readfailure.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-reviewstays 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.shalready handles a missing file with a clear error (lines 46-49), soalways()is safe too:Note
exec-file(line 642) carries the samesteps.claude-review.outcome == 'success'gate, so it needs widening in the same way. The execution file does exist in this scenario -- the log showsLog saved to /home/runner/work/_temp/claude-execution-output.jsonbefore the action errors -- andupload-review-executionalready 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 therun-review-guardend-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.