Skip to content

Commit 59a3492

Browse files
KyleAMathewsclaudeautofix-ci[bot]lachlancollins
authored
feat: add automated PR comments on release (#304)
* feat: add automated PR comments on release This adds automation to comment on PRs when they are included in a release. The workflow parses CHANGELOGs to extract PR numbers and posts a comment with version info and links to the relevant CHANGELOG files. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * ci: apply automated fixes * refactor: make PR commenting reusable across TanStack repos Converts the PR commenting feature into a reusable composite action that any TanStack repo can use. Changes: - Move script to .github/comment-on-release/ directory - Create composite action at .github/comment-on-release/action.yml - Update release workflow to use the composite action - Other TanStack repos can now use: tanstack/config/.github/comment-on-release@main 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * docs: add README for comment-on-release action Explains how other TanStack repos can adopt this reusable action. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * format * Slight tweaks * Tweak output comment, fix URL --------- Co-authored-by: Claude <[email protected]> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Lachlan Collins <[email protected]>
1 parent 447e255 commit 59a3492

File tree

4 files changed

+288
-0
lines changed

4 files changed

+288
-0
lines changed
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# Comment on Release Action
2+
3+
A reusable GitHub Action that automatically comments on PRs when they are included in a release.
4+
5+
## What It Does
6+
7+
When packages are published via Changesets:
8+
9+
1. Parses each published package's CHANGELOG to find PR numbers in the latest version
10+
2. Groups PRs by number (handling cases where one PR affects multiple packages)
11+
3. Posts a comment on each PR with release info and CHANGELOG links
12+
13+
## Example Comment
14+
15+
```
16+
🎉 This PR has been released!
17+
18+
- [@tanstack/[email protected]](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md#500)
19+
- [@tanstack/[email protected]](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md#500)
20+
21+
Thank you for your contribution!
22+
```
23+
24+
## Usage
25+
26+
Add this step to your `.github/workflows/release.yml` file after the `changesets/action` step:
27+
28+
```yaml
29+
- name: Run Changesets (version or publish)
30+
id: changesets
31+
uses: changesets/[email protected]
32+
with:
33+
version: pnpm run changeset:version
34+
publish: pnpm run changeset:publish
35+
commit: 'ci: Version Packages'
36+
title: 'ci: Version Packages'
37+
env:
38+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
39+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
40+
41+
- name: Comment on PRs about release
42+
if: steps.changesets.outputs.published == 'true'
43+
uses: tanstack/config/.github/comment-on-release@main
44+
with:
45+
published-packages: ${{ steps.changesets.outputs.publishedPackages }}
46+
```
47+
48+
## Requirements
49+
50+
- Must be using [Changesets](https://github.com/changesets/changesets) for releases
51+
- 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
53+
- The `gh` CLI must be available (automatically available in GitHub Actions)
54+
55+
## Inputs
56+
57+
| Input | Required | Description |
58+
| -------------------- | -------- | ------------------------------------------------------------------ |
59+
| `published-packages` | Yes | JSON string of published packages from `changesets/action` outputs |
60+
61+
## How It Works
62+
63+
The action:
64+
65+
1. Receives the list of published packages from the Changesets action
66+
2. For each package, reads its CHANGELOG at `packages/{package-name}/CHANGELOG.md`
67+
3. Extracts PR numbers from the latest version section using regex
68+
4. Groups all PRs and tracks which packages they contributed to
69+
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
71+
72+
## Troubleshooting
73+
74+
**No comments are posted:**
75+
76+
- Verify your CHANGELOGs have PR links in the correct format
77+
- Check that `steps.changesets.outputs.published` is `true`
78+
- Ensure the workflow has `pull-requests: write` permission
79+
80+
**Script fails to find CHANGELOGs:**
81+
82+
- The script expects packages at `packages/{package-name}/CHANGELOG.md`
83+
- Package name should match after removing the scope (e.g., `@tanstack/query-core` → `query-core`)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: Comment on PRs about release
2+
description: Automatically comments on PRs when they are included in a release
3+
inputs:
4+
published-packages:
5+
description: 'JSON string of published packages from changesets/action'
6+
required: true
7+
runs:
8+
using: composite
9+
steps:
10+
- name: Comment on PRs
11+
shell: bash
12+
env:
13+
PUBLISHED_PACKAGES: ${{ inputs.published-packages }}
14+
REPOSITORY: ${{ github.repository }}
15+
run: node {{ github.action_path }}/comment-on-release.ts
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
#!/usr/bin/env node
2+
3+
import { readFileSync } from 'node:fs'
4+
import { resolve } from 'node:path'
5+
import { execSync } from 'node:child_process'
6+
7+
interface PublishedPackage {
8+
name: string
9+
version: string
10+
}
11+
12+
interface PRInfo {
13+
number: number
14+
packages: Array<{ name: string; pkgPath: string; version: string }>
15+
}
16+
17+
/**
18+
* Parse CHANGELOG.md to extract PR numbers from the latest version entry
19+
*/
20+
function extractPRsFromChangelog(
21+
changelogPath: string,
22+
version: string,
23+
): Array<number> {
24+
try {
25+
const content = readFileSync(changelogPath, 'utf-8')
26+
const lines = content.split('\n')
27+
28+
let inTargetVersion = false
29+
let foundVersion = false
30+
const prNumbers = new Set<number>()
31+
32+
for (let i = 0; i < lines.length; i++) {
33+
const line = lines[i]
34+
35+
// Check for version header (e.g., "## 0.21.0")
36+
if (line.startsWith('## ')) {
37+
const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/)
38+
if (versionMatch) {
39+
if (versionMatch[1] === version) {
40+
inTargetVersion = true
41+
foundVersion = true
42+
} else if (inTargetVersion) {
43+
// We've moved to the next version, stop processing
44+
break
45+
}
46+
}
47+
}
48+
49+
// Extract PR numbers from links like [#302](https://github.com/TanStack/config/pull/302)
50+
if (inTargetVersion) {
51+
const prMatches = line.matchAll(
52+
/\[#(\d+)\]\(https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/\d+\)/g,
53+
)
54+
for (const match of prMatches) {
55+
prNumbers.add(parseInt(match[1], 10))
56+
}
57+
}
58+
}
59+
60+
if (!foundVersion) {
61+
console.warn(
62+
`Warning: Could not find version ${version} in ${changelogPath}`,
63+
)
64+
}
65+
66+
return Array.from(prNumbers)
67+
} catch (error) {
68+
console.error(`Error reading changelog at ${changelogPath}:`, error)
69+
return []
70+
}
71+
}
72+
73+
/**
74+
* Group PRs by their numbers and collect all packages they contributed to
75+
*/
76+
function groupPRsByNumber(
77+
publishedPackages: Array<PublishedPackage>,
78+
): Map<number, PRInfo> {
79+
const prMap = new Map<number, PRInfo>()
80+
81+
for (const pkg of publishedPackages) {
82+
const pkgPath = `packages/${pkg.name.replace('@tanstack/', '')}`
83+
const changelogPath = resolve(process.cwd(), pkgPath, 'CHANGELOG.md')
84+
85+
const prNumbers = extractPRsFromChangelog(changelogPath, pkg.version)
86+
87+
for (const prNumber of prNumbers) {
88+
if (!prMap.has(prNumber)) {
89+
prMap.set(prNumber, { number: prNumber, packages: [] })
90+
}
91+
prMap.get(prNumber)!.packages.push({
92+
name: pkg.name,
93+
pkgPath: pkgPath,
94+
version: pkg.version,
95+
})
96+
}
97+
}
98+
99+
return prMap
100+
}
101+
102+
/**
103+
* Post a comment on a GitHub PR using gh CLI
104+
*/
105+
async function commentOnPR(pr: PRInfo, repository: string): Promise<void> {
106+
const { number, packages } = pr
107+
108+
// Build the comment body
109+
let comment = `🎉 This PR has been released!\n\n`
110+
111+
for (const pkg of packages) {
112+
// 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('.', '')}`
114+
comment += `- [${pkg.name}@${pkg.version}](${changelogUrl})\n`
115+
}
116+
117+
comment += `\nThank you for your contribution!`
118+
119+
try {
120+
// Use gh CLI to post the comment
121+
execSync(`gh pr comment ${number} --body ${JSON.stringify(comment)}`, {
122+
stdio: 'inherit',
123+
})
124+
console.log(`✓ Commented on PR #${number}`)
125+
} catch (error) {
126+
console.error(`✗ Failed to comment on PR #${number}:`, error)
127+
}
128+
}
129+
130+
/**
131+
* Main function
132+
*/
133+
async function main() {
134+
// Read published packages from environment variable (set by GitHub Actions)
135+
const publishedPackagesJson = process.env.PUBLISHED_PACKAGES
136+
const repository = process.env.REPOSITORY
137+
138+
if (!publishedPackagesJson) {
139+
console.log('No packages were published. Skipping PR comments.')
140+
return
141+
}
142+
143+
if (!repository) {
144+
console.log('Repository is missing. Skipping PR comments.')
145+
return
146+
}
147+
148+
let publishedPackages: Array<PublishedPackage>
149+
try {
150+
publishedPackages = JSON.parse(publishedPackagesJson)
151+
} catch (error) {
152+
console.error('Failed to parse PUBLISHED_PACKAGES:', error)
153+
process.exit(1)
154+
}
155+
156+
if (publishedPackages.length === 0) {
157+
console.log('No packages were published. Skipping PR comments.')
158+
return
159+
}
160+
161+
console.log(`Processing ${publishedPackages.length} published package(s)...`)
162+
163+
// Group PRs by number
164+
const prMap = groupPRsByNumber(publishedPackages)
165+
166+
if (prMap.size === 0) {
167+
console.log('No PRs found in CHANGELOGs. Nothing to comment on.')
168+
return
169+
}
170+
171+
console.log(`Found ${prMap.size} PR(s) to comment on...`)
172+
173+
// Comment on each PR
174+
for (const pr of prMap.values()) {
175+
await commentOnPR(pr, repository)
176+
}
177+
178+
console.log('✓ Done!')
179+
}
180+
181+
main().catch((error) => {
182+
console.error('Fatal error:', error)
183+
process.exit(1)
184+
})

.github/workflows/release.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ jobs:
3131
- name: Run Tests
3232
run: pnpm run test:ci
3333
- name: Run Changesets (version or publish)
34+
id: changesets
3435
uses: changesets/[email protected]
3536
with:
3637
version: pnpm run changeset:version
@@ -40,3 +41,8 @@ jobs:
4041
env:
4142
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
4243
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
44+
- name: Comment on PRs about release
45+
if: steps.changesets.outputs.published == 'true'
46+
uses: ./.github/comment-on-release
47+
with:
48+
published-packages: ${{ steps.changesets.outputs.publishedPackages }}

0 commit comments

Comments
 (0)