Skip to content

Commit 53e68b4

Browse files
authored
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.
1 parent 6dab341 commit 53e68b4

7 files changed

Lines changed: 276 additions & 7 deletions

File tree

components/git/release.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ const releaseOptions = {
2121
security: {
2222
describe: 'Demarcate the new security release as a security release',
2323
type: 'boolean'
24+
},
25+
startLTS: {
26+
describe: 'Mark the release as the transition from Current to LTS',
27+
type: 'boolean'
2428
}
2529
};
2630

docs/git-node.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ Options:
217217
--help Show help [boolean]
218218
--prepare Prepare a new release of Node.js [boolean]
219219
--security Demarcate the new security release as a security release [boolean]
220+
--startLTS Mark the release as the transition from Current to LTS [boolean]
220221
```
221222

222223
### Example

lib/prepare_release.js

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ const {
1212
getUnmarkedDeprecations,
1313
updateDeprecations
1414
} = require('./deprecations');
15+
const {
16+
getEOLDate,
17+
getStartLTSBlurb,
18+
updateTestProcessRelease
19+
} = require('./release/utils');
1520

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

@@ -21,6 +26,7 @@ class ReleasePreparation {
2126
this.dir = dir;
2227
this.isSecurityRelease = argv.security;
2328
this.isLTS = false;
29+
this.isLTSTransition = argv.startLTS;
2430
this.ltsCodename = '';
2531
this.date = '';
2632
this.config = getMergedConfig(this.dir);
@@ -91,6 +97,20 @@ class ReleasePreparation {
9197
await this.createProposalBranch();
9298
cli.stopSpinner(`Created new proposal branch for ${newVersion}`);
9399

100+
if (this.isLTSTransition) {
101+
// For releases transitioning into LTS, fetch the new code name.
102+
this.ltsCodename = await this.getLTSCodename(versionComponents.major);
103+
// Update test for new LTS code name.
104+
const testFile = path.resolve(
105+
'test',
106+
'parallel',
107+
'test-process-release.js'
108+
);
109+
cli.startSpinner(`Updating ${testFile}`);
110+
await this.updateTestProcessRelease(testFile);
111+
cli.stopSpinner(`Updating ${testFile}`);
112+
}
113+
94114
// Update version and release info in src/node_version.h.
95115
cli.startSpinner(`Updating 'src/node_version.h' for ${newVersion}`);
96116
await this.updateNodeVersion();
@@ -218,7 +238,7 @@ class ReleasePreparation {
218238

219239
if (changelog.includes('SEMVER-MAJOR')) {
220240
newVersion = `${lastTag.major + 1}.0.0`;
221-
} else if (changelog.includes('SEMVER-MINOR')) {
241+
} else if (changelog.includes('SEMVER-MINOR') || this.isLTSTransition) {
222242
newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`;
223243
} else {
224244
newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`;
@@ -249,6 +269,15 @@ class ReleasePreparation {
249269
]).trim();
250270
}
251271

272+
async getLTSCodename(version) {
273+
const { cli } = this;
274+
return await cli.prompt(
275+
'Enter the LTS code name for this release line\n' +
276+
'(Refs: https://github.com/nodejs/Release/blob/master/CODENAMES.md):',
277+
{ questionType: 'input', noSeparator: true, defaultAnswer: '' }
278+
);
279+
}
280+
252281
async updateREPLACEMEs() {
253282
const { newVersion } = this;
254283

@@ -260,7 +289,7 @@ class ReleasePreparation {
260289
}
261290

262291
async updateMainChangelog() {
263-
const { versionComponents, newVersion } = this;
292+
const { date, isLTSTransition, versionComponents, newVersion } = this;
264293

265294
// Remove the leading 'v'.
266295
const lastRef = this.getLastRef().substring(1);
@@ -274,6 +303,20 @@ class ReleasePreparation {
274303
const lastRefLink = `<a href="${hrefLink}#${lastRef}">${lastRef}</a>`;
275304

276305
for (let idx = 0; idx < arr.length; idx++) {
306+
if (isLTSTransition) {
307+
if (arr[idx].includes(hrefLink)) {
308+
const eolDate = getEOLDate(date);
309+
const eol = eolDate.toISOString().split('-').slice(0, 2).join('-');
310+
arr[idx] = arr[idx].replace('**Current**', '**Long Term Support**');
311+
arr[idx] = arr[idx].replace('"Current"', `"LTS Until ${eol}"`);
312+
arr[idx] = arr[idx].replace('<sup>Current</sup>', '<sup>LTS</sup>');
313+
} else if (arr[idx].includes('**Long Term Support**')) {
314+
arr[idx] = arr[idx].replace(
315+
'**Long Term Support**',
316+
'Long Term Support'
317+
);
318+
}
319+
}
277320
if (arr[idx].includes(`<b>${lastRefLink}</b><br/>`)) {
278321
arr.splice(idx, 1, `<b>${newRefLink}</b><br/>`, `${lastRefLink}<br/>`);
279322
break;
@@ -289,6 +332,7 @@ class ReleasePreparation {
289332
newVersion,
290333
date,
291334
isLTS,
335+
isLTSTransition,
292336
ltsCodename,
293337
username
294338
} = this;
@@ -313,15 +357,35 @@ class ReleasePreparation {
313357
const newHeader =
314358
`<a href="#${newVersion}">${newVersion}</a><br/>`;
315359
for (let idx = 0; idx < arr.length; idx++) {
316-
if (arr[idx].includes(topHeader)) {
317-
arr.splice(idx, 0, newHeader);
360+
if (isLTSTransition && arr[idx].includes('<th>Current</th>')) {
361+
// Create a new column for LTS.
362+
arr.splice(idx, 0, `<th>LTS '${ltsCodename}'</th>`);
318363
idx++;
364+
} else if (arr[idx].includes(topHeader)) {
365+
if (isLTSTransition) {
366+
// New release needs to go into the new column for LTS.
367+
const toAppend = [
368+
newHeader,
369+
'</td>',
370+
arr[idx - 1]
371+
];
372+
arr.splice(idx, 0, ...toAppend);
373+
idx += toAppend.length;
374+
} else {
375+
arr.splice(idx, 0, newHeader);
376+
idx++;
377+
}
319378
} else if (arr[idx].includes(`<a id="${lastRef.substring(1)}"></a>`)) {
320379
const toAppend = [];
321380
toAppend.push(`<a id="${newVersion}"></a>`);
322381
toAppend.push(releaseHeader);
323382
toAppend.push('### Notable Changes\n');
324-
toAppend.push(notableChanges);
383+
if (isLTSTransition) {
384+
toAppend.push(`${getStartLTSBlurb(this)}\n`);
385+
}
386+
if (notableChanges.trim()) {
387+
toAppend.push(notableChanges);
388+
}
325389
toAppend.push('### Commits\n');
326390
toAppend.push(allCommits);
327391
toAppend.push('');
@@ -347,7 +411,7 @@ class ReleasePreparation {
347411
}
348412

349413
async updateNodeVersion() {
350-
const { versionComponents } = this;
414+
const { ltsCodename, versionComponents } = this;
351415

352416
const filePath = path.resolve('src', 'node_version.h');
353417
const data = await fs.readFile(filePath, 'utf8');
@@ -364,7 +428,16 @@ class ReleasePreparation {
364428
arr[idx] = '#define NODE_VERSION_IS_RELEASE 1';
365429
} else if (line.includes('#define NODE_VERSION_IS_LTS')) {
366430
this.isLTS = arr[idx].split(' ')[2] === '1';
367-
this.ltsCodename = arr[idx + 1].split(' ')[2].slice(1, -1);
431+
if (this.isLTSTransition) {
432+
if (this.isLTS) {
433+
throw new Error('Previous release was already marked as LTS.');
434+
}
435+
this.isLTS = true;
436+
arr[idx] = '#define NODE_VERSION_IS_LTS 1';
437+
arr[idx + 1] = `#define NODE_VERSION_LTS_CODENAME "${ltsCodename}"`;
438+
} else {
439+
this.ltsCodename = arr[idx + 1].split(' ')[2].slice(1, -1);
440+
}
368441
}
369442
});
370443

