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);
+ });
+});