Skip to content

refactor(ci): give the review skip-actors list one owner - #3466

Merged
kyle-sexton merged 3 commits into
mainfrom
claude/coupling-skip-actors-ssot
Sep 1, 2026
Merged

refactor(ci): give the review skip-actors list one owner#3466
kyle-sexton merged 3 commits into
mainfrom
claude/coupling-skip-actors-ssot

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

No linked issue

This PR cannot make its own security-review check green, by design — a human needs to merge it. The reusable security-review workflow requires the caller's workflow file to match the default branch's copy. This PR edits that caller, so the action self-skips and the required check fails. The job log states it plainly: "merging the caller-workflow change is what clears this shape — a re-run cannot." I split this out of #3464 rather than bypass that certification.

Summary

The review skip-actors bot list was restated at five sites and had already drifted. Route-lane finding from the /coupling:reduce pass merged in #3457, split out of #3464 for the reason above.

scripts/verify-security-review-evidence.sh's default was missing cursor[bot], which 2b4d8abf added to both workflow lines only — so the two guards silently disagreed about that actor on any invocation that does not set the env.

Fix

One owned data file, .github/claude-skip-actors, and one fail-closed parser, scripts/read-skip-actors.sh, following the pattern .github/claude-security-paths already establishes. Both workflows and both verify-*.sh defaults now resolve through it.

The reusable's with: skip-actors cannot read a file, so a prep job reads the list and passes it across needs, mirroring the reusable's own paths-file discipline.

Verification

Parser behavior, every shape — it can never succeed with a partial or empty list:

empty file                rc=2  stdout=[]   "names no actors; an empty exception must be an explicit consumer decision"
comments/blanks only      rc=2  stdout=[]
missing file              rc=2  stdout=[]
embedded space or comma   rc=2  stdout=[]   "malformed entry"
comments + entries        rc=0  [dependabot[bot],claude[bot]]
trailing whitespace, CRLF rc=0  (trimmed / CR stripped)

Suite 8/8. Mutation-tested: removing the empty-set check → 1 failure; removing the malformed-entry check → 2 failures.

Fail direction traced at every consumer, which is what matters here. An independent adversarial verifier confirmed that a malformed or empty list can never cause a security review to be skipped: the security lane's plain assignment aborts the prep job under bash -e; security-review then reports skipped; and security-review-evidence still runs under always() and exits 1 on its first step (needs.skip-actors.result != 'success') — correctly ordered before the guard, which would otherwise exit 0 on LANE_RESULT=skipped. Even if an empty list did reach a guard, ",," cannot match *",actor,"*, so the review runs rather than being skipped.

Behavior fix shown end to end: a cursor[bot] actor under the old drifted default → guard exit 1; under the file-sourced default → skip-listed, exit 0.

Two verifier findings were fixed before this PR was opened:

  • claude-review.yml's read step used echo "list=$(...)", which returns echo's status and so did not fail the step on a parser error. It is now a plain assignment, matching the security caller's fail-closed form.
  • The prep job's git show ... 2>/dev/null swallowed every error, silently falling back to the head copy on a failed base fetch. An unresolvable base ref is now a hard error; an absent file at a resolvable base is an explicit notice. The job comment now states the real property rather than a stronger one: reading the base copy protects the list's content, but the workflow, parser, and checkout-with-base composite are all head-controlled on pull_request — not a regression (on main the literal lived in the head-controlled workflow file), and bounded by the reusable's allowed_bots.

Gates: actionlint on both workflows, shellcheck, check-shell-portability.sh, zizmor (0 findings), typos, editorconfig-checker, runner-policy, affected-tests.sh --run ("All 3 selected suites passed") — all pass.

Related

Split from #3464. Follow-up to #3457.


Generated by Claude Code

claude added 2 commits August 28, 2026 07:36
The literal skip-actors list was restated at five sites across both
review workflows and both evidence guards, and the restatement drifted:
2b4d8ab added cursor[bot] to both workflow lines and left
verify-security-review-evidence.sh's default behind, so the guard judged
cursor[bot] PRs in scope while the lane skipped them.

.github/claude-skip-actors now carries the one statement of ADR 0002's
skip-actor exception (same repo-owned-data shape as
.github/claude-skip-actors' sibling .github/claude-security-paths), and
scripts/read-skip-actors.sh owns the one parse, fail-closed on a
missing file, an empty active set, or a malformed entry. The
claude-security-review caller reads the PR's BASE copy in a prep job
(mirroring the reusable's paths-file discipline) and hands it to the
lane's skip-actors input across needs, with the evidence job failing
closed when that read did not succeed; the claude-review caller reads it
in a step; both evidence guards default from it when SKIP_ACTORS is not
in the environment.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
Two adversarial-review findings on the skip-actors single-sourcing:

- claude-review.yml read the list inside echo's argument, so under
  bash -e a parser failure returned echo's status, emitted `list=`, and
  left the step green — red arrived only because the guard's own default
  re-read the file. The read is now a plain assignment first, matching
  the security caller's fail-closed form.
- claude-security-review.yml's prep job swallowed every git error behind
  one 2>/dev/null, so a failed base fetch silently fell back to the head
  copy. Each branch is now explicit: an unresolvable base ref hard-fails
  the step; only the list file absent at a resolvable base (the adopting
  PR's pre-adoption base) falls back to the head copy, with a ::notice.
  The job comment now states the real property — base-reading protects
  the list's CONTENT, while the workflow, parser, and composite remain
  head-controlled on pull_request, bounded by the reusable's
  allowed_bots — instead of implying full head-tamper protection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QkMx972GYEQ3vCb98pLw8m
@kyle-sexton
kyle-sexton merged commit 649f07c into main Sep 1, 2026
60 of 61 checks passed
@kyle-sexton
kyle-sexton deleted the claude/coupling-skip-actors-ssot branch September 1, 2026 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants