1+ // @ts -check
12import fetch from 'node-fetch'
23
4+ // Helper function to create a fresh copy of the categories structure
5+ function createCategoriesStructure ( ) {
6+ const RELEASE_CATEGORIES = {
7+ '### Core Changes' : [ ] ,
8+ '### Minor Changes' : [ ] ,
9+ '### Documentation Changes' : [ ] ,
10+ '### Example Changes' : [ ] ,
11+ '### Misc Changes' : [ ] ,
12+ '### Patches' : [ ] ,
13+ '### Credits' : [ ] ,
14+ }
15+ return RELEASE_CATEGORIES
16+ }
17+
318async function main ( ) {
19+ // Check if we have command line arguments for version comparison
20+ const args = process . argv . slice ( 2 )
21+
22+ if ( args . length === 2 ) {
23+ // Mode 1: Compare between two versions/commits
24+ const [ fromVersion , toVersion ] = args
25+ return await generateLogBetweenVersions ( fromVersion , toVersion )
26+ } else if ( args . length === 0 ) {
27+ // Mode 2: Original behavior - generate logs for latest canary releases
28+ return await generateLatestCanaryLogs ( )
29+ } else {
30+ console . error (
31+ 'Usage: node generate-release-log.mjs [fromVersion toVersion]'
32+ )
33+ console . error (
34+ ' With no arguments: generates logs for latest canary releases'
35+ )
36+ console . error (
37+ ' With two arguments: generates logs between two versions/commits'
38+ )
39+ process . exit ( 1 )
40+ }
41+ }
42+
43+ async function generateLogBetweenVersions ( fromVersion , toVersion ) {
44+ console . log ( `Fetching commits between ${ fromVersion } and ${ toVersion } ...` )
45+
46+ try {
47+ // Use GitHub API to compare commits between two references
48+ const response = await fetch (
49+ `https://api.github.com/repos/vercel/next.js/compare/${ fromVersion } ...${ toVersion } `
50+ )
51+
52+ if ( ! response . ok ) {
53+ throw new Error (
54+ `GitHub API error: ${ response . status } ${ response . statusText } `
55+ )
56+ }
57+
58+ const compareData = await response . json ( )
59+ const commits = compareData . commits || [ ]
60+
61+ console . log (
62+ `Found ${ commits . length } commits between ${ fromVersion } and ${ toVersion } `
63+ )
64+
65+ // Filter out version bump commits and other non-meaningful commits
66+ const filteredCommits = commits . filter ( ( commit ) => {
67+ const message = commit . commit . message . split ( '\n' ) [ 0 ]
68+ const author = commit . author ?. login || commit . commit . author . name
69+
70+ // Skip version bump commits (usually just version number changes)
71+ if ( message . match ( / ^ v \d + \. \d + \. \d + ( - \w + \. \d + ) ? $ / ) ) {
72+ console . log ( `Skipping version bump commit: ${ message } ` )
73+ return false
74+ }
75+
76+ // Skip automated release commits
77+ if ( author === 'vercel-release-bot' && message . match ( / ^ v \d + \. \d + \. \d + / ) ) {
78+ console . log ( `Skipping automated release commit: ${ message } ` )
79+ return false
80+ }
81+
82+ return true
83+ } )
84+
85+ console . log ( `After filtering: ${ filteredCommits . length } commits to include` )
86+
87+ // Categorize commits based on their commit messages
88+ const categorizedCommits = categorizeCommits ( filteredCommits )
89+
90+ // Generate formatted output
91+ const content = formatCommitLog ( categorizedCommits , fromVersion , toVersion )
92+
93+ return {
94+ fromVersion,
95+ toVersion,
96+ commitCount : filteredCommits . length ,
97+ content,
98+ }
99+ } catch ( error ) {
100+ console . error ( 'Error fetching commits:' , error . message )
101+ process . exit ( 1 )
102+ }
103+ }
104+
105+ async function generateLatestCanaryLogs ( ) {
4106 const releasesArray = await fetch (
5107 'https://api.github.com/repos/vercel/next.js/releases?per_page=100'
6108 ) . then ( ( r ) => r . json ( ) )
@@ -24,15 +126,7 @@ async function main() {
24126
25127 const releases = allReleases . filter ( ( v ) => v . tag_name . includes ( targetVersion ) )
26128
27- const lineItems = {
28- '### Core Changes' : [ ] ,
29- '### Minor Changes' : [ ] ,
30- '### Documentation Changes' : [ ] ,
31- '### Example Changes' : [ ] ,
32- '### Misc Changes' : [ ] ,
33- '### Patches' : [ ] ,
34- '### Credits' : [ ] ,
35- }
129+ const lineItems = createCategoriesStructure ( )
36130
37131 Object . keys ( lineItems ) . forEach ( ( header ) => {
38132 releases . forEach ( ( release ) => {
@@ -90,10 +184,13 @@ async function main() {
90184 let creditsMessage = `Huge thanks to `
91185
92186 if ( items . length > 1 ) {
93- creditsMessage += items . slice ( 0 , items . length - 1 ) . join ( `, ` )
187+ creditsMessage += items
188+ . slice ( 0 , items . length - 1 )
189+ . map ( ( name ) => `@${ name } ` )
190+ . join ( `, ` )
94191 creditsMessage += `, and `
95192 }
96- creditsMessage += items [ items . length - 1 ]
193+ creditsMessage += `@ ${ items [ items . length - 1 ] } `
97194 creditsMessage += ` for helping!`
98195
99196 finalMessage . push ( creditsMessage )
@@ -112,6 +209,105 @@ async function main() {
112209 }
113210}
114211
212+ function categorizeCommits ( commits ) {
213+ const categories = createCategoriesStructure ( )
214+
215+ commits . forEach ( ( commit ) => {
216+ const message = commit . commit . message
217+ const author = commit . author ?. login || commit . commit . author . name
218+ const sha = commit . sha . substring ( 0 , 7 )
219+
220+ // Extract commit message without merge info
221+ const cleanMessage = message . split ( '\n' ) [ 0 ]
222+
223+ // Categorize based on commit message patterns
224+ if ( cleanMessage . includes ( 'feat:' ) || cleanMessage . includes ( 'feature:' ) ) {
225+ categories [ '### Core Changes' ] . push (
226+ `- ${ cleanMessage } (${ author } , ${ sha } )`
227+ )
228+ } else if (
229+ cleanMessage . includes ( 'fix:' ) ||
230+ cleanMessage . includes ( 'bugfix:' )
231+ ) {
232+ categories [ '### Patches' ] . push ( `- ${ cleanMessage } (${ author } , ${ sha } )` )
233+ } else if (
234+ cleanMessage . includes ( 'docs:' ) ||
235+ cleanMessage . includes ( 'documentation:' )
236+ ) {
237+ categories [ '### Documentation Changes' ] . push (
238+ `- ${ cleanMessage } (${ author } , ${ sha } )`
239+ )
240+ } else if (
241+ cleanMessage . includes ( 'example:' ) ||
242+ cleanMessage . includes ( 'examples:' )
243+ ) {
244+ categories [ '### Example Changes' ] . push (
245+ `- ${ cleanMessage } (${ author } , ${ sha } )`
246+ )
247+ } else if (
248+ cleanMessage . includes ( 'chore:' ) ||
249+ cleanMessage . includes ( 'refactor:' )
250+ ) {
251+ categories [ '### Minor Changes' ] . push (
252+ `- ${ cleanMessage } (${ author } , ${ sha } )`
253+ )
254+ } else {
255+ categories [ '### Misc Changes' ] . push (
256+ `- ${ cleanMessage } (${ author } , ${ sha } )`
257+ )
258+ }
259+
260+ // Extract contributors for credits
261+ if ( author && ! categories [ '### Credits' ] . includes ( author ) ) {
262+ categories [ '### Credits' ] . push ( author )
263+ }
264+ } )
265+
266+ return categories
267+ }
268+
269+ function formatCommitLog ( categorizedCommits , fromVersion , toVersion ) {
270+ let finalMessage = [ ]
271+
272+ // Add header
273+ finalMessage . push ( `# Changes between ${ fromVersion } and ${ toVersion } ` )
274+ finalMessage . push ( '' )
275+
276+ Object . keys ( categorizedCommits ) . forEach ( ( header ) => {
277+ const items = categorizedCommits [ header ]
278+
279+ if ( ! items . length ) {
280+ return
281+ }
282+
283+ finalMessage . push ( header )
284+ finalMessage . push ( '' )
285+
286+ if ( header === '### Credits' ) {
287+ const uniqueCredits = [ ...new Set ( items ) ]
288+ let creditsMessage = `Huge thanks to `
289+
290+ if ( uniqueCredits . length > 1 ) {
291+ creditsMessage += uniqueCredits
292+ . slice ( 0 , uniqueCredits . length - 1 )
293+ . map ( ( name ) => `@${ name } ` )
294+ . join ( `, ` )
295+ creditsMessage += `, and `
296+ }
297+ creditsMessage += `@${ uniqueCredits [ uniqueCredits . length - 1 ] } `
298+ creditsMessage += ` for helping!`
299+
300+ finalMessage . push ( creditsMessage )
301+ } else {
302+ items . forEach ( ( item ) => finalMessage . push ( item ) )
303+ }
304+
305+ finalMessage . push ( '' )
306+ } )
307+
308+ return finalMessage . join ( '\n' )
309+ }
310+
115311main ( ) . then ( ( result ) => {
116312 console . log ( result . content )
117313} )
0 commit comments