Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: automate creation of the first LTS release #514

Merged
merged 4 commits into from
Nov 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
}