diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index ac948bfd..9374013b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,19 @@ ## Linear issue - + ## Risk tier + ## Risk tier - [ ] T0 ## Semver impact - [ ] fix """ +# Linear's closing magic words, from https://linear.app/docs/github. Restated here +# rather than imported from check_pr.py so the matrix is an independent assertion +# about what the checker must accept, not a tautology. +CLOSING_MAGIC_WORDS = ( + "close", "closes", "closed", "closing", + "fix", "fixes", "fixed", "fixing", + "resolve", "resolves", "resolved", "resolving", + "complete", "completes", "completed", "completing", + "implement", "implements", "implemented", "implementing", +) + # (name, mode, env, expected_exit) CASES = [ # --- linear-link: rejections --- @@ -67,10 +78,35 @@ def body_without(section_swap): ("linear: linear.app/changelog", "linear", {"PR_BODY": "see https://linear.app/changelog", "PR_BRANCH": "x"}, 1), ("linear: linear.app/not-an-issue", "linear", {"PR_BODY": "https://linear.app/not-an-issue", "PR_BRANCH": "x"}, 1), ("linear: unfilled template (example in comment)", "linear", {"PR_BODY": UNFILLED, "PR_BRANCH": "x"}, 1), - # --- linear-link: acceptances --- - ("linear: issue URL in body", "linear", {"PR_BODY": "https://linear.app/autonominetwork/issue/V2-719/foo"}, 0), + # A bare reference in the body does not link the PR in Linear (V2-1161). + ("linear: bare key in body only", "linear", {"PR_BODY": "V2-1161", "PR_BRANCH": "x"}, 1), + ("linear: bare issue URL in body only", "linear", {"PR_BODY": "https://linear.app/autonominetwork/issue/V2-719/foo", "PR_BRANCH": "x"}, 1), + ("linear: magic word without a key", "linear", {"PR_BODY": "Closes the gap", "PR_BRANCH": "x"}, 1), + ("linear: magic word on its own line from the key", "linear", {"PR_BODY": "Closes\n\nV2-1161", "PR_BRANCH": "x"}, 1), + # Linear's linking-only families attach the PR but do not drive the Merged + # transition, so they are not accepted as the closing form. + ("linear: 'part of' is linking-only", "linear", {"PR_BODY": "part of V2-1161", "PR_BRANCH": "x"}, 1), + ("linear: 'ref' is linking-only", "linear", {"PR_BODY": "ref V2-1161", "PR_BRANCH": "x"}, 1), + ("linear: 'towards' is linking-only", "linear", {"PR_BODY": "towards V2-1161", "PR_BRANCH": "x"}, 1), + ("linear: 'relates to' is linking-only", "linear", {"PR_BODY": "relates to V2-1161", "PR_BRANCH": "x"}, 1), + ("linear: magic word as a word prefix", "linear", {"PR_BODY": "prefix V2-1161", "PR_BRANCH": "x"}, 1), + ("linear: magic word as a word suffix", "linear", {"PR_BODY": "fixture V2-1161", "PR_BRANCH": "x"}, 1), + # --- linear-link: acceptances (Linear's full closing set, any tense) --- + ("linear: Closes + key in body", "linear", {"PR_BODY": "Closes V2-1161", "PR_BRANCH": "x"}, 0), + ("linear: lower-cased magic word", "linear", {"PR_BODY": "closes v2-1161", "PR_BRANCH": "x"}, 0), + ("linear: Closes + issue URL in body", "linear", {"PR_BODY": "Closes https://linear.app/autonominetwork/issue/V2-719/foo", "PR_BRANCH": "x"}, 0), + ("linear: closing form inside prose", "linear", {"PR_BODY": "This one closes V2-1161 at last.", "PR_BRANCH": "x"}, 0), + ("linear: 'linear issue' phrase", "linear", {"PR_BODY": "Linear issue V2-1161", "PR_BRANCH": "x"}, 0), ("linear: key in branch (lowercased)", "linear", {"PR_BRANCH": "chrisoneil/v2-720-ci-check"}, 0), ("linear: key in title", "linear", {"PR_TITLE": "AUTO-42 do the thing", "PR_BRANCH": "x"}, 0), +] + [ + # One case per closing magic word Linear documents, capitalised as an author + # would write it — e.g. "Fixed V2-1161" links and closes in Linear, so it must + # pass here too. + (f"linear: '{word}'", "linear", + {"PR_BODY": f"{word.capitalize()} V2-1161", "PR_BRANCH": "x"}, 0) + for word in CLOSING_MAGIC_WORDS +] + [ # --- pr-template: acceptances --- ("template: valid T0 body", "template", {"PR_BASE": "main", "PR_BODY": VALID_BODY}, 0), ("template: rc-* base is a no-op pass", "template", {"PR_BASE": "rc-2025.10", "PR_BODY": "anything"}, 0), @@ -86,6 +122,9 @@ def body_without(section_swap): ("- [x] T0 — docs / tooling / CI.", "- [x] T2 — behavioural."))}, 1), ("template: no tier checked", "template", {"PR_BASE": "main", "PR_BODY": body_without( ("- [x] T0 — docs / tooling / CI.", "- [ ] T0 — docs / tooling / CI."))}, 1), + ("template: bare key under '## Linear issue'", "template", {"PR_BASE": "main", "PR_BODY": body_without( + ("- Closes https://linear.app/autonominetwork/issue/V2-719/add-the-standard-pr-template", + "- V2-719"))}, 1), ("template: two tiers checked", "template", {"PR_BASE": "main", "PR_BODY": body_without( ("- [x] T0 — docs / tooling / CI.", "- [x] T0 a\n- [x] T2 b"))}, 1), ] diff --git a/CLAUDE.md b/CLAUDE.md index 5dcf95a6..a126dcf4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,8 +115,15 @@ description. - **Fill every field.** Leave nothing blank; if a value isn't determinable, ask before opening the PR. -- **Link the Linear issue** — an issue key like `V2-123` or a `linear.app` URL. - CI blocks PRs with no linked Linear issue. +- **Link the Linear issue with a closing magic word** — write `Closes V2-123` in + the `## Linear issue` section, one line per issue. Any of Linear's closing + words works, in any tense (`close` / `fix` / `resolve` / `complete` / + `implement`, plus their `-s`, `-d` and `-ing` forms), and the key may be a + `linear.app` issue URL. A bare `V2-123` does **not** link the PR at all, and + the linking-only words (`ref`, `part of`, `towards`, `relates to`) attach it + without driving the Merged transition — CI rejects both. An issue key in the + branch name or PR title also links, but write the closing form anyway; it is + what moves the issue to Merged when the PR lands on `main`. - **Check exactly one Risk tier box and exactly one Semver impact box.** Propose them from the change; a human confirms them at review. - An **ADR link is required for Tier 2/3**.