Skip to content

Commit 71d14b2

Browse files
authored
ci: comment on issues auto-closed by a merged PR (merged != released) (#151)
## Problem When a PR with `Closes #123` merges to `main`, GitHub closes the issue immediately β€” but with bumpy the fix is only *merged*, not *published*. To anyone watching the issue, "closed" reads as "shipped," which is misleading until the next release actually goes out. ## Approach Rather than fight GitHub's auto-close (deferring it isn't possible; reopening spams notifications), just leave a clarifying comment on each auto-closed issue: > βœ… A fix has been merged to `main` in #142 and will ship in the next release. > > GitHub auto-closed this issue on merge, but the change isn't published yet β€” it'll go out with the next `bumpy` release. 🐸 ## How it works - Triggers on `pull_request_target: [closed]`, scoped to base branch `main`, gated on `merged == true`. - Uses the PR's GraphQL `closingIssuesReferences` to find exactly which issues were closed β€” resolves every `Closes/Fixes/Resolves` variant natively, no body regex. - One comment per closed issue. ## Safety - `pull_request_target` gives the `issues: write` token, but the workflow **never checks out or runs PR code** β€” it only queries linked issues and posts a comment. - The workflow file always runs from `main`, so a fork PR can't alter it. - Only the PR number (int) and repo slug reach the shell β€” injection-safe. ## Design notes - **Kept separate from `release.yaml` on purpose.** That workflow runs on the same merge (`push: [main]`), but a `push` event lacks the PR/closing-issue data, and its always-run `plan` job is deliberately zero-permission. This workflow is pure `gh` (no bun/build), so it's cheap to keep standalone. - **Toggle is file presence** β€” delete the workflow to disable. No config for v1. - **Activates after merge to `main`** β€” `pull_request_target` always uses the base-branch copy, so it won't run from this PR itself. - **Scoped to `main` only** β€” a `next` prerelease merge won't trigger it (the "next release" wording wouldn't fit). ## Deferred The release-time follow-up ("πŸŽ‰ Released in vX.Y.Z") is intentionally left out β€” doing it reliably means distinguishing issue refs from PR refs in the release body, which is better solved by having bumpy track it than by regex. Happy to tackle in a follow-up.
1 parent 4960ffc commit 71d14b2

2 files changed

Lines changed: 122 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# 🐸 Comment on issues auto-closed by a merged PR
2+
#
3+
# GitHub closes an issue the moment a PR that says `Closes #123` merges β€” but with
4+
# bumpy the fix isn't *released* yet, just merged to main. To a watcher, "closed"
5+
# reads as "shipped", which is misleading. This drops a clarifying comment so they
6+
# know the change lands in the next release.
7+
#
8+
# Runs on `pull_request_target` to get an issues:write token, but it NEVER checks out
9+
# or runs any PR code β€” it only asks GitHub which issues the PR closed and posts a
10+
# comment. The workflow file itself always comes from the base branch (main), so a
11+
# fork PR can't alter it, and no untrusted string is interpolated into a shell command.
12+
#
13+
# Toggle: this feature is on simply because this file exists. Delete it to turn off.
14+
name: Merge Issue Note
15+
16+
on:
17+
pull_request_target:
18+
types: [closed]
19+
branches: [main] # base branch β€” stable channel only (a `next` merge is a prerelease)
20+
21+
permissions:
22+
issues: write
23+
24+
jobs:
25+
note:
26+
if: github.event.pull_request.merged == true
27+
runs-on: ubuntu-latest
28+
steps:
29+
- name: Comment on auto-closed issues
30+
env:
31+
GH_TOKEN: ${{ github.token }}
32+
REPO: ${{ github.repository }}
33+
PR: ${{ github.event.pull_request.number }}
34+
run: |
35+
set -euo pipefail
36+
37+
# Ask GitHub which issues this PR closes. closingIssuesReferences resolves
38+
# every Closes/Fixes/Resolves variant for us β€” no brittle body parsing.
39+
issues=$(gh api graphql \
40+
-f query='
41+
query($owner: String!, $repo: String!, $pr: Int!) {
42+
repository(owner: $owner, name: $repo) {
43+
pullRequest(number: $pr) {
44+
closingIssuesReferences(first: 50) { nodes { number } }
45+
}
46+
}
47+
}' \
48+
-f owner="${REPO%/*}" \
49+
-f repo="${REPO#*/}" \
50+
-F pr="$PR" \
51+
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number')
52+
53+
if [ -z "$issues" ]; then
54+
echo "PR #$PR closed no linked issues β€” nothing to do."
55+
exit 0
56+
fi
57+
58+
for n in $issues; do
59+
echo "Commenting on issue #$n"
60+
gh issue comment "$n" --repo "$REPO" --body \
61+
"βœ… A fix has been merged to \`main\` in #${PR} and will ship in the next release.
62+
63+
GitHub auto-closed this issue on merge, but the change isn't published yet β€” it'll go out with the next \`bumpy\` release. 🐸"
64+
done

β€Ždocs/github-actions.mdβ€Ž

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,64 @@ jobs:
306306

307307
`bumpy ci release --auto-publish` collapses version + publish into a single run, skipping the Version Packages PR. This forfeits the preview/review gate on version bumps β€” every merge to main with a bump file ships immediately. It's also incompatible with the [split-job pattern](#release-workflow-recommended-split-jobs) above, since both paths run in one command. Prefer the default flow. See [the CLI reference](cli.md#bumpy-ci-release) if you still need it.
308308

309+
## Telling issues they're merged, not released (optional)
310+
311+
With bumpy, a fix merges to `main` well before it's published β€” the Version Packages PR sits in between. But when a PR says `Closes #123`, GitHub closes that issue the instant the PR merges, so anyone watching sees "closed" and reasonably assumes it shipped. It hasn't yet.
312+
313+
**This is optional** β€” set it up only if that gap bothers your issue reporters. Rather than fight GitHub's auto-close (you can't defer it, and reopening spams notifications), this leaves a clarifying comment on each auto-closed issue:
314+
315+
> βœ… A fix has been merged to `main` in #142 and will ship in the next release. GitHub auto-closed this issue on merge, but the change isn't published yet.
316+
317+
```yaml
318+
# .github/workflows/bumpy-issue-note.yml
319+
name: Issue Merge Note
320+
on:
321+
pull_request_target:
322+
types: [closed]
323+
branches: [main] # your stable release branch β€” the "next release" wording assumes it
324+
325+
permissions:
326+
issues: write
327+
328+
jobs:
329+
note:
330+
if: github.event.pull_request.merged == true
331+
runs-on: ubuntu-latest
332+
steps:
333+
- name: Comment on auto-closed issues
334+
env:
335+
GH_TOKEN: ${{ github.token }}
336+
REPO: ${{ github.repository }}
337+
PR: ${{ github.event.pull_request.number }}
338+
run: |
339+
set -euo pipefail
340+
# closingIssuesReferences resolves every Closes/Fixes/Resolves variant β€” no body parsing
341+
issues=$(gh api graphql \
342+
-f query='
343+
query($owner: String!, $repo: String!, $pr: Int!) {
344+
repository(owner: $owner, name: $repo) {
345+
pullRequest(number: $pr) {
346+
closingIssuesReferences(first: 50) { nodes { number } }
347+
}
348+
}
349+
}' \
350+
-f owner="${REPO%/*}" -f repo="${REPO#*/}" -F pr="$PR" \
351+
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number')
352+
[ -z "$issues" ] && { echo "PR #$PR closed no linked issues."; exit 0; }
353+
for n in $issues; do
354+
gh issue comment "$n" --repo "$REPO" --body \
355+
"βœ… A fix has been merged to \`main\` in #${PR} and will ship in the next release. GitHub auto-closed this issue on merge, but the change isn't published yet."
356+
done
357+
```
358+
359+
This needs no bumpy β€” it's pure `gh`, so it runs in seconds with no `bun install` or build. `pull_request_target` is what grants the `issues: write` token, but the job **never checks out or runs the PR's code** β€” it only reads the PR's linked issues and posts a comment. The only PR-controlled value that reaches the shell is the PR number (an integer), so there's nothing to inject.
360+
361+
> **It won't run until it's on your default branch.** Like any `pull_request_target` workflow, GitHub uses the copy on the default branch β€” so it doesn't fire for the PR that _adds_ it. It starts working once merged to `main`.
362+
363+
> **Scoped to your stable branch.** The `branches: [main]` filter is on the base branch. A merge to a [prerelease channel](prereleases.md) like `next` won't trigger it, since "the next release" would be a prerelease. Add channel branches to the filter if you want them covered.
364+
365+
> **Want a "now released" follow-up too?** You can close the loop with a second comment when the release actually publishes β€” your GitHub release job already fires a `release: published` event (that's what `BUMPY_GH_TOKEN` enables). A workflow on that event can post "πŸŽ‰ Released in `vX.Y.Z`" linking the release. Doing it reliably means telling issue references apart from PR references in the release body, so it's a bit more involved than this snippet β€” but the durable, meaningful link (the release itself) lives there.
366+
309367
## Advanced: per-package conditional builds
310368

311369
If you have one expensive package whose build you only want to run when that package itself is being released, use `ci plan`'s `packages` output to gate per-package steps:

0 commit comments

Comments
Β (0)