From 53e68b4737c59fae88c740330770f8245bde774b Mon Sep 17 00:00:00 2001 From: Richard Lau Date: Sun, 1 Nov 2020 08:40:34 +0000 Subject: [PATCH] feat: automate creation of the first LTS release (#514) Add an option to `git node release` that marks the release being created as the transition from Current to LTS. --- components/git/release.js | 4 + docs/git-node.md | 1 + lib/prepare_release.js | 97 +++++++++++++++++-- lib/release/utils.js | 61 ++++++++++++ .../release/expected-test-process-release.js | 26 +++++ .../release/original-test-process-release.js | 24 +++++ test/unit/prepare_release.test.js | 70 +++++++++++++ 7 files changed, 276 insertions(+), 7 deletions(-) create mode 100644 lib/release/utils.js create mode 100644 test/fixtures/release/expected-test-process-release.js create mode 100644 test/fixtures/release/original-test-process-release.js create mode 100644 test/unit/prepare_release.test.js diff --git a/components/git/release.js b/components/git/release.js index d711d11a..0d7fd347 100644 --- a/components/git/release.js +++ b/components/git/release.js @@ -21,6 +21,10 @@ const releaseOptions = { security: { describe: 'Demarcate the new security release as a security release', type: 'boolean' + }, + startLTS: { + describe: 'Mark the release as the transition from Current to LTS', + type: 'boolean' } }; diff --git a/docs/git-node.md b/docs/git-node.md index dbc53e11..55a6074c 100644 --- a/docs/git-node.md +++ b/docs/git-node.md @@ -217,6 +217,7 @@ Options: --help Show help [boolean] --prepare Prepare a new release of Node.js [boolean] --security Demarcate the new security release as a security release [boolean] + --startLTS Mark the release as the transition from Current to LTS [boolean] ``` ### Example diff --git a/lib/prepare_release.js b/lib/prepare_release.js index 1e5d0a2f..57b9e73a 100644 --- a/lib/prepare_release.js +++ b/lib/prepare_release.js @@ -12,6 +12,11 @@ const { getUnmarkedDeprecations, updateDeprecations } = require('./deprecations'); +const { + getEOLDate, + getStartLTSBlurb, + updateTestProcessRelease +} = require('./release/utils'); const isWindows = process.platform === 'win32'; @@ -21,6 +26,7 @@ class ReleasePreparation { this.dir = dir; this.isSecurityRelease = argv.security; this.isLTS = false; + this.isLTSTransition = argv.startLTS; this.ltsCodename = ''; this.date = ''; this.config = getMergedConfig(this.dir); @@ -91,6 +97,20 @@ class ReleasePreparation { await this.createProposalBranch(); cli.stopSpinner(`Created new proposal branch for ${newVersion}`); + if (this.isLTSTransition) { + // For releases transitioning into LTS, fetch the new code name. + this.ltsCodename = await this.getLTSCodename(versionComponents.major); + // Update test for new LTS code name. + const testFile = path.resolve( + 'test', + 'parallel', + 'test-process-release.js' + ); + cli.startSpinner(`Updating ${testFile}`); + await this.updateTestProcessRelease(testFile); + cli.stopSpinner(`Updating ${testFile}`); + } + // Update version and release info in src/node_version.h. cli.startSpinner(`Updating 'src/node_version.h' for ${newVersion}`); await this.updateNodeVersion(); @@ -218,7 +238,7 @@ class ReleasePreparation { if (changelog.includes('SEMVER-MAJOR')) { newVersion = `${lastTag.major + 1}.0.0`; - } else if (changelog.includes('SEMVER-MINOR')) { + } else if (changelog.includes('SEMVER-MINOR') || this.isLTSTransition) { newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`; } else { newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`; @@ -249,6 +269,15 @@ class ReleasePreparation { ]).trim(); } + async getLTSCodename(version) { + const { cli } = this; + return await cli.prompt( + 'Enter the LTS code name for this release line\n' + + '(Refs: https://github.com/nodejs/Release/blob/master/CODENAMES.md):', + { questionType: 'input', noSeparator: true, defaultAnswer: '' } + ); + } + async updateREPLACEMEs() { const { newVersion } = this; @@ -260,7 +289,7 @@ class ReleasePreparation { } async updateMainChangelog() { - const { versionComponents, newVersion } = this; + const { date, isLTSTransition, versionComponents, newVersion } = this; // Remove the leading 'v'. const lastRef = this.getLastRef().substring(1); @@ -274,6 +303,20 @@ class ReleasePreparation { const lastRefLink = `${lastRef}`; for (let idx = 0; idx < arr.length; idx++) { + if (isLTSTransition) { + if (arr[idx].includes(hrefLink)) { + const eolDate = getEOLDate(date); + const eol = eolDate.toISOString().split('-').slice(0, 2).join('-'); + arr[idx] = arr[idx].replace('**Current**', '**Long Term Support**'); + arr[idx] = arr[idx].replace('"Current"', `"LTS Until ${eol}"`); + arr[idx] = arr[idx].replace('Current', 'LTS'); + } else if (arr[idx].includes('**Long Term Support**')) { + arr[idx] = arr[idx].replace( + '**Long Term Support**', + 'Long Term Support' + ); + } + } if (arr[idx].includes(`${lastRefLink}
`)) { arr.splice(idx, 1, `${newRefLink}
`, `${lastRefLink}
`); break; @@ -289,6 +332,7 @@ class ReleasePreparation { newVersion, date, isLTS, + isLTSTransition, ltsCodename, username } = this; @@ -313,15 +357,35 @@ class ReleasePreparation { const newHeader = `${newVersion}
`; for (let idx = 0; idx < arr.length; idx++) { - if (arr[idx].includes(topHeader)) { - arr.splice(idx, 0, newHeader); + if (isLTSTransition && arr[idx].includes('Current')) { + // Create a new column for LTS. + arr.splice(idx, 0, `LTS '${ltsCodename}'`); idx++; + } else if (arr[idx].includes(topHeader)) { + if (isLTSTransition) { + // New release needs to go into the new column for LTS. + const toAppend = [ + newHeader, + '', + arr[idx - 1] + ]; + arr.splice(idx, 0, ...toAppend); + idx += toAppend.length; + } else { + arr.splice(idx, 0, newHeader); + idx++; + } } else if (arr[idx].includes(``)) { const toAppend = []; toAppend.push(``); toAppend.push(releaseHeader); toAppend.push('### Notable Changes\n'); - toAppend.push(notableChanges); + if (isLTSTransition) { + toAppend.push(`${getStartLTSBlurb(this)}\n`); + } + if (notableChanges.trim()) { + toAppend.push(notableChanges); + } toAppend.push('### Commits\n'); toAppend.push(allCommits); toAppend.push(''); @@ -347,7 +411,7 @@ class ReleasePreparation { } async updateNodeVersion() { - const { versionComponents } = this; + const { ltsCodename, versionComponents } = this; const filePath = path.resolve('src', 'node_version.h'); const data = await fs.readFile(filePath, 'utf8'); @@ -364,7 +428,16 @@ class ReleasePreparation { 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].slice(1, -1); + if (this.isLTSTransition) { + if (this.isLTS) { + throw new Error('Previous release was already marked as LTS.'); + } + this.isLTS = true; + arr[idx] = '#define NODE_VERSION_IS_LTS 1'; + arr[idx + 1] = `#define NODE_VERSION_LTS_CODENAME "${ltsCodename}"`; + } else { + this.ltsCodename = arr[idx + 1].split(' ')[2].slice(1, -1); + } } }); @@ -382,10 +455,17 @@ class ReleasePreparation { writeJson(nmvFilePath, { NODE_MODULE_VERSION: nmvArray }); } + async updateTestProcessRelease(testFile) { + const data = await fs.readFile(testFile, { encoding: 'utf8' }); + const updated = updateTestProcessRelease(data, this); + await fs.writeFile(testFile, updated); + } + async createReleaseCommit() { const { cli, isLTS, + isLTSTransition, ltsCodename, newVersion, isSecurityRelease, @@ -405,6 +485,9 @@ class ReleasePreparation { format: 'plaintext' }); messageBody.push('Notable changes:\n\n'); + if (isLTSTransition) { + messageBody.push(`${getStartLTSBlurb(this)}\n\n`); + } messageBody.push(notableChanges); messageBody.push('\nPR-URL: TODO'); diff --git a/lib/release/utils.js b/lib/release/utils.js new file mode 100644 index 00000000..75362f2b --- /dev/null +++ b/lib/release/utils.js @@ -0,0 +1,61 @@ +'use strict'; + +function getEOLDate(ltsStartDate) { + // Maintenance LTS lasts for 18 months. + const result = getLTSMaintenanceStartDate(ltsStartDate); + result.setMonth(result.getMonth() + 18); + return result; +} + +function getLTSMaintenanceStartDate(ltsStartDate) { + // Active LTS lasts for one year. + const result = new Date(ltsStartDate); + result.setMonth(result.getMonth() + 12); + return result; +} + +function getStartLTSBlurb({ date, ltsCodename, versionComponents }) { + const dateFormat = { month: 'long', year: 'numeric' }; + // TODO pull these from the schedule.json in the Release repo? + const mainDate = getLTSMaintenanceStartDate(date); + const mainStart = mainDate.toLocaleString('en-US', dateFormat); + const eolDate = getEOLDate(date); + const eol = eolDate.toLocaleString('en-US', dateFormat); + const { major } = versionComponents; + return [ + /* eslint-disable max-len */ + `This release marks the transition of Node.js ${major}.x into Long Term Support (LTS)`, + `with the codename '${ltsCodename}'. The ${major}.x release line now moves into "Active LTS"`, + `and will remain so until ${mainStart}. After that time, it will move into`, + `"Maintenance" until end of life in ${eol}.` + /* eslint-enable */ + ].join('\n'); +} + +function updateTestProcessRelease(test, { versionComponents, ltsCodename }) { + if (test.includes(ltsCodename)) { + return test; + } + const inLines = test.split('\n'); + const outLines = []; + const { major, minor } = versionComponents; + for (const line of inLines) { + if (line === '} else {') { + outLines.push(`} else if (versionParts[0] === '${major}' ` + + `&& versionParts[1] >= ${minor}) {` + ); + outLines.push( + ` assert.strictEqual(process.release.lts, '${ltsCodename}');` + ); + } + outLines.push(line); + } + return outLines.join('\n'); +} + +module.exports = { + getEOLDate, + getLTSMaintenanceStartDate, + getStartLTSBlurb, + updateTestProcessRelease +}; diff --git a/test/fixtures/release/expected-test-process-release.js b/test/fixtures/release/expected-test-process-release.js new file mode 100644 index 00000000..ebb259f7 --- /dev/null +++ b/test/fixtures/release/expected-test-process-release.js @@ -0,0 +1,26 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const versionParts = process.versions.node.split('.'); + +assert.strictEqual(process.release.name, 'node'); + +// It's expected that future LTS release lines will have additional +// branches in here +if (versionParts[0] === '4' && versionParts[1] >= 2) { + assert.strictEqual(process.release.lts, 'Argon'); +} else if (versionParts[0] === '6' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Boron'); +} else if (versionParts[0] === '8' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Carbon'); +} else if (versionParts[0] === '10' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Dubnium'); +} else if (versionParts[0] === '12' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Erbium'); +} else if (versionParts[0] === '14' && versionParts[1] >= 15) { + assert.strictEqual(process.release.lts, 'Fermium'); +} else { + assert.strictEqual(process.release.lts, undefined); +} diff --git a/test/fixtures/release/original-test-process-release.js b/test/fixtures/release/original-test-process-release.js new file mode 100644 index 00000000..13263894 --- /dev/null +++ b/test/fixtures/release/original-test-process-release.js @@ -0,0 +1,24 @@ +'use strict'; + +require('../common'); + +const assert = require('assert'); +const versionParts = process.versions.node.split('.'); + +assert.strictEqual(process.release.name, 'node'); + +// It's expected that future LTS release lines will have additional +// branches in here +if (versionParts[0] === '4' && versionParts[1] >= 2) { + assert.strictEqual(process.release.lts, 'Argon'); +} else if (versionParts[0] === '6' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Boron'); +} else if (versionParts[0] === '8' && versionParts[1] >= 9) { + assert.strictEqual(process.release.lts, 'Carbon'); +} else if (versionParts[0] === '10' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Dubnium'); +} else if (versionParts[0] === '12' && versionParts[1] >= 13) { + assert.strictEqual(process.release.lts, 'Erbium'); +} else { + assert.strictEqual(process.release.lts, undefined); +} diff --git a/test/unit/prepare_release.test.js b/test/unit/prepare_release.test.js new file mode 100644 index 00000000..9e5ca0b0 --- /dev/null +++ b/test/unit/prepare_release.test.js @@ -0,0 +1,70 @@ +'use strict'; + +const assert = require('assert'); +const { readFileSync } = require('fs'); +const utils = require('../../lib/release/utils'); + +describe('prepare_release: utils.getEOLDate', () => { + it('calculates the correct EOL date', () => { + const test = utils.getEOLDate('2020-10-27'); + const expected = new Date('2023-04-27'); + const format = { month: 'short', year: 'numeric' }; + assert.strictEqual( + test.toLocaleString('en-US', format), + expected.toLocaleString('en-US', format) + ); + }); +}); + +describe('prepare_release: utils.getLTSMaintenanceStartDate', () => { + it('calculates the correct LTS maintenance start date', () => { + const test = utils.getLTSMaintenanceStartDate('2020-10-27'); + const expected = new Date('2021-10-27'); + const format = { month: 'short', year: 'numeric' }; + assert.strictEqual( + test.toLocaleString('en-US', format), + expected.toLocaleString('en-US', format) + ); + }); +}); + +describe('prepare_release: utils.getStartLTSBlurb', () => { + it('generates first LTS release text with correct dates', () => { + const expected = [ + /* eslint-disable max-len */ + 'This release marks the transition of Node.js 14.x into Long Term Support (LTS)', + 'with the codename \'Fermium\'. The 14.x release line now moves into "Active LTS"', + 'and will remain so until October 2021. After that time, it will move into', + '"Maintenance" until end of life in April 2023.' + /* eslint-enable max-len */ + ].join('\n'); + const text = utils.getStartLTSBlurb({ + date: '2020-10-27', + ltsCodename: 'Fermium', + versionComponents: { major: 14 } + }); + assert.strictEqual(text, expected); + }); +}); + +describe('prepare_release: utils.updateTestProcessRelease', () => { + it('inserts test for a new LTS codename', () => { + const expected = readFileSync( + `${__dirname}/../fixtures/release/expected-test-process-release.js`, + { encoding: 'utf8' } + ); + const test = readFileSync( + `${__dirname}/../fixtures/release/original-test-process-release.js`, + { encoding: 'utf8' } + ); + const context = { + ltsCodename: 'Fermium', + versionComponents: { + major: 14, + minor: 15 + } + }; + const updated = utils.updateTestProcessRelease(test, context); + assert.strictEqual(updated, expected); + }); +});