Skip to content

Commit 33e4a5b

Browse files
authored
script: improve release log generation (#83686)
1 parent ee87a4b commit 33e4a5b

File tree

1 file changed

+207
-11
lines changed

1 file changed

+207
-11
lines changed

scripts/generate-release-log.mjs

Lines changed: 207 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,108 @@
1+
// @ts-check
12
import 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+
318
async 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+
115311
main().then((result) => {
116312
console.log(result.content)
117313
})

0 commit comments

Comments
 (0)