Skip to content

Commit

Permalink
feat: automate creation of the first LTS release (#514)
Browse files Browse the repository at this point in the history
Add an option to `git node release` that marks the release being
created as the transition from Current to LTS.
  • Loading branch information
richardlau authored Nov 1, 2020
1 parent 6dab341 commit 53e68b4
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 7 deletions.
4 changes: 4 additions & 0 deletions components/git/release.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
};

Expand Down
1 change: 1 addition & 0 deletions docs/git-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
97 changes: 90 additions & 7 deletions lib/prepare_release.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const {
getUnmarkedDeprecations,
updateDeprecations
} = require('./deprecations');
const {
getEOLDate,
getStartLTSBlurb,
updateTestProcessRelease
} = require('./release/utils');

const isWindows = process.platform === 'win32';

Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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}`;
Expand Down Expand Up @@ -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;

Expand All @@ -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);
Expand All @@ -274,6 +303,20 @@ class ReleasePreparation {
const lastRefLink = `<a href="${hrefLink}#${lastRef}">${lastRef}</a>`;

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('<sup>Current</sup>', '<sup>LTS</sup>');
} else if (arr[idx].includes('**Long Term Support**')) {
arr[idx] = arr[idx].replace(
'**Long Term Support**',
'Long Term Support'
);
}
}
if (arr[idx].includes(`<b>${lastRefLink}</b><br/>`)) {
arr.splice(idx, 1, `<b>${newRefLink}</b><br/>`, `${lastRefLink}<br/>`);
break;
Expand All @@ -289,6 +332,7 @@ class ReleasePreparation {
newVersion,
date,
isLTS,
isLTSTransition,
ltsCodename,
username
} = this;
Expand All @@ -313,15 +357,35 @@ class ReleasePreparation {
const newHeader =
`<a href="#${newVersion}">${newVersion}</a><br/>`;
for (let idx = 0; idx < arr.length; idx++) {
if (arr[idx].includes(topHeader)) {
arr.splice(idx, 0, newHeader);
if (isLTSTransition && arr[idx].includes('<th>Current</th>')) {
// Create a new column for LTS.
arr.splice(idx, 0, `<th>LTS '${ltsCodename}'</th>`);
idx++;
} else if (arr[idx].includes(topHeader)) {
if (isLTSTransition) {
// New release needs to go into the new column for LTS.
const toAppend = [
newHeader,
'</td>',
arr[idx - 1]
];
arr.splice(idx, 0, ...toAppend);
idx += toAppend.length;
} else {
arr.splice(idx, 0, newHeader);
idx++;
}
} else if (arr[idx].includes(`<a id="${lastRef.substring(1)}"></a>`)) {
const toAppend = [];
toAppend.push(`<a id="${newVersion}"></a>`);
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('');
Expand All @@ -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');
Expand All @@ -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);
}
}
});

Expand All @@ -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,
Expand All @@ -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');

Expand Down
61 changes: 61 additions & 0 deletions lib/release/utils.js
Original file line number Diff line number Diff line change
@@ -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
};
26 changes: 26 additions & 0 deletions test/fixtures/release/expected-test-process-release.js
Original file line number Diff line number Diff line change
@@ -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);
}
24 changes: 24 additions & 0 deletions test/fixtures/release/original-test-process-release.js
Original file line number Diff line number Diff line change
@@ -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);
}
Loading

0 comments on commit 53e68b4

Please sign in to comment.