Skip to content

Commit 99aaace

Browse files
KyleAMathewsclaude
andauthored
feat: add automated commenting on linked issues when releases are published (#323)
Enhances the comment-on-release action to automatically notify both PRs and their linked issues when changes are released. Key changes: - Query GitHub GraphQL API to find issues closed/fixed by each PR - Post release notifications on linked issues with package versions - Add duplicate comment detection to prevent spam - Add explicit `issues: write` permission to release workflow This matches the functionality from electric-sql/electric#3521, ensuring both contributors and issue reporters are notified when their work ships. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2ea4a0c commit 99aaace

File tree

4 files changed

+191
-12
lines changed

4 files changed

+191
-12
lines changed

.github/comment-on-release/README.md

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Comment on Release Action
22

3-
A reusable GitHub Action that automatically comments on PRs when they are included in a release.
3+
A reusable GitHub Action that automatically comments on PRs and linked issues when they are included in a release.
44

55
## What It Does
66

@@ -9,8 +9,12 @@ When packages are published via Changesets:
99
1. Parses each published package's CHANGELOG to find PR numbers in the latest version
1010
2. Groups PRs by number (handling cases where one PR affects multiple packages)
1111
3. Posts a comment on each PR with release info and CHANGELOG links
12+
4. Finds issues that each PR closes/fixes using GitHub's GraphQL API
13+
5. Posts comments on linked issues notifying them of the release
1214

13-
## Example Comment
15+
## Example Comments
16+
17+
### On a PR:
1418

1519
```
1620
🎉 This PR has been released!
@@ -21,6 +25,16 @@ When packages are published via Changesets:
2125
Thank you for your contribution!
2226
```
2327

28+
### On a linked issue:
29+
30+
```
31+
🎉 The PR fixing this issue (#123) has been released!
32+
33+
- [@tanstack/query-core@5.0.0](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md#500)
34+
35+
Thank you for reporting!
36+
```
37+
2438
## Usage
2539

2640
Add this step to your `.github/workflows/release.yml` file after the `changesets/action` step:
@@ -49,7 +63,7 @@ Add this step to your `.github/workflows/release.yml` file after the `changesets
4963
5064
- Must be using [Changesets](https://github.com/changesets/changesets) for releases
5165
- CHANGELOGs must include PR links in the format: `[#123](https://github.com/org/repo/pull/123)`
52-
- Requires `pull-requests: write` permission in the workflow
66+
- Requires `pull-requests: write` and `issues: write` permissions in the workflow
5367
- The `gh` CLI must be available (automatically available in GitHub Actions)
5468

5569
## Inputs
@@ -67,17 +81,27 @@ The action:
6781
3. Extracts PR numbers from the latest version section using regex
6882
4. Groups all PRs and tracks which packages they contributed to
6983
5. Posts a single comment per PR listing all packages it was released in
70-
6. Uses the `gh` CLI to post comments via the GitHub API
84+
6. For each PR, queries GitHub's GraphQL API to find linked issues (via `closes #N` or `fixes #N` keywords)
85+
7. Groups issues and tracks which PRs fixed them
86+
8. Posts comments on linked issues notifying them of the release
87+
9. Checks for duplicate comments to avoid spamming
88+
10. Uses the `gh` CLI to post comments via the GitHub API
7189

7290
## Troubleshooting
7391

7492
**No comments are posted:**
7593

7694
- Verify your CHANGELOGs have PR links in the correct format
7795
- Check that `steps.changesets.outputs.published` is `true`
78-
- Ensure the workflow has `pull-requests: write` permission
96+
- Ensure the workflow has `pull-requests: write` and `issues: write` permissions
7997

8098
**Script fails to find CHANGELOGs:**
8199

82100
- The script expects packages at `packages/{package-name}/CHANGELOG.md`
83101
- Package name should match after removing the scope (e.g., `@tanstack/query-core` → `query-core`)
102+
103+
**Issues aren't being commented on:**
104+
105+
- Verify that PRs use GitHub's closing keywords (`closes #N`, `fixes #N`, `resolves #N`, etc.) in the PR description
106+
- Check that the linked issues exist and are accessible
107+
- Ensure the `issues: write` permission is granted in the workflow

.github/comment-on-release/action.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
name: Comment on PRs about release
2-
description: Automatically comments on PRs when they are included in a release
1+
name: Comment on PRs and issues about release
2+
description: Automatically comments on PRs and linked issues when they are included in a release
33
inputs:
44
published-packages:
55
description: 'JSON string of published packages from changesets/action'
66
required: true
77
runs:
88
using: composite
99
steps:
10-
- name: Comment on PRs
10+
- name: Comment on PRs and issues
1111
shell: bash
1212
run: node ${{ github.action_path }}/comment-on-release.ts
1313
env:

.github/comment-on-release/comment-on-release.ts

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ interface PRInfo {
1414
packages: Array<{ name: string; pkgPath: string; version: string }>
1515
}
1616

17+
interface IssueInfo {
18+
number: number
19+
prs: Set<number>
20+
packages: Array<{ name: string; pkgPath: string; version: string }>
21+
}
22+
1723
/**
1824
* Parse CHANGELOG.md to extract PR numbers from the latest version entry
1925
*/
@@ -99,18 +105,86 @@ function groupPRsByNumber(
99105
return prMap
100106
}
101107

108+
/**
109+
* Check if we've already commented on a PR/issue to avoid duplicates
110+
*/
111+
function hasExistingComment(number: number, type: 'pr' | 'issue'): boolean {
112+
try {
113+
const result = execSync(
114+
`gh api repos/\${GITHUB_REPOSITORY}/issues/${number}/comments --jq '[.[] | select(.body | contains("has been released!"))] | length'`,
115+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
116+
)
117+
const count = parseInt(result.trim(), 10)
118+
return count > 0
119+
} catch (error) {
120+
console.warn(
121+
`Warning: Could not check existing comments for ${type} #${number}`,
122+
)
123+
return false
124+
}
125+
}
126+
127+
/**
128+
* Find issues that a PR closes/fixes using GitHub's GraphQL API
129+
*/
130+
function findLinkedIssues(prNumber: number, repository: string): Array<number> {
131+
const [owner, repo] = repository.split('/')
132+
const query = `
133+
query($owner: String!, $repo: String!, $pr: Int!) {
134+
repository(owner: $owner, name: $repo) {
135+
pullRequest(number: $pr) {
136+
closingIssuesReferences(first: 10) {
137+
nodes {
138+
number
139+
}
140+
}
141+
}
142+
}
143+
}
144+
`
145+
146+
try {
147+
const result = execSync(
148+
`gh api graphql -f query='${query}' -F owner='${owner}' -F repo='${repo}' -F pr=${prNumber} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`,
149+
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
150+
)
151+
152+
const issueNumbers = result
153+
.trim()
154+
.split('\n')
155+
.filter((line) => line)
156+
.map((line) => parseInt(line, 10))
157+
158+
if (issueNumbers.length > 0) {
159+
console.log(
160+
` PR #${prNumber} links to issues: ${issueNumbers.join(', ')}`,
161+
)
162+
}
163+
164+
return issueNumbers
165+
} catch (error) {
166+
return []
167+
}
168+
}
169+
102170
/**
103171
* Post a comment on a GitHub PR using gh CLI
104172
*/
105173
async function commentOnPR(pr: PRInfo, repository: string): Promise<void> {
106174
const { number, packages } = pr
107175

176+
// Check for duplicate comments
177+
if (hasExistingComment(number, 'pr')) {
178+
console.log(`↷ Already commented on PR #${number}, skipping`)
179+
return
180+
}
181+
108182
// Build the comment body
109183
let comment = `🎉 This PR has been released!\n\n`
110184

111185
for (const pkg of packages) {
112186
// Link to the package's changelog and version anchor
113-
const changelogUrl = `https://github.com/${repository}/blob/main/${pkg.pkgPath}/CHANGELOG.md#${pkg.version.replaceAll('.', '')}`
187+
const changelogUrl = `https://github.com/${repository}/blob/main/${pkg.pkgPath}/CHANGELOG.md#${pkg.version.replace(/\./g, '')}`
114188
comment += `- [${pkg.name}@${pkg.version}](${changelogUrl})\n`
115189
}
116190

@@ -127,6 +201,49 @@ async function commentOnPR(pr: PRInfo, repository: string): Promise<void> {
127201
}
128202
}
129203

204+
/**
205+
* Post a comment on a GitHub issue using gh CLI
206+
*/
207+
async function commentOnIssue(
208+
issue: IssueInfo,
209+
repository: string,
210+
): Promise<void> {
211+
const { number, prs, packages } = issue
212+
213+
// Check for duplicate comments
214+
if (hasExistingComment(number, 'issue')) {
215+
console.log(`↷ Already commented on issue #${number}, skipping`)
216+
return
217+
}
218+
219+
const prLinks = Array.from(prs)
220+
.map((pr) => `#${pr}`)
221+
.join(', ')
222+
const prWord = prs.size === 1 ? 'PR' : 'PRs'
223+
224+
// Build the comment body
225+
let comment = `🎉 The ${prWord} fixing this issue (${prLinks}) has been released!\n\n`
226+
227+
for (const pkg of packages) {
228+
// Link to the package's changelog and version anchor
229+
const changelogUrl = `https://github.com/${repository}/blob/main/${pkg.pkgPath}/CHANGELOG.md#${pkg.version.replace(/\./g, '')}`
230+
comment += `- [${pkg.name}@${pkg.version}](${changelogUrl})\n`
231+
}
232+
233+
comment += `\nThank you for reporting!`
234+
235+
try {
236+
// Use gh CLI to post the comment
237+
execSync(
238+
`gh issue comment ${number} --body '${comment.replace(/'/g, '"')}'`,
239+
{ stdio: 'inherit' },
240+
)
241+
console.log(`✓ Commented on issue #${number}`)
242+
} catch (error) {
243+
console.error(`✗ Failed to comment on issue #${number}:`, error)
244+
}
245+
}
246+
130247
/**
131248
* Main function
132249
*/
@@ -170,12 +287,49 @@ async function main() {
170287

171288
console.log(`Found ${prMap.size} PR(s) to comment on...`)
172289

173-
// Comment on each PR
290+
// Collect issues linked to PRs
291+
const issueMap = new Map<number, IssueInfo>()
292+
293+
// Comment on each PR and collect linked issues
174294
for (const pr of prMap.values()) {
175295
await commentOnPR(pr, repository)
296+
297+
// Find issues that this PR closes/fixes
298+
const linkedIssues = findLinkedIssues(pr.number, repository)
299+
for (const issueNumber of linkedIssues) {
300+
if (!issueMap.has(issueNumber)) {
301+
issueMap.set(issueNumber, {
302+
number: issueNumber,
303+
prs: new Set(),
304+
packages: [],
305+
})
306+
}
307+
const issueInfo = issueMap.get(issueNumber)!
308+
issueInfo.prs.add(pr.number)
309+
310+
// Merge packages, avoiding duplicates
311+
for (const pkg of pr.packages) {
312+
if (
313+
!issueInfo.packages.some(
314+
(p) => p.name === pkg.name && p.version === pkg.version,
315+
)
316+
) {
317+
issueInfo.packages.push(pkg)
318+
}
319+
}
320+
}
321+
}
322+
323+
if (issueMap.size > 0) {
324+
console.log(`\nFound ${issueMap.size} linked issue(s) to comment on...`)
325+
326+
// Comment on each linked issue
327+
for (const issue of issueMap.values()) {
328+
await commentOnIssue(issue, repository)
329+
}
176330
}
177331

178-
console.log('✓ Done!')
332+
console.log('\n✓ Done!')
179333
}
180334

181335
main().catch((error) => {

.github/workflows/release.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ permissions:
1515
contents: write
1616
id-token: write
1717
pull-requests: write
18+
issues: write
1819

1920
jobs:
2021
release:
@@ -41,7 +42,7 @@ jobs:
4142
env:
4243
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4344
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
44-
- name: Comment on PRs about release
45+
- name: Comment on PRs and issues about release
4546
if: steps.changesets.outputs.published == 'true'
4647
uses: ./.github/comment-on-release
4748
with:

0 commit comments

Comments
 (0)