Skip to content

Gate the PR closing keyword in CI, conditioned on branch shape #263

Description

@FrankRay78

Problem

#248 made /raise-pr derive and verify a Closes #<N> keyword. That rule is prose read by a model, and nothing enforces it. The same step 6 that mandates the keyword also says "Do not follow a fixed template. Decide which sections earn their place" — so a future model, or a future edit to raise-pr.md, can silently drop back to the pre-#248 state. #248's own evidence table shows that regression is invisible until someone audits eight PRs by hand.

Constitution Principle I's carve-out does not end at recording RED/GREEN once:

MUST still make the check repeatable. Where the tool can run in CI, add it there, so the invariant is gated rather than verified once.

Why this was deferred out of #248

#248 argued no proportionate gate existed, on the grounds that "every PR body carries a closing keyword" would false-positive on legitimately issue-less PRs (dependabot bumps, chore/normalise-cs-line-endings). That reasoning was wrong, and is corrected here so it is not cited again: it defeats only the unconditional gate. A gate conditioned on branch shape has no such problem, because the condition is the shape itself rather than a maintained exception list.

Measured over the last 25 merged PRs, the separation is exact:

  • All 6 branches matching ^feature/[0-9]+- carry a closing keyword — so the gate never fails one.
  • Every non-matching branch is exempt by construction, including dependabot/* and chore/normalise-cs-line-endings.
  • chore/234-format-ship-cadence carries a keyword without matching, which is a false negative — the safe direction.

Zero false positives, no allowlist to maintain.

Suggested shape

A step in the existing .github/workflows/dotnet.yml (already triggered on pull_request), roughly 20 lines against the ~20 lines of prose it guards — a ~1:1 ratio, so feedback_guard_outgrows_the_fix does not fire:

  • Parse ^feature/([0-9]+)- from github.head_ref; exit 0 when it does not match.
  • Reuse the rule raise-pr.md step 6 already establishes: gh issue view <N> --json state,url and exit 0 unless the url is exactly this repo's /issues/<N> path — that rejects a PR at that number and an issue in another repo.
  • Require a closing keyword for #<N> in the PR body, with a documented No-close: #<N> — <reason> escape for a branch that only partly implements its issue.
  • Add types: [opened, edited, reopened, synchronize] to the trigger, or re-fetch the body with gh pr view, so a later body edit is re-checked.

Open decision

The No-close: escape hatch is new repo policy, not a mechanical detail — it decides how a partial-implementation branch opts out of a hard gate. Worth settling before implementing.

Deliberately not a .claude/hooks/ gate: the *.tests.sh matrix that hooks/README.md mandates would put this at ~250-400 lines (~15:1), and that form should be refused. Hooks also fail open and can lock out the tooling that would fix them; a CI step fails loudly with no lockout risk.

Related

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions