@@ -11,7 +11,7 @@ if (!token || !repoFull) {
1111
1212const octokit = new Octokit ( { auth : token } ) ;
1313
14- // Section definitions
14+ // Section definitions and prefixes
1515const SECTIONS = {
1616 "[Task]" : "🚀 Tasks" ,
1717 "[Composite]" : "🚀 Tasks" ,
@@ -43,10 +43,12 @@ PREFIXES.forEach(p => {
4343 PREFIX_ALIASES [ `${ p } :` ] = norm ;
4444} ) ;
4545
46+ // Strip prefix from title
4647function stripPrefix ( title ) {
4748 return title . replace ( / ^ \[ [ ^ \] ] + \] \s * / , "" ) . replace ( / ^ [ a - z / ] + : \s * / i, "" ) . trim ( ) ;
4849}
4950
51+ // Extract normalized prefix from title
5052function getPrefix ( title ) {
5153 if ( ! title ) return null ;
5254 const matchBracket = title . match ( / ^ \[ ( [ ^ \] ] + ) \] / ) ;
@@ -62,113 +64,100 @@ function getPrefix(title) {
6264 return null ;
6365}
6466
65- // Fetch all linked issues of a PR via GraphQL
66- async function getLinkedIssues ( prNumber ) {
67- const query = `
68- query($owner: String!, $repo: String!, $prNumber: Int!) {
69- repository(owner: $owner, name: $repo) {
70- pullRequest(number: $prNumber) {
71- closingIssuesReferences(first: 10) {
72- nodes {
73- number
74- title
75- labels(first: 10) {
76- nodes { name }
67+ // Fetch all merged PRs in dev that are not in master
68+ async function fetchPendingPRs ( latestTag ) {
69+ if ( latestTag ) {
70+ // Compare commits after last release
71+ const { data : compare } = await octokit . repos . compareCommits ( {
72+ owner, repo,
73+ base : latestTag ,
74+ head : "dev" ,
75+ } ) ;
76+ const devShas = compare . commits . map ( c => c . sha ) ;
77+ const pendingPRs = [ ] ;
78+ for ( const sha of devShas ) {
79+ const { data : prs } = await octokit . repos . listPullRequestsAssociatedWithCommit ( {
80+ owner, repo, commit_sha : sha
81+ } ) ;
82+ prs . forEach ( pr => {
83+ if ( pr . merged_at && ! pendingPRs . some ( p => p . number === pr . number ) ) {
84+ pendingPRs . push ( pr ) ;
7785 }
78- }
79- }
80- }
86+ } ) ;
8187 }
82- }
83- ` ;
84- const variables = { owner, repo, prNumber } ;
85- const result = await octokit . graphql ( query , variables ) ;
86- return result . repository . pullRequest . closingIssuesReferences . nodes ;
88+ return pendingPRs ;
89+ } else {
90+ // No release → take all merged PRs in dev
91+ const { data : prs } = await octokit . pulls . list ( {
92+ owner, repo,
93+ state : "closed" ,
94+ base : "dev" ,
95+ per_page : 100
96+ } ) ;
97+ return prs . filter ( p => p . merged_at ) ;
98+ }
8799}
88100
89101async function main ( ) {
90- // 1. Get latest release ( tag) on master
102+ // 1. Get latest release tag
91103 const { data : releases } = await octokit . repos . listReleases ( { owner, repo } ) ;
92104 const latestRelease = releases . find ( r => ! r . draft ) ;
93- const latestTag = latestRelease ?. tag_name || null ;
94-
95- // 2. Compare dev vs master (or master vs master if no release)
96- const baseRef = latestTag || "master" ;
97- const { data : compare } = await octokit . repos . compareCommits ( {
98- owner,
99- repo,
100- base : baseRef ,
101- head : "dev" ,
102- } ) ;
103-
104- const devShas = compare . commits . map ( c => c . sha ) ;
105-
106- // 3. Collect merged PRs in dev after last release
107- const pendingPRs = [ ] ;
108- for ( const sha of devShas ) {
109- const { data : prs } = await octokit . repos . listPullRequestsAssociatedWithCommit ( {
110- owner,
111- repo,
112- commit_sha : sha ,
113- } ) ;
114- prs . forEach ( pr => {
115- if ( pr . merged_at && ! pendingPRs . some ( p => p . number === pr . number ) ) {
116- pendingPRs . push ( pr ) ;
117- }
118- } ) ;
119- }
105+ const latestTag = latestRelease ?. tag_name ;
106+
107+ // 2. Fetch all pending PRs from dev
108+ const pendingPRs = await fetchPendingPRs ( latestTag ) ;
120109
121110 if ( ! pendingPRs . length ) {
122111 console . log ( "No merged PRs in dev after last release found." ) ;
123112 return ;
124113 }
125114
126- // 4. Group by linked issue
127- const issueMap = new Map ( ) ; // key: issue number, value: {title, prefix, prs: []}
128- const standalonePRs = [ ] ;
115+ // 3. Map issues and attach PRs
116+ const issueMap = new Map ( ) ;
129117
130118 for ( const pr of pendingPRs ) {
131- const linkedIssues = await getLinkedIssues ( pr . number ) ;
119+ // Fetch linked issues via the API
120+ const { data : linkedIssues } = await octokit . pulls . listLinkedIssues ( {
121+ owner, repo, pull_number : pr . number
122+ } ) . catch ( ( ) => ( { data : [ ] } ) ) ;
123+
132124 if ( linkedIssues . length ) {
133- for ( const issue of linkedIssues ) {
125+ linkedIssues . forEach ( issue => {
134126 if ( ! issueMap . has ( issue . number ) ) {
135- const prefix = getPrefix ( issue . title ) || "Other" ;
136- issueMap . set ( issue . number , {
137- title : issue . title ,
138- prefix,
139- prs : [ ] ,
140- } ) ;
127+ issueMap . set ( issue . number , { issue, prs : [ ] } ) ;
141128 }
142- issueMap . get ( issue . number ) . prs . push ( { number : pr . number , title : pr . title , user : pr . user . login } ) ;
143- }
129+ issueMap . get ( issue . number ) . prs . push ( pr ) ;
130+ } ) ;
144131 } else {
145- // PR without linked issue
146- standalonePRs . push ( { number : pr . number , title : pr . title , user : pr . user . login , prefix : getPrefix ( pr . title ) || "Other" } ) ;
132+ // PR without issue → special case
133+ issueMap . set ( `pr- ${ pr . number } ` , { prs : [ pr ] , isStandalone : true } ) ;
147134 }
148135 }
149136
150- // 5. Prepare sections
137+ // 4. Group by sections
151138 const sectionGroups = { } ;
152139 Object . keys ( SECTIONS ) . forEach ( k => sectionGroups [ k ] = [ ] ) ;
153140
154- // Add issues
155- for ( const [ issueNum , issueData ] of issueMap . entries ( ) ) {
156- const section = SECTIONS [ issueData . prefix ] ? issueData . prefix : "Other" ;
157- const prList = issueData . prs . map ( pr => `#${ pr . number } by @${ pr . user } ` ) . join ( ", " ) ;
158- const line = issueData . prs . length > 0
159- ? `• ${ issueData . prefix } ${ issueData . title } (#${ issueNum } )\n ↳ PRs: ${ prList } `
160- : `• ${ issueData . prefix } ${ issueData . title } (#${ issueNum } )` ;
161- sectionGroups [ section ] . push ( line ) ;
162- }
163-
164- // Add standalone PRs
165- for ( const pr of standalonePRs ) {
166- const section = SECTIONS [ pr . prefix ] ? pr . prefix : "Other" ;
167- const line = `• ${ pr . prefix } ${ pr . title } (#${ pr . number } ) by @${ pr . user } ` ;
168- sectionGroups [ section ] . push ( line ) ;
141+ for ( const entry of issueMap . values ( ) ) {
142+ if ( entry . isStandalone ) {
143+ const pr = entry . prs [ 0 ] ;
144+ const prefix = getPrefix ( pr . title ) || "Other" ;
145+ const section = SECTIONS [ prefix ] ? prefix : "Other" ;
146+ const line = section === "Other"
147+ ? `• ${ pr . title } (#${ pr . number } ) by @${ pr . user . login } `
148+ : `• ${ prefix } ${ stripPrefix ( pr . title ) } (#${ pr . number } ) by @${ pr . user . login } ` ;
149+ sectionGroups [ section ] . push ( line ) ;
150+ } else {
151+ const issueTitle = entry . issue . title ;
152+ const prefix = getPrefix ( issueTitle ) || "Other" ;
153+ const section = SECTIONS [ prefix ] ? prefix : "Other" ;
154+ const prRefs = entry . prs . map ( p => `#${ p . number } by @${ p . user . login } ` ) . join ( ", " ) ;
155+ const line = `• ${ prefix } ${ stripPrefix ( issueTitle ) } (#${ entry . issue . number } )\n ↳ PRs: ${ prRefs } ` ;
156+ sectionGroups [ section ] . push ( line ) ;
157+ }
169158 }
170159
171- // 6 . Order sections: Tasks first, Other last
160+ // 5 . Order sections
172161 const orderedSections = [ "[Task]" , ...Object . keys ( SECTIONS ) . filter ( k => k !== "[Task]" && k !== "Other" ) , "Other" ] ;
173162
174163 let body = "# 🚀 Release Notes\n\n" ;
@@ -179,7 +168,7 @@ async function main() {
179168 }
180169 }
181170
182- // 7 . Determine next patch version
171+ // 6 . Determine next version
183172 let nextVersion = "v0.1.0" ;
184173 const latestNonDraft = releases . find ( r => ! r . draft ) || releases [ 0 ] ;
185174 if ( latestNonDraft ) {
@@ -190,22 +179,20 @@ async function main() {
190179 }
191180 }
192181
193- // 8 . Create or update draft release
182+ // 7 . Create/ update draft release
194183 const draft = releases . find ( r => r . draft ) ;
195184 if ( draft ) {
196185 console . log ( "Updating existing draft release:" , draft . tag_name ) ;
197186 await octokit . repos . updateRelease ( {
198- owner,
199- repo,
187+ owner, repo,
200188 release_id : draft . id ,
201189 name : nextVersion ,
202190 body,
203191 } ) ;
204192 } else {
205193 console . log ( "Creating new draft release" ) ;
206194 await octokit . repos . createRelease ( {
207- owner,
208- repo,
195+ owner, repo,
209196 tag_name : nextVersion ,
210197 name : nextVersion ,
211198 body,
0 commit comments