Skip to content

fix(pr-pipeline): give the main-run review the lanes' credential chain and collapse exact renames - #10969

Merged
kyle-sexton merged 6 commits into
mainfrom
fix/pr-review-main-credential-chain
Oct 29, 2026
Merged

kyle-sexton merged 6 commits into
mainfrom
fix/pr-review-main-credential-chain

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Refs: #10743, #10961

Summary

pr-review-main.yml (from #10743) ran the main-bound review on REVIEWS_CLAUDE_API_KEY alone. That key's credit is exhausted. Runs 40890788166, 40891251085 and 40892239796 each ended class=credit-stop on REVIEWS_CLAUDE_API_KEY with no verdict. So the lanes stayed unbound, and every lane-source pull request held for a person.

This PR gives the main-run review the same credential chain and handoff as the hosted review lanes. It also does part 2 of #10961: it collapses exact renames in the review diff, so a pull request of pure moves (#10952: 2.9 MB) fits under the size cap.

Fix

Credential chain. The order is taken from pr-review-hosted.yml's secrets: block, and the tests assert it against that block.

  • review (matrix of the two lanes) runs on the API key. When run-claude-cli hands off (a credit stop, or a 401 or 402), or the key is empty (no-key), the step "Hand the lane on" writes the lane's reason under the lane's own job output. Matrix outputs merge, and per-name outputs are the documented way to keep them apart. That leg skips the verdict and the outcome report and stays green.
  • review-handoffs holds no secret, runs no model and has permissions: {}. It checks each reason against a closed set (credit-stop auth no-key) and emits fixed lane rows as the next leg's matrix.
  • review-subscription runs on REVIEWS_CLAUDE_CODE_OAUTH_TOKEN, with fallback-ready set when the fallback token is set. It hands off on limit-stop, credit-stop or auth, or on an empty token when the fallback token is set.
  • subscription-handoffs is a second route job with the same shape (limit-stop credit-stop auth no-key).
  • review-fallback runs on FALLBACK_CLAUDE_CODE_OAUTH_TOKEN. It is the last leg and hands nothing on.

Each leg is its own job on a new VM with one credential. It repeats every step of the key job from a fresh main checkout: the trigger gate, the kill switch, the lane-source select, the diff, runner isolation, the read-only token, and tools: Read with the same allow and deny rules. Every leg writes the identical pr-review-main-<lane>-<pr>-<tree> verdict, so the record job (which now needs all three legs) and the gate do not depend on which leg ran. The lane record names the credential and a fallback-reason built from fixed literals, as in pr-verify. A lane that no leg gives a verdict stays unbound (fail closed).

Exact renames. The new .github/scripts/pr-review-main/write-diff.py prints today's whole-file --no-renames patch unless that patch is over MAX_DIFF_BYTES. Only then does it diff with -M100%, so a file moved unchanged prints only its rename from and rename to lines. Even then these print whole, as --no-renames shows them:

  • an edited rename;
  • a move whose source or destination is a lane source (babysit_review_evidence.lane_source, the predicate the select step already uses);
  • a move that changes the file mode, or moves a symlink (120000) or submodule (160000);
  • a move whose new path (or its last directory and name) an added line names, repeated until no shown line names a collapsed move.

The prompt now says a rename-only entry's content is unseen and that a change referencing a moved path is a finding. The binary, size and long-line caps apply after.

Leg routing. The route jobs run on !cancelled(), so one lane's failed leg does not drop the other lane's handoff. Every leg checks out ${{ github.sha }}, the main commit whose copy of the workflow runs, so all legs run the same main.

Registries. .github/lane-funding.json gains the review-subscription and review-fallback lanes. .github/artifact-retention.json lists the two new verdict uploaders. docs/conventions/pr-pipeline/README.md documents the chain and the rename rule. The change also adds a changelog fragment.

Deviation from the brief. The credential check does not test every leg's secret in the key job. callers.test.mjs:1873 forbids one step from reading both an API key and an OAuth token. Instead, the key leg always hands an empty key on, and the subscription leg checks its own token and the fallback token. With nothing set, no leg reviews and nothing goes red, as before.

Verification

  • node --test in .github/scripts/pr-review-main: 31 pass. The new tests cover:
    • leg order and inputs, taken from pr-review-hosted.yml and checked to be equal in pr-review-security-hosted.yml;
    • each secret's value reaching only its leg's launcher step;
    • the route jq, run against valid and invalid reasons;
    • the handoff step, run with valid reasons, an empty reason and an injected newline;
    • each later leg's steps equal to the key job's, apart from the credential and the fallback reason;
    • fixture repositories: pure moves under the cap (whole) and over it (rename lines only); a move named by an added line, and one named by that move (whole), beside a move nothing names (collapsed); a moved symlink; moves into and out of a lane source and a mode change; an edited rename;
    • one lane handed on beside one that reviewed routing only the handed-on lane, and the prompt text.
  • The new tests fail when the code is broken on purpose: swapping the subscription credential, adding limit-stop to the key route, using -M50%, dropping the lane-source check, dropping the under-cap whole patch, the symlink rule, the named-move rule, or its directory-and-name form each turns a test red.
  • callers.test.mjs 200 pass; check-artifact-retention.sh, runner-policy.mjs, sync-hold-floor.py --check (no hold-path change) and check-changelog-fragments.sh are clean.
  • ci-lanes-pr-hold.test.sh 243 pass, ci-review-lanes.test.sh 125 pass, and the babysit review_evidence, merge_holds_sync, risk, gh and merge suites 619 pass.
  • actionlint 1.7.12 is clean. zizmor 1.31.1's offline audits with CI's arguments are clean. The online audits were not run locally.
  • Expect this PR's own main-run review to fail. The PR edits pr-review-main.yml, and main's key-only copy reviews it, so it will credit-stop with no verdict. That red run is the bug this PR fixes, not a regression. The PR holds for a person either way, because pr-review-main.yml is a gate floor path.

Related

🤖 Generated with Claude Code

…n and collapse exact renames

pr-review-main ran on REVIEWS_CLAUDE_API_KEY alone, so an exhausted key
ended every main-run review in a credit stop with no verdict and left
every lane-source pull request unbound. Each lane now runs the review
lanes' chain: the key leg, then review-subscription on the role's
subscription token for a lane the key leg handed off (credit stop, 401,
402) or with no key, then review-fallback on the central fallback token
for a lane that leg handed off. Each leg is its own job with one
credential and repeats the key job's steps; route jobs with no secret
pass on only lanes whose handoff reason is in a closed set. Every leg
writes the same verdict, and a handed-off leg stays green.

The review diff now shows a file moved unchanged as its rename lines
alone (-M100%), so pure moves fit under the size cap; an edited rename,
a move into or out of a lane source, and a mode change still show whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-29T15:33:49.688730Z 0fd10f6 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

claude-security-review reviewed 66871e7fab47e3b07a1d49921a58b9aba4bbc40f: 0 finding(s), 0 as inline comments.

Incremental review of workflow.test.mjs, pr-review-main.yml and the pr-pipeline README (since 92811e0): no security issues found.

  • The key leg now checks out refs/heads/main (the trusted default branch) in place of github.sha. It records the commit with a step whose only input is matrix.lane (set by the route job). The recorded value is git rev-parse --verify HEAD, so there is no untrusted input or expression injection.
  • Later legs check out ${{ matrix.base }}. The route jobs (review-handoffs, subscription-handoffs) pass a base only if it matches \A[0-9a-f]{40}\z. Otherwise the route fails, so no later leg runs for that lane. That is fail-closed, and it keeps ref: from taking a branch name, an option-like string, or a newline-injected value. The tests cover the empty, main, uppercase, trailing-newline and 39-character cases.
  • persist-credentials: false, the credential separation and the handoff-reason allowlist are unchanged. Token scope, the kill switch and trusted-actor checks are not touched. No trust control is widened.
  • If only a later leg is re-run, it reuses the commit the original key leg recorded (main's tip at that time). That matches the intended design and is not a vulnerability.

No spend change. No manifest fields in this diff.

critical-findings: 0

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-review: 1 inline finding(s) on 0fd10f613025cb142a015d5484067f9889001525; the summary is in this lane's pull request comment.

claude-lane: claude-review

Comment thread .github/workflows/pr-review-main.yml Outdated
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

claude-review reviewed 66871e7fab47e3b07a1d49921a58b9aba4bbc40f: 0 finding(s), 0 as inline comments.

Incremental review of .github/workflows/pr-review-main.yml, .github/scripts/pr-review-main/workflow.test.mjs and docs/conventions/pr-pipeline/README.md since 92811e0. I found nothing to flag. I read the diffs and the surrounding test helpers and workflow lines. I did not run the tests or the workflow.

What changed: the key leg now checks out refs/heads/main and records the commit it got, as <lane>-base-sha. The route jobs pass that commit to the next leg as matrix.base, only after a \A[0-9a-f]{40}\z check in jq. The later legs check out ${{ matrix.base }} in place of github.sha.

What I checked:

  • Test helpers: KEY, SUB, step, index and jobText exist in the test file. The new assertions use them correctly.
  • No github.sha left: no github.sha reference remains in the workflow, so the new doesNotMatch(text, /github\.sha/) holds.
  • Checkout refs: the key leg checks out refs/heads/main (line 105) and the later legs use ${{ matrix.base }} (lines 583 and 1053). The test pins both.
  • Route jq and bash: the second if fails on an empty, short, uppercase, non-hex or newline-suffixed value, because \z is a strict end of string. A failed jq inside matrix=$(...) stops the step under bash -e. The test exercises each case and the no-handoff case.
  • Normalisation: the new .replace(...) in the repeated-steps test strips the differing ref comments and value, so the legs stay held equal otherwise.
  • Trust: the route jobs hold no secret. The ref they pass on is validated as 40 hex before it reaches actions/checkout.
  • Docs: the README paragraph matches the new behaviour.

No change here spends anything beyond prepaid credits or subscription usage, and nothing calls for a hold.

@github-actions

Copy link
Copy Markdown
Contributor

PR body contract — issue linkage

This PR body does not yet satisfy the issue-linkage contract:

  • Missing a native closing keyword (Closes/Fixes/Resolves #N). If this PR references an issue it must not close, put "Refs: #N" (or "Relates to: #N") on its own line. If it relates to no GitHub issue at all, state "No linked issue" (or "No related issue:") in the body instead.

Edit the body and this comment updates itself on the next run.

@github-actions github-actions Bot added the needs-issue-linkage Pull request body has no issue linkage line; set by the PR-contract check, advisory. label Oct 29, 2026
kyle-sexton and others added 4 commits October 29, 2026 11:45
…ferenced moves

Security review of the main-run review diff and leg chain:

- write-diff.py prints today's whole-file --no-renames patch unless it is
  over MAX_DIFF_BYTES. Only then do exact renames collapse, and a move
  whose new path (or its last directory and name) an added line names
  prints whole, repeated until no shown line names a collapsed move.
- A move of a symlink (120000) or submodule (160000) prints whole.
- The prompt says a rename-only entry's content is unseen and a change
  referencing a moved path is a finding.
- The route jobs run on !cancelled(), so one lane's failed leg does not
  drop the other lane's handoff, and every leg checks out github.sha, the
  main commit whose copy of the workflow runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s later legs

- write-diff.py prints whole, even over the size cap, a move that may run
  or load: an executable (100755), a path under a skills, agents, commands
  or hooks directory, or a file a tool reads by name (SKILL.md,
  hooks.json, plugin.json, marketplace.json, conftest.py, __init__.py,
  settings.json). A move is also shown when an added line names its bare
  name or its name without the extension.
- review-subscription and review-fallback run on !cancelled() and their
  route job's success, so one lane's failed leg no longer skips the other
  lane's next leg through the implicit success() on every ancestor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

claude-security-review: 1 inline finding(s) on 92811e048b04b7d3ed91e24dba80a3d491922a2f; the summary is in this lane's pull request comment.

claude-lane: claude-security-review

Comment thread .github/workflows/pr-review-main.yml Outdated
…n commit

Checking every leg out at github.sha let a re-run of an old run execute
that commit's review code (trusted actors, write-diff.py, the prompt,
lane_source), bypassing later tightenings. The key leg checks out the
main tip again and records the commit under the lane's own output; the
route job passes it on only as exactly 40 hex characters and fails
otherwise, and the later legs check out that commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kyle-sexton
kyle-sexton added this pull request to the merge queue Oct 29, 2026
Merged via the queue into main with commit 9b6151a Oct 29, 2026
40 checks passed
@kyle-sexton
kyle-sexton deleted the fix/pr-review-main-credential-chain branch October 29, 2026 16:58
@github-actions

Copy link
Copy Markdown
Contributor

Post-merge mutation testing

Killed 6, survived 1, timeout 0, no coverage 0, invalid 0, not run 0.
Covered-code score 86%; mutation score 86%.

File Killed Survived Timeout No coverage Invalid Not run
.github/scripts/pr-review-main/write-diff.py 6 1 0 0 0 0

Surviving mutants

Location Operator Disposition Cause
.github/scripts/pr-review-main/write-diff.py:110 ROR productive input-gap

The run's summary has the full report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-issue-linkage Pull request body has no issue linkage line; set by the PR-contract check, advisory.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant