From 4e6da655d2d5f9585b0535941e019018cd57cd10 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 26 Feb 2020 11:42:21 -0800 Subject: [PATCH 01/16] git-node: add git-node-release --- components/git/release.js | 102 +++++++++ lib/release.js | 427 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 530 insertions(+) create mode 100644 components/git/release.js create mode 100644 lib/release.js diff --git a/components/git/release.js b/components/git/release.js new file mode 100644 index 00000000..2dd49311 --- /dev/null +++ b/components/git/release.js @@ -0,0 +1,102 @@ +'use strict'; + +const semver = require('semver'); +const CLI = require('../../lib/cli'); +const Release = require('../../lib/release'); +const Request = require('../../lib/request'); +const { runPromise } = require('../../lib/run'); +const yargs = require('yargs'); + +const PREPARE = 'prepare'; +const PROMOTE = 'promote'; + +const releaseOptions = { + prepare: { + describe: 'Prepare a new release with the given version number', + type: 'boolean' + }, + security: { + describe: 'Prepare a new security release', + type: 'boolean' + } +}; + +function builder(yargs) { + return yargs + .options(releaseOptions).positional('newVersion', { + describe: 'Version number of the release to be created' + }) + .example('git node release 1.2.3', + 'Prepare a new release of Node.js tagged v1.2.3'); +} + +function handler(argv) { + if (argv.newVersion) { + const newVersion = semver.coerce(argv.newVersion); + if (semver.valid(newVersion)) { + return release(PREPARE, argv); + } + } + + // If more than one action is provided or no valid action + // is provided, show help. + yargs.showHelp(); +} + +function release(state, argv) { + const logStream = process.stdout.isTTY ? process.stdout : process.stderr; + const cli = new CLI(logStream); + + const req = new Request(); + const dir = process.cwd(); + + return runPromise(main(state, argv, cli, req, dir)).catch((err) => { + if (cli.spinner.enabled) { + cli.spinner.fail(); + } + throw err; + }); +} + +module.exports = { + command: 'release [newVersion|options]', + describe: + 'Manage an in-progress release or start a new one.', + builder, + handler +}; + +async function main(state, argv, cli, req, dir) { + const release = new Release(state, argv, cli, req, dir); + + // TODO(codebytere): check if this command is being run by + // someone on the Releasers team in GitHub before proceeding. + + if (state === PREPARE) { + if (release.warnForWrongBranch()) return; + + // Check the branch diff to determine if the releaser + // wants to backport any more commits before proceeding. + cli.startSpinner('Fetching branch-diff'); + const raw = release.checkBranchDiff(); + const diff = raw.split('*'); + cli.stopSpinner('Got branch diff'); + + const staging = `v${semver.major(argv.newVersion)}.x-staging`; + const proceed = await cli.prompt( + `There are ${diff.length} commits that may be backported ` + + `to ${staging} - do you still want to proceed?`, + false); + + if (!proceed) { + const seeDiff = await cli.prompt( + 'Do you want to see the branch diff?', true); + if (seeDiff) console.log(raw); + return; + } + + return release.prepare(); + } else if (state === PROMOTE) { + return release.promote(); + } +} diff --git a/lib/release.js b/lib/release.js new file mode 100644 index 00000000..90535c11 --- /dev/null +++ b/lib/release.js @@ -0,0 +1,427 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs').promises; +const semver = require('semver'); +const replace = require('replace-in-file'); + +const { getMergedConfig } = require('./config'); +const { runAsync, runSync } = require('./run'); +const { writeJson, readJson } = require('./file'); + +class Release { + constructor(state, argv, cli, req, dir) { + this.cli = cli; + this.dir = dir; + this.newVersion = argv.newVersion; + this.isSecurityRelease = argv.security; + this.isLTS = false; + this.ltsCodename = ''; + this.date = ''; + this.config = getMergedConfig(this.dir); + + const { upstream, owner, repo, newVersion } = this; + + this.versionComponents = { + major: semver.major(newVersion), + minor: semver.minor(newVersion), + patch: semver.patch(newVersion) + }; + + this.stagingBranch = `v${this.versionComponents.major}.x-staging`; + + const upstreamHref = runSync('git', [ + 'config', '--get', + `remote.${upstream}.url`]).trim(); + if (!new RegExp(`${owner}/${repo}(?:.git)?$`).test(upstreamHref)) { + cli.warn('Remote repository URL does not point to the expected ' + + `repository ${owner}/${repo}`); + } + } + + async prepare() { + const { cli, newVersion, versionComponents } = this; + + // Create new proposal branch. + const shouldBranch = await cli.prompt( + `Create new proposal branch for ${newVersion}?`, true); + if (!shouldBranch) return this.abort(); + await this.createProposalBranch(); + + // Update version and release info in src/node_version.h. + const shouldUpdateNodeVersion = await cli.prompt( + `Update 'src/node_version.h' for ${newVersion}?`, true); + if (!shouldUpdateNodeVersion) return this.abort(); + await this.updateNodeVersion(); + + // Check whether to update NODE_MODULE_VERSION (default false). + const shouldUpdateNodeModuleVersion = await cli.prompt( + 'Update NODE_MODULE_VERSION?', false); + if (shouldUpdateNodeModuleVersion) { + const runtime = await cli.promptInput( + 'Specify runtime (ex. \'node\') for new NODE_MODULE_VERSION:', + false); + const variant = await cli.promptInput( + 'Specify variant (ex. \'v8_7.9\') for new NODE_MODULE_VERSION:', + false); + const versions = await cli.promptInput( + 'Specify versions (ex. \'14.0.0-pre\') for new NODE_MODULE_VERSION:', + false); + this.updateNodeModuleVersion(runtime, variant, versions); + } + + // Update any REPLACEME tags in the docs. + const shouldUpdateREPLACEMEs = await cli.prompt( + 'Update REPLACEME items in docs?', true); + if (!shouldUpdateREPLACEMEs) return this.abort(); + await this.updateREPLACEMEs(); + + this.date = await cli.promptInput( + 'Enter release date in YYYY-MM-DD format:'); + + // Update Changelogs + const shouldUpdateChangelogs = await cli.prompt( + 'Update changelogs?', true); + if (!shouldUpdateChangelogs) return this.abort(); + + cli.startSpinner('Updating CHANGELOG.md'); + await this.updateMainChangelog(); + cli.stopSpinner('Updated CHANGELOG.md'); + + cli.startSpinner(`Updating CHANGELOG_V${versionComponents.major}.md`); + await this.updateMajorChangelog(); + cli.stopSpinner(`Updated CHANGELOG_V${versionComponents.major}.md`); + + await cli.prompt('Finished editing the changelogs?', false); + + // Create release commit. + const shouldCreateReleaseCommit = await cli.prompt( + 'Create release commit?', true); + if (!shouldCreateReleaseCommit) return this.abort(); + + // Proceed with release only after the releaser has amended + // it to their liking. + const created = await this.createReleaseCommit(); + if (!created) { + await cli.prompt( + 'Type y when you have finished editing the release commit:', + false); + } + + // Open pull request against the release branch. + const shouldOpenPR = await cli.prompt( + 'Push branch and open pull request?', true); + if (!shouldOpenPR) return this.abort(); + this.openPullRequest(); + + cli.separator(); + cli.ok(`Release preparation for ${newVersion} complete.\n`); + cli.info( + 'Please proceed to Jenkins and begin the following CI jobs:\n' + + ' * https://ci.nodejs.org/job/node-test-pull-request/\n' + + ' * https://ci.nodejs.org/job/citgm-smoker/'); + cli.info( + 'If this release deps/v8 changes, you\'ll also need to run:\n' + + ' * https://ci.nodejs.org/job/node-test-commit-v8-linux/' + ); + } + + async promote() { + // TODO(codebytere): implement. + } + + get owner() { + return this.config.owner || 'nodejs'; + } + + get repo() { + return this.config.repo || 'node'; + } + + get upstream() { + return this.config.upstream; + } + + get username() { + return this.config.username; + } + + async abort() { + const { cli, newVersion } = this; + + // TODO(codebytere): figure out what kind of cleanup we want to do here. + + cli.ok(`Aborted \`git node release\` for version ${newVersion}`); + } + + getCurrentBranch() { + return runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim(); + } + + getLastRef() { + return runSync('git', ['describe', '--abbrev=0', '--tags']).trim(); + } + + getChangelog() { + return runSync('npx', [ + 'changelog-maker', + '--group', + '--start-ref', + this.getLastRef() + ]).trim(); + } + + openPullRequest() { + const { newVersion, upstream, cli, versionComponents } = this; + const proposalBranch = `v${newVersion}-proposal`; + const releaseBranch = `v${versionComponents.major}.x`; + + const pushed = runSync('git', ['push', upstream, proposalBranch]).trim(); + + if (pushed) { + runSync('open', + [`https://github.com/nodejs/node/compare/${releaseBranch}...${proposalBranch}?expand=1`]); + } else { + cli.warn(`Failed to push ${proposalBranch} to ${upstream}.`); + } + } + + async updateREPLACEMEs() { + const { newVersion } = this; + + await replace({ + files: 'doc/api/*.md', + from: /REPLACEME/g, + to: `v${newVersion}` + }); + } + + async updateMainChangelog() { + const { versionComponents, newVersion } = this; + + // Remove the leading 'v'. + const lastRef = this.getLastRef().substring(1); + + const mainChangelogPath = path.resolve('CHANGELOG.md'); + const data = await fs.readFile(mainChangelogPath, 'utf8'); + const arr = data.split('\n'); + + const hrefLink = `doc/changelogs/CHANGELOG_V${versionComponents.major}.md`; + const newRefLink = `${newVersion}`; + const lastRefLink = `${lastRef}`; + + for (let idx = 0; idx < arr.length; idx++) { + if (arr[idx].includes(`${lastRefLink}
`)) { + arr.splice(idx, 1, `${newRefLink}
`, `${lastRefLink}
`); + break; + } + }; + + await fs.writeFile(mainChangelogPath, arr.join('\n')); + } + + async updateMajorChangelog() { + const { + versionComponents, + newVersion, + date, + isLTS, + ltsCodename, + username + } = this; + + const releaseInfo = isLTS ? `'${ltsCodename}' (LTS)` : '(Current)'; + const lastRef = this.getLastRef(); + const majorChangelogPath = path.resolve( + 'doc', + 'changelogs', + `CHANGELOG_V${versionComponents.major}.md` + ); + + const data = await fs.readFile(majorChangelogPath, 'utf8'); + const arr = data.split('\n'); + + const allCommits = runSync('npx', [ + 'changelog-maker', + '--group', + '--filter-release', + '--start-ref', + lastRef + ]); + + const notableChanges = this.checkBranchDiff(true); + const releaseHeader = `## ${date}, Version ${newVersion}` + + ` ${releaseInfo}, @${username}\n`; + + for (let idx = 0; idx < arr.length; idx++) { + if (arr[idx].includes(``)) { + const toAppend = []; + toAppend.push(``); + toAppend.push(releaseHeader); + toAppend.push('### Notable Changes\n'); + toAppend.push(notableChanges); + toAppend.push('### Commits\n'); + toAppend.push(allCommits); + + arr.splice(idx, 0, ...toAppend); + break; + } + }; + + await fs.writeFile(majorChangelogPath, arr.join('\n')); + } + + async createProposalBranch() { + const { upstream, newVersion, stagingBranch } = this; + const proposalBranch = `v${newVersion}-proposal`; + + await runAsync('git', [ + 'checkout', + '-b', + proposalBranch, + `${upstream}/${stagingBranch}` + ]); + } + + async updateNodeVersion() { + const { versionComponents } = this; + + const filePath = path.resolve('src', 'node_version.h'); + const data = await fs.readFile(filePath, 'utf8'); + const arr = data.split('\n'); + + arr.forEach((line, idx) => { + if (line.includes('#define NODE_MAJOR_VERSION')) { + arr[idx] = `#define NODE_MAJOR_VERSION ${versionComponents.major}`; + } else if (line.includes('#define NODE_MINOR_VERSION')) { + arr[idx] = `#define NODE_MINOR_VERSION ${versionComponents.minor}`; + } else if (line.includes('#define NODE_PATCH_VERSION')) { + arr[idx] = `#define NODE_PATCH_VERSION ${versionComponents.patch}`; + } else if (line.includes('#define NODE_VERSION_IS_RELEASE')) { + arr[idx] = '#define NODE_VERSION_IS_RELEASE 1'; + } else if (line.includes('#define NODE_VERSION_IS_LTS')) { + this.isLTS = arr[idx].split(' ')[2] === 1; + this.ltsCodename = arr[idx + 1].split(' ')[2]; + } + }); + + await fs.writeFile(filePath, arr.join('\n')); + } + + updateNodeModuleVersion(runtime, variant, versions) { + const nmvFilePath = path.resolve('doc', 'abi_version_registry.json'); + const nmvArray = readJson(nmvFilePath).NODE_MODULE_VERSION; + + const latestNMV = nmvArray[0]; + const modules = latestNMV.modules + 1; + nmvArray.unshift({ modules, runtime, variant, versions }); + + writeJson(nmvFilePath, { NODE_MODULE_VERSION: nmvArray }); + } + + async createReleaseCommit() { + const { cli, isLTS, newVersion, isSecurityRelease, date } = this; + + const releaseType = isLTS ? 'LTS' : 'Current'; + const messageTitle = `${date} Version ${newVersion} (${releaseType})`; + + const messageBody = []; + if (isSecurityRelease) { + messageBody.push('This is a security release.\n\n'); + } + + messageBody.push('Notable changes:\n\n'); + + // TODO(codebytere): add notable changes formatted for plaintext. + + // Create commit and then allow releaser to amend. + runSync('git', ['add', '.']); + runSync('git', [ + 'commit', + '-m', + messageTitle, + '-m', + messageBody.join('') + ]); + + cli.log(`${messageTitle}\n\n${messageBody.join('')}`); + const useMessage = await cli.prompt('Continue with this commit message?'); + if (useMessage) { + return true; + } else { + const lastCommit = runSync('git', ['rev-parse', '--short', 'HEAD']); + cli.warn(`Please manually edit commit ${lastCommit} by running ` + + '`git commit --amend` before proceeding.'); + } + + return false; + } + + checkBranchDiff(onlyNotableChanges = false) { + const { versionComponents, stagingBranch, upstream, newVersion } = this; + + let branchDiffOptions; + if (onlyNotableChanges) { + const proposalBranch = `v${newVersion}-proposal`; + const releaseBranch = `v${versionComponents.major}.x`; + + branchDiffOptions = [ + 'branch-diff', + `${upstream}/${releaseBranch}`, + proposalBranch, + '--require-label=notable-change', + '-format=simple' + ]; + } else { + const excludeLabels = [ + 'semver-major', + `dont-land-on-v${versionComponents.major}.x`, + `backport-requested-v${versionComponents.major}.x` + ]; + + if (versionComponents.patch !== 0) { + excludeLabels.push('semver-minor'); + } + + branchDiffOptions = [ + 'branch-diff', + stagingBranch, + 'master', + `--exclude-label=${excludeLabels.join(',')}`, + '--filter-release', + '--format=simple' + ]; + } + + return runSync('npx', branchDiffOptions); + } + + warnForWrongBranch() { + const { cli, stagingBranch, versionComponents } = this; + const rev = this.getCurrentBranch(); + + if (rev === stagingBranch) { + return false; + } + + if (rev === 'HEAD') { + cli.warn( + 'You are in detached HEAD state. Please run git-node on a valid ' + + 'branch'); + return true; + } + + cli.warn( + 'You are trying to create a new release proposal branch for ' + + `v${versionComponents.major}, but you're checked out on ` + + `${rev} and not ${stagingBranch}.`); + cli.separator(); + cli.info( + `Switch to \`${stagingBranch}\` with \`git` + + `checkout ${stagingBranch}\` before proceeding.`); + cli.separator(); + return true; + } +} + +module.exports = Release; diff --git a/package.json b/package.json index d9ffe353..71a92d0f 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "mkdirp": "^0.5.1", "node-fetch": "^2.6.0", "ora": "^4.0.3", + "replace-in-file": "^5.0.2", "rimraf": "^3.0.0", "yargs": "^15.0.2" }, From 8a3bb80a4f637f5752182b0e70767424500afc59 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sun, 1 Mar 2020 09:26:15 -0800 Subject: [PATCH 02/16] feat: verify Releaser status --- components/git/release.js | 21 +++++++++++++++++---- lib/release.js | 14 +++++--------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index 2dd49311..8ff3c438 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -1,15 +1,20 @@ 'use strict'; const semver = require('semver'); +const yargs = require('yargs'); + +const auth = require('../../lib/auth'); const CLI = require('../../lib/cli'); const Release = require('../../lib/release'); const Request = require('../../lib/request'); +const TeamInfo = require('../../lib/team_info'); const { runPromise } = require('../../lib/run'); -const yargs = require('yargs'); const PREPARE = 'prepare'; const PROMOTE = 'promote'; +const RELEASERS = 'releasers'; + const releaseOptions = { prepare: { describe: 'Prepare a new release with the given version number', @@ -69,8 +74,16 @@ module.exports = { async function main(state, argv, cli, req, dir) { const release = new Release(state, argv, cli, req, dir); - // TODO(codebytere): check if this command is being run by - // someone on the Releasers team in GitHub before proceeding. + cli.startSpinner('Verifying Releaser status'); + const credentials = await auth({ github: true }); + const request = new Request(credentials); + const info = new TeamInfo(cli, request, 'nodejs', RELEASERS); + const releasers = await info.getMembers(); + if (!releasers.some(r => r.login === release.username)) { + cli.stopSpinner(`${release.username} is not a Releaser; aborting release`); + return; + } + cli.stopSpinner('Verified Releaser status'); if (state === PREPARE) { if (release.warnForWrongBranch()) return; @@ -91,7 +104,7 @@ async function main(state, argv, cli, req, dir) { if (!proceed) { const seeDiff = await cli.prompt( 'Do you want to see the branch diff?', true); - if (seeDiff) console.log(raw); + if (seeDiff) cli.log(raw); return; } diff --git a/lib/release.js b/lib/release.js index 90535c11..352df0e4 100644 --- a/lib/release.js +++ b/lib/release.js @@ -103,6 +103,10 @@ class Release { // it to their liking. const created = await this.createReleaseCommit(); if (!created) { + const lastCommitSha = runSync('git', ['rev-parse', '--short', 'HEAD']); + cli.warn(`Please manually edit commit ${lastCommitSha} by running ` + + '`git commit --amend` before proceeding.'); + await cli.prompt( 'Type y when you have finished editing the release commit:', false); @@ -346,15 +350,7 @@ class Release { cli.log(`${messageTitle}\n\n${messageBody.join('')}`); const useMessage = await cli.prompt('Continue with this commit message?'); - if (useMessage) { - return true; - } else { - const lastCommit = runSync('git', ['rev-parse', '--short', 'HEAD']); - cli.warn(`Please manually edit commit ${lastCommit} by running ` + - '`git commit --amend` before proceeding.'); - } - - return false; + return useMessage; } checkBranchDiff(onlyNotableChanges = false) { From 7318babca52ce87776399e7523638a007b2573b7 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sun, 1 Mar 2020 09:29:42 -0800 Subject: [PATCH 03/16] Address review feedback --- lib/release.js | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/release.js b/lib/release.js index 352df0e4..351900b1 100644 --- a/lib/release.js +++ b/lib/release.js @@ -304,7 +304,7 @@ class Release { } else if (line.includes('#define NODE_VERSION_IS_RELEASE')) { arr[idx] = '#define NODE_VERSION_IS_RELEASE 1'; } else if (line.includes('#define NODE_VERSION_IS_LTS')) { - this.isLTS = arr[idx].split(' ')[2] === 1; + this.isLTS = arr[idx].split(' ')[2] === '1'; this.ltsCodename = arr[idx + 1].split(' ')[2]; } }); @@ -354,34 +354,48 @@ class Release { } checkBranchDiff(onlyNotableChanges = false) { - const { versionComponents, stagingBranch, upstream, newVersion } = this; + const { + versionComponents, + stagingBranch, + upstream, + newVersion, + isLTS + } = this; let branchDiffOptions; if (onlyNotableChanges) { const proposalBranch = `v${newVersion}-proposal`; const releaseBranch = `v${versionComponents.major}.x`; + const notableLabels = [ + 'notable-change', + 'semver-minor' + ]; + branchDiffOptions = [ 'branch-diff', `${upstream}/${releaseBranch}`, proposalBranch, - '--require-label=notable-change', + `--require-label=${notableLabels.join(',')}`, '-format=simple' ]; } else { const excludeLabels = [ 'semver-major', `dont-land-on-v${versionComponents.major}.x`, - `backport-requested-v${versionComponents.major}.x` + `backport-requested-v${versionComponents.major}.x`, + `backported-to-v${versionComponents.major}.x`, + `backport-blocked-v${versionComponents.major}.x` ]; - if (versionComponents.patch !== 0) { + if (isLTS) { excludeLabels.push('semver-minor'); } branchDiffOptions = [ 'branch-diff', stagingBranch, + // TODO(codebytere): use Current branch instead of master for LTS 'master', `--exclude-label=${excludeLabels.join(',')}`, '--filter-release', From cfda0427212bc9120ce6a864ce80c88892078460 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sun, 1 Mar 2020 09:42:51 -0800 Subject: [PATCH 04/16] Add notables to commmit msg body --- components/git/release.js | 2 +- lib/release.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index 8ff3c438..3f709bcd 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -91,7 +91,7 @@ async function main(state, argv, cli, req, dir) { // Check the branch diff to determine if the releaser // wants to backport any more commits before proceeding. cli.startSpinner('Fetching branch-diff'); - const raw = release.checkBranchDiff(); + const raw = release.getBranchDiff(); const diff = raw.split('*'); cli.stopSpinner('Got branch diff'); diff --git a/lib/release.js b/lib/release.js index 351900b1..08d7afc4 100644 --- a/lib/release.js +++ b/lib/release.js @@ -108,7 +108,7 @@ class Release { '`git commit --amend` before proceeding.'); await cli.prompt( - 'Type y when you have finished editing the release commit:', + 'Finished editing the release commit?', false); } @@ -125,7 +125,7 @@ class Release { ' * https://ci.nodejs.org/job/node-test-pull-request/\n' + ' * https://ci.nodejs.org/job/citgm-smoker/'); cli.info( - 'If this release deps/v8 changes, you\'ll also need to run:\n' + + 'If this release has deps/v8 changes, you\'ll also need to run:\n' + ' * https://ci.nodejs.org/job/node-test-commit-v8-linux/' ); } @@ -253,7 +253,7 @@ class Release { lastRef ]); - const notableChanges = this.checkBranchDiff(true); + const notableChanges = this.getBranchDiff(true); const releaseHeader = `## ${date}, Version ${newVersion}` + ` ${releaseInfo}, @${username}\n`; @@ -334,9 +334,9 @@ class Release { messageBody.push('This is a security release.\n\n'); } + const notableChanges = this.getBranchDiff(true); messageBody.push('Notable changes:\n\n'); - - // TODO(codebytere): add notable changes formatted for plaintext. + messageBody.push(notableChanges); // Create commit and then allow releaser to amend. runSync('git', ['add', '.']); @@ -349,11 +349,11 @@ class Release { ]); cli.log(`${messageTitle}\n\n${messageBody.join('')}`); - const useMessage = await cli.prompt('Continue with this commit message?'); + const useMessage = await cli.prompt('Continue with this commit message?', true); return useMessage; } - checkBranchDiff(onlyNotableChanges = false) { + getBranchDiff(onlyNotableChanges = false) { const { versionComponents, stagingBranch, From f09a47c7eb1d40011969d52528effc3553afde92 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Sun, 1 Mar 2020 21:18:50 -0800 Subject: [PATCH 05/16] refactor: address review comments --- components/git/release.js | 56 ++++++++------------ lib/{release.js => prepare_release.js} | 73 ++++++++++++-------------- package.json | 1 + 3 files changed, 58 insertions(+), 72 deletions(-) rename lib/{release.js => prepare_release.js} (89%) diff --git a/components/git/release.js b/components/git/release.js index 3f709bcd..e78cd568 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -3,25 +3,24 @@ const semver = require('semver'); const yargs = require('yargs'); -const auth = require('../../lib/auth'); const CLI = require('../../lib/cli'); -const Release = require('../../lib/release'); -const Request = require('../../lib/request'); -const TeamInfo = require('../../lib/team_info'); +const ReleasePreparation = require('../../lib/prepare_release'); const { runPromise } = require('../../lib/run'); const PREPARE = 'prepare'; const PROMOTE = 'promote'; -const RELEASERS = 'releasers'; - const releaseOptions = { prepare: { describe: 'Prepare a new release with the given version number', type: 'boolean' }, + promote: { + describe: 'Promote new release with the given version number', + type: 'boolean' + }, security: { - describe: 'Prepare a new security release', + describe: 'Demarcate the new security release as a security release', type: 'boolean' } }; @@ -29,9 +28,9 @@ const releaseOptions = { function builder(yargs) { return yargs .options(releaseOptions).positional('newVersion', { - describe: 'Version number of the release to be created' + describe: 'Version number of the release to be prepared or promoted' }) - .example('git node release 1.2.3', + .example('git node release --prepare 1.2.3', 'Prepare a new release of Node.js tagged v1.2.3'); } @@ -39,7 +38,11 @@ function handler(argv) { if (argv.newVersion) { const newVersion = semver.coerce(argv.newVersion); if (semver.valid(newVersion)) { - return release(PREPARE, argv); + if (argv.prepare) { + return release(PREPARE, argv); + } else if (argv.promote) { + return release(PROMOTE, argv); + } } } @@ -51,11 +54,9 @@ function handler(argv) { function release(state, argv) { const logStream = process.stdout.isTTY ? process.stdout : process.stderr; const cli = new CLI(logStream); - - const req = new Request(); const dir = process.cwd(); - return runPromise(main(state, argv, cli, req, dir)).catch((err) => { + return runPromise(main(state, argv, cli, dir)).catch((err) => { if (cli.spinner.enabled) { cli.spinner.fail(); } @@ -71,34 +72,23 @@ module.exports = { handler }; -async function main(state, argv, cli, req, dir) { - const release = new Release(state, argv, cli, req, dir); - - cli.startSpinner('Verifying Releaser status'); - const credentials = await auth({ github: true }); - const request = new Request(credentials); - const info = new TeamInfo(cli, request, 'nodejs', RELEASERS); - const releasers = await info.getMembers(); - if (!releasers.some(r => r.login === release.username)) { - cli.stopSpinner(`${release.username} is not a Releaser; aborting release`); - return; - } - cli.stopSpinner('Verified Releaser status'); - +async function main(state, argv, cli, dir) { if (state === PREPARE) { - if (release.warnForWrongBranch()) return; + const prep = new ReleasePreparation(argv, cli, dir); + + if (prep.warnForWrongBranch()) return; // Check the branch diff to determine if the releaser // wants to backport any more commits before proceeding. cli.startSpinner('Fetching branch-diff'); - const raw = release.getBranchDiff(); + const raw = prep.getBranchDiff(); const diff = raw.split('*'); cli.stopSpinner('Got branch diff'); const staging = `v${semver.major(argv.newVersion)}.x-staging`; const proceed = await cli.prompt( - `There are ${diff.length} commits that may be backported ` + - `to ${staging} - do you still want to proceed?`, + `There are ${diff.length - 1} commits that may be ` + + `backported to ${staging} - do you still want to proceed?`, false); if (!proceed) { @@ -108,8 +98,8 @@ async function main(state, argv, cli, req, dir) { return; } - return release.prepare(); + return prep.prepare(); } else if (state === PROMOTE) { - return release.promote(); + // TODO(codebytere): implement release promotion. } } diff --git a/lib/release.js b/lib/prepare_release.js similarity index 89% rename from lib/release.js rename to lib/prepare_release.js index 08d7afc4..594b0813 100644 --- a/lib/release.js +++ b/lib/prepare_release.js @@ -9,8 +9,10 @@ const { getMergedConfig } = require('./config'); const { runAsync, runSync } = require('./run'); const { writeJson, readJson } = require('./file'); -class Release { - constructor(state, argv, cli, req, dir) { +const isWindows = process.platform === 'win32'; + +class ReleasePreparation { + constructor(argv, cli, dir) { this.cli = cli; this.dir = dir; this.newVersion = argv.newVersion; @@ -49,10 +51,9 @@ class Release { await this.createProposalBranch(); // Update version and release info in src/node_version.h. - const shouldUpdateNodeVersion = await cli.prompt( - `Update 'src/node_version.h' for ${newVersion}?`, true); - if (!shouldUpdateNodeVersion) return this.abort(); + cli.startSpinner(`Updating 'src/node_version.h' for ${newVersion}`); await this.updateNodeVersion(); + cli.stopSpinner(`Updating 'src/node_version.h' for ${newVersion}`); // Check whether to update NODE_MODULE_VERSION (default false). const shouldUpdateNodeModuleVersion = await cli.prompt( @@ -71,19 +72,14 @@ class Release { } // Update any REPLACEME tags in the docs. - const shouldUpdateREPLACEMEs = await cli.prompt( - 'Update REPLACEME items in docs?', true); - if (!shouldUpdateREPLACEMEs) return this.abort(); + cli.startSpinner('Updating REPLACEME items in docs'); await this.updateREPLACEMEs(); + cli.stopSpinner('Updated REPLACEME items in docs'); + // Fetch date to use in release commit & changelogs. this.date = await cli.promptInput( 'Enter release date in YYYY-MM-DD format:'); - // Update Changelogs - const shouldUpdateChangelogs = await cli.prompt( - 'Update changelogs?', true); - if (!shouldUpdateChangelogs) return this.abort(); - cli.startSpinner('Updating CHANGELOG.md'); await this.updateMainChangelog(); cli.stopSpinner('Updated CHANGELOG.md'); @@ -101,8 +97,8 @@ class Release { // Proceed with release only after the releaser has amended // it to their liking. - const created = await this.createReleaseCommit(); - if (!created) { + const createDefaultCommit = await this.createReleaseCommit(); + if (!createDefaultCommit) { const lastCommitSha = runSync('git', ['rev-parse', '--short', 'HEAD']); cli.warn(`Please manually edit commit ${lastCommitSha} by running ` + '`git commit --amend` before proceeding.'); @@ -130,10 +126,6 @@ class Release { ); } - async promote() { - // TODO(codebytere): implement. - } - get owner() { return this.config.owner || 'nodejs'; } @@ -167,9 +159,14 @@ class Release { } getChangelog() { - return runSync('npx', [ - 'changelog-maker', + const changelogMaker = path.join( + __dirname, + '../node_modules/.bin/changelog-maker' + (isWindows ? '.cmd' : '') + ); + + return runSync(changelogMaker, [ '--group', + '--filter-release', '--start-ref', this.getLastRef() ]).trim(); @@ -245,15 +242,8 @@ class Release { const data = await fs.readFile(majorChangelogPath, 'utf8'); const arr = data.split('\n'); - const allCommits = runSync('npx', [ - 'changelog-maker', - '--group', - '--filter-release', - '--start-ref', - lastRef - ]); - - const notableChanges = this.getBranchDiff(true); + const allCommits = this.getChangelog(); + const notableChanges = this.getBranchDiff({ onlyNotableChanges: true }); const releaseHeader = `## ${date}, Version ${newVersion}` + ` ${releaseInfo}, @${username}\n`; @@ -334,7 +324,7 @@ class Release { messageBody.push('This is a security release.\n\n'); } - const notableChanges = this.getBranchDiff(true); + const notableChanges = this.getBranchDiff({ onlyNotableChanges: true }); messageBody.push('Notable changes:\n\n'); messageBody.push(notableChanges); @@ -349,11 +339,12 @@ class Release { ]); cli.log(`${messageTitle}\n\n${messageBody.join('')}`); - const useMessage = await cli.prompt('Continue with this commit message?', true); + const useMessage = await cli.prompt( + 'Continue with this commit message?', true); return useMessage; } - getBranchDiff(onlyNotableChanges = false) { + getBranchDiff(opts) { const { versionComponents, stagingBranch, @@ -363,7 +354,7 @@ class Release { } = this; let branchDiffOptions; - if (onlyNotableChanges) { + if (opts.onlyNotableChanges) { const proposalBranch = `v${newVersion}-proposal`; const releaseBranch = `v${versionComponents.major}.x`; @@ -373,7 +364,6 @@ class Release { ]; branchDiffOptions = [ - 'branch-diff', `${upstream}/${releaseBranch}`, proposalBranch, `--require-label=${notableLabels.join(',')}`, @@ -388,12 +378,12 @@ class Release { `backport-blocked-v${versionComponents.major}.x` ]; - if (isLTS) { + const isSemverMinor = versionComponents.patch === 0; + if (isLTS && !isSemverMinor) { excludeLabels.push('semver-minor'); } branchDiffOptions = [ - 'branch-diff', stagingBranch, // TODO(codebytere): use Current branch instead of master for LTS 'master', @@ -403,7 +393,12 @@ class Release { ]; } - return runSync('npx', branchDiffOptions); + const branchDiff = path.join( + __dirname, + '../node_modules/.bin/branch-diff' + (isWindows ? '.cmd' : '') + ); + + return runSync(branchDiff, branchDiffOptions); } warnForWrongBranch() { @@ -434,4 +429,4 @@ class Release { } } -module.exports = Release; +module.exports = ReleasePreparation; diff --git a/package.json b/package.json index 71a92d0f..ad86c29c 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ ], "license": "MIT", "dependencies": { + "branch-diff": "^1.8.1", "chalk": "^3.0.0", "cheerio": "^1.0.0-rc.3", "clipboardy": "^2.1.0", From 09d5c0ba0c030fe8300f20f31d7bfc31c8524ab5 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 2 Mar 2020 09:51:12 -0800 Subject: [PATCH 06/16] fix: update for new cli.prompt --- components/git/release.js | 4 ++-- lib/prepare_release.js | 31 ++++++++++++++++--------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index e78cd568..b52ea6aa 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -89,11 +89,11 @@ async function main(state, argv, cli, dir) { const proceed = await cli.prompt( `There are ${diff.length - 1} commits that may be ` + `backported to ${staging} - do you still want to proceed?`, - false); + { defaultAnswer: false }); if (!proceed) { const seeDiff = await cli.prompt( - 'Do you want to see the branch diff?', true); + 'Do you want to see the branch diff?'); if (seeDiff) cli.log(raw); return; } diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 594b0813..9ae192d8 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -46,7 +46,7 @@ class ReleasePreparation { // Create new proposal branch. const shouldBranch = await cli.prompt( - `Create new proposal branch for ${newVersion}?`, true); + `Create new proposal branch for ${newVersion}?`); if (!shouldBranch) return this.abort(); await this.createProposalBranch(); @@ -57,17 +57,17 @@ class ReleasePreparation { // Check whether to update NODE_MODULE_VERSION (default false). const shouldUpdateNodeModuleVersion = await cli.prompt( - 'Update NODE_MODULE_VERSION?', false); + 'Update NODE_MODULE_VERSION?', { defaultAnswer: false }); if (shouldUpdateNodeModuleVersion) { - const runtime = await cli.promptInput( + const runtime = await cli.prompt( 'Specify runtime (ex. \'node\') for new NODE_MODULE_VERSION:', - false); - const variant = await cli.promptInput( + { questionType: 'input', noSeparator: true }); + const variant = await cli.prompt( 'Specify variant (ex. \'v8_7.9\') for new NODE_MODULE_VERSION:', - false); - const versions = await cli.promptInput( + { questionType: 'input', noSeparator: true }); + const versions = await cli.prompt( 'Specify versions (ex. \'14.0.0-pre\') for new NODE_MODULE_VERSION:', - false); + { questionType: 'input', noSeparator: true }); this.updateNodeModuleVersion(runtime, variant, versions); } @@ -77,8 +77,8 @@ class ReleasePreparation { cli.stopSpinner('Updated REPLACEME items in docs'); // Fetch date to use in release commit & changelogs. - this.date = await cli.promptInput( - 'Enter release date in YYYY-MM-DD format:'); + this.date = await cli.prompt('Enter release date in YYYY-MM-DD format:', + { questionType: 'input' }); cli.startSpinner('Updating CHANGELOG.md'); await this.updateMainChangelog(); @@ -88,11 +88,12 @@ class ReleasePreparation { await this.updateMajorChangelog(); cli.stopSpinner(`Updated CHANGELOG_V${versionComponents.major}.md`); - await cli.prompt('Finished editing the changelogs?', false); + await cli.prompt('Finished editing the changelogs?', + { defaultAnswer: false }); // Create release commit. const shouldCreateReleaseCommit = await cli.prompt( - 'Create release commit?', true); + 'Create release commit?'); if (!shouldCreateReleaseCommit) return this.abort(); // Proceed with release only after the releaser has amended @@ -105,12 +106,12 @@ class ReleasePreparation { await cli.prompt( 'Finished editing the release commit?', - false); + { defaultAnswer: false }); } // Open pull request against the release branch. const shouldOpenPR = await cli.prompt( - 'Push branch and open pull request?', true); + 'Push branch and open pull request?'); if (!shouldOpenPR) return this.abort(); this.openPullRequest(); @@ -340,7 +341,7 @@ class ReleasePreparation { cli.log(`${messageTitle}\n\n${messageBody.join('')}`); const useMessage = await cli.prompt( - 'Continue with this commit message?', true); + 'Continue with this commit message?'); return useMessage; } From ee5122238f477a7b54b8295664e5dca168b78503 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 2 Mar 2020 22:00:50 -0800 Subject: [PATCH 07/16] Create proposal branch automatically --- lib/prepare_release.js | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 9ae192d8..a78333d0 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -45,30 +45,26 @@ class ReleasePreparation { const { cli, newVersion, versionComponents } = this; // Create new proposal branch. - const shouldBranch = await cli.prompt( - `Create new proposal branch for ${newVersion}?`); - if (!shouldBranch) return this.abort(); + cli.startSpinner(`Creating new proposal branch for ${newVersion}`); await this.createProposalBranch(); + cli.stopSpinner(`Created new proposal branch for ${newVersion}`); // Update version and release info in src/node_version.h. cli.startSpinner(`Updating 'src/node_version.h' for ${newVersion}`); await this.updateNodeVersion(); - cli.stopSpinner(`Updating 'src/node_version.h' for ${newVersion}`); + cli.stopSpinner(`Updated 'src/node_version.h' for ${newVersion}`); // Check whether to update NODE_MODULE_VERSION (default false). const shouldUpdateNodeModuleVersion = await cli.prompt( 'Update NODE_MODULE_VERSION?', { defaultAnswer: false }); if (shouldUpdateNodeModuleVersion) { - const runtime = await cli.prompt( - 'Specify runtime (ex. \'node\') for new NODE_MODULE_VERSION:', - { questionType: 'input', noSeparator: true }); const variant = await cli.prompt( 'Specify variant (ex. \'v8_7.9\') for new NODE_MODULE_VERSION:', { questionType: 'input', noSeparator: true }); const versions = await cli.prompt( 'Specify versions (ex. \'14.0.0-pre\') for new NODE_MODULE_VERSION:', { questionType: 'input', noSeparator: true }); - this.updateNodeModuleVersion(runtime, variant, versions); + this.updateNodeModuleVersion('node', variant, versions); } // Update any REPLACEME tags in the docs. @@ -94,7 +90,10 @@ class ReleasePreparation { // Create release commit. const shouldCreateReleaseCommit = await cli.prompt( 'Create release commit?'); - if (!shouldCreateReleaseCommit) return this.abort(); + if (!shouldCreateReleaseCommit) { + cli.warn(`Aborting \`git node release\` for version ${newVersion}`); + return; + } // Proceed with release only after the releaser has amended // it to their liking. @@ -112,7 +111,10 @@ class ReleasePreparation { // Open pull request against the release branch. const shouldOpenPR = await cli.prompt( 'Push branch and open pull request?'); - if (!shouldOpenPR) return this.abort(); + if (!shouldOpenPR) { + cli.warn(`Aborting \`git node release\` for version ${newVersion}`); + return; + } this.openPullRequest(); cli.separator(); @@ -143,14 +145,6 @@ class ReleasePreparation { return this.config.username; } - async abort() { - const { cli, newVersion } = this; - - // TODO(codebytere): figure out what kind of cleanup we want to do here. - - cli.ok(`Aborted \`git node release\` for version ${newVersion}`); - } - getCurrentBranch() { return runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim(); } From f9c8cbbaac26f8fd02cc5d0fffc0bc4f26dde157 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 10 Mar 2020 18:25:27 -0700 Subject: [PATCH 08/16] Address some feedback from @mylesborins --- components/git/release.js | 4 ++-- lib/prepare_release.js | 40 ++++++++++++++++++++++++--------------- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index b52ea6aa..4c055b2b 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -36,7 +36,7 @@ function builder(yargs) { function handler(argv) { if (argv.newVersion) { - const newVersion = semver.coerce(argv.newVersion); + const newVersion = semver.clean(argv.newVersion); if (semver.valid(newVersion)) { if (argv.prepare) { return release(PREPARE, argv); @@ -81,7 +81,7 @@ async function main(state, argv, cli, dir) { // Check the branch diff to determine if the releaser // wants to backport any more commits before proceeding. cli.startSpinner('Fetching branch-diff'); - const raw = prep.getBranchDiff(); + const raw = prep.getBranchDiff({ onlyNotableChanges: false }); const diff = raw.split('*'); cli.stopSpinner('Got branch diff'); diff --git a/lib/prepare_release.js b/lib/prepare_release.js index a78333d0..73429698 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -15,7 +15,7 @@ class ReleasePreparation { constructor(argv, cli, dir) { this.cli = cli; this.dir = dir; - this.newVersion = argv.newVersion; + this.newVersion = semver.clean(argv.newVersion); this.isSecurityRelease = argv.security; this.isLTS = false; this.ltsCodename = ''; @@ -108,14 +108,23 @@ class ReleasePreparation { { defaultAnswer: false }); } + // Push branch to upstream. + const shouldPushBranch = await cli.prompt( + 'Push branch to upstream?'); + if (!shouldPushBranch) { + cli.warn(`Aborting \`git node release\` for version ${newVersion}`, + { defaultAnswer: false }); + return; + } + this.pushBranch(); + // Open pull request against the release branch. const shouldOpenPR = await cli.prompt( - 'Push branch and open pull request?'); - if (!shouldOpenPR) { - cli.warn(`Aborting \`git node release\` for version ${newVersion}`); - return; + `Open pull request against ${versionComponents.major}.x?`, + { defaultAnswer: false }); + if (shouldOpenPR) { + this.openPullRequest(); } - this.openPullRequest(); cli.separator(); cli.ok(`Release preparation for ${newVersion} complete.\n`); @@ -167,19 +176,20 @@ class ReleasePreparation { ]).trim(); } + pushBranch() { + const { newVersion, upstream } = this; + const proposalBranch = `v${newVersion}-proposal`; + + runSync('git', ['push', upstream, proposalBranch]).trim(); + } + openPullRequest() { - const { newVersion, upstream, cli, versionComponents } = this; + const { newVersion, versionComponents } = this; const proposalBranch = `v${newVersion}-proposal`; const releaseBranch = `v${versionComponents.major}.x`; - const pushed = runSync('git', ['push', upstream, proposalBranch]).trim(); - - if (pushed) { - runSync('open', - [`https://github.com/nodejs/node/compare/${releaseBranch}...${proposalBranch}?expand=1`]); - } else { - cli.warn(`Failed to push ${proposalBranch} to ${upstream}.`); - } + runSync('open', + [`https://github.com/nodejs/node/compare/${releaseBranch}...${proposalBranch}?expand=1`]); } async updateREPLACEMEs() { From 7e8f11a71ba63e0c34f584ced2ce21bd5899828a Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 10 Mar 2020 20:06:54 -0700 Subject: [PATCH 09/16] Automatically calculate version based on commits --- components/git/release.js | 50 +++++++++++------- lib/prepare_release.js | 106 ++++++++++++++++++++++++++++++-------- 2 files changed, 114 insertions(+), 42 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index 4c055b2b..f275a722 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -35,15 +35,10 @@ function builder(yargs) { } function handler(argv) { - if (argv.newVersion) { - const newVersion = semver.clean(argv.newVersion); - if (semver.valid(newVersion)) { - if (argv.prepare) { - return release(PREPARE, argv); - } else if (argv.promote) { - return release(PROMOTE, argv); - } - } + if (argv.prepare) { + return release(PREPARE, argv); + } else if (argv.promote) { + return release(PROMOTE, argv); } // If more than one action is provided or no valid action @@ -78,6 +73,18 @@ async function main(state, argv, cli, dir) { if (prep.warnForWrongBranch()) return; + // If the new version was automatically calculated, confirm it. + if (!argv.newVersion) { + const create = await cli.prompt( + `Create release with new version ${prep.newVersion}?`, + { defaultAnswer: true }); + + if (!create) { + cli.error('Aborting release preparation process'); + return; + } + } + // Check the branch diff to determine if the releaser // wants to backport any more commits before proceeding. cli.startSpinner('Fetching branch-diff'); @@ -85,17 +92,20 @@ async function main(state, argv, cli, dir) { const diff = raw.split('*'); cli.stopSpinner('Got branch diff'); - const staging = `v${semver.major(argv.newVersion)}.x-staging`; - const proceed = await cli.prompt( - `There are ${diff.length - 1} commits that may be ` + - `backported to ${staging} - do you still want to proceed?`, - { defaultAnswer: false }); - - if (!proceed) { - const seeDiff = await cli.prompt( - 'Do you want to see the branch diff?'); - if (seeDiff) cli.log(raw); - return; + const outstandingCommits = diff.length - 1; + if (outstandingCommits !== 0) { + const staging = `v${semver.major(prep.newVersion)}.x-staging`; + const proceed = await cli.prompt( + `There are ${outstandingCommits} commits that may be ` + + `backported to ${staging} - do you still want to proceed?`, + { defaultAnswer: false }); + + if (!proceed) { + const seeDiff = await cli.prompt( + 'Do you want to see the branch diff?'); + if (seeDiff) cli.log(raw); + return; + } } return prep.prepare(); diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 73429698..892742f8 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -15,13 +15,24 @@ class ReleasePreparation { constructor(argv, cli, dir) { this.cli = cli; this.dir = dir; - this.newVersion = semver.clean(argv.newVersion); this.isSecurityRelease = argv.security; this.isLTS = false; this.ltsCodename = ''; this.date = ''; this.config = getMergedConfig(this.dir); + // Allow passing optional new version. + if (argv.newVersion) { + const newVersion = semver.clean(argv.newVersion); + if (!semver.valid(newVersion)) { + cli.warn(`${newVersion} is not a valid semantic version.`); + return; + } + this.newVersion = newVersion; + } else { + this.newVersion = this.calculateNewVersion(); + } + const { upstream, owner, repo, newVersion } = this; this.versionComponents = { @@ -54,17 +65,20 @@ class ReleasePreparation { await this.updateNodeVersion(); cli.stopSpinner(`Updated 'src/node_version.h' for ${newVersion}`); - // Check whether to update NODE_MODULE_VERSION (default false). - const shouldUpdateNodeModuleVersion = await cli.prompt( - 'Update NODE_MODULE_VERSION?', { defaultAnswer: false }); - if (shouldUpdateNodeModuleVersion) { - const variant = await cli.prompt( - 'Specify variant (ex. \'v8_7.9\') for new NODE_MODULE_VERSION:', - { questionType: 'input', noSeparator: true }); - const versions = await cli.prompt( - 'Specify versions (ex. \'14.0.0-pre\') for new NODE_MODULE_VERSION:', - { questionType: 'input', noSeparator: true }); - this.updateNodeModuleVersion('node', variant, versions); + // Check whether to update NODE_MODULE_VERSION. + const isSemverMajor = versionComponents.minor === 0; + if (isSemverMajor) { + const shouldUpdateNodeModuleVersion = await cli.prompt( + 'Update NODE_MODULE_VERSION?', { defaultAnswer: false }); + if (shouldUpdateNodeModuleVersion) { + const variant = await cli.prompt( + 'Specify variant (ex. \'v8_7.9\') for new NODE_MODULE_VERSION:', + { questionType: 'input', noSeparator: true }); + const versions = await cli.prompt( + 'Specify versions (ex. \'14.0.0-pre\') for new NODE_MODULE_VERSION:', + { questionType: 'input', noSeparator: true }); + this.updateNodeModuleVersion('node', variant, versions); + } } // Update any REPLACEME tags in the docs. @@ -154,6 +168,32 @@ class ReleasePreparation { return this.config.username; } + calculateNewVersion() { + let newVersion; + + const lastTagVersion = semver.clean(this.getLastRef()); + const lastTag = { + major: semver.major(lastTagVersion), + minor: semver.minor(lastTagVersion), + patch: semver.patch(lastTagVersion) + }; + + const raw = this.getBranchDiff({ + onlyNotableChanges: false, + comparisonBranch: `v${lastTagVersion}` + }); + + if (raw.includes('SEMVER-MAJOR')) { + newVersion = `${lastTag.major + 1}.0.0`; + } else if (raw.includes('SEMVER-MINOR')) { + newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`; + } else { + newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`; + } + + return newVersion; + } + getCurrentBranch() { return runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim(); } @@ -253,7 +293,13 @@ class ReleasePreparation { ` ${releaseInfo}, @${username}\n`; for (let idx = 0; idx < arr.length; idx++) { - if (arr[idx].includes(``)) { + const topHeader = + `${lastRef.substring(1)}
`; + if (arr[idx].includes(topHeader)) { + const newHeader = + `${newVersion}
`; + arr.splice(idx, 1, newHeader); + } else if (arr[idx].includes(``)) { const toAppend = []; toAppend.push(``); toAppend.push(releaseHeader); @@ -261,6 +307,7 @@ class ReleasePreparation { toAppend.push(notableChanges); toAppend.push('### Commits\n'); toAppend.push(allCommits); + toAppend.push(''); arr.splice(idx, 0, ...toAppend); break; @@ -345,23 +392,33 @@ class ReleasePreparation { cli.log(`${messageTitle}\n\n${messageBody.join('')}`); const useMessage = await cli.prompt( - 'Continue with this commit message?'); + 'Continue with this commit message?', { defaultAnswer: false }); return useMessage; } getBranchDiff(opts) { const { - versionComponents, - stagingBranch, + versionComponents = {}, upstream, newVersion, isLTS } = this; + let majorVersion; + let stagingBranch; + if (Object.keys(versionComponents).length !== 0) { + majorVersion = versionComponents.major; + stagingBranch = this.stagingBranch; + } else { + stagingBranch = this.getCurrentBranch(); + const stagingBranchSemver = semver.coerce(stagingBranch); + majorVersion = stagingBranchSemver.major; + } + let branchDiffOptions; if (opts.onlyNotableChanges) { const proposalBranch = `v${newVersion}-proposal`; - const releaseBranch = `v${versionComponents.major}.x`; + const releaseBranch = `v${majorVersion}.x`; const notableLabels = [ 'notable-change', @@ -377,10 +434,10 @@ class ReleasePreparation { } else { const excludeLabels = [ 'semver-major', - `dont-land-on-v${versionComponents.major}.x`, - `backport-requested-v${versionComponents.major}.x`, - `backported-to-v${versionComponents.major}.x`, - `backport-blocked-v${versionComponents.major}.x` + `dont-land-on-v${majorVersion}.x`, + `backport-requested-v${majorVersion}.x`, + `backported-to-v${majorVersion}.x`, + `backport-blocked-v${majorVersion}.x` ]; const isSemverMinor = versionComponents.patch === 0; @@ -388,10 +445,15 @@ class ReleasePreparation { excludeLabels.push('semver-minor'); } + let comparisonBranch = 'master'; + if (opts.comparisonBranch) { + comparisonBranch = opts.comparisonBranch; + } + branchDiffOptions = [ stagingBranch, // TODO(codebytere): use Current branch instead of master for LTS - 'master', + comparisonBranch, `--exclude-label=${excludeLabels.join(',')}`, '--filter-release', '--format=simple' From 15bbc23bdf7dc4d4749a43cca737095dc2efb70f Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 11 Mar 2020 08:39:48 -0700 Subject: [PATCH 10/16] fix: create release commit w/o markdown --- components/git/release.js | 4 ++-- lib/prepare_release.js | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index f275a722..79609f3c 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -12,11 +12,11 @@ const PROMOTE = 'promote'; const releaseOptions = { prepare: { - describe: 'Prepare a new release with the given version number', + describe: 'Prepare a new release of Node.js', type: 'boolean' }, promote: { - describe: 'Promote new release with the given version number', + describe: 'Promote new release of Node.js', type: 'boolean' }, security: { diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 892742f8..823113ef 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -376,7 +376,10 @@ class ReleasePreparation { messageBody.push('This is a security release.\n\n'); } - const notableChanges = this.getBranchDiff({ onlyNotableChanges: true }); + const notableChanges = this.getBranchDiff({ + onlyNotableChanges: true, + simple: true + }); messageBody.push('Notable changes:\n\n'); messageBody.push(notableChanges); @@ -429,8 +432,11 @@ class ReleasePreparation { `${upstream}/${releaseBranch}`, proposalBranch, `--require-label=${notableLabels.join(',')}`, - '-format=simple' ]; + + if (opts.simple) { + branchDiffOptions.push('--simple') + } } else { const excludeLabels = [ 'semver-major', @@ -455,8 +461,7 @@ class ReleasePreparation { // TODO(codebytere): use Current branch instead of master for LTS comparisonBranch, `--exclude-label=${excludeLabels.join(',')}`, - '--filter-release', - '--format=simple' + '--filter-release' ]; } From 6cda82a2bf856073c6927375976836379ef6c3a3 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 11 Mar 2020 09:40:05 -0700 Subject: [PATCH 11/16] fix: use changelog to determine new version --- lib/prepare_release.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 823113ef..bfcfb41c 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -178,14 +178,11 @@ class ReleasePreparation { patch: semver.patch(lastTagVersion) }; - const raw = this.getBranchDiff({ - onlyNotableChanges: false, - comparisonBranch: `v${lastTagVersion}` - }); + const changelog = this.getChangelog(); - if (raw.includes('SEMVER-MAJOR')) { + if (changelog.includes('SEMVER-MAJOR')) { newVersion = `${lastTag.major + 1}.0.0`; - } else if (raw.includes('SEMVER-MINOR')) { + } else if (changelog.includes('SEMVER-MINOR')) { newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`; } else { newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`; @@ -431,11 +428,11 @@ class ReleasePreparation { branchDiffOptions = [ `${upstream}/${releaseBranch}`, proposalBranch, - `--require-label=${notableLabels.join(',')}`, + `--require-label=${notableLabels.join(',')}` ]; if (opts.simple) { - branchDiffOptions.push('--simple') + branchDiffOptions.push('--simple'); } } else { const excludeLabels = [ @@ -451,15 +448,10 @@ class ReleasePreparation { excludeLabels.push('semver-minor'); } - let comparisonBranch = 'master'; - if (opts.comparisonBranch) { - comparisonBranch = opts.comparisonBranch; - } - branchDiffOptions = [ stagingBranch, // TODO(codebytere): use Current branch instead of master for LTS - comparisonBranch, + 'master', `--exclude-label=${excludeLabels.join(',')}`, '--filter-release' ]; From c0693d736594d11df4521019dd8729951f30b89d Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 12 Mar 2020 09:02:34 -0700 Subject: [PATCH 12/16] fix: use Current branch instead of master for LTS comparison --- lib/prepare_release.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index bfcfb41c..8be7bc2a 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -443,15 +443,23 @@ class ReleasePreparation { `backport-blocked-v${majorVersion}.x` ]; + let comparisonBranch = 'master'; const isSemverMinor = versionComponents.patch === 0; - if (isLTS && !isSemverMinor) { - excludeLabels.push('semver-minor'); + if (isLTS) { + // Assume Current branch matches tag with highest semver value. + const tags = runSync('git', + ['tag', '-l', '--sort', '-version:refname']).trim(); + const highestVersionTag = tags.split('\n')[0]; + comparisonBranch = `v${semver.coerce(highestVersionTag).major}.x`; + + if (!isSemverMinor) { + excludeLabels.push('semver-minor'); + } } branchDiffOptions = [ stagingBranch, - // TODO(codebytere): use Current branch instead of master for LTS - 'master', + comparisonBranch, `--exclude-label=${excludeLabels.join(',')}`, '--filter-release' ]; From 66d6a06a97e6f40775ea7c39f5dd3ce83a48a5aa Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 17 Mar 2020 08:12:51 -0700 Subject: [PATCH 13/16] Move initial branchDiff to prepare --- components/git/release.js | 24 ------------------------ lib/prepare_release.js | 27 +++++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index 79609f3c..d711d11a 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -1,6 +1,5 @@ 'use strict'; -const semver = require('semver'); const yargs = require('yargs'); const CLI = require('../../lib/cli'); @@ -85,29 +84,6 @@ async function main(state, argv, cli, dir) { } } - // Check the branch diff to determine if the releaser - // wants to backport any more commits before proceeding. - cli.startSpinner('Fetching branch-diff'); - const raw = prep.getBranchDiff({ onlyNotableChanges: false }); - const diff = raw.split('*'); - cli.stopSpinner('Got branch diff'); - - const outstandingCommits = diff.length - 1; - if (outstandingCommits !== 0) { - const staging = `v${semver.major(prep.newVersion)}.x-staging`; - const proceed = await cli.prompt( - `There are ${outstandingCommits} commits that may be ` + - `backported to ${staging} - do you still want to proceed?`, - { defaultAnswer: false }); - - if (!proceed) { - const seeDiff = await cli.prompt( - 'Do you want to see the branch diff?'); - if (seeDiff) cli.log(raw); - return; - } - } - return prep.prepare(); } else if (state === PROMOTE) { // TODO(codebytere): implement release promotion. diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 8be7bc2a..6ee403cd 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -55,6 +55,33 @@ class ReleasePreparation { async prepare() { const { cli, newVersion, versionComponents } = this; + // Check the branch diff to determine if the releaser + // wants to backport any more commits before proceeding. + cli.startSpinner('Fetching branch-diff'); + const raw = this.getBranchDiff({ + onlyNotableChanges: false, + comparisonBranch: newVersion + }); + + const diff = raw.split('*'); + cli.stopSpinner('Got branch diff'); + + const outstandingCommits = diff.length - 1; + if (outstandingCommits !== 0) { + const staging = `v${semver.major(newVersion)}.x-staging`; + const proceed = await cli.prompt( + `There are ${outstandingCommits} commits that may be ` + + `backported to ${staging} - do you still want to proceed?`, + { defaultAnswer: false }); + + if (!proceed) { + const seeDiff = await cli.prompt( + 'Do you want to see the branch diff?'); + if (seeDiff) cli.log(raw); + return; + } + } + // Create new proposal branch. cli.startSpinner(`Creating new proposal branch for ${newVersion}`); await this.createProposalBranch(); From 079b860e5ddf37cca1e2fba351becce3f38dc418 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 19 Mar 2020 15:17:59 -0700 Subject: [PATCH 14/16] Remove push/pr automation --- lib/prepare_release.js | 38 +++----------------------------------- 1 file changed, 3 insertions(+), 35 deletions(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 6ee403cd..225bafbc 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -149,28 +149,12 @@ class ReleasePreparation { { defaultAnswer: false }); } - // Push branch to upstream. - const shouldPushBranch = await cli.prompt( - 'Push branch to upstream?'); - if (!shouldPushBranch) { - cli.warn(`Aborting \`git node release\` for version ${newVersion}`, - { defaultAnswer: false }); - return; - } - this.pushBranch(); - - // Open pull request against the release branch. - const shouldOpenPR = await cli.prompt( - `Open pull request against ${versionComponents.major}.x?`, - { defaultAnswer: false }); - if (shouldOpenPR) { - this.openPullRequest(); - } - cli.separator(); cli.ok(`Release preparation for ${newVersion} complete.\n`); cli.info( - 'Please proceed to Jenkins and begin the following CI jobs:\n' + + 'To finish the release proposal, run: \n' + + ` $ git push ${`v${semver.major(newVersion)}.x-staging`}\n` + + 'Finally, proceed to Jenkins and begin the following CI jobs:\n' + ' * https://ci.nodejs.org/job/node-test-pull-request/\n' + ' * https://ci.nodejs.org/job/citgm-smoker/'); cli.info( @@ -240,22 +224,6 @@ class ReleasePreparation { ]).trim(); } - pushBranch() { - const { newVersion, upstream } = this; - const proposalBranch = `v${newVersion}-proposal`; - - runSync('git', ['push', upstream, proposalBranch]).trim(); - } - - openPullRequest() { - const { newVersion, versionComponents } = this; - const proposalBranch = `v${newVersion}-proposal`; - const releaseBranch = `v${versionComponents.major}.x`; - - runSync('open', - [`https://github.com/nodejs/node/compare/${releaseBranch}...${proposalBranch}?expand=1`]); - } - async updateREPLACEMEs() { const { newVersion } = this; From 30dbaa8ed0434d996d0a84fcfa0811a3a4d1b866 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 24 Mar 2020 09:24:29 -0700 Subject: [PATCH 15/16] Fix accidental splice removal --- lib/prepare_release.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 225bafbc..4fdd18e2 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -290,7 +290,7 @@ class ReleasePreparation { if (arr[idx].includes(topHeader)) { const newHeader = `${newVersion}
`; - arr.splice(idx, 1, newHeader); + arr.splice(idx, 0, newHeader); } else if (arr[idx].includes(``)) { const toAppend = []; toAppend.push(``); From be4c95277b63b9c2602664f9ece2df002be5145b Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 24 Mar 2020 09:26:01 -0700 Subject: [PATCH 16/16] Add changelog-maker dep per @rvagg feedback --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ad86c29c..00e7bc46 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "dependencies": { "branch-diff": "^1.8.1", "chalk": "^3.0.0", + "changelog-maker": "^2.3.2", "cheerio": "^1.0.0-rc.3", "clipboardy": "^2.1.0", "core-validate-commit": "^3.13.1",