@@ -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 */
105173async 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
181335main ( ) . catch ( ( error ) => {
0 commit comments