Skip to content

Commit 3a615ea

Browse files
committed
ci(issues): a PR from a <kind>/<n> branch must close #<n> and nothing else
1 parent 74d993f commit 3a615ea

3 files changed

Lines changed: 36 additions & 3 deletions

File tree

.github/workflows/issue-state.yml

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ name: Issue state
55
# - task:draft becomes task:ready when approved, specified, and unblocked;
66
# - a reporter's reply moves bug:needs-info back to bug:triage;
77
# - a feature whose last sub-issue closed gets a completion-conditions note;
8-
# - bug:needs-info with no activity for two weeks closes as not planned.
8+
# - bug:needs-info with no activity for two weeks closes as not planned;
9+
# - a PR from a <kind>/<n> branch must close issue <n> and no other.
910
# Actions taken with the workflow token do not trigger this workflow again,
1011
# so every job removes the label it replaces itself.
1112

@@ -14,14 +15,16 @@ on:
1415
types: [labeled, edited, closed]
1516
issue_comment:
1617
types: [created]
18+
pull_request:
19+
types: [opened, edited, synchronize]
1720
schedule:
1821
- cron: "17 6 * * *"
1922
workflow_dispatch:
2023

2124
permissions: {}
2225

2326
concurrency:
24-
group: issue-state-${{ github.event.issue.number || 'schedule' }}
27+
group: issue-state-${{ github.event.issue.number || github.event.pull_request.number || 'schedule' }}
2528
cancel-in-progress: false
2629

2730
env:
@@ -195,3 +198,30 @@ jobs:
195198
});
196199
core.info(`#${issue.number}: closed as not planned (stale bug:needs-info)`);
197200
}
201+
202+
branch-matches-issue:
203+
name: Branch name closes its issue
204+
if: github.event_name == 'pull_request'
205+
runs-on: ubuntu-24.04
206+
timeout-minutes: 5
207+
steps:
208+
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
209+
with:
210+
script: |
211+
const pr = context.payload.pull_request;
212+
const m = pr.head.ref.match(/^(bug|feature|task)\/(\d+)$/);
213+
if (!m) {
214+
core.info(`${pr.head.ref} is not a <kind>/<n> branch; nothing to check`);
215+
return;
216+
}
217+
const expected = Number(m[2]);
218+
const closes = [...(pr.body ?? "").matchAll(/\b(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved)\s+#(\d+)/gi)]
219+
.map((x) => Number(x[1]));
220+
if (!closes.includes(expected)) {
221+
core.setFailed(`Branch ${pr.head.ref} must close #${expected}: add "Closes #${expected}" to the PR body.`);
222+
return;
223+
}
224+
const others = closes.filter((n) => n !== expected);
225+
if (others.length) {
226+
core.setFailed(`Branch ${pr.head.ref} closes #${expected} but also ${others.map((n) => `#${n}`).join(", ")}; one branch closes one issue.`);
227+
}

docs/DELIVERY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ that are mechanical: adding a state label removes the previous one, so every
109109
transition is a single add; `task:draft` becomes `task:ready` when approved,
110110
specified and unblocked; a reporter's reply moves `bug:needs-info` back to
111111
`bug:triage`; a silent `bug:needs-info` closes after two weeks; a feature
112-
whose last sub-issue closed gets the completion-conditions note. The repo's
112+
whose last sub-issue closed gets the completion-conditions note; a PR from a
113+
`<kind>/<n>` branch must close `#<n>` and nothing else. The repo's
113114
skills — `spec-session`, `triage-bug`, `deliver` — handle the transitions an
114115
agent makes as part of its own procedure.
115116

docs/agent-rules/delivery.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ rules mechanically, so nobody has to remember them:
170170
comment does not reopen it automatically, the maintainer does.
171171
- When the last sub-issue of a `feature:planned` issue closes, the workflow
172172
comments that completion conditions are due.
173+
- A pull request from a `<kind>/<n>` branch fails its check unless its body
174+
closes `#<n>` and closes nothing else.
173175

174176
The judgment calls stay manual by design: `bug:new``bug:triage`,
175177
`bug:triage``bug:ready`, `feature:spec``feature:ready`, and the

0 commit comments

Comments
 (0)