@@ -382,10 +455,17 @@ class ReleasePreparation {
382455
writeJson(nmvFilePath, { NODE_MODULE_VERSION: nmvArray });
383456
}
384457

458+
async updateTestProcessRelease(testFile) {
459+
const data = await fs.readFile(testFile, { encoding: 'utf8' });
460+
const updated = updateTestProcessRelease(data, this);
461+
await fs.writeFile(testFile, updated);
462+
}
463+
385464
async createReleaseCommit() {
386465
const {
387466
cli,
388467
isLTS,
468+
isLTSTransition,
389469
ltsCodename,
390470
newVersion,
391471
isSecurityRelease,
@@ -405,6 +485,9 @@ class ReleasePreparation {
405485
format: 'plaintext'
406486
});
407487
messageBody.push('Notable changes:\n\n');
488+
if (isLTSTransition) {
489+
messageBody.push(`${getStartLTSBlurb(this)}\n\n`);
490+
}
408491
messageBody.push(notableChanges);
409492
messageBody.push('\nPR-URL: TODO');
410493

lib/release/utils.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
'use strict';
2+
3+
function getEOLDate(ltsStartDate) {
4+
// Maintenance LTS lasts for 18 months.
5+
const result = getLTSMaintenanceStartDate(ltsStartDate);
6+
result.setMonth(result.getMonth() + 18);
7+
return result;
8+
}
9+
10+
function getLTSMaintenanceStartDate(ltsStartDate) {
11+
// Active LTS lasts for one year.
12+
const result = new Date(ltsStartDate);
13+
result.setMonth(result.getMonth() + 12);
14+
return result;
15+
}
16+
17+
function getStartLTSBlurb({ date, ltsCodename, versionComponents }) {
18+
const dateFormat = { month: 'long', year: 'numeric' };
19+
// TODO pull these from the schedule.json in the Release repo?
20+
const mainDate = getLTSMaintenanceStartDate(date);
21+
const mainStart = mainDate.toLocaleString('en-US', dateFormat);
22+
const eolDate = getEOLDate(date);
23+
const eol = eolDate.toLocaleString('en-US', dateFormat);
24+
const { major } = versionComponents;
25+
return [
26+
/* eslint-disable max-len */
27+
`This release marks the transition of Node.js ${major}.x into Long Term Support (LTS)`,
28+
`with the codename '${ltsCodename}'. The ${major}.x release line now moves into "Active LTS"`,
29+
`and will remain so until ${mainStart}. After that time, it will move into`,
30+
`"Maintenance" until end of life in ${eol}.`
31+
/* eslint-enable */
32+
].join('\n');
33+
}
34+
35+
function updateTestProcessRelease(test, { versionComponents, ltsCodename }) {
36+
if (test.includes(ltsCodename)) {
37+
return test;
38+
}
39+
const inLines = test.split('\n');
40+
const outLines = [];
41+
const { major, minor } = versionComponents;
42+
for (const line of inLines) {
43+
if (line === '} else {') {
44+
outLines.push(`} else if (versionParts[0] === '${major}' ` +
45+
`&& versionParts[1] >= ${minor}) {`
46+
);
47+
outLines.push(
48+
` assert.strictEqual(process.release.lts, '${ltsCodename}');`
49+
);
50+
}
51+
outLines.push(line);
52+
}
53+
return outLines.join('\n');
54+
}
55+
56+
module.exports = {
57+
getEOLDate,
58+
getLTSMaintenanceStartDate,
59+
getStartLTSBlurb,
60+
updateTestProcessRelease
61+
};
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const assert = require('assert');
6+
const versionParts = process.versions.node.split('.');
7+
8+
assert.strictEqual(process.release.name, 'node');
9+
10+
// It's expected that future LTS release lines will have additional
11+
// branches in here
12+
if (versionParts[0] === '4' && versionParts[1] >= 2) {
13+
assert.strictEqual(process.release.lts, 'Argon');
14+
} else if (versionParts[0] === '6' && versionParts[1] >= 9) {
15+
assert.strictEqual(process.release.lts, 'Boron');
16+
} else if (versionParts[0] === '8' && versionParts[1] >= 9) {
17+
assert.strictEqual(process.release.lts, 'Carbon');
18+
} else if (versionParts[0] === '10' && versionParts[1] >= 13) {
19+
assert.strictEqual(process.release.lts, 'Dubnium');
20+
} else if (versionParts[0] === '12' && versionParts[1] >= 13) {
21+
assert.strictEqual(process.release.lts, 'Erbium');
22+
} else if (versionParts[0] === '14' && versionParts[1] >= 15) {
23+
assert.strictEqual(process.release.lts, 'Fermium');
24+
} else {
25+
assert.strictEqual(process.release.lts, undefined);
26+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use strict';
2+
3+
require('../common');
4+
5+
const assert = require('assert');
6+
const versionParts = process.versions.node.split('.');
7+
8+
assert.strictEqual(process.release.name, 'node');
9+
10+
// It's expected that future LTS release lines will have additional
11+
// branches in here
12+
if (versionParts[0] === '4' && versionParts[1] >= 2) {
13+
assert.strictEqual(process.release.lts, 'Argon');
14+
} else if (versionParts[0] === '6' && versionParts[1] >= 9) {
15+
assert.strictEqual(process.release.lts, 'Boron');
16+
} else if (versionParts[0] === '8' && versionParts[1] >= 9) {
17+
assert.strictEqual(process.release.lts, 'Carbon');
18+
} else if (versionParts[0] === '10' && versionParts[1] >= 13) {
19+
assert.strictEqual(process.release.lts, 'Dubnium');
20+
} else if (versionParts[0] === '12' && versionParts[1] >= 13) {
21+
assert.strictEqual(process.release.lts, 'Erbium');
22+
} else {
23+
assert.strictEqual(process.release.lts, undefined);
24+
}

0 commit comments

Comments
 (0)