11#!/usr/bin/env node
22/**
33 * ✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025.
4- *
4+ *
55 * GitHub Release Notes Generator
66 *
77 * Features:
2626const args = process . argv . slice ( 2 ) ;
2727
2828function getArgValue ( flag ) {
29- const idx = args . indexOf ( flag ) ;
30- return idx !== - 1 && args [ idx + 1 ] && ! args [ idx + 1 ] . startsWith ( "-" ) ? args [ idx + 1 ] : null ;
29+ const idx = args . indexOf ( flag ) ;
30+ return idx !== - 1 && args [ idx + 1 ] && ! args [ idx + 1 ] . startsWith ( "-" )
31+ ? args [ idx + 1 ]
32+ : null ;
3133}
3234if ( args . includes ( "--help" ) || args . length < 3 ) {
33- console . log ( `
35+ console . log ( `
3436Usage: GITHUB_TOKEN=<token> node generate-release-notes.js <owner> <repo> <tag> [options]
3537✨ @ UnschooledGamer (baked With AI, Modified by @ UnschooledGamer) ~ 2025
3638
@@ -45,175 +47,199 @@ Options:
4547 --stdout-only Output to stdout only
4648 --changelog-only Output changelog only
4749` ) ;
48- process . exit ( 0 ) ;
50+ process . exit ( 0 ) ;
4951}
5052
5153const [ owner , repo , currentTag , previousTagArg ] = args ;
5254const token = process . env . GITHUB_TOKEN ;
5355if ( ! token ) {
54- console . error ( "❌ Missing GITHUB_TOKEN environment variable." ) ;
55- process . exit ( 1 ) ;
56+ console . error ( "❌ Missing GITHUB_TOKEN environment variable." ) ;
57+ process . exit ( 1 ) ;
5658}
5759
5860const flags = {
59- plain : args . includes ( "--plain" ) ,
60- importantOnly : args . includes ( "--important-only" ) ,
61- mergeOnly : args . includes ( "--merge-only" ) ,
62- quiet : args . includes ( "--quiet" ) || args . includes ( "--stdout-only" ) ,
63- format : getArgValue ( "--format" ) || "md" ,
64- fromTag : getArgValue ( "--from-tag" ) ,
65- changelogOnly : args . includes ( "--changelog-only" ) ,
61+ plain : args . includes ( "--plain" ) ,
62+ importantOnly : args . includes ( "--important-only" ) ,
63+ mergeOnly : args . includes ( "--merge-only" ) ,
64+ quiet : args . includes ( "--quiet" ) || args . includes ( "--stdout-only" ) ,
65+ format : getArgValue ( "--format" ) || "md" ,
66+ fromTag : getArgValue ( "--from-tag" ) ,
67+ changelogOnly : args . includes ( "--changelog-only" ) ,
6668} ;
6769
6870function log ( ...msg ) {
69- if ( ! flags . quiet ) console . error ( ...msg ) ;
71+ if ( ! flags . quiet ) console . error ( ...msg ) ;
7072}
7173
7274const headers = {
73- " Authorization" : `token ${ token } ` ,
74- " Accept" : "application/vnd.github+json" ,
75- "User-Agent" : "release-notes-script"
75+ Authorization : `token ${ token } ` ,
76+ Accept : "application/vnd.github+json" ,
77+ "User-Agent" : "release-notes-script" ,
7678} ;
7779
7880async function getPreviousTag ( ) {
79- const res = await fetch ( `https://api.github.com/repos/${ owner } /${ repo } /tags` , { headers } ) ;
80- const tags = await res . json ( ) ;
81- if ( ! Array . isArray ( tags ) || tags . length < 2 ) return null ;
82- return tags [ 1 ] . name ;
81+ const res = await fetch (
82+ `https://api.github.com/repos/${ owner } /${ repo } /tags` ,
83+ { headers } ,
84+ ) ;
85+ const tags = await res . json ( ) ;
86+ if ( ! Array . isArray ( tags ) || tags . length < 2 ) return null ;
87+ return tags [ 1 ] . name ;
8388}
8489
8590async function getCommits ( previousTag , currentTag ) {
86- const url = `https://api.github.com/repos/${ owner } /${ repo } /compare/${ previousTag } ...${ currentTag } ` ;
87- const res = await fetch ( url , { headers } ) ;
88- if ( ! res . ok ) throw new Error ( `Failed to fetch commits: ${ res . status } ` ) ;
89- const data = await res . json ( ) ;
90- return data . commits || [ ] ;
91+ const url = `https://api.github.com/repos/${ owner } /${ repo } /compare/${ previousTag } ...${ currentTag } ` ;
92+ const res = await fetch ( url , { headers } ) ;
93+ if ( ! res . ok ) throw new Error ( `Failed to fetch commits: ${ res . status } ` ) ;
94+ const data = await res . json ( ) ;
95+ return data . commits || [ ] ;
9196}
9297
9398function categorizeCommits ( commits , { mergeOnly, importantOnly } ) {
94- const sections = {
95- feat : [ ] ,
96- fix : [ ] ,
97- perf : [ ] ,
98- refactor : [ ] ,
99- docs : [ ] ,
100- chore : [ ] ,
101- test : [ ] ,
102- add : [ ] ,
103- revert : [ ] ,
104- update : [ ] ,
105- other : [ ] ,
106- } ;
107-
108- for ( const c of commits ) {
109- const msg = c . commit . message . split ( "\n" ) [ 0 ] ;
110- const isMerge = msg . startsWith ( "Merge pull request" ) || msg . startsWith ( "Merge branch" ) ;
111-
112- if ( mergeOnly && ! isMerge ) continue ;
113-
114- const type = Object . keys ( sections ) . find ( k => msg . toLowerCase ( ) . startsWith ( `${ k } :` ) || msg . toLowerCase ( ) . startsWith ( `${ k } ` ) ) || "other" ;
115-
116- if ( importantOnly && ! [ "feat" , "fix" , "refactor" , "perf" , "add" , "revert" , "update" ] . includes ( type ) ) continue ;
117-
118- const author = c . author ?. login
119- ? `[${ c . author . login } ](https://github.com/${ c . author . login } )`
120- : "unknown" ;
121-
122- const entry = `- ${ msg } (${ c . sha . slice ( 0 , 7 ) } ) by ${ author } ` ;
123- sections [ type ] . push ( entry ) ;
124- }
125-
126- return sections ;
99+ const sections = {
100+ feat : [ ] ,
101+ fix : [ ] ,
102+ perf : [ ] ,
103+ refactor : [ ] ,
104+ docs : [ ] ,
105+ chore : [ ] ,
106+ test : [ ] ,
107+ add : [ ] ,
108+ revert : [ ] ,
109+ update : [ ] ,
110+ other : [ ] ,
111+ } ;
112+
113+ for ( const c of commits ) {
114+ const msg = c . commit . message . split ( "\n" ) [ 0 ] ;
115+ const isMerge =
116+ msg . startsWith ( "Merge pull request" ) || msg . startsWith ( "Merge branch" ) ;
117+
118+ if ( mergeOnly && ! isMerge ) continue ;
119+
120+ const type =
121+ Object . keys ( sections ) . find (
122+ ( k ) =>
123+ msg . toLowerCase ( ) . startsWith ( `${ k } :` ) ||
124+ msg . toLowerCase ( ) . startsWith ( `${ k } ` ) ,
125+ ) || "other" ;
126+
127+ if (
128+ importantOnly &&
129+ ! [ "feat" , "fix" , "refactor" , "perf" , "add" , "revert" , "update" ] . includes (
130+ type ,
131+ )
132+ )
133+ continue ;
134+
135+ const author = c . author ?. login
136+ ? `[${ c . author . login } ](https://github.com/${ c . author . login } )`
137+ : "unknown" ;
138+
139+ const entry = `- ${ msg } (${ c . sha . slice ( 0 , 7 ) } ) by ${ author } ` ;
140+ sections [ type ] . push ( entry ) ;
141+ }
142+
143+ return sections ;
127144}
128145
129146const emojis = {
130- feat : flags . plain ? "" : "✨ " ,
131- fix : flags . plain ? "" : "🐞 " ,
132- perf : flags . plain ? "" : "⚡ " ,
133- refactor : flags . plain ? "" : "🔧 " ,
134- docs : flags . plain ? "" : "📝 " ,
135- chore : flags . plain ? "" : "🧹 " ,
136- test : flags . plain ? "" : "🧪 " ,
137- other : flags . plain ? "" : "📦 " ,
138- revert : flags . plain ? "" : "⏪ " ,
139- add : flags . plain ? "" : "➕ " ,
140- update : flags . plain ? "" : "🔄 " ,
147+ feat : flags . plain ? "" : "✨ " ,
148+ fix : flags . plain ? "" : "🐞 " ,
149+ perf : flags . plain ? "" : "⚡ " ,
150+ refactor : flags . plain ? "" : "🔧 " ,
151+ docs : flags . plain ? "" : "📝 " ,
152+ chore : flags . plain ? "" : "🧹 " ,
153+ test : flags . plain ? "" : "🧪 " ,
154+ other : flags . plain ? "" : "📦 " ,
155+ revert : flags . plain ? "" : "⏪ " ,
156+ add : flags . plain ? "" : "➕ " ,
157+ update : flags . plain ? "" : "🔄 " ,
141158} ;
142159
143160function formatMarkdown ( tag , prevTag , sections , { plain } ) {
144-
145- const lines = [
146- flags . changelogOnly ? "" : `Changes since [${ prevTag } ](https://github.com/${ owner } /${ repo } /releases/tag/${ prevTag } )` ,
147- "" ,
148- ] ;
149-
150- for ( const [ type , list ] of Object . entries ( sections ) ) {
151- if ( list . length === 0 ) continue ;
152- const header = plain ? `## ${ type } ` : `## ${ emojis [ type ] } ${ type [ 0 ] . toUpperCase ( ) + type . slice ( 1 ) } ` ;
153- lines . push ( header , "" , list . join ( "\n" ) , "" ) ;
154- }
155-
156- // Compact single-line mode for super small output
157- // if (plain) {
158- // const compact = Object.entries(sections)
159- // .filter(([_, list]) => list.length)
160- // .map(([type, list]) => `${type.toUpperCase()}: ${list.length} commits`)
161- // .join(" | ");
162- // lines.push(`\n_Summary: ${compact}_`);
163- // }
164-
165- return lines . join ( "\n" ) ;
161+ const lines = [
162+ flags . changelogOnly
163+ ? ""
164+ : `Changes since [${ prevTag } ](https://github.com/${ owner } /${ repo } /releases/tag/${ prevTag } )` ,
165+ "" ,
166+ ] ;
167+
168+ for ( const [ type , list ] of Object . entries ( sections ) ) {
169+ if ( list . length === 0 ) continue ;
170+ const header = plain
171+ ? `## ${ type } `
172+ : `## ${ emojis [ type ] } ${ type [ 0 ] . toUpperCase ( ) + type . slice ( 1 ) } ` ;
173+ lines . push ( header , "" , list . join ( "\n" ) , "" ) ;
174+ }
175+
176+ // Compact single-line mode for super small output
177+ // if (plain) {
178+ // const compact = Object.entries(sections)
179+ // .filter(([_, list]) => list.length)
180+ // .map(([type, list]) => `${type.toUpperCase()}: ${list.length} commits`)
181+ // .join(" | ");
182+ // lines.push(`\n_Summary: ${compact}_`);
183+ // }
184+
185+ return lines . join ( "\n" ) ;
166186}
167187
168188function formatJSON ( tag , prevTag , sections , plain = true ) {
169-
170- const lines = [
171- "" ,
172- flags . changelogOnly ? "" : `Changes since [${ prevTag } ](https://github.com/${ owner } /${ repo } /releases/tag/${ prevTag } )` ,
173- "" ,
174- ] ;
175-
176- // todo: split into function
177- for ( const [ type , list ] of Object . entries ( sections ) ) {
178- if ( list . length === 0 ) continue ;
179- const header = plain ? `## ${ type } ` : `## ${ emojis [ type ] } ${ type [ 0 ] . toUpperCase ( ) + type . slice ( 1 ) } ` ;
180- lines . push ( header , "" , list . join ( "\n" ) , "" ) ;
181- }
182- return JSON . stringify ( {
183- release : tag ,
184- previous : prevTag ,
185- sections : Object . fromEntries (
186- Object . entries ( sections ) . filter ( ( [ _ , v ] ) => v . length )
187- ) ,
188- notes : lines . join ( "\n" )
189- } , null , 2 ) ;
189+ const lines = [
190+ "" ,
191+ flags . changelogOnly
192+ ? ""
193+ : `Changes since [${ prevTag } ](https://github.com/${ owner } /${ repo } /releases/tag/${ prevTag } )` ,
194+ "" ,
195+ ] ;
196+
197+ // todo: split into function
198+ for ( const [ type , list ] of Object . entries ( sections ) ) {
199+ if ( list . length === 0 ) continue ;
200+ const header = plain
201+ ? `## ${ type } `
202+ : `## ${ emojis [ type ] } ${ type [ 0 ] . toUpperCase ( ) + type . slice ( 1 ) } ` ;
203+ lines . push ( header , "" , list . join ( "\n" ) , "" ) ;
204+ }
205+ return JSON . stringify (
206+ {
207+ release : tag ,
208+ previous : prevTag ,
209+ sections : Object . fromEntries (
210+ Object . entries ( sections ) . filter ( ( [ _ , v ] ) => v . length ) ,
211+ ) ,
212+ notes : lines . join ( "\n" ) ,
213+ } ,
214+ null ,
215+ 2 ,
216+ ) ;
190217}
191218
192-
193219async function main ( ) {
194- log ( `🔍 Generating release notes for ${ owner } /${ repo } @ ${ currentTag } ...` ) ;
195-
196- const prevTag = flags . fromTag || await getPreviousTag ( ) ;
197- if ( ! prevTag ) {
198- console . error ( "No previous tag found. Use --from-tag to specify one." ) ;
199- process . exit ( 1 ) ;
200- }
201-
202- const commits = await getCommits ( prevTag , currentTag ) ;
203- if ( ! commits . length ) {
204- console . error ( "No commits found." ) ;
205- process . exit ( 1 ) ;
206- }
207- const categorized = categorizeCommits ( commits , flags ) ;
208- let output ;
209-
210- if ( flags . format === "json" ) {
211- output = formatJSON ( currentTag , prevTag , categorized ) ;
212- } else {
213- output = formatMarkdown ( currentTag , prevTag , categorized , flags ) ;
214- }
215-
216- process . stdout . write ( output + "\n" ) ;
220+ log ( `🔍 Generating release notes for ${ owner } /${ repo } @ ${ currentTag } ...` ) ;
221+
222+ const prevTag = flags . fromTag || ( await getPreviousTag ( ) ) ;
223+ if ( ! prevTag ) {
224+ console . error ( "No previous tag found. Use --from-tag to specify one." ) ;
225+ process . exit ( 1 ) ;
226+ }
227+
228+ const commits = await getCommits ( prevTag , currentTag ) ;
229+ if ( ! commits . length ) {
230+ console . error ( "No commits found." ) ;
231+ process . exit ( 1 ) ;
232+ }
233+ const categorized = categorizeCommits ( commits , flags ) ;
234+ let output ;
235+
236+ if ( flags . format === "json" ) {
237+ output = formatJSON ( currentTag , prevTag , categorized ) ;
238+ } else {
239+ output = formatMarkdown ( currentTag , prevTag , categorized , flags ) ;
240+ }
241+
242+ process . stdout . write ( output + "\n" ) ;
217243}
218244
219- main ( ) . catch ( err => console . error ( err ) ) ;
245+ main ( ) . catch ( ( err ) => console . error ( err ) ) ;
0 commit comments