Skip to content

Commit 301e511

Browse files
authored
chore: improve the changelog script (#4561)
* adds automatic group headings * handles multi-line commit messages better * drops support for multi-line markdown output * adds `--release-notes` flag that omits linkifying usernames to allow the 'Contributors' section in release notes to be populated correctly
1 parent c830694 commit 301e511

File tree

1 file changed

+102
-25
lines changed

1 file changed

+102
-25
lines changed

scripts/changelog.js

Lines changed: 102 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
'use strict'
22

3+
const execSync = require('child_process').execSync
4+
35
/*
46
Usage:
57
@@ -12,32 +14,32 @@ Ordinarily this is run via the gen-changelog shell script, which appends
1214
the result to the changelog.
1315
1416
*/
15-
const execSync = require('child_process').execSync
16-
const branch = process.argv[2] || 'origin/latest'
17-
const log = execSync(`git log --reverse --pretty='format:%h' ${branch}...`)
18-
.toString()
19-
.split(/\n/)
20-
21-
function printCommit (c) {
22-
console.log(`* [\`${c.hash}\`](${c.url})`)
23-
for (const pr of c.prs) {
24-
console.log(` [#${pr.number}](${pr.url})`)
25-
// remove the (#111) relating to this pull request from the commit message,
26-
// since we manually add the link outside of the commit message
27-
const msgRe = new RegExp(`\\s*\\(#${pr.number}\\)`, 'g')
28-
c.message = c.message.replace(msgRe, '')
17+
18+
const parseArgs = (argv) => {
19+
const result = {
20+
releaseNotes: false,
21+
branch: 'origin/latest',
2922
}
30-
// no need to indent this output, it's already got 2 spaces
31-
console.log(c.message)
32-
// no credit for deps commits, leading spaces are important here
33-
if (!c.message.startsWith(' deps')) {
34-
for (const user of c.credit) {
35-
console.log(` ([${user.name}](${user.url}))`)
23+
24+
for (const arg of argv) {
25+
if (arg === '--release-notes') {
26+
result.releaseNotes = true
27+
continue
3628
}
29+
30+
result.branch = arg
3731
}
32+
33+
return result
3834
}
3935

4036
const main = async () => {
37+
const { branch, releaseNotes } = parseArgs(process.argv.slice(2))
38+
39+
const log = execSync(`git log --reverse --pretty='format:%h' ${branch}...`)
40+
.toString()
41+
.split(/\n/)
42+
4143
const query = `
4244
fragment commitCredit on GitObject {
4345
... on Commit {
@@ -75,14 +77,54 @@ const main = async () => {
7577
const response = execSync(`gh api graphql -f query='${query}'`).toString()
7678
const body = JSON.parse(response)
7779

80+
const output = {
81+
Features: [],
82+
'Bug Fixes': [],
83+
Documentation: [],
84+
Dependencies: [],
85+
}
86+
7887
for (const [hash, data] of Object.entries(body.data.repository)) {
88+
if (!data) {
89+
console.error('no data for hash', hash)
90+
continue
91+
}
92+
93+
const message = data.message.replace(/^\s+/gm, '') // remove leading spaces
94+
.replace(/(\r?\n)+/gm, '\n') // replace multiple newlines with one
95+
.replace(/([^\s]+@\d+\.\d+\.\d+.*)/gm, '`$1`') // wrap package@version in backticks
96+
97+
const lines = message.split('\n')
98+
// the title is the first line of the commit, 'let' because we change it later
99+
let title = lines.shift()
100+
// the body is the rest of the commit with some normalization
101+
const body = lines.join('\n') // re-join our normalized commit into a string
102+
.split(/\n?\*/gm) // split on lines starting with a literal *
103+
.filter((line) => line.trim().length > 0) // remove blank lines
104+
.map((line) => {
105+
const clean = line.replace(/\n/gm, ' ') // replace new lines for this bullet with spaces
106+
return clean.startsWith('*') ? clean : `* ${clean}` // make sure the line starts with *
107+
})
108+
.join('\n') // re-join with new lines
109+
110+
const type = title.startsWith('feat') ? 'Features'
111+
: title.startsWith('fix') ? 'Bug Fixes'
112+
: title.startsWith('docs') ? 'Documentation'
113+
: title.startsWith('deps') ? 'Dependencies'
114+
: null
115+
116+
const prs = data.associatedPullRequests.nodes.filter((pull) => pull.merged)
117+
for (const pr of prs) {
118+
title = title.replace(new RegExp(`\\s*\\(#${pr.number}\\)`, 'g'), '')
119+
}
120+
79121
const commit = {
80122
hash: hash.slice(1), // remove leading _
81123
url: data.url,
82-
message: data.message.replace(/(\r?\n)+/gm, '\n') // swap multiple new lines with one
83-
.replace(/^/gm, ' ') // add two spaces to the start of each line
84-
.replace(/([^\s]+@\d+\.\d+\.\d+.*)/g, '`$1`'), // wrap package@version in backticks
85-
prs: data.associatedPullRequests.nodes.filter((pull) => pull.merged),
124+
title,
125+
type,
126+
body,
127+
prs,
86128
credit: data.authors.nodes.map((author) => {
87129
if (author.user && author.user.login) {
88130
return {
@@ -100,7 +142,42 @@ const main = async () => {
100142
}),
101143
}
102144

103-
printCommit(commit)
145+
if (commit.type) {
146+
output[commit.type].push(commit)
147+
}
148+
}
149+
150+
for (const key of Object.keys(output)) {
151+
if (output[key].length > 0) {
152+
const groupHeading = `### ${key}`
153+
console.group(groupHeading)
154+
console.log() // blank line after heading
155+
156+
for (const commit of output[key]) {
157+
let groupCommit = `* [\`${commit.hash}\`](${commit.url})`
158+
for (const pr of commit.prs) {
159+
groupCommit += ` [#${pr.number}](${pr.url})`
160+
}
161+
groupCommit += ` ${commit.title}`
162+
if (key !== 'Dependencies') {
163+
for (const user of commit.credit) {
164+
if (releaseNotes) {
165+
groupCommit += ` (${user.name})`
166+
} else {
167+
groupCommit += ` ([${user.name}](${user.url}))`
168+
}
169+
}
170+
}
171+
console.group(groupCommit)
172+
if (commit.body && commit.body.length) {
173+
console.log(commit.body)
174+
}
175+
console.groupEnd(groupCommit)
176+
}
177+
178+
console.log() // blank line at end of group
179+
console.groupEnd(groupHeading)
180+
}
104181
}
105182
}
106183

0 commit comments

Comments
 (0)