From 9ee6605d59e8d595e052ce6ee08f51be8ddef883 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 8 Apr 2020 22:52:49 -0700 Subject: [PATCH 01/12] git-node: add release promotion step --- components/git/release.js | 42 +++++- lib/promote_release.js | 300 ++++++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 335 insertions(+), 8 deletions(-) create mode 100644 lib/promote_release.js diff --git a/components/git/release.js b/components/git/release.js index d711d11a9..f88a90a3f 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -2,12 +2,17 @@ const yargs = require('yargs'); +const auth = require('../../lib/auth'); const CLI = require('../../lib/cli'); const ReleasePreparation = require('../../lib/prepare_release'); +const ReleasePromotion = require('../../lib/promote_release'); +const TeamInfo = require('../../lib/team_info'); +const Request = require('../../lib/request'); const { runPromise } = require('../../lib/run'); const PREPARE = 'prepare'; const PROMOTE = 'promote'; +const RELEASERS = 'releasers'; const releaseOptions = { prepare: { @@ -27,10 +32,14 @@ const releaseOptions = { function builder(yargs) { return yargs .options(releaseOptions).positional('newVersion', { - describe: 'Version number of the release to be prepared or promoted' + describe: 'Version number of the release to be prepared' + }).positional('prid', { + describe: 'PR number of the release to be promoted' }) .example('git node release --prepare 1.2.3', - 'Prepare a new release of Node.js tagged v1.2.3'); + 'Prepare a new release of Node.js tagged v1.2.3') + .example('git node release --promote 12345', + 'Promote a prepared release of Node.js with PR #12345'); } function handler(argv) { @@ -59,7 +68,7 @@ function release(state, argv) { } module.exports = { - command: 'release [newVersion|options]', + command: 'release [newVersion|prid|options]', describe: 'Manage an in-progress release or start a new one.', builder, @@ -67,15 +76,17 @@ module.exports = { }; async function main(state, argv, cli, dir) { + let release; + if (state === PREPARE) { - const prep = new ReleasePreparation(argv, cli, dir); + release = new ReleasePreparation(argv, cli, dir); - if (prep.warnForWrongBranch()) return; + if (release.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}?`, + `Create release with new version ${release.newVersion}?`, { defaultAnswer: true }); if (!create) { @@ -84,8 +95,23 @@ async function main(state, argv, cli, dir) { } } - return prep.prepare(); + return release.prepare(); } else if (state === PROMOTE) { - // TODO(codebytere): implement release promotion. + release = new ReleasePromotion(argv, cli, 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'); + + return release.promote(); } } diff --git a/lib/promote_release.js b/lib/promote_release.js new file mode 100644 index 000000000..ed46ee45f --- /dev/null +++ b/lib/promote_release.js @@ -0,0 +1,300 @@ +'use strict'; + +const path = require('path'); +const { promises: fs } = require('fs'); +const semver = require('semver'); + +const { getMergedConfig } = require('./config'); +const { runSync } = require('./run'); +const auth = require('./auth'); +const PRData = require('./pr_data'); +const PRChecker = require('./pr_checker'); +const Request = require('./request'); + +const isWindows = process.platform === 'win32'; + +class ReleasePromotion { + constructor(argv, cli, dir) { + this.cli = cli; + this.dir = dir; + this.isLTS = false; + this.prid = argv.prid; + this.ltsCodename = ''; + this.date = ''; + this.config = getMergedConfig(this.dir); + } + + async promote() { + const { version, prid, cli } = this; + + // In the promotion stage, we can pull most relevant data + // from the release commit created in the preparation stage. + await this.parseDataFromReleaseCommit(); + + // Verify that PR is ready to promote. + cli.startSpinner('Verifying PR promotion readiness'); + const { + jenkinsReady, + githubCIReady, + isApproved + } = await this.verifyPRAttributes(); + if (!jenkinsReady) { + cli.stopSpinner(`Jenkins CI is failing for #${prid}`); + const proceed = await cli.prompt('Do you want to proceed?'); + if (!proceed) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + } else if (!githubCIReady) { + cli.stopSpinner(`GitHub CI is failing for #${prid}`); + const proceed = await cli.prompt('Do you want to proceed?'); + if (!proceed) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + } else if (!isApproved) { + cli.stopSpinner(`#${prid} does not have sufficient approvals`); + const proceed = await cli.prompt('Do you want to proceed?'); + if (!proceed) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + } + cli.stopSpinner(`The release PR for ${version} is ready to promote!`); + + // Create and sign the release tag. + const shouldTagAndSignRelease = await cli.prompt( + 'Tag and sign the release?'); + if (!shouldTagAndSignRelease) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + this.secureTagRelease(); + + // Set up for next release. + cli.startSpinner('Setting up for next release'); + await this.setupForNextRelease(); + cli.startSpinner('Successfully set up for next release'); + + const shouldMergeProposalBranch = await cli.prompt( + 'Merge proposal branch into staging branch?'); + if (!shouldMergeProposalBranch) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + + // Merge vX.Y.Z-proposal into vX.x. + cli.startSpinner('Merging proposal branch'); + await this.mergeProposalBranch(); + cli.startSpinner('Merged proposal branch'); + + // Cherry pick release commit to master. + const shouldCherryPick = await cli.prompt( + 'Cherry-pick release commit to master?', { defaultAnswer: true }); + if (!shouldCherryPick) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + await this.cherryPickToMaster(); + + // Push release tag. + const shouldPushTag = await cli.prompt('Push release tag?', + { defaultAnswer: true }); + if (!shouldPushTag) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + this.pushReleaseTag(); + + // Promote and sign the release builds. + const shouldPromote = await cli.prompt('Promote and sign release builds?', + { defaultAnswer: true }); + if (!shouldPromote) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } + + const defaultKeyPath = '~/.ssh/node_id_rsa'; + const keyPath = await cli.prompt( + `Please enter the path to your ssh key (Default ${defaultKeyPath}): `, + { questionType: 'input', defaultAnswer: defaultKeyPath }); + this.promoteAndSignRelease(keyPath); + + cli.separator(); + cli.ok(`Release promotion for ${version} complete.\n`); + cli.info( + 'To finish this release, you\'ll need to: \n' + + ` 1) Check the release at: https://nodejs.org/dist/v${version}\n` + + ' 2) Create the blog post for nodejs.org\n' + + ' 3) Create the release on GitHub\n' + + 'Finally, proceed to Twitter and announce the new release!'); + } + + async verifyPRAttributes() { + const { cli, prid, owner, repo } = this; + + const credentials = await auth({ github: true }); + const request = new Request(credentials); + + const data = new PRData({ prid, owner, repo }, cli, request); + await data.getAll(); + + const checker = new PRChecker(cli, data, { prid, owner, repo }); + const jenkinsReady = checker.checkJenkinsCI(); + const githubCIReady = checker.checkGitHubCI(); + const isApproved = checker.checkReviewsAndWait(false /* checkComments */); + + return { + jenkinsReady, + githubCIReady, + isApproved + }; + } + + async parseDataFromReleaseCommit() { + const { cli } = this; + + const releaseCommitMessage = runSync( + 'git', ['log', '-n', '1', '--pretty=format:\'%s\'']).trim(); + + const components = releaseCommitMessage.split(' '); + + // Parse out release date. + if (!/\d{4}-\d{2}-\d{2}/.match(components[0])) { + cli.error(`Release commit contains invalid date: ${components[0]}`); + return; + } + this.date = components[0]; + + // Parse out release version. + const version = semver.clean(components[2]); + if (!semver.valid(version)) { + cli.error(`Release commit contains invalid semantic version: ${version}`); + return; + } + + this.version = version; + this.stagingBranch = `v${semver.major(version)}.x-staging`; + this.versionComponents = { + major: semver.major(version), + minor: semver.minor(version), + patch: semver.patch(version) + }; + + // Parse out LTS status and codename. + if (components.length === 5) { + this.isLTS = true; + this.ltsCodename = components[3]; + } + } + + getCommitSha(position = 0) { + return runSync('git', ['rev-parse', `HEAD~${position}`]); + } + + get owner() { + return this.config.owner || 'nodejs'; + } + + get repo() { + return this.config.repo || 'node'; + } + + get username() { + return this.config.username; + } + + secureTagRelease() { + const { version, isLTS, ltsCodename } = this; + + const secureTag = path.join( + __dirname, + '../node_modules/.bin/git-secure-tag' + (isWindows ? '.cmd' : '') + ); + + const releaseInfo = isLTS ? `'${ltsCodename}' (LTS)` : '(Current)'; + const secureTagOptions = [ + `v${version}`, + this.getCommitSha(), + '-sm', + `"${this.date} Node.js v${version} ${releaseInfo} Release"` + ]; + + return runSync(secureTag, secureTagOptions); + } + + // Set up the branch so that nightly builds are produced with the next + // version number and a pre-release tag. + async setupForNextRelease() { + const { versionComponents, prid } = this; + + // Update node_version.h for next patch release. + const filePath = path.resolve('src', 'node_version.h'); + const data = await fs.readFile(filePath, 'utf8'); + const arr = data.split('\n'); + + const patchVersion = versionComponents.patch + 1; + arr.forEach((line, idx) => { + if (line.includes('#define NODE_PATCH_VERSION')) { + arr[idx] = `#define NODE_PATCH_VERSION ${patchVersion}`; + } else if (line.includes('#define NODE_VERSION_IS_RELEASE')) { + arr[idx] = '#define NODE_VERSION_IS_RELEASE 0'; + } + }); + + await fs.writeFile(filePath, arr.join('\n')); + + const workingOnVersion = + `${versionComponents.major}.${versionComponents.minor}.${patchVersion}`; + + // Create 'Working On' commit. + runSync('git', ['add', filePath]); + return runSync('git', [ + 'commit', + '-m', + `Working on ${workingOnVersion}`, + '-m', + `PR-URL: https://github.com/nodejs/node/pull/${prid}` + ]); + } + + async mergeProposalBranch() { + const { stagingBranch, versionComponents, version } = this; + + const releaseBranch = `v${versionComponents.major}.x`; + const proposalBranch = `v${version}-proposal`; + + runSync('git', ['checkout', releaseBranch]); + runSync('git', ['merge', '--ff-only', proposalBranch]); + runSync('git', ['push', 'upstream', releaseBranch]); + runSync('git', ['checkout', stagingBranch]); + runSync('git', ['rebase', releaseBranch]); + runSync('git', ['push', 'upstream', stagingBranch]); + } + + pushReleaseTag() { + const { version } = this; + + const tagVersion = `v${version}`; + return runSync('git', ['push', 'upstream', tagVersion]); + } + + promoteAndSignRelease(keyPath) { + return runSync('./tools/release.sh', ['-i', keyPath]); + } + + async cherryPickToMaster() { + // Since we've committed the Working On commit, + // the release commit will be 1 removed from + // tip-of-tree (e.g HEAD~1). + const releaseCommitSha = this.getCommitSha(1); + runSync('git', ['checkout', 'master']); + + // There will be conflicts. + runSync('git', ['cherry-pick', releaseCommitSha]); + // TODO(codebytere): gracefully handle conflicts and + // wait for the releaser to resolve. + } +} + +module.exports = ReleasePromotion; diff --git a/package.json b/package.json index 6534c4613..49734f5e1 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "figures": "^3.2.0", "fs-extra": "^9.0.0", "ghauth": "^4.0.0", + "git-secure-tag": "^2.3.1", "inquirer": "^7.1.0", "listr": "^0.14.3", "listr-input": "^0.2.1", From 15c4cb0bc17ad676d7bd620d831e9102856ee3a6 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 9 Apr 2020 10:33:43 -0700 Subject: [PATCH 02/12] Move newVersion to named arg --- components/git/release.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index f88a90a3f..f5d19890a 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -26,17 +26,22 @@ const releaseOptions = { security: { describe: 'Demarcate the new security release as a security release', type: 'boolean' + }, + newVersion: { + describe: 'Version number of the release to be prepared', + type: 'string' } }; function builder(yargs) { return yargs - .options(releaseOptions).positional('newVersion', { - describe: 'Version number of the release to be prepared' - }).positional('prid', { - describe: 'PR number of the release to be promoted' + .options(releaseOptions).positional('prid', { + describe: 'PR number of the release to be promoted', + type: 'number' }) - .example('git node release --prepare 1.2.3', + .example('git node release --prepare --security', + 'Prepare a new security release of Node.js with auto-determined version') + .example('git node release --prepare --newVersion=1.2.3', 'Prepare a new release of Node.js tagged v1.2.3') .example('git node release --promote 12345', 'Promote a prepared release of Node.js with PR #12345'); @@ -68,7 +73,7 @@ function release(state, argv) { } module.exports = { - command: 'release [newVersion|prid|options]', + command: 'release [prid|options]', describe: 'Manage an in-progress release or start a new one.', builder, From 0ec332753f8a27d6b213f890eaed9a41101f0df7 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Thu, 9 Apr 2020 10:33:56 -0700 Subject: [PATCH 03/12] Split out promotion verification steps --- lib/promote_release.js | 60 ++++++++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index ed46ee45f..ccf830136 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -25,42 +25,55 @@ class ReleasePromotion { } async promote() { - const { version, prid, cli } = this; - // In the promotion stage, we can pull most relevant data // from the release commit created in the preparation stage. await this.parseDataFromReleaseCommit(); + const { prid, cli, version } = this; + // Verify that PR is ready to promote. - cli.startSpinner('Verifying PR promotion readiness'); const { jenkinsReady, githubCIReady, isApproved } = await this.verifyPRAttributes(); + + cli.startSpinner('Verifying Jenkins CI status'); if (!jenkinsReady) { - cli.stopSpinner(`Jenkins CI is failing for #${prid}`); + cli.stopSpinner( + `Jenkins CI is failing for #${prid}`, cli.SPINNER_STATUS.FAILED); const proceed = await cli.prompt('Do you want to proceed?'); if (!proceed) { cli.warn(`Aborting release promotion for version ${version}`); return; } - } else if (!githubCIReady) { - cli.stopSpinner(`GitHub CI is failing for #${prid}`); + } + cli.stopSpinner('Jenkins CI is passing'); + + cli.startSpinner('Verifying GitHub CI status'); + if (!githubCIReady) { + cli.stopSpinner( + `GitHub CI is failing for #${prid}`, cli.SPINNER_STATUS.FAILED); const proceed = await cli.prompt('Do you want to proceed?'); if (!proceed) { cli.warn(`Aborting release promotion for version ${version}`); return; } - } else if (!isApproved) { - cli.stopSpinner(`#${prid} does not have sufficient approvals`); + } + cli.stopSpinner('GitHub CI is passing'); + + cli.startSpinner('Verifying PR approval status'); + if (!isApproved) { + cli.stopSpinner( + `#${prid} does not have sufficient approvals`, + cli.SPINNER_STATUS.FAILED); const proceed = await cli.prompt('Do you want to proceed?'); if (!proceed) { cli.warn(`Aborting release promotion for version ${version}`); return; } } - cli.stopSpinner(`The release PR for ${version} is ready to promote!`); + cli.stopSpinner(`#${prid} has necessary approvals`); // Create and sign the release tag. const shouldTagAndSignRelease = await cli.prompt( @@ -74,7 +87,7 @@ class ReleasePromotion { // Set up for next release. cli.startSpinner('Setting up for next release'); await this.setupForNextRelease(); - cli.startSpinner('Successfully set up for next release'); + cli.stopSpinner('Successfully set up for next release'); const shouldMergeProposalBranch = await cli.prompt( 'Merge proposal branch into staging branch?'); @@ -86,7 +99,7 @@ class ReleasePromotion { // Merge vX.Y.Z-proposal into vX.x. cli.startSpinner('Merging proposal branch'); await this.mergeProposalBranch(); - cli.startSpinner('Merged proposal branch'); + cli.stopSpinner('Merged proposal branch'); // Cherry pick release commit to master. const shouldCherryPick = await cli.prompt( @@ -95,7 +108,17 @@ class ReleasePromotion { cli.warn(`Aborting release promotion for version ${version}`); return; } - await this.cherryPickToMaster(); + this.cherryPickToMaster(); + + // There will be cherry-pick conflicts the Releaser will + // need to resolve, so confirm they've been resolved before + // proceeding with next steps. + const didResolveConflicts = await cli.prompt( + 'Finished resolving cherry-pick conflicts?', { defaultAnswer: true }); + if (!didResolveConflicts) { + cli.warn(`Aborting release promotion for version ${version}`); + return; + } // Push release tag. const shouldPushTag = await cli.prompt('Push release tag?', @@ -142,7 +165,7 @@ class ReleasePromotion { const checker = new PRChecker(cli, data, { prid, owner, repo }); const jenkinsReady = checker.checkJenkinsCI(); const githubCIReady = checker.checkGitHubCI(); - const isApproved = checker.checkReviewsAndWait(false /* checkComments */); + const isApproved = checker.checkReviewsAndWait(new Date(), false); return { jenkinsReady, @@ -160,7 +183,7 @@ class ReleasePromotion { const components = releaseCommitMessage.split(' '); // Parse out release date. - if (!/\d{4}-\d{2}-\d{2}/.match(components[0])) { + if (!components[0].match(/\d{4}-\d{2}-\d{2}/)) { cli.error(`Release commit contains invalid date: ${components[0]}`); return; } @@ -283,17 +306,14 @@ class ReleasePromotion { return runSync('./tools/release.sh', ['-i', keyPath]); } - async cherryPickToMaster() { - // Since we've committed the Working On commit, - // the release commit will be 1 removed from - // tip-of-tree (e.g HEAD~1). + cherryPickToMaster() { + // Since we've committed the Working On commit, the release + // commit will be 1 removed from tip-of-tree (e.g HEAD~1). const releaseCommitSha = this.getCommitSha(1); runSync('git', ['checkout', 'master']); // There will be conflicts. runSync('git', ['cherry-pick', releaseCommitSha]); - // TODO(codebytere): gracefully handle conflicts and - // wait for the releaser to resolve. } } From a1c3ef1c76f7dff9576fad23aaa02b6cf2571559 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 14 Apr 2020 10:58:14 -0700 Subject: [PATCH 04/12] Fix verification spinners --- lib/promote_release.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index ccf830136..44cf947cc 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -47,8 +47,9 @@ class ReleasePromotion { cli.warn(`Aborting release promotion for version ${version}`); return; } + } else { + cli.stopSpinner('Jenkins CI is passing'); } - cli.stopSpinner('Jenkins CI is passing'); cli.startSpinner('Verifying GitHub CI status'); if (!githubCIReady) { @@ -59,8 +60,9 @@ class ReleasePromotion { cli.warn(`Aborting release promotion for version ${version}`); return; } + } else { + cli.stopSpinner('GitHub CI is passing'); } - cli.stopSpinner('GitHub CI is passing'); cli.startSpinner('Verifying PR approval status'); if (!isApproved) { @@ -72,8 +74,9 @@ class ReleasePromotion { cli.warn(`Aborting release promotion for version ${version}`); return; } + } else { + cli.stopSpinner(`#${prid} has necessary approvals`); } - cli.stopSpinner(`#${prid} has necessary approvals`); // Create and sign the release tag. const shouldTagAndSignRelease = await cli.prompt( From 61dbeb78a0bcfa21ed4ed83a49f119b56d7f9408 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 14 Apr 2020 11:01:34 -0700 Subject: [PATCH 05/12] Verify ncurc is set up --- components/git/release.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/components/git/release.js b/components/git/release.js index f5d19890a..d64ed1821 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -110,12 +110,19 @@ async function main(state, argv, cli, dir) { 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`); + if (release.username === undefined) { + cli.stopSpinner('Failed to verify Releaser status'); + cli.info( + 'Username was undefined - do you have your .ncurc set up correctly?'); return; + } else { + if (!releasers.some(r => r.login === release.username)) { + cli.stopSpinner( + `${release.username} is not a Releaser; aborting release`); + return; + } + cli.stopSpinner('Verified Releaser status'); } - cli.stopSpinner('Verified Releaser status'); return release.promote(); } From ea29f87ccb897997b819089eb8b41af002096957 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 14 Apr 2020 11:02:56 -0700 Subject: [PATCH 06/12] Properly trim getCommitSha result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Michaƫl Zasso --- lib/promote_release.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index 44cf947cc..0d7bef06d 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -215,7 +215,7 @@ class ReleasePromotion { } getCommitSha(position = 0) { - return runSync('git', ['rev-parse', `HEAD~${position}`]); + return runSync('git', ['rev-parse', `HEAD~${position}`]).trim(); } get owner() { From c0db1cecacd941e553e344940a546626629661be Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 4 May 2020 08:52:05 -0700 Subject: [PATCH 07/12] Address some feedback --- lib/promote_release.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index 0d7bef06d..6c2a2e57b 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -116,6 +116,13 @@ class ReleasePromotion { // There will be cherry-pick conflicts the Releaser will // need to resolve, so confirm they've been resolved before // proceeding with next steps. + cli.separator(); + cli.info(`After cherry-picking: + * The version macros in src/node_version.h should contain whatever values + were previously on master. + * NODE_VERSION_IS_RELEASE should be 0. + `); + cli.separator(); const didResolveConflicts = await cli.prompt( 'Finished resolving cherry-pick conflicts?', { defaultAnswer: true }); if (!didResolveConflicts) { @@ -186,11 +193,12 @@ class ReleasePromotion { const components = releaseCommitMessage.split(' '); // Parse out release date. - if (!components[0].match(/\d{4}-\d{2}-\d{2}/)) { + const match = components[0].match(/\d{4}-\d{2}-\d{2}/); + if (!match) { cli.error(`Release commit contains invalid date: ${components[0]}`); return; } - this.date = components[0]; + this.date = match[0]; // Parse out release version. const version = semver.clean(components[2]); @@ -243,7 +251,7 @@ class ReleasePromotion { `v${version}`, this.getCommitSha(), '-sm', - `"${this.date} Node.js v${version} ${releaseInfo} Release"` + `${this.date} Node.js v${version} ${releaseInfo} Release` ]; return runSync(secureTag, secureTagOptions); @@ -271,7 +279,7 @@ class ReleasePromotion { await fs.writeFile(filePath, arr.join('\n')); const workingOnVersion = - `${versionComponents.major}.${versionComponents.minor}.${patchVersion}`; + `v${versionComponents.major}.${versionComponents.minor}.${patchVersion}`; // Create 'Working On' commit. runSync('git', ['add', filePath]); From 7d23d70a6261d37aa669368c3c543e8b8b50fbd5 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 4 May 2020 08:58:11 -0700 Subject: [PATCH 08/12] Ensure synced with upstream/master --- lib/promote_release.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/promote_release.js b/lib/promote_release.js index 6c2a2e57b..06dd1d04e 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -323,6 +323,9 @@ class ReleasePromotion { const releaseCommitSha = this.getCommitSha(1); runSync('git', ['checkout', 'master']); + // Pull master from upstream, in case it's not up-to-date. + runSync('git', ['pull', '--rebase', 'upstream', 'master']); + // There will be conflicts. runSync('git', ['cherry-pick', releaseCommitSha]); } From 692c6b725246319733e5433ad044f4fc06862fb8 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Mon, 4 May 2020 09:00:21 -0700 Subject: [PATCH 09/12] fix: run release script asynchronously --- lib/promote_release.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index 06dd1d04e..ed08b7a18 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -5,7 +5,7 @@ const { promises: fs } = require('fs'); const semver = require('semver'); const { getMergedConfig } = require('./config'); -const { runSync } = require('./run'); +const { runSync, runAsync } = require('./run'); const auth = require('./auth'); const PRData = require('./pr_data'); const PRChecker = require('./pr_checker'); @@ -101,7 +101,7 @@ class ReleasePromotion { // Merge vX.Y.Z-proposal into vX.x. cli.startSpinner('Merging proposal branch'); - await this.mergeProposalBranch(); + this.mergeProposalBranch(); cli.stopSpinner('Merged proposal branch'); // Cherry pick release commit to master. @@ -151,7 +151,7 @@ class ReleasePromotion { const keyPath = await cli.prompt( `Please enter the path to your ssh key (Default ${defaultKeyPath}): `, { questionType: 'input', defaultAnswer: defaultKeyPath }); - this.promoteAndSignRelease(keyPath); + await this.promoteAndSignRelease(keyPath); cli.separator(); cli.ok(`Release promotion for ${version} complete.\n`); @@ -292,7 +292,7 @@ class ReleasePromotion { ]); } - async mergeProposalBranch() { + mergeProposalBranch() { const { stagingBranch, versionComponents, version } = this; const releaseBranch = `v${versionComponents.major}.x`; @@ -313,8 +313,8 @@ class ReleasePromotion { return runSync('git', ['push', 'upstream', tagVersion]); } - promoteAndSignRelease(keyPath) { - return runSync('./tools/release.sh', ['-i', keyPath]); + async promoteAndSignRelease(keyPath) { + await runAsync('./tools/release.sh', ['-i', keyPath]); } cherryPickToMaster() { From c1ab158b0bbf69a814df1958162d041bdd2902ec Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Wed, 20 May 2020 11:52:23 -0700 Subject: [PATCH 10/12] Run secureTagRelease() asynchronously --- lib/promote_release.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index ed08b7a18..b4d82b07c 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -238,7 +238,7 @@ class ReleasePromotion { return this.config.username; } - secureTagRelease() { + async secureTagRelease() { const { version, isLTS, ltsCodename } = this; const secureTag = path.join( @@ -254,7 +254,7 @@ class ReleasePromotion { `${this.date} Node.js v${version} ${releaseInfo} Release` ]; - return runSync(secureTag, secureTagOptions); + await runAsync(secureTag, secureTagOptions); } // Set up the branch so that nightly builds are produced with the next From ddfe6efdfc240ef352426869430e0e0a3f9d05f0 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 26 May 2020 08:49:05 -0700 Subject: [PATCH 11/12] Run cherry-pick asynchronously --- lib/promote_release.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index b4d82b07c..16c8023f1 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -111,7 +111,7 @@ class ReleasePromotion { cli.warn(`Aborting release promotion for version ${version}`); return; } - this.cherryPickToMaster(); + await this.cherryPickToMaster(); // There will be cherry-pick conflicts the Releaser will // need to resolve, so confirm they've been resolved before @@ -317,7 +317,7 @@ class ReleasePromotion { await runAsync('./tools/release.sh', ['-i', keyPath]); } - cherryPickToMaster() { + async cherryPickToMaster() { // Since we've committed the Working On commit, the release // commit will be 1 removed from tip-of-tree (e.g HEAD~1). const releaseCommitSha = this.getCommitSha(1); @@ -327,7 +327,7 @@ class ReleasePromotion { runSync('git', ['pull', '--rebase', 'upstream', 'master']); // There will be conflicts. - runSync('git', ['cherry-pick', releaseCommitSha]); + await runAsync('git', ['cherry-pick', releaseCommitSha]); } } From 3377b44ecfde06a108b73ff2795fa0e16cb2a6e5 Mon Sep 17 00:00:00 2001 From: Shelley Vohr Date: Tue, 26 May 2020 08:50:26 -0700 Subject: [PATCH 12/12] Remove unneccessary quotes --- lib/promote_release.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/promote_release.js b/lib/promote_release.js index 16c8023f1..66502c767 100644 --- a/lib/promote_release.js +++ b/lib/promote_release.js @@ -246,7 +246,7 @@ class ReleasePromotion { '../node_modules/.bin/git-secure-tag' + (isWindows ? '.cmd' : '') ); - const releaseInfo = isLTS ? `'${ltsCodename}' (LTS)` : '(Current)'; + const releaseInfo = isLTS ? `${ltsCodename} (LTS)` : '(Current)'; const secureTagOptions = [ `v${version}`, this.getCommitSha(),