From 371effe21a24c1b594d03fc7a8370aa598d80e73 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:47:40 -0800 Subject: [PATCH 01/13] ci: make the Engine Review Pass commit status the only required context The Engine Review Pass job and the commit status it posts shared a name, so both counted toward the required context. A commit status holds one latest value per SHA, but check runs pile up per event. A run that evaluated a head before its review landed left a failed check run behind, and that kept the PR blocked after a later review passed. PR 3782 needed ten runs rerun by hand to get unblocked. Rename the job to Engine Review Evaluator so it no longer matches the required context, and stop failing it on a missing or blocking review: the status carries that verdict. The job now fails when the status cannot be posted, which used to be only a warning, and API calls retry. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 09325fcde..3bc91bada 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1163,7 +1163,11 @@ jobs: # All engines post through one shared GitHub account, so this is an audit trail and not a cryptographic gate. engine-review-pass: - name: "Engine Review Pass" + # The required "Engine Review Pass" context is the commit status this job + # posts, which holds one latest value per SHA. The job must not share that + # name: its check runs pile up per event, and an old failure keeps a PR + # blocked after a later review passes. + name: "Engine Review Evaluator" if: (github.event_name == 'pull_request' && github.event.action != 'ready_for_review') || github.event_name == 'pull_request_review' || github.event_name == 'issue_comment' # Review bursts must not cancel in-progress runs. concurrency: @@ -1180,14 +1184,17 @@ jobs: - name: Verify engine review pass uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: + retries: 3 script: | const owner = context.repo.owner; const repo = context.repo.repo; + // The review verdict is the commit status posted on the PR head SHA, so a + // missing or blocking review does not fail this job. failRun is for when + // the verdict could not be evaluated or posted. // issue_comment events run in the default-branch context, so a failed job - // there is attributed to main even though it evaluated a PR head. The PR - // verdict is carried by the commit status posted on the PR head SHA; only - // fail the job itself for events that run in the pull request's context. + // there is attributed to main even though it evaluated a PR head; only fail + // the job itself for events that run in the pull request's context. const failRun = (msg) => { if (context.eventName === 'pull_request' || context.eventName === 'pull_request_review') { core.setFailed(msg); @@ -1262,7 +1269,7 @@ jobs: }); core.info(`Created commit status '${state}' on ${headSha}: ${description}`); } catch (err) { - core.warning(`Failed to create commit status on ${headSha}: ${err.message}`); + failRun(`Failed to create commit status on ${headSha}: ${err.message}`); } } @@ -1275,7 +1282,7 @@ jobs: if (!ALLOWED_ENGINES.has(authorEngine)) { const msg = 'Invalid author engine. Please use an engine branch prefix such as gemini/ or claude/.'; await setCommitStatus('failure', msg); - failRun(msg); + core.info(`Engine Review Pass failed: ${msg}`); return; } @@ -1396,14 +1403,14 @@ jobs: const blockedEngines = blockedMarkers.map((m) => m.engine).join(', '); const msg = `Blocked by ${blockedEngines}`; await setCommitStatus('failure', msg); - failRun(`Engine Review Pass blocked: ${msg}`); + core.info(`Engine Review Pass blocked: ${msg}`); return; } if (nonAuthorPassMarkers.length === 0) { const msg = 'No passing review from a non-author engine for current head SHA'; await setCommitStatus('failure', msg); - failRun(`Engine Review Pass failed: ${msg}`); + core.info(`Engine Review Pass failed: ${msg}`); return; } From d5020b5b5eeab14a77d966f3c1947e1ffbfe8e88 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:55:06 -0800 Subject: [PATCH 02/13] ci: keep a required failure when a review verdict cannot be posted With the job renamed, a failed status write only fails a non-required job. If an earlier review had already posted success, a later blocking review that could not be written left the PR mergeable. When a failure verdict cannot be posted, create an Engine Review Pass check run with a fixed external_id, which takes the required name for that case only. The next posted success clears it, so normal runs add no check runs under the required name and stale failures do not return. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 52 ++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 3bc91bada..98cfda152 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1174,6 +1174,7 @@ jobs: group: ${{ github.workflow }}-engine-review-pass-${{ github.event.pull_request.number || github.event.issue.number }} cancel-in-progress: false permissions: + checks: write statuses: write pull-requests: read issues: read @@ -1250,6 +1251,48 @@ jobs: const ALLOWED_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); + // If a failure verdict cannot be posted, an earlier success status would + // still satisfy the required context. This check run takes the required + // name in that case only, and the next posted success clears it. + const REQUIRED_CONTEXT = 'Engine Review Pass'; + const UNDELIVERED_ID = 'engine-review-status-undelivered'; + + async function raiseUndeliveredFailure(description) { + try { + await github.rest.checks.create({ + owner, + repo, + head_sha: headSha, + name: REQUIRED_CONTEXT, + external_id: UNDELIVERED_ID, + status: 'completed', + conclusion: 'failure', + output: { title: 'Failure verdict was not posted', summary: description }, + }); + } catch (err) { + failRun(`Failed to create fallback check run on ${headSha}: ${err.message}`); + } + } + + async function clearUndeliveredFailures() { + try { + const runs = await github.paginate(github.rest.checks.listForRef, { + owner, + repo, + ref: headSha, + check_name: REQUIRED_CONTEXT, + per_page: 100, + }); + for (const run of runs) { + if (run.external_id === UNDELIVERED_ID && run.conclusion === 'failure') { + await github.rest.checks.update({ owner, repo, check_run_id: run.id, conclusion: 'success' }); + } + } + } catch (err) { + failRun(`Failed to clear fallback check run on ${headSha}: ${err.message}`); + } + } + let statusPosted = false; async function setCommitStatus(state, description) { if (statusPosted) { @@ -1263,13 +1306,20 @@ jobs: repo, sha: headSha, state, - context: 'Engine Review Pass', + context: REQUIRED_CONTEXT, description: description.slice(0, 140), target_url: `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`, }); core.info(`Created commit status '${state}' on ${headSha}: ${description}`); } catch (err) { failRun(`Failed to create commit status on ${headSha}: ${err.message}`); + if (state === 'failure') { + await raiseUndeliveredFailure(description); + } + return; + } + if (state === 'success') { + await clearUndeliveredFailures(); } } From 9e4a2d7cbc1528e91f4d58b65eab59109d533748 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:04:33 -0800 Subject: [PATCH 03/13] ci: evaluate every PR at a head SHA and refuse fork PRs in the review gate Renaming the job left no failure signal that was specific to one PR or that a read-only token could write, which a Codex review showed in two ways. The commit status belongs to the SHA, so a PR with a passing review could overwrite the failure of another PR at the same commit. The status now passes only if every open PR at that head passes. A fork's token cannot post a verdict, so a blocking review on a fork PR could not retract a success posted earlier by a comment event. Fork PRs now always get a failure, so that success cannot exist. This is a behaviour change: fork PRs can no longer clear this gate. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 53 +++++++++++++++++++++++++-------- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 98cfda152..9ba36f3c9 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1323,23 +1323,27 @@ jobs: } } - try { - const headRef = pr.head?.ref || ''; + // Returns { failure: message } or { passEngines: [names] } for one pull request. + async function evaluatePullRequest(candidate) { + // A fork's token cannot post a verdict, so its PR could not retract a success + // posted earlier by a run with write access. Forks never get one. + if (candidate.head?.repo?.fork || candidate.head?.repo?.full_name !== candidate.base?.repo?.full_name) { + return { failure: 'Fork pull requests are not eligible. Push the branch to this repository.' }; + } + + const headRef = candidate.head?.ref || ''; const slashIndex = headRef.indexOf('/'); const rawPrefix = slashIndex > 0 ? headRef.slice(0, slashIndex).trim() : ''; const authorEngine = rawPrefix.toLowerCase(); if (!ALLOWED_ENGINES.has(authorEngine)) { - const msg = 'Invalid author engine. Please use an engine branch prefix such as gemini/ or claude/.'; - await setCommitStatus('failure', msg); - core.info(`Engine Review Pass failed: ${msg}`); - return; + return { failure: 'Invalid author engine. Please use an engine branch prefix such as gemini/ or claude/.' }; } core.info(`Author engine: ${authorEngine} (from branch ${headRef})`); core.info(`Head SHA: ${headSha}`); - const prNumber = pr.number; + const prNumber = candidate.number; core.info(`Fetching reviews for pull request #${prNumber}...`); const reviews = await github.paginate(github.rest.pulls.listReviews, { @@ -1451,20 +1455,43 @@ jobs: if (blockedMarkers.length > 0) { const blockedEngines = blockedMarkers.map((m) => m.engine).join(', '); - const msg = `Blocked by ${blockedEngines}`; - await setCommitStatus('failure', msg); - core.info(`Engine Review Pass blocked: ${msg}`); - return; + return { failure: `Blocked by ${blockedEngines}` }; } if (nonAuthorPassMarkers.length === 0) { - const msg = 'No passing review from a non-author engine for current head SHA'; + return { failure: 'No passing review from a non-author engine for current head SHA' }; + } + + return { passEngines: nonAuthorPassMarkers.map((m) => m.engine) }; + } + + try { + // The status belongs to the SHA, so every open PR at this head shares it. + // It may only pass if all of them pass. + const associated = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, { + owner, + repo, + commit_sha: headSha, + per_page: 100, + }); + const sharingHead = associated.filter((p) => p.state === 'open' && p.head?.sha === headSha); + const candidates = sharingHead.some((p) => p.number === pr.number) ? sharingHead : [pr, ...sharingHead]; + + const verdicts = []; + for (const candidate of candidates) { + verdicts.push({ number: candidate.number, ...(await evaluatePullRequest(candidate)) }); + } + + const failed = verdicts.filter((v) => v.failure); + if (failed.length > 0) { + const msg = + candidates.length === 1 ? failed[0].failure : failed.map((v) => `PR #${v.number}: ${v.failure}`).join('; '); await setCommitStatus('failure', msg); core.info(`Engine Review Pass failed: ${msg}`); return; } - const passEngines = nonAuthorPassMarkers.map((m) => m.engine).join(', '); + const passEngines = [...new Set(verdicts.flatMap((v) => v.passEngines))].join(', '); const successMsg = `Passed by ${passEngines}`; await setCommitStatus('success', successMsg); core.info(`Engine Review Pass succeeded: ${successMsg}`); From 03ae812e8a9729e8ef4e3b4a8f98023e9ef75be3 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:15:40 -0800 Subject: [PATCH 04/13] ci: rerun stale Engine Review Pass runs when a review passes Replaces the rename in the earlier commits on this branch. Two Codex reviews found that once the job stops carrying the verdict, an old success status stays authoritative until something overwrites it, and a race, a read-only token or a timeout can each prevent that write. Restore the job and script to main, so a job under the required name still fails without any token and shows pending while it runs. Fix the original problem directly: when a verdict passes, rerun the oldest failed or cancelled evaluation at that SHA. It re-reads the reviews and, if it passes, reruns the next. If rerunning fails, the old result stands and the PR stays blocked, as it does today. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 158 +++++++++++--------------------- 1 file changed, 56 insertions(+), 102 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 9ba36f3c9..a6e33b6cd 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1163,18 +1163,14 @@ jobs: # All engines post through one shared GitHub account, so this is an audit trail and not a cryptographic gate. engine-review-pass: - # The required "Engine Review Pass" context is the commit status this job - # posts, which holds one latest value per SHA. The job must not share that - # name: its check runs pile up per event, and an old failure keeps a PR - # blocked after a later review passes. - name: "Engine Review Evaluator" + name: "Engine Review Pass" if: (github.event_name == 'pull_request' && github.event.action != 'ready_for_review') || github.event_name == 'pull_request_review' || github.event_name == 'issue_comment' # Review bursts must not cancel in-progress runs. concurrency: group: ${{ github.workflow }}-engine-review-pass-${{ github.event.pull_request.number || github.event.issue.number }} cancel-in-progress: false permissions: - checks: write + actions: write statuses: write pull-requests: read issues: read @@ -1185,17 +1181,14 @@ jobs: - name: Verify engine review pass uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: - retries: 3 script: | const owner = context.repo.owner; const repo = context.repo.repo; - // The review verdict is the commit status posted on the PR head SHA, so a - // missing or blocking review does not fail this job. failRun is for when - // the verdict could not be evaluated or posted. // issue_comment events run in the default-branch context, so a failed job - // there is attributed to main even though it evaluated a PR head; only fail - // the job itself for events that run in the pull request's context. + // there is attributed to main even though it evaluated a PR head. The PR + // verdict is carried by the commit status posted on the PR head SHA; only + // fail the job itself for events that run in the pull request's context. const failRun = (msg) => { if (context.eventName === 'pull_request' || context.eventName === 'pull_request_review') { core.setFailed(msg); @@ -1251,48 +1244,6 @@ jobs: const ALLOWED_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); - // If a failure verdict cannot be posted, an earlier success status would - // still satisfy the required context. This check run takes the required - // name in that case only, and the next posted success clears it. - const REQUIRED_CONTEXT = 'Engine Review Pass'; - const UNDELIVERED_ID = 'engine-review-status-undelivered'; - - async function raiseUndeliveredFailure(description) { - try { - await github.rest.checks.create({ - owner, - repo, - head_sha: headSha, - name: REQUIRED_CONTEXT, - external_id: UNDELIVERED_ID, - status: 'completed', - conclusion: 'failure', - output: { title: 'Failure verdict was not posted', summary: description }, - }); - } catch (err) { - failRun(`Failed to create fallback check run on ${headSha}: ${err.message}`); - } - } - - async function clearUndeliveredFailures() { - try { - const runs = await github.paginate(github.rest.checks.listForRef, { - owner, - repo, - ref: headSha, - check_name: REQUIRED_CONTEXT, - per_page: 100, - }); - for (const run of runs) { - if (run.external_id === UNDELIVERED_ID && run.conclusion === 'failure') { - await github.rest.checks.update({ owner, repo, check_run_id: run.id, conclusion: 'success' }); - } - } - } catch (err) { - failRun(`Failed to clear fallback check run on ${headSha}: ${err.message}`); - } - } - let statusPosted = false; async function setCommitStatus(state, description) { if (statusPosted) { @@ -1306,44 +1257,69 @@ jobs: repo, sha: headSha, state, - context: REQUIRED_CONTEXT, + context: 'Engine Review Pass', description: description.slice(0, 140), target_url: `https://github.com/${owner}/${repo}/actions/runs/${context.runId}`, }); core.info(`Created commit status '${state}' on ${headSha}: ${description}`); } catch (err) { - failRun(`Failed to create commit status on ${headSha}: ${err.message}`); - if (state === 'failure') { - await raiseUndeliveredFailure(description); - } - return; - } - if (state === 'success') { - await clearUndeliveredFailures(); + core.warning(`Failed to create commit status on ${headSha}: ${err.message}`); } } - // Returns { failure: message } or { passEngines: [names] } for one pull request. - async function evaluatePullRequest(candidate) { - // A fork's token cannot post a verdict, so its PR could not retract a success - // posted earlier by a run with write access. Forks never get one. - if (candidate.head?.repo?.fork || candidate.head?.repo?.full_name !== candidate.base?.repo?.full_name) { - return { failure: 'Fork pull requests are not eligible. Push the branch to this repository.' }; + // A failed or cancelled run of this job stays on the head SHA and blocks the + // PR even after a later run passes. Rerun the oldest one: it re-reads the + // reviews and, if it passes, reruns the next. One at a time, because a queued + // run is cancelled by the next one in the concurrency group. Best-effort: if + // this fails, the old result stands and the PR stays blocked until a rerun. + async function rerunStaleEvaluation() { + const stale = (conclusion) => conclusion === 'failure' || conclusion === 'cancelled'; + try { + const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { + owner, + repo, + head_sha: headSha, + per_page: 100, + }); + const candidates = runs + .filter((r) => r.name === context.workflow && r.id !== context.runId && r.status === 'completed' && stale(r.conclusion)) + .sort((a, b) => a.id - b.id); + for (const run of candidates) { + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: run.id, + per_page: 100, + }); + // Runs that are red for another job's sake would rerun forever. + if (jobs.some((j) => j.name === 'Engine Review Pass' && stale(j.conclusion))) { + await github.rest.actions.reRunWorkflowFailedJobs({ owner, repo, run_id: run.id }); + core.info(`Reran stale evaluation in run ${run.id}`); + return; + } + } + } catch (err) { + core.warning(`Could not rerun stale evaluations on ${headSha}: ${err.message}`); } + } - const headRef = candidate.head?.ref || ''; + try { + const headRef = pr.head?.ref || ''; const slashIndex = headRef.indexOf('/'); const rawPrefix = slashIndex > 0 ? headRef.slice(0, slashIndex).trim() : ''; const authorEngine = rawPrefix.toLowerCase(); if (!ALLOWED_ENGINES.has(authorEngine)) { - return { failure: 'Invalid author engine. Please use an engine branch prefix such as gemini/ or claude/.' }; + const msg = 'Invalid author engine. Please use an engine branch prefix such as gemini/ or claude/.'; + await setCommitStatus('failure', msg); + failRun(msg); + return; } core.info(`Author engine: ${authorEngine} (from branch ${headRef})`); core.info(`Head SHA: ${headSha}`); - const prNumber = candidate.number; + const prNumber = pr.number; core.info(`Fetching reviews for pull request #${prNumber}...`); const reviews = await github.paginate(github.rest.pulls.listReviews, { @@ -1455,46 +1431,24 @@ jobs: if (blockedMarkers.length > 0) { const blockedEngines = blockedMarkers.map((m) => m.engine).join(', '); - return { failure: `Blocked by ${blockedEngines}` }; + const msg = `Blocked by ${blockedEngines}`; + await setCommitStatus('failure', msg); + failRun(`Engine Review Pass blocked: ${msg}`); + return; } if (nonAuthorPassMarkers.length === 0) { - return { failure: 'No passing review from a non-author engine for current head SHA' }; - } - - return { passEngines: nonAuthorPassMarkers.map((m) => m.engine) }; - } - - try { - // The status belongs to the SHA, so every open PR at this head shares it. - // It may only pass if all of them pass. - const associated = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, { - owner, - repo, - commit_sha: headSha, - per_page: 100, - }); - const sharingHead = associated.filter((p) => p.state === 'open' && p.head?.sha === headSha); - const candidates = sharingHead.some((p) => p.number === pr.number) ? sharingHead : [pr, ...sharingHead]; - - const verdicts = []; - for (const candidate of candidates) { - verdicts.push({ number: candidate.number, ...(await evaluatePullRequest(candidate)) }); - } - - const failed = verdicts.filter((v) => v.failure); - if (failed.length > 0) { - const msg = - candidates.length === 1 ? failed[0].failure : failed.map((v) => `PR #${v.number}: ${v.failure}`).join('; '); + const msg = 'No passing review from a non-author engine for current head SHA'; await setCommitStatus('failure', msg); - core.info(`Engine Review Pass failed: ${msg}`); + failRun(`Engine Review Pass failed: ${msg}`); return; } - const passEngines = [...new Set(verdicts.flatMap((v) => v.passEngines))].join(', '); + const passEngines = nonAuthorPassMarkers.map((m) => m.engine).join(', '); const successMsg = `Passed by ${passEngines}`; await setCommitStatus('success', successMsg); core.info(`Engine Review Pass succeeded: ${successMsg}`); + await rerunStaleEvaluation(); } catch (err) { core.error(`Unexpected error: ${err.message}`); await setCommitStatus('failure', `Error: ${err.message}`.slice(0, 140)); From d6366a8f06d1a39b06b385597c95fec525201bdd Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:17:52 -0800 Subject: [PATCH 05/13] ci: keep repairing stale review runs past a rejected rerun A rejected rerun, such as an expired run, ended the whole search, so every later pass retried the same run and newer ones stayed red. Catch errors per run and move on to the next. Also treat timed_out as stale, because this job has a timeout and only failure and cancelled were retried. Found by Codex at 03ae812e. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index a6e33b6cd..e7bbe7621 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1273,7 +1273,7 @@ jobs: // run is cancelled by the next one in the concurrency group. Best-effort: if // this fails, the old result stands and the PR stays blocked until a rerun. async function rerunStaleEvaluation() { - const stale = (conclusion) => conclusion === 'failure' || conclusion === 'cancelled'; + const stale = (conclusion) => ['failure', 'cancelled', 'timed_out'].includes(conclusion); try { const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { owner, @@ -1285,17 +1285,22 @@ jobs: .filter((r) => r.name === context.workflow && r.id !== context.runId && r.status === 'completed' && stale(r.conclusion)) .sort((a, b) => a.id - b.id); for (const run of candidates) { - const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { - owner, - repo, - run_id: run.id, - per_page: 100, - }); - // Runs that are red for another job's sake would rerun forever. - if (jobs.some((j) => j.name === 'Engine Review Pass' && stale(j.conclusion))) { - await github.rest.actions.reRunWorkflowFailedJobs({ owner, repo, run_id: run.id }); - core.info(`Reran stale evaluation in run ${run.id}`); - return; + try { + const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { + owner, + repo, + run_id: run.id, + per_page: 100, + }); + // Runs that are red for another job's sake would rerun forever. + if (jobs.some((j) => j.name === 'Engine Review Pass' && stale(j.conclusion))) { + await github.rest.actions.reRunWorkflowFailedJobs({ owner, repo, run_id: run.id }); + core.info(`Reran stale evaluation in run ${run.id}`); + return; + } + } catch (err) { + // An expired run must not stop newer ones from being repaired. + core.warning(`Could not rerun run ${run.id}: ${err.message}`); } } } catch (err) { From 565c4b59e5545abbb32feeeb3eced3ab1f52593e Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:20:56 -0800 Subject: [PATCH 06/13] ci: close two fail-open gaps in the Engine Review Pass gate Both are on main today and were reported by Codex. A native changes-requested review was ignored unless it held a REVIEW marker. A member's latest decision now blocks until they approve or it is dismissed, matching GitHub's rule. Decisions are read from the raw review list because the marker filter drops reviews with an empty body. A blocking comment could leave an earlier success in place if its run died or could not post a status, because comment runs have no job under the required name on the PR head. A member's marker comment now replaces the status with pending before evaluating, so those cases leave the PR blocked. Plain comments skip this, so bots do not make a passing PR flicker. If a comment run cannot post a status, the run now fails. Behaviour change: a native changes-requested review now blocks the gate. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 50 +++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index e7bbe7621..3e3b5b2e8 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1244,13 +1244,16 @@ jobs: const ALLOWED_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); + // The marker must be alone on its own line. + const markerRegex = /^REVIEW: (PASS|BLOCKED|FAIL) ([A-Za-z0-9._-]+) ([0-9A-Fa-f]{40})\s*$/; + let statusPosted = false; async function setCommitStatus(state, description) { if (statusPosted) { core.info(`Commit status already posted. Skipping duplicate (${state}: ${description})`); return; } - statusPosted = true; + statusPosted = state !== 'pending'; try { await github.rest.repos.createCommitStatus({ owner, @@ -1264,6 +1267,11 @@ jobs: core.info(`Created commit status '${state}' on ${headSha}: ${description}`); } catch (err) { core.warning(`Failed to create commit status on ${headSha}: ${err.message}`); + // A comment run has no check run on the PR head to carry the verdict, so + // make the lost write visible even though the run belongs to main. + if (context.eventName === 'issue_comment') { + core.setFailed(`Could not post Engine Review Pass on ${headSha}: ${err.message}`); + } } } @@ -1309,6 +1317,18 @@ jobs: } try { + // A comment run has no job under the required name on the PR head, so an + // earlier success would outlive a run that dies or cannot post its verdict. + // Replacing it first leaves the PR blocked instead. + const comment = context.payload.comment; + if ( + context.eventName === 'issue_comment' && + ALLOWED_ASSOCIATIONS.has((comment?.author_association || '').toUpperCase()) && + (comment?.body || '').split(/\r?\n/).some((line) => markerRegex.test(line)) + ) { + await setCommitStatus('pending', 'Evaluating review marker'); + } + const headRef = pr.head?.ref || ''; const slashIndex = headRef.indexOf('/'); const rawPrefix = slashIndex > 0 ? headRef.slice(0, slashIndex).trim() : ''; @@ -1382,8 +1402,6 @@ jobs: return a.id - b.id; }); - // The marker must be alone on its own line. - const markerRegex = /^REVIEW: (PASS|BLOCKED|FAIL) ([A-Za-z0-9._-]+) ([0-9A-Fa-f]{40})\s*$/; const headShaLower = headSha.toLowerCase(); const authorEngineLower = authorEngine.toLowerCase(); @@ -1434,9 +1452,29 @@ jobs: return m.action === 'PASS' && m.engine.toLowerCase() !== authorEngineLower; }); - if (blockedMarkers.length > 0) { - const blockedEngines = blockedMarkers.map((m) => m.engine).join(', '); - const msg = `Blocked by ${blockedEngines}`; + // Native reviews follow GitHub's rule: a member's latest decision stands, and + // a later comment does not clear it. These are read from the raw list because + // eligibleReviews drops reviews with an empty body. + const latestDecisions = new Map(); + const decisions = reviews + .filter((r) => ['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(r.state) && r.user?.login) + .sort((a, b) => new Date(a.submitted_at).getTime() - new Date(b.submitted_at).getTime() || a.id - b.id); + for (const r of decisions) { + latestDecisions.set(r.user.login, r); + } + const changesRequestedBy = Array.from(latestDecisions.values()) + .filter((r) => r.state === 'CHANGES_REQUESTED' && ALLOWED_ASSOCIATIONS.has((r.author_association || '').toUpperCase())) + .map((r) => r.user.login); + + if (blockedMarkers.length > 0 || changesRequestedBy.length > 0) { + const reasons = []; + if (blockedMarkers.length > 0) { + reasons.push(`Blocked by ${blockedMarkers.map((m) => m.engine).join(', ')}`); + } + if (changesRequestedBy.length > 0) { + reasons.push(`Changes requested by ${changesRequestedBy.join(', ')}`); + } + const msg = reasons.join('; '); await setCommitStatus('failure', msg); failRun(`Engine Review Pass blocked: ${msg}`); return; From ef2fbba1d95d2849a47dda450d0e43b49239d9af Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:25:04 -0800 Subject: [PATCH 07/13] ci: post pending when a review marker is edited out of a comment The pending step only read a comment's new text, so editing a marker away skipped it, and a run that then died left the withdrawn success in place. Check the previous text as well. Found by Codex at 565c4b59. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 3e3b5b2e8..1c54e652f 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1320,11 +1320,13 @@ jobs: // A comment run has no job under the required name on the PR head, so an // earlier success would outlive a run that dies or cannot post its verdict. // Replacing it first leaves the PR blocked instead. + // Includes the previous text, because editing a marker away withdraws it. const comment = context.payload.comment; + const commentTexts = [comment?.body, context.payload.changes?.body?.from]; if ( context.eventName === 'issue_comment' && ALLOWED_ASSOCIATIONS.has((comment?.author_association || '').toUpperCase()) && - (comment?.body || '').split(/\r?\n/).some((line) => markerRegex.test(line)) + commentTexts.some((text) => (text || '').split(/\r?\n/).some((line) => markerRegex.test(line))) ) { await setCommitStatus('pending', 'Evaluating review marker'); } From 24f76417dfdd20bc521fd6084e0b46f11258e5a6 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:27:47 -0800 Subject: [PATCH 08/13] ci: retry the Engine Review Pass script's API calls Restoring the workflow from main dropped this. Without it, one transient 5xx on a status write can lose a comment run's verdict. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 1c54e652f..b978a9bfa 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1181,6 +1181,7 @@ jobs: - name: Verify engine review pass uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 with: + retries: 3 script: | const owner = context.repo.owner; const repo = context.repo.repo; From 9e30f0d933b4b7199112dcc45f2a74951723e98b Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:28:18 -0800 Subject: [PATCH 09/13] ci: rerun the stale Engine Review Pass job by ID Rerun the gate job directly rather than a run's failed jobs, so cancelled and timed-out evaluations are covered without relying on how the failed-jobs endpoint treats them, and unrelated jobs are left alone. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index b978a9bfa..395c073fe 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1302,8 +1302,9 @@ jobs: per_page: 100, }); // Runs that are red for another job's sake would rerun forever. - if (jobs.some((j) => j.name === 'Engine Review Pass' && stale(j.conclusion))) { - await github.rest.actions.reRunWorkflowFailedJobs({ owner, repo, run_id: run.id }); + const staleJob = jobs.find((j) => j.name === 'Engine Review Pass' && stale(j.conclusion)); + if (staleJob) { + await github.rest.actions.reRunJobForWorkflowRun({ owner, repo, job_id: staleJob.id }); core.info(`Reran stale evaluation in run ${run.id}`); return; } From da0bc8c762cb427cf16dce20053c024263fec903 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:33:35 -0800 Subject: [PATCH 10/13] ci: re-evaluate the review gate when a native review is edited Editing a review to remove its PASS marker or add a blocking one left the earlier success in place until another event arrived. Subscribe to pull_request_review edited. If a comment run cannot load the PR it has no head SHA to post a status to, so an earlier success stands. Fail the run so that is visible. Both found by Codex at 9e30f0d9. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 395c073fe..cfc0f287a 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -4,7 +4,7 @@ on: pull_request: types: [opened, synchronize, reopened, ready_for_review] pull_request_review: - types: [submitted, dismissed] + types: [submitted, edited, dismissed] pull_request_review_comment: types: [created, edited, deleted] issue_comment: @@ -1212,7 +1212,9 @@ jobs: }); pr = prResponse.data; } catch (err) { - failRun(`Could not load pull request #${context.payload.issue.number}: ${err.message}`); + // Without the head SHA there is nowhere to post a status, so an earlier + // success stands. Fail even though this run belongs to main. + core.setFailed(`Could not load pull request #${context.payload.issue.number}: ${err.message}`); return; } } From 3ec301ffbbb368ae0028a250f2167db4767f704a Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:39:55 -0800 Subject: [PATCH 11/13] ci: post pending on every comment run, not only marker comments A queued run with a marker can be replaced by a run for a plain comment. That run skipped pending, so if it timed out during its fetches the earlier success survived a withdrawn or blocking review. Post pending on every comment run. This also removes the marker and author checks. The cost is a brief pending status on a passing PR whenever anyone comments. Found by Codex at da0bc8c7. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index cfc0f287a..e7663b4e2 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1247,9 +1247,6 @@ jobs: const ALLOWED_ASSOCIATIONS = new Set(['OWNER', 'MEMBER', 'COLLABORATOR']); - // The marker must be alone on its own line. - const markerRegex = /^REVIEW: (PASS|BLOCKED|FAIL) ([A-Za-z0-9._-]+) ([0-9A-Fa-f]{40})\s*$/; - let statusPosted = false; async function setCommitStatus(state, description) { if (statusPosted) { @@ -1323,16 +1320,10 @@ jobs: try { // A comment run has no job under the required name on the PR head, so an // earlier success would outlive a run that dies or cannot post its verdict. - // Replacing it first leaves the PR blocked instead. - // Includes the previous text, because editing a marker away withdraws it. - const comment = context.payload.comment; - const commentTexts = [comment?.body, context.payload.changes?.body?.from]; - if ( - context.eventName === 'issue_comment' && - ALLOWED_ASSOCIATIONS.has((comment?.author_association || '').toUpperCase()) && - commentTexts.some((text) => (text || '').split(/\r?\n/).some((line) => markerRegex.test(line))) - ) { - await setCommitStatus('pending', 'Evaluating review marker'); + // Replacing it first leaves the PR blocked instead. This runs for every + // comment, because a queued run with a marker can be replaced by one without. + if (context.eventName === 'issue_comment') { + await setCommitStatus('pending', 'Evaluating reviews'); } const headRef = pr.head?.ref || ''; @@ -1408,6 +1399,8 @@ jobs: return a.id - b.id; }); + // The marker must be alone on its own line. + const markerRegex = /^REVIEW: (PASS|BLOCKED|FAIL) ([A-Za-z0-9._-]+) ([0-9A-Fa-f]{40})\s*$/; const headShaLower = headSha.toLowerCase(); const authorEngineLower = authorEngine.toLowerCase(); From 78cba83ef3d57f866797e534e9e66068adf68d0d Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:45:41 -0800 Subject: [PATCH 12/13] ci: skip stale-run repair when the PR head has moved A repair rerun joins this job's concurrency group, where it replaces a queued job. If the PR had advanced, that could be the evaluation of the new head, leaving it unevaluated. Re-read the PR and skip the repair if its head no longer matches. Found by Codex at 3ec301ff. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index e7663b4e2..027c04560 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1283,6 +1283,14 @@ jobs: async function rerunStaleEvaluation() { const stale = (conclusion) => ['failure', 'cancelled', 'timed_out'].includes(conclusion); try { + // A rerun joins this job's concurrency group, where it would replace a + // queued evaluation of a newer head. + const { data: current } = await github.rest.pulls.get({ owner, repo, pull_number: pr.number }); + if (current.head?.sha !== headSha) { + core.info(`Not rerunning stale evaluations: PR head moved from ${headSha}`); + return; + } + const runs = await github.paginate(github.rest.actions.listWorkflowRunsForRepo, { owner, repo, From c5eba42fa2fb346173aa2bdc54f166a108f3b929 Mon Sep 17 00:00:00 2001 From: Christopher <131207452+PetrefiedThunder@users.noreply.github.com> Date: Wed, 4 Nov 2026 23:49:36 -0800 Subject: [PATCH 13/13] ci: limit stale-run repair to the PR's own runs Repair matched runs by SHA only, so a pass on one PR could rerun another PR's failed evaluation at the same commit, and that run would then post failure to the status they share. Found by Codex at 78cba83e. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/pr-guards.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pr-guards.yml b/.github/workflows/pr-guards.yml index 027c04560..a9ad3f096 100644 --- a/.github/workflows/pr-guards.yml +++ b/.github/workflows/pr-guards.yml @@ -1299,6 +1299,8 @@ jobs: }); const candidates = runs .filter((r) => r.name === context.workflow && r.id !== context.runId && r.status === 'completed' && stale(r.conclusion)) + // Another PR at this SHA may not have passed, and its run shares this status. + .filter((r) => r.pull_requests?.some((p) => p.number === pr.number)) .sort((a, b) => a.id - b.id); for (const run of candidates) { try {