X Tutup
Skip to content

Commit 2acf9ac

Browse files
authored
fix: improve release notes (electron#16343)
* fix: use version name in release notes * fix: omit previously-released notes * fix: sniff semantic commit types from PR subjects instead of only from commit messages * fix: do not use unrecognized semantic commit types * chore: do not hardcode Release-Notes comment text It used to be '<!-- One-line Change Summary Here-->', it's currently a link to a best-practices page, and it'll probably change again in the future. Let's just match on <!--.*--> instead. * chore: copyedit the help page * chore: use clerk's OMIT_FROM_RELEASE_NOTES_KEYS * chore: tweak comments * chore: rename 'breaks' property as 'breaking'
1 parent 102d8fe commit 2acf9ac

File tree

3 files changed

+116
-40
lines changed

3 files changed

+116
-40
lines changed

script/prepare-release.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,21 +50,21 @@ async function getNewVersion (dryRun) {
5050
}
5151
}
5252

53-
async function getReleaseNotes (currentBranch) {
53+
async function getReleaseNotes (currentBranch, newVersion) {
5454
if (bumpType === 'nightly') {
5555
return { text: 'Nightlies do not get release notes, please compare tags for info.' }
5656
}
5757
console.log(`Generating release notes for ${currentBranch}.`)
58-
const releaseNotes = await releaseNotesGenerator(currentBranch)
58+
const releaseNotes = await releaseNotesGenerator(currentBranch, newVersion)
5959
if (releaseNotes.warning) {
6060
console.warn(releaseNotes.warning)
6161
}
6262
return releaseNotes
6363
}
6464

6565
async function createRelease (branchToTarget, isBeta) {
66-
const releaseNotes = await getReleaseNotes(branchToTarget)
6766
const newVersion = await getNewVersion()
67+
const releaseNotes = await getReleaseNotes(branchToTarget, newVersion)
6868
await tagRelease(newVersion)
6969

7070
console.log(`Checking for existing draft release.`)
@@ -194,7 +194,8 @@ async function prepareRelease (isBeta, notesOnly) {
194194
} else {
195195
const currentBranch = (args.branch) ? args.branch : await getCurrentBranch(gitDir)
196196
if (notesOnly) {
197-
const releaseNotes = await getReleaseNotes(currentBranch)
197+
const newVersion = await getNewVersion(true)
198+
const releaseNotes = await getReleaseNotes(currentBranch, newVersion)
198199
console.log(`Draft release notes are: \n${releaseNotes.text}`)
199200
} else {
200201
const changes = await changesToRelease(currentBranch)

script/release-notes/index.js

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env node
22

33
const { GitProcess } = require('dugite')
4+
const minimist = require('minimist')
45
const path = require('path')
56
const semver = require('semver')
67

@@ -120,13 +121,17 @@ const getPreviousPoint = async (point) => {
120121
}
121122
}
122123

123-
async function getReleaseNotes (range, explicitLinks) {
124+
async function getReleaseNotes (range, newVersion, explicitLinks) {
124125
const rangeList = range.split('..') || ['HEAD']
125126
const to = rangeList.pop()
126127
const from = rangeList.pop() || (await getPreviousPoint(to))
127-
console.log(`Generating release notes between ${from} and ${to}`)
128128

129-
const notes = await notesGenerator.get(from, to)
129+
if (!newVersion) {
130+
newVersion = to
131+
}
132+
133+
console.log(`Generating release notes between ${from} and ${to} for version ${newVersion}`)
134+
const notes = await notesGenerator.get(from, to, newVersion)
130135
const ret = {
131136
text: notesGenerator.render(notes, explicitLinks)
132137
}
@@ -139,15 +144,33 @@ async function getReleaseNotes (range, explicitLinks) {
139144
}
140145

141146
async function main () {
142-
// TODO: minimist/commander
143-
const explicitLinks = process.argv.slice(2).some(arg => arg === '--explicit-links')
144-
if (process.argv.length > 4) {
145-
console.log('Use: script/release-notes/index.js [--explicit-links] [tag | tag1..tag2]')
146-
return 1
147+
const opts = minimist(process.argv.slice(2), {
148+
boolean: [ 'explicit-links', 'help' ],
149+
string: [ 'version' ]
150+
})
151+
opts.range = opts._.shift()
152+
if (opts.help || !opts.range) {
153+
const name = path.basename(process.argv[1])
154+
console.log(`
155+
easy usage: ${name} version
156+
157+
full usage: ${name} [begin..]end [--version version] [--explicit-links]
158+
159+
* 'begin' and 'end' are two git references -- tags, branches, etc --
160+
from which the release notes are generated.
161+
* if omitted, 'begin' defaults to the previous tag in end's branch.
162+
* if omitted, 'version' defaults to 'end'. Specifying a version is
163+
useful if you're making notes on a new version that isn't tagged yet.
164+
* 'explicit-links' makes every note's issue, commit, or pull an MD link
165+
166+
For example, these invocations are equivalent:
167+
${process.argv[1]} v4.0.1
168+
${process.argv[1]} v4.0.0..v4.0.1 --version v4.0.1
169+
`)
170+
return 0
147171
}
148172

149-
const range = process.argv[2] || 'HEAD'
150-
const notes = await getReleaseNotes(range, explicitLinks)
173+
const notes = await getReleaseNotes(opts.range, opts.version, opts['explicit-links'])
151174
console.log(notes.text)
152175
if (notes.warning) {
153176
throw new Error(notes.warning)

script/release-notes/notes.js

Lines changed: 78 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,18 @@ const setPullRequest = (commit, owner, repo, number) => {
6363
}
6464
}
6565

66+
// copied from https://github.com/electron/clerk/blob/master/src/index.ts#L4-L13
67+
const OMIT_FROM_RELEASE_NOTES_KEYS = [
68+
'no-notes',
69+
'no notes',
70+
'no_notes',
71+
'none',
72+
'no',
73+
'nothing',
74+
'empty',
75+
'blank'
76+
]
77+
6678
const getNoteFromBody = body => {
6779
if (!body) {
6880
return null
@@ -76,21 +88,15 @@ const getNoteFromBody = body => {
7688
.find(paragraph => paragraph.startsWith(NOTE_PREFIX))
7789

7890
if (note) {
79-
const placeholder = '<!-- One-line Change Summary Here-->'
8091
note = note
8192
.slice(NOTE_PREFIX.length)
82-
.replace(placeholder, '')
93+
.replace(/<!--.*-->/, '') // '<!-- change summary here-->'
8394
.replace(/\r?\n/, ' ') // remove newlines
8495
.trim()
8596
}
8697

87-
if (note) {
88-
if (note.match(/^[Nn]o[ _-][Nn]otes\.?$/)) {
89-
return NO_NOTES
90-
}
91-
if (note.match(/^[Nn]one\.?$/)) {
92-
return NO_NOTES
93-
}
98+
if (note && OMIT_FROM_RELEASE_NOTES_KEYS.includes(note.toLowerCase())) {
99+
return NO_NOTES
94100
}
95101

96102
return note
@@ -137,8 +143,11 @@ const parseCommitMessage = (commitMessage, owner, repo, commit = {}) => {
137143

138144
// if the subject begins with 'word:', treat it as a semantic commit
139145
if ((match = subject.match(/^(\w+):\s(.*)$/))) {
140-
commit.type = match[1].toLocaleLowerCase()
141-
subject = match[2]
146+
const type = match[1].toLocaleLowerCase()
147+
if (knownTypes.has(type)) {
148+
commit.type = type
149+
subject = match[2]
150+
}
142151
}
143152

144153
// Check for GitHub commit message that indicates a PR
@@ -221,7 +230,6 @@ const parseCommitMessage = (commitMessage, owner, repo, commit = {}) => {
221230
}
222231

223232
commit.subject = subject.trim()
224-
225233
return commit
226234
}
227235

@@ -389,7 +397,7 @@ const getDependencyCommits = async (pool, from, to) => {
389397
**** Main
390398
***/
391399

392-
const getNotes = async (fromRef, toRef) => {
400+
const getNotes = async (fromRef, toRef, newVersion) => {
393401
if (!fs.existsSync(CACHE_DIR)) {
394402
fs.mkdirSync(CACHE_DIR)
395403
}
@@ -437,45 +445,89 @@ const getNotes = async (fromRef, toRef) => {
437445
// scrape PRs for release note 'Notes:' comments
438446
for (const commit of pool.commits) {
439447
let pr = commit.pr
448+
let prSubject
440449
while (pr && !commit.note) {
441450
const prData = await getPullRequest(pr.number, pr.owner, pr.repo)
442451
if (!prData || !prData.data) {
443452
break
444453
}
445454

446455
// try to pull a release note from the pull comment
447-
commit.note = getNoteFromBody(prData.data.body)
448-
if (commit.note) {
449-
break
450-
}
456+
const prParsed = {}
457+
parseCommitMessage(`${prData.data.title}\n\n${prData.data.body}`, pr.owner, pr.repo, prParsed)
458+
commit.note = commit.note || prParsed.note
459+
commit.type = commit.type || prParsed.type
460+
prSubject = prSubject || prParsed.subject
451461

452-
// if the PR references another PR, maybe follow it
453-
parseCommitMessage(`${prData.data.title}\n\n${prData.data.body}`, pr.owner, pr.repo, commit)
454-
pr = pr.number !== commit.pr.number ? commit.pr : null
462+
pr = prParsed.pr && (prParsed.pr.number !== pr.number) ? prParsed.pr : null
455463
}
464+
465+
// if we still don't have a note, it's because someone missed a 'Notes:
466+
// comment in a PR somewhere... use the PR subject as a fallback.
467+
commit.note = commit.note || prSubject
456468
}
457469

458-
// remove uninteresting commits
470+
// remove non-user-facing commits
459471
pool.commits = pool.commits
460472
.filter(commit => commit.note !== NO_NOTES)
461473
.filter(commit => !((commit.note || commit.subject).match(/^[Bb]ump v\d+\.\d+\.\d+/)))
462474

475+
// if this is a stable release,
476+
// remove notes for changes that already landed in a previous major/minor series
477+
if (semver.valid(newVersion) && !semver.prerelease(newVersion)) {
478+
// load all the prDatas
479+
await Promise.all(
480+
pool.commits.map(commit => new Promise(async (resolve) => {
481+
const { pr } = commit
482+
if (typeof pr === 'object') {
483+
const prData = await getPullRequest(pr.number, pr.owner, pr.repo)
484+
if (prData) {
485+
commit.prData = prData
486+
}
487+
}
488+
resolve()
489+
}))
490+
)
491+
492+
// remove items that already landed in a previous major/minor series
493+
pool.commits = pool.commits
494+
.filter(commit => {
495+
if (!commit.prData) {
496+
return true
497+
}
498+
const reducer = (accumulator, current) => {
499+
if (!semver.valid(accumulator)) { return current }
500+
if (!semver.valid(current)) { return accumulator }
501+
return semver.lt(accumulator, current) ? accumulator : current
502+
}
503+
const earliestRelease = commit.prData.data.labels
504+
.map(label => label.name.match(/merged\/(\d+)-(\d+)-x/))
505+
.filter(label => !!label)
506+
.map(label => `${label[1]}.${label[2]}.0`)
507+
.reduce(reducer, null)
508+
if (!semver.valid(earliestRelease)) {
509+
return true
510+
}
511+
return semver.diff(earliestRelease, newVersion).includes('patch')
512+
})
513+
}
514+
463515
const notes = {
464-
breaks: [],
516+
breaking: [],
465517
docs: [],
466518
feat: [],
467519
fix: [],
468520
other: [],
469521
unknown: [],
470-
ref: toRef
522+
name: newVersion
471523
}
472524

473525
pool.commits.forEach(commit => {
474526
const str = commit.type
475527
if (!str) {
476528
notes.unknown.push(commit)
477529
} else if (breakTypes.has(str)) {
478-
notes.breaks.push(commit)
530+
notes.breaking.push(commit)
479531
} else if (docTypes.has(str)) {
480532
notes.docs.push(commit)
481533
} else if (featTypes.has(str)) {
@@ -562,7 +614,7 @@ const renderCommit = (commit, explicitLinks) => {
562614
}
563615

564616
const renderNotes = (notes, explicitLinks) => {
565-
const rendered = [ `# Release Notes for ${notes.ref}\n\n` ]
617+
const rendered = [ `# Release Notes for ${notes.name}\n\n` ]
566618

567619
const renderSection = (title, commits) => {
568620
if (commits.length === 0) {
@@ -582,7 +634,7 @@ const renderNotes = (notes, explicitLinks) => {
582634
rendered.push(...lines.sort(), '\n')
583635
}
584636

585-
renderSection('Breaking Changes', notes.breaks)
637+
renderSection('Breaking Changes', notes.breaking)
586638
renderSection('Features', notes.feat)
587639
renderSection('Fixes', notes.fix)
588640
renderSection('Other Changes', notes.other)

0 commit comments

Comments
 (0)
X Tutup