Skip to content

chore(#114): enforce single Closes-keyword per PR body#125

Merged
atlas-apex merged 2 commits into
devfrom
chore/GH-114-single-closes-per-pr
Apr 25, 2026
Merged

chore(#114): enforce single Closes-keyword per PR body#125
atlas-apex merged 2 commits into
devfrom
chore/GH-114-single-closes-per-pr

Conversation

@atlas-apex

Copy link
Copy Markdown
Collaborator

Summary

Caps distinct auto-closing references (close(s/d) / fix(es/ed) / resolve(s/d) followed by #N or owner/repo#N) at one per PR body. Closes the loophole where the title validator already limited the title to a single ticket reference but a body with Closes #1 Closes #2 Closes #3 would auto-close all three on merge, defeating the "one ticket per PR" rule.

  • Scans with fenced code blocks stripped, so closing keywords inside examples don't count.
  • Distinct counting: the same #N referenced twice (e.g. via Fixes and Closes to the same issue) is one.
  • Cross-repo refs (owner/repo#N) count normally.
  • Opt-in escape hatch: .pr.allow_multiple_closes: true disables for teams that batch rollbacks / dep bumps.
  • Per-PR bypass: <!-- multi-close: approved --> in the body prints a visible stderr WARN and lets that PR through. Grep-able trace.

Changed files:

  • .claude/hooks/validate-pr-create.sh — new close-count check appended after the required-sections check landed in [Chore] Require Testing section in PR body (extend validate-pr-create.sh) #113. Shares the same _lib-read-config.sh reader, same $BODY_CONTENT extraction path, same error-accumulator pattern.
  • .claude/hooks/tests/test_single_closes_per_pr.sh — 10 cases.
  • .claude/project-config.defaults.json — added pr.allow_multiple_closes: false and pr.multi_close_skip_marker.
  • docs/rule-audit.md — existing "One ticket at a time" row flipped from no to partial with the new footnote.

Testing

$ bash .claude/hooks/tests/test_single_closes_per_pr.sh
PASS [one Closes → pass]
PASS [no closing keyword → pass (cross-ref is ok)]
PASS [two distinct Closes → block]
PASS [three mixed keywords → block]
PASS [same number twice → pass (only one distinct)]
PASS [closing keyword inside code block → ignored]
PASS [multi-close skip marker bypasses]
PASS [cross-ref without keyword + one Closes → pass]
PASS [opt-in config disables the check]
PASS [cross-repo closing ref counted]
PASS: 10   FAIL: 0

This PR itself has exactly one closing keyword (Closes #114 at the bottom) — the now-live-on-branch check let it through.

What a cross-ref looks like

Closing refs (these count):

Closes #1
Fixes #1
Resolves me2resh/apexyard#1

Cross-refs (these DO NOT count — they're informational, not auto-close):

depends on #99
related: #47, #55
see me2resh/apexyard#42

The distinction is the GitHub closing keyword — anything without one is cross-reference and safe.

Scope — what this does NOT do

  • Does not retroactively rewrite merged PRs.
  • Does not block on "no closing keyword at all" — a PR without Closes #N is valid (the title still carries the ticket ref, and some PRs are QA-handed-off via the qa label flow without auto-close).
  • Does not check semantic uniqueness (two Closes pointing at issues that track the same underlying bug) — that's out of scope; the rule is about counting, not deduping conceptually.

Follow-ups

  • If teams see many <!-- multi-close: approved --> bypasses in the log, it's a signal that allow_multiple_closes: true might be the better config for their workflow.
  • The closing-keyword list is hardcoded (GitHub's fixed set). If GitHub adds new keywords, the hook needs updating — captured as a mental note rather than a separate ticket for now.

Glossary

Term Definition
Closing keyword GitHub's fixed set that auto-closes referenced issues on PR merge: close(s/d), fix(es/ed), resolve(s/d), case-insensitive.
Cross-reference A #N mention without a closing keyword — informational only, doesn't auto-close anything.
Umbrella PR A PR legitimately closing multiple tickets (rollback of N PRs, dependency bump touching N tracking tickets). Handled via opt-in config or per-PR skip marker.

Closes #114

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants