Skip to content

ci(claude-security-review): infra-failed review reports success and satisfies a required context, authorizing merge without a security pass #266

Description

@kyle-sexton

🤖 Agent-authored (autonomous babysit lane) while babysitting melodic-software/claude-code-plugins#1588 to merge.

Problem

claude-security-review.yml's review job reports success when the review could not execute at all. On a PR that IS in scope for the caller's paths filter, a rate-limited (or otherwise infra-failed) SDK call yields a green check. In claude-code-plugins that check is security-review / security-review — the sole required context on main — so the PR becomes merge-authorized by a check certifying a security pass that never ran.

This is a defect against the required check's own stated contract, not a request to change the advisory posture. ADR 0002's #509 addendum (claude-code-plugins/docs/adr/0002-default-on-ai-review-advisory-with-earned-promotion.md) defines the requirement as execution evidence:

Execution evidence — proof that the security pass RAN on every PR — is promoted to a required status check on the protected base, so a PR cannot merge without the security workflow having reported. […] The required check proves the pass ran; it does not gate on the verdict.

An infra failure is exactly the case where the pass did not run. The check asserts execution evidence that does not exist.

The three states the check currently conflates

PR state Conclusion Correct?
In scope, review ran, verdict produced success Yes
Not in scope (paths miss → job-level skip) skipped → read as success Yes — ADR 0002's deliberate design
In scope, review could not run (429/401/crash) success No — this is the defect

Row 2 is load-bearing and must not regress: the caller deliberately has no workflow-level paths filter because that would wedge prose PRs on a permanently Pending required check. Row 3 is the hole.

Empirical evidence (claude-code-plugins#1588)

  • Changed file plugins/disk-hygiene/skills/clean/scripts/test_hygiene.py matches the caller's plugins/*/skills/** entry, so the PR was in scope. security-review / changes agreed and the review job proceeded.
  • Original run 30217744377: security-review / security-review concluded pass in 25s with {"subtype":"success","is_error":true,"num_turns":1,"duration_ms":480,"total_cost_usd":0,"api_error_status":429,"class":"rate-limit"}. No review was performed.
  • gh pr checks 1588 showed every check green. babysit_merge.py reported ready: true, blockers: [], and requiredContexts: ["security-review / security-review"] — the rate-limited job was the only thing standing between that PR and main.
  • I re-ran the workflow manually before merging. The genuine run took 3m17s (vs 25s) and returned a real verdict ("No exploitable security issues found"). Had the lane merged on the first green — which it was entitled to do — the PR would have merged with zero effective security review.

The same 429 hit claude-review in the same window; that lane is advisory-only and not a required context, so it carries no merge consequence. The defect is specific to the security lane because it is the required one.

Why neutral and skipped cannot be the fix

Verified against GitHub's official docs (about status checks): success, neutral, and skipped all satisfy a required status check and permit merging. Only failure, cancelled, and action_required block. The docs are explicit:

A job that is skipped will report its status as "Success". It will not prevent a pull request from merging, even if it is a required check.

So "report a distinct non-success conclusion" does not work as an approach — neutral is indistinguishable from success to the ruleset, and skipped is already consumed to mean not-applicable. Closing this requires a conclusion in the blocking set, or a separate job that carries the requirement.

Recommendation

Option A — RECOMMENDED. Conclude failure in the review job when the PR was in scope and no verdict was produced. Single-repo change, no ruleset change, keeps the required context name stable. The infra-failure class token already lands in the annotation and PR comment as of e295107, so the signal needed to distinguish "in scope but did not run" from "correctly skipped" already exists — this is a conclusion-mapping change, not new detection.

Option B — Split execution evidence from the review lane. Leave the review job's conclusion exactly as it is today and add a small assertion job that concludes failure when the head SHA has no review verdict, then move the required context to it in github-iac. Architecturally cleaner (it makes the execution certificate a first-class artifact rather than a side effect of the review job's exit) and it extends the existing two-job changes + security-review shape naturally. Costs a cross-repo change: ci-workflows and the github-iac ruleset.

I lean A on cost and reversibility; B is the better long-term shape if the execution-evidence contract is expected to grow more consumers.

Decision needed from a human: tension with #228

#228 records an explicit, operator-visible non-goal: "failing the gate on infra errors (current pass-through behavior is correct)", on the rationale that "an infra failure is not a code-quality signal."

I believe that rationale is correct and this proposal does not contradict it — a red execution-evidence check is not a code-quality claim, it is the absence of one, and ADR 0002 already separates the two. But the literal words of that non-goal would forbid Option A, and #228 carries needs-human. Flagging rather than assuming: someone should confirm whether that non-goal was scoped to the verdict semantics (my reading) or to the check's conclusion in all cases (in which case only Option B is admissible).

Relationship to existing issues

Distinct defect class, cross-referencing both:

#228's proposal 2 (error-class surfacing) has landed and is what makes this cheaply fixable; this issue consumes that work rather than duplicating it.

Acceptance criteria

  • A PR in scope for the caller's paths filter whose review SDK call fails with 429/401/402/403 does not produce a required-check state that permits merging.
  • A PR with no in-scope paths still yields a name-stable skipped check the ruleset reads as success (no prose-PR wedge — regression-test this explicitly).
  • A PR whose review executes and returns a verdict still merges on an advisory verdict, including a verdict with findings. The verdict stays advisory per ADR 0002; only execution is gated.
  • The chosen behavior is verified against a replay of run 30217744377's shape, not only reasoned about.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    automatedOpened by automation.priority: highSignificant impact, or blocks an imminent release; staff this cycle.status: readyTriaged, unblocked, and fully specified; eligible to pick up.

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions