feat(claude-security-review): declare the lane's execution verdict as workflow outputs - #461
Conversation
… workflow outputs The lane already computes an authoritative verdict — the claude-lane-outcome composite classifies every attempt — but it never left the job. With no workflow_call outputs and no job-level outputs, a caller that must decide whether a review actually happened had only the job log to read, and a log is not a contract: claude-code-plugins#2517 grepped it for the phrases naming a validation skip and matched the inline github-script source that mentions them, reddening the pull requests the guard exists to approve. Surface relevant, review-ran, review-failed, and failure-class instead. review-detail stays unsurfaced — it is prose shaped for a human reading a marker comment, and branching on it is the log-grepping this replaces. Purely additive: no input, job name, check name, or permission changes, so consumers on the current pin are unaffected until they re-pin. Closes #460 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeEvWwrK8KdQTFb5n9CrUP
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
|
Claude finished @kyle-sexton's task in 3m 20s —— View job Code reviewTodo list
(Note: the SummaryTraced the new
One finding posted inline: .github/workflows/claude-security-review.yml#L459-464 — the new "DECLARED EVIDENCE SURFACE" comment enumerates "the four legitimate no-verdict paths" (out of scope, fork PR, skip-listed actor, kill-switch), but the job's pre-existing Everything else — the wiring, the job/composite name agreement, the empty-on-skip semantics, and the deliberate omission of Note: I was unable to execute |
A GitHub expression in a plain string trips noTemplateCurlyInString; the repository's convention is an escaped template literal. Formatting is biome's own output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeEvWwrK8KdQTFb5n9CrUP
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 43deeac351
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
Claude finished @kyle-sexton's task in 1m 33s —— View job Security Review Complete ✅Todo list
SummaryThis is a pure
I wasn't able to execute FindingsNone. This is a well-scoped, additive change with no security-relevant issues. The extensive in-file comments correctly document the no-verdict/empty-output contract, and the new test pins the exact wiring so a future rename in the composite or a re-ordering in the workflow can't silently start emptying these outputs. |
|
Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
|
Last security-reviewed head: |
… review Two review findings, both on the new block's prose, both real. The `relevant` output is not set on every non-cancelled run. The relevance step hard-errors on a rejected `!` / `?` / `+` pattern, which leaves the output unset while the lane fails OPEN and reviews anyway — so empty is a third state, and a consumer reading it as 'false' would wave a reviewed in-scope run through as out of scope. Documented as such, and pinned by a test. The skip enumeration undercounted: the job's `if:` also excludes a non-dependabot bot actor whose PR author is not OWNER/MEMBER/COLLABORATOR, a built-in gate distinct from the caller-supplied `skip-actors` list. Named, and the paragraph no longer reads as an exhaustive tally. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeEvWwrK8KdQTFb5n9CrUP
|
Claude finished @kyle-sexton's task in 1m 16s —— View job Security Review Complete ✅Todo list
SummaryThe last security-reviewed head was I traced both findings raised earlier in this thread against the current tree and both are now resolved:
No new inputs, secrets, permissions, or trust-boundary-relevant code paths were introduced by this commit — it's a comment/description correction plus a test that pins it. I found no security-relevant issues. I was unable to execute FindingsNone. |
…its log (#2548) Closes #2541 ## Summary The security-evidence guard inferred a verdict the lane had already computed, through two channels that were wrong in the same way. It downloaded the lane's job log and grepped it. The lane's `Report review outcome` step is an inline `github-script` whose SOURCE is echoed into that same log and contains the skip phrases as string literals, so the grep matched every successful in-scope pull request and reddened exactly the ones the guard exists to approve (#2517). Anchoring to `##[warning]`/`##[error]` lines removed the false positive but left the guard coupled to log text nothing upstream pins. It also re-derived the PR's security scope in its own Python `fnmatch` implementation while the lane matches with `git check-ignore` — two matchers that disagree on exactly the patterns that distinguish them, and where the `set -e` scope-verdict defect lived. ## Fix Both channels are replaced by the lane's declared outputs (melodic-software/ci-workflows#461), passed in from `needs.security-review`: | lane says | guard does | | --- | --- | | job `skipped` / `cancelled` | not applicable | | job not `success` | defer to the job's own red | | `relevant: false` | out of scope, not applicable | | `review-ran: true` | evidence OK | | `review-ran: false`, `review-failed: true` | defer — the lane rules this GREEN on purpose so a provider outage cannot lock every merge; the guard says loudly that nothing was reviewed and does not overturn it | | `review-ran: false`, `review-failed: false` | FAIL — the action skipped itself; merging the caller change clears it, a re-run cannot | | no verdict at all | FAIL, unless the live head has moved — a run retired as superseded is the one legitimate cause, and it is identified by comparing the live head against the event head rather than assumed | | `LANE_RESULT` empty | FAIL — the guard is not wired to the lane, so it can determine nothing | The guard is now coupled to the pin: it fails closed when the outputs are absent at an unmoved head, so re-pinning BACKWARDS past the release carrying them turns it red rather than quietly blind. The caller's pin is bumped in this PR for exactly that reason. `actions: read` is gone with the log read; `pull-requests: read` covers the one remaining API call. ## Verification - `scripts/verify-security-review-evidence.sh.test.sh` rewritten to EXECUTE the guard against environment fixtures in an `env -i` child shell, instead of re-implementing its regexes inside the harness and asserting on the copies — the shape that let the tests agree with a guard that was failing every pull request. 20 cases, all pass locally, covering every row of the table above plus two static checks (no log read, no second paths matcher). - `shellcheck --rcfile .shellcheckrc` clean on both scripts, including the deliberately-enabled SC2310: the absent-verdict classifier reports on stdout with exit reserved for faults, the same verdict/status split the scope check needed. - The tests now run in CI, in the guard's own job, ahead of the guard. They ran nowhere before. ## Related - #2517 — the log-grep false positive this removes the possibility of - #2337 — the false-pass class the guard exists to catch - melodic-software/ci-workflows#461 — the declared outputs this consumes --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

Closes #460
Summary
The security lane already computes an authoritative verdict on every run — the
claude-lane-outcomecomposite classifies the attempt — but the verdict never left the job.on.workflow_calldeclared nooutputs:and neither did thesecurity-reviewjob, so a caller that must decide whether a review actually happened had only the job log to read.A log is not a contract, and
melodic-software/claude-code-plugins#2517is what that costs: its evidence guard grepped the log for the phrases naming a workflow-validation skip and matched the inlinegithub-scriptSOURCE that mentions them as string literals, reddening exactly the in-scope pull requests the guard exists to approve.Fix
on.workflow_call.outputsnow declares four values, and thesecurity-reviewjob forwards three of them from thereview-outcomestep:relevant— from thechangesjob, so a consumer can tell an out-of-scope skip from a no-verdict skipreview-ran,review-failed,failure-class— from the composite that owns the classificationEvery output is EMPTY when
security-reviewdoes not run, and the declaration says so in place: a consumer must read empty as "no verdict", never as a pass. The four legitimate no-verdict paths (out of scope, fork PR, skip-listed actor, either kill-switch) plus a superseded head all land there.review-detailis deliberately not surfaced — it is a free-text projection shaped for a human reading a marker comment, and a consumer branching on it would be back to parsing prose.The change is additive. No input, job name, check name, or permission changes, so every consumer on the current pin is unaffected until it re-pins.
Verification
.github/scripts/claude-security-review-declared-outputs.test.cjs(6 cases) pins the declared names, the wiring of each output to the job that computes it, the job-level forwarding, the agreement between these names andclaude-lane-outcome's own declared outputs (so a rename cannot silently empty them), and thatreview-detailstays unsurfaced.node --test .github/scripts/*.test.cjs— 700 pass, 0 fail.actionlintclean on the edited workflow (the threeproperty ... is not definedfindings raised against the first commit's intermediate state were the job-leveloutputs:block not yet added; the final tree is clean).Related