|
@@ -63,6 +63,18 @@ const setPullRequest = (commit, owner, repo, number) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+// copied from https://github.com/electron/clerk/blob/master/src/index.ts#L4-L13
|
|
|
+const OMIT_FROM_RELEASE_NOTES_KEYS = [
|
|
|
+ 'no-notes',
|
|
|
+ 'no notes',
|
|
|
+ 'no_notes',
|
|
|
+ 'none',
|
|
|
+ 'no',
|
|
|
+ 'nothing',
|
|
|
+ 'empty',
|
|
|
+ 'blank'
|
|
|
+]
|
|
|
+
|
|
|
const getNoteFromBody = body => {
|
|
|
if (!body) {
|
|
|
return null
|
|
@@ -76,21 +88,15 @@ const getNoteFromBody = body => {
|
|
|
.find(paragraph => paragraph.startsWith(NOTE_PREFIX))
|
|
|
|
|
|
if (note) {
|
|
|
- const placeholder = '<!-- One-line Change Summary Here-->'
|
|
|
note = note
|
|
|
.slice(NOTE_PREFIX.length)
|
|
|
- .replace(placeholder, '')
|
|
|
+ .replace(/<!--.*-->/, '') // '<!-- change summary here-->'
|
|
|
.replace(/\r?\n/, ' ') // remove newlines
|
|
|
.trim()
|
|
|
}
|
|
|
|
|
|
- if (note) {
|
|
|
- if (note.match(/^[Nn]o[ _-][Nn]otes\.?$/)) {
|
|
|
- return NO_NOTES
|
|
|
- }
|
|
|
- if (note.match(/^[Nn]one\.?$/)) {
|
|
|
- return NO_NOTES
|
|
|
- }
|
|
|
+ if (note && OMIT_FROM_RELEASE_NOTES_KEYS.includes(note.toLowerCase())) {
|
|
|
+ return NO_NOTES
|
|
|
}
|
|
|
|
|
|
return note
|
|
@@ -137,8 +143,11 @@ const parseCommitMessage = (commitMessage, owner, repo, commit = {}) => {
|
|
|
|
|
|
// if the subject begins with 'word:', treat it as a semantic commit
|
|
|
if ((match = subject.match(/^(\w+):\s(.*)$/))) {
|
|
|
- commit.type = match[1].toLocaleLowerCase()
|
|
|
- subject = match[2]
|
|
|
+ const type = match[1].toLocaleLowerCase()
|
|
|
+ if (knownTypes.has(type)) {
|
|
|
+ commit.type = type
|
|
|
+ subject = match[2]
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// Check for GitHub commit message that indicates a PR
|
|
@@ -221,7 +230,6 @@ const parseCommitMessage = (commitMessage, owner, repo, commit = {}) => {
|
|
|
}
|
|
|
|
|
|
commit.subject = subject.trim()
|
|
|
-
|
|
|
return commit
|
|
|
}
|
|
|
|
|
@@ -389,7 +397,7 @@ const getDependencyCommits = async (pool, from, to) => {
|
|
|
**** Main
|
|
|
***/
|
|
|
|
|
|
-const getNotes = async (fromRef, toRef) => {
|
|
|
+const getNotes = async (fromRef, toRef, newVersion) => {
|
|
|
if (!fs.existsSync(CACHE_DIR)) {
|
|
|
fs.mkdirSync(CACHE_DIR)
|
|
|
}
|
|
@@ -437,6 +445,7 @@ const getNotes = async (fromRef, toRef) => {
|
|
|
// scrape PRs for release note 'Notes:' comments
|
|
|
for (const commit of pool.commits) {
|
|
|
let pr = commit.pr
|
|
|
+ let prSubject
|
|
|
while (pr && !commit.note) {
|
|
|
const prData = await getPullRequest(pr.number, pr.owner, pr.repo)
|
|
|
if (!prData || !prData.data) {
|
|
@@ -444,30 +453,73 @@ const getNotes = async (fromRef, toRef) => {
|
|
|
}
|
|
|
|
|
|
// try to pull a release note from the pull comment
|
|
|
- commit.note = getNoteFromBody(prData.data.body)
|
|
|
- if (commit.note) {
|
|
|
- break
|
|
|
- }
|
|
|
+ const prParsed = {}
|
|
|
+ parseCommitMessage(`${prData.data.title}\n\n${prData.data.body}`, pr.owner, pr.repo, prParsed)
|
|
|
+ commit.note = commit.note || prParsed.note
|
|
|
+ commit.type = commit.type || prParsed.type
|
|
|
+ prSubject = prSubject || prParsed.subject
|
|
|
|
|
|
- // if the PR references another PR, maybe follow it
|
|
|
- parseCommitMessage(`${prData.data.title}\n\n${prData.data.body}`, pr.owner, pr.repo, commit)
|
|
|
- pr = pr.number !== commit.pr.number ? commit.pr : null
|
|
|
+ pr = prParsed.pr && (prParsed.pr.number !== pr.number) ? prParsed.pr : null
|
|
|
}
|
|
|
+
|
|
|
+ // if we still don't have a note, it's because someone missed a 'Notes:
|
|
|
+ // comment in a PR somewhere... use the PR subject as a fallback.
|
|
|
+ commit.note = commit.note || prSubject
|
|
|
}
|
|
|
|
|
|
- // remove uninteresting commits
|
|
|
+ // remove non-user-facing commits
|
|
|
pool.commits = pool.commits
|
|
|
.filter(commit => commit.note !== NO_NOTES)
|
|
|
.filter(commit => !((commit.note || commit.subject).match(/^[Bb]ump v\d+\.\d+\.\d+/)))
|
|
|
|
|
|
+ // if this is a stable release,
|
|
|
+ // remove notes for changes that already landed in a previous major/minor series
|
|
|
+ if (semver.valid(newVersion) && !semver.prerelease(newVersion)) {
|
|
|
+ // load all the prDatas
|
|
|
+ await Promise.all(
|
|
|
+ pool.commits.map(commit => new Promise(async (resolve) => {
|
|
|
+ const { pr } = commit
|
|
|
+ if (typeof pr === 'object') {
|
|
|
+ const prData = await getPullRequest(pr.number, pr.owner, pr.repo)
|
|
|
+ if (prData) {
|
|
|
+ commit.prData = prData
|
|
|
+ }
|
|
|
+ }
|
|
|
+ resolve()
|
|
|
+ }))
|
|
|
+ )
|
|
|
+
|
|
|
+ // remove items that already landed in a previous major/minor series
|
|
|
+ pool.commits = pool.commits
|
|
|
+ .filter(commit => {
|
|
|
+ if (!commit.prData) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ const reducer = (accumulator, current) => {
|
|
|
+ if (!semver.valid(accumulator)) { return current }
|
|
|
+ if (!semver.valid(current)) { return accumulator }
|
|
|
+ return semver.lt(accumulator, current) ? accumulator : current
|
|
|
+ }
|
|
|
+ const earliestRelease = commit.prData.data.labels
|
|
|
+ .map(label => label.name.match(/merged\/(\d+)-(\d+)-x/))
|
|
|
+ .filter(label => !!label)
|
|
|
+ .map(label => `${label[1]}.${label[2]}.0`)
|
|
|
+ .reduce(reducer, null)
|
|
|
+ if (!semver.valid(earliestRelease)) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ return semver.diff(earliestRelease, newVersion).includes('patch')
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
const notes = {
|
|
|
- breaks: [],
|
|
|
+ breaking: [],
|
|
|
docs: [],
|
|
|
feat: [],
|
|
|
fix: [],
|
|
|
other: [],
|
|
|
unknown: [],
|
|
|
- ref: toRef
|
|
|
+ name: newVersion
|
|
|
}
|
|
|
|
|
|
pool.commits.forEach(commit => {
|
|
@@ -475,7 +527,7 @@ const getNotes = async (fromRef, toRef) => {
|
|
|
if (!str) {
|
|
|
notes.unknown.push(commit)
|
|
|
} else if (breakTypes.has(str)) {
|
|
|
- notes.breaks.push(commit)
|
|
|
+ notes.breaking.push(commit)
|
|
|
} else if (docTypes.has(str)) {
|
|
|
notes.docs.push(commit)
|
|
|
} else if (featTypes.has(str)) {
|
|
@@ -562,7 +614,7 @@ const renderCommit = (commit, explicitLinks) => {
|
|
|
}
|
|
|
|
|
|
const renderNotes = (notes, explicitLinks) => {
|
|
|
- const rendered = [ `# Release Notes for ${notes.ref}\n\n` ]
|
|
|
+ const rendered = [ `# Release Notes for ${notes.name}\n\n` ]
|
|
|
|
|
|
const renderSection = (title, commits) => {
|
|
|
if (commits.length === 0) {
|
|
@@ -582,7 +634,7 @@ const renderNotes = (notes, explicitLinks) => {
|
|
|
rendered.push(...lines.sort(), '\n')
|
|
|
}
|
|
|
|
|
|
- renderSection('Breaking Changes', notes.breaks)
|
|
|
+ renderSection('Breaking Changes', notes.breaking)
|
|
|
renderSection('Features', notes.feat)
|
|
|
renderSection('Fixes', notes.fix)
|
|
|
renderSection('Other Changes', notes.other)
|