electron/script/release-notes/index.js
Charles Kerr 2acf9ac72f
fix: improve release notes (#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'
2019-01-10 14:01:38 -06:00

187 lines
5.6 KiB
JavaScript
Executable file

#!/usr/bin/env node
const { GitProcess } = require('dugite')
const minimist = require('minimist')
const path = require('path')
const semver = require('semver')
const notesGenerator = require('./notes.js')
const gitDir = path.resolve(__dirname, '..', '..')
const semverify = version => version.replace(/^origin\//, '').replace('x', '0').replace(/-/g, '.')
const runGit = async (args) => {
const response = await GitProcess.exec(args, gitDir)
if (response.exitCode !== 0) {
throw new Error(response.stderr.trim())
}
return response.stdout.trim()
}
const tagIsSupported = tag => tag && !tag.includes('nightly') && !tag.includes('unsupported')
const tagIsBeta = tag => tag.includes('beta')
const tagIsStable = tag => tagIsSupported(tag) && !tagIsBeta(tag)
const getTagsOf = async (point) => {
return (await runGit(['tag', '--merged', point]))
.split('\n')
.map(tag => tag.trim())
.filter(tag => semver.valid(tag))
.sort(semver.compare)
}
const getTagsOnBranch = async (point) => {
const masterTags = await getTagsOf('master')
if (point === 'master') {
return masterTags
}
const masterTagsSet = new Set(masterTags)
return (await getTagsOf(point)).filter(tag => !masterTagsSet.has(tag))
}
const getBranchOf = async (point) => {
const branches = (await runGit(['branch', '-a', '--contains', point]))
.split('\n')
.map(branch => branch.trim())
.filter(branch => !!branch)
const current = branches.find(branch => branch.startsWith('* '))
return current ? current.slice(2) : branches.shift()
}
const getAllBranches = async () => {
return (await runGit(['branch', '--remote']))
.split('\n')
.map(branch => branch.trim())
.filter(branch => !!branch)
.filter(branch => branch !== 'origin/HEAD -> origin/master')
.sort()
}
const getStabilizationBranches = async () => {
return (await getAllBranches())
.filter(branch => /^origin\/\d+-\d+-x$/.test(branch))
}
const getPreviousStabilizationBranch = async (current) => {
const stabilizationBranches = (await getStabilizationBranches())
.filter(branch => branch !== current && branch !== `origin/${current}`)
if (!semver.valid(current)) {
// since we don't seem to be on a stabilization branch right now,
// pick a placeholder name that will yield the newest branch
// as a comparison point.
current = 'v999.999.999'
}
let newestMatch = null
for (const branch of stabilizationBranches) {
if (semver.gte(semverify(branch), semverify(current))) {
continue
}
if (newestMatch && semver.lte(semverify(branch), semverify(newestMatch))) {
continue
}
newestMatch = branch
}
return newestMatch
}
const getPreviousPoint = async (point) => {
const currentBranch = await getBranchOf(point)
const currentTag = (await getTagsOf(point)).filter(tag => tagIsSupported(tag)).pop()
const currentIsStable = tagIsStable(currentTag)
try {
// First see if there's an earlier tag on the same branch
// that can serve as a reference point.
let tags = (await getTagsOnBranch(`${point}^`)).filter(tag => tagIsSupported(tag))
if (currentIsStable) {
tags = tags.filter(tag => tagIsStable(tag))
}
if (tags.length) {
return tags.pop()
}
} catch (error) {
console.log('error', error)
}
// Otherwise, use the newest stable release that preceeds this branch.
// To reach that you may have to walk past >1 branch, e.g. to get past
// 2-1-x which never had a stable release.
let branch = currentBranch
while (branch) {
const prevBranch = await getPreviousStabilizationBranch(branch)
const tags = (await getTagsOnBranch(prevBranch)).filter(tag => tagIsStable(tag))
if (tags.length) {
return tags.pop()
}
branch = prevBranch
}
}
async function getReleaseNotes (range, newVersion, explicitLinks) {
const rangeList = range.split('..') || ['HEAD']
const to = rangeList.pop()
const from = rangeList.pop() || (await getPreviousPoint(to))
if (!newVersion) {
newVersion = to
}
console.log(`Generating release notes between ${from} and ${to} for version ${newVersion}`)
const notes = await notesGenerator.get(from, to, newVersion)
const ret = {
text: notesGenerator.render(notes, explicitLinks)
}
if (notes.unknown.length) {
ret.warning = `You have ${notes.unknown.length} unknown release notes. Please fix them before releasing.`
}
return ret
}
async function main () {
const opts = minimist(process.argv.slice(2), {
boolean: [ 'explicit-links', 'help' ],
string: [ 'version' ]
})
opts.range = opts._.shift()
if (opts.help || !opts.range) {
const name = path.basename(process.argv[1])
console.log(`
easy usage: ${name} version
full usage: ${name} [begin..]end [--version version] [--explicit-links]
* 'begin' and 'end' are two git references -- tags, branches, etc --
from which the release notes are generated.
* if omitted, 'begin' defaults to the previous tag in end's branch.
* if omitted, 'version' defaults to 'end'. Specifying a version is
useful if you're making notes on a new version that isn't tagged yet.
* 'explicit-links' makes every note's issue, commit, or pull an MD link
For example, these invocations are equivalent:
${process.argv[1]} v4.0.1
${process.argv[1]} v4.0.0..v4.0.1 --version v4.0.1
`)
return 0
}
const notes = await getReleaseNotes(opts.range, opts.version, opts['explicit-links'])
console.log(notes.text)
if (notes.warning) {
throw new Error(notes.warning)
}
}
if (process.mainModule === module) {
main().catch((err) => {
console.error('Error Occurred:', err)
process.exit(1)
})
}
module.exports = getReleaseNotes