Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions .github/comment-on-release/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Comment on Release Action

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

## What It Does

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

## Example Comment
## Example Comments

### On a PR:

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

### On a linked issue:

```
🎉 The PR fixing this issue (#123) has been released!

- [@tanstack/query-core@5.0.0](https://github.com/TanStack/query/blob/main/packages/query-core/CHANGELOG.md#500)

Thank you for reporting!
```

## Usage

Add this step to your `.github/workflows/release.yml` file after the `changesets/action` step:
Expand Down Expand Up @@ -49,7 +63,7 @@ Add this step to your `.github/workflows/release.yml` file after the `changesets

- Must be using [Changesets](https://github.com/changesets/changesets) for releases
- CHANGELOGs must include PR links in the format: `[#123](https://github.com/org/repo/pull/123)`
- Requires `pull-requests: write` permission in the workflow
- Requires `pull-requests: write` and `issues: write` permissions in the workflow
- The `gh` CLI must be available (automatically available in GitHub Actions)

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

## Troubleshooting

**No comments are posted:**

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

**Script fails to find CHANGELOGs:**

- The script expects packages at `packages/{package-name}/CHANGELOG.md`
- Package name should match after removing the scope (e.g., `@tanstack/query-core` → `query-core`)

**Issues aren't being commented on:**

- Verify that PRs use GitHub's closing keywords (`closes #N`, `fixes #N`, `resolves #N`, etc.) in the PR description
- Check that the linked issues exist and are accessible
- Ensure the `issues: write` permission is granted in the workflow
6 changes: 3 additions & 3 deletions .github/comment-on-release/action.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
name: Comment on PRs about release
description: Automatically comments on PRs when they are included in a release
name: Comment on PRs and issues about release
description: Automatically comments on PRs and linked issues when they are included in a release
inputs:
published-packages:
description: 'JSON string of published packages from changesets/action'
required: true
runs:
using: composite
steps:
- name: Comment on PRs
- name: Comment on PRs and issues
shell: bash
run: node ${{ github.action_path }}/comment-on-release.ts
env:
Expand Down
160 changes: 157 additions & 3 deletions .github/comment-on-release/comment-on-release.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ interface PRInfo {
packages: Array<{ name: string; pkgPath: string; version: string }>
}

interface IssueInfo {
number: number
prs: Set<number>
packages: Array<{ name: string; pkgPath: string; version: string }>
}

/**
* Parse CHANGELOG.md to extract PR numbers from the latest version entry
*/
Expand Down Expand Up @@ -99,18 +105,86 @@ function groupPRsByNumber(
return prMap
}

/**
* Check if we've already commented on a PR/issue to avoid duplicates
*/
function hasExistingComment(number: number, type: 'pr' | 'issue'): boolean {
try {
const result = execSync(
`gh api repos/\${GITHUB_REPOSITORY}/issues/${number}/comments --jq '[.[] | select(.body | contains("has been released!"))] | length'`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
)
const count = parseInt(result.trim(), 10)
return count > 0
} catch (error) {
console.warn(
`Warning: Could not check existing comments for ${type} #${number}`,
)
return false
}
}

/**
* Find issues that a PR closes/fixes using GitHub's GraphQL API
*/
function findLinkedIssues(prNumber: number, repository: string): Array<number> {
const [owner, repo] = repository.split('/')
const query = `
query($owner: String!, $repo: String!, $pr: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $pr) {
closingIssuesReferences(first: 10) {
nodes {
number
}
}
}
}
}
`

try {
const result = execSync(
`gh api graphql -f query='${query}' -F owner='${owner}' -F repo='${repo}' -F pr=${prNumber} --jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number'`,
{ encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] },
)

const issueNumbers = result
.trim()
.split('\n')
.filter((line) => line)
.map((line) => parseInt(line, 10))

if (issueNumbers.length > 0) {
console.log(
` PR #${prNumber} links to issues: ${issueNumbers.join(', ')}`,
)
}

return issueNumbers
} catch (error) {
return []
}
}

/**
* Post a comment on a GitHub PR using gh CLI
*/
async function commentOnPR(pr: PRInfo, repository: string): Promise<void> {
const { number, packages } = pr

// Check for duplicate comments
if (hasExistingComment(number, 'pr')) {
console.log(`↷ Already commented on PR #${number}, skipping`)
return
}

// Build the comment body
let comment = `🎉 This PR has been released!\n\n`

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

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

/**
* Post a comment on a GitHub issue using gh CLI
*/
async function commentOnIssue(
issue: IssueInfo,
repository: string,
): Promise<void> {
const { number, prs, packages } = issue

// Check for duplicate comments
if (hasExistingComment(number, 'issue')) {
console.log(`↷ Already commented on issue #${number}, skipping`)
return
}

const prLinks = Array.from(prs)
.map((pr) => `#${pr}`)
.join(', ')
const prWord = prs.size === 1 ? 'PR' : 'PRs'

// Build the comment body
let comment = `🎉 The ${prWord} fixing this issue (${prLinks}) has been released!\n\n`

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

comment += `\nThank you for reporting!`

try {
// Use gh CLI to post the comment
execSync(
`gh issue comment ${number} --body '${comment.replace(/'/g, '"')}'`,
{ stdio: 'inherit' },
)
console.log(`✓ Commented on issue #${number}`)
} catch (error) {
console.error(`✗ Failed to comment on issue #${number}:`, error)
}
}

/**
* Main function
*/
Expand Down Expand Up @@ -170,12 +287,49 @@ async function main() {

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

// Comment on each PR
// Collect issues linked to PRs
const issueMap = new Map<number, IssueInfo>()

// Comment on each PR and collect linked issues
for (const pr of prMap.values()) {
await commentOnPR(pr, repository)

// Find issues that this PR closes/fixes
const linkedIssues = findLinkedIssues(pr.number, repository)
for (const issueNumber of linkedIssues) {
if (!issueMap.has(issueNumber)) {
issueMap.set(issueNumber, {
number: issueNumber,
prs: new Set(),
packages: [],
})
}
const issueInfo = issueMap.get(issueNumber)!
issueInfo.prs.add(pr.number)

// Merge packages, avoiding duplicates
for (const pkg of pr.packages) {
if (
!issueInfo.packages.some(
(p) => p.name === pkg.name && p.version === pkg.version,
)
) {
issueInfo.packages.push(pkg)
}
}
}
}

if (issueMap.size > 0) {
console.log(`\nFound ${issueMap.size} linked issue(s) to comment on...`)

// Comment on each linked issue
for (const issue of issueMap.values()) {
await commentOnIssue(issue, repository)
}
}

console.log('✓ Done!')
console.log('\n✓ Done!')
}

main().catch((error) => {
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ permissions:
contents: write
id-token: write
pull-requests: write
issues: write

jobs:
release:
Expand All @@ -41,7 +42,7 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Comment on PRs about release
- name: Comment on PRs and issues about release
if: steps.changesets.outputs.published == 'true'
uses: ./.github/comment-on-release
with:
Expand Down
Loading