@node-core/utils 5.6.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/git/release.js +65 -15
- package/components/git/security.js +12 -10
- package/components/git/v8.js +15 -3
- package/components/git/wpt.js +1 -1
- package/lib/prepare_release.js +15 -1
- package/lib/promote_release.js +493 -0
- package/lib/security_blog.js +68 -31
- package/lib/session.js +4 -0
- package/lib/update-v8/backport.js +74 -10
- package/lib/update-v8/index.js +1 -0
- package/package.json +2 -1
@@ -1,14 +1,31 @@
|
|
1
|
+
import auth from '../../lib/auth.js';
|
1
2
|
import CLI from '../../lib/cli.js';
|
2
3
|
import ReleasePreparation from '../../lib/prepare_release.js';
|
4
|
+
import ReleasePromotion from '../../lib/promote_release.js';
|
5
|
+
import TeamInfo from '../../lib/team_info.js';
|
6
|
+
import Request from '../../lib/request.js';
|
3
7
|
import { runPromise } from '../../lib/run.js';
|
4
8
|
|
5
|
-
export const command = 'release [
|
9
|
+
export const command = 'release [prid|options]';
|
6
10
|
export const describe = 'Manage an in-progress release or start a new one.';
|
7
11
|
|
8
12
|
const PREPARE = 'prepare';
|
9
13
|
const PROMOTE = 'promote';
|
14
|
+
const RELEASERS = 'releasers';
|
10
15
|
|
11
16
|
const releaseOptions = {
|
17
|
+
filterLabel: {
|
18
|
+
describe: 'Labels separated by "," to filter security PRs',
|
19
|
+
type: 'string'
|
20
|
+
},
|
21
|
+
'gpg-sign': {
|
22
|
+
describe: 'GPG-sign commits, will be passed to the git process',
|
23
|
+
alias: 'S'
|
24
|
+
},
|
25
|
+
newVersion: {
|
26
|
+
describe: 'Version number of the release to be prepared',
|
27
|
+
type: 'string'
|
28
|
+
},
|
12
29
|
prepare: {
|
13
30
|
describe: 'Prepare a new release of Node.js',
|
14
31
|
type: 'boolean'
|
@@ -21,14 +38,16 @@ const releaseOptions = {
|
|
21
38
|
describe: 'Default relase date when --prepare is used. It must be YYYY-MM-DD',
|
22
39
|
type: 'string'
|
23
40
|
},
|
41
|
+
run: {
|
42
|
+
describe: 'Run steps that involve touching more than the local clone, ' +
|
43
|
+
'including `git push` commands. Might not work if a passphrase ' +
|
44
|
+
'required to push to the remote clone.',
|
45
|
+
type: 'boolean'
|
46
|
+
},
|
24
47
|
security: {
|
25
48
|
describe: 'Demarcate the new security release as a security release',
|
26
49
|
type: 'boolean'
|
27
50
|
},
|
28
|
-
filterLabel: {
|
29
|
-
describe: 'Labels separated by "," to filter security PRs',
|
30
|
-
type: 'string'
|
31
|
-
},
|
32
51
|
skipBranchDiff: {
|
33
52
|
describe: 'Skips the initial branch-diff check when preparing releases',
|
34
53
|
type: 'boolean'
|
@@ -49,11 +68,16 @@ let yargsInstance;
|
|
49
68
|
export function builder(yargs) {
|
50
69
|
yargsInstance = yargs;
|
51
70
|
return yargs
|
52
|
-
.options(releaseOptions).positional('
|
53
|
-
describe: '
|
71
|
+
.options(releaseOptions).positional('prid', {
|
72
|
+
describe: 'PR number or URL of the release proposal to be promoted',
|
73
|
+
type: 'string'
|
54
74
|
})
|
55
|
-
.example('git node release --prepare
|
56
|
-
'Prepare a release of Node.js
|
75
|
+
.example('git node release --prepare --security',
|
76
|
+
'Prepare a new security release of Node.js with auto-determined version')
|
77
|
+
.example('git node release --prepare --newVersion=1.2.3',
|
78
|
+
'Prepare a new release of Node.js tagged v1.2.3')
|
79
|
+
.example('git node release --promote 12345',
|
80
|
+
'Promote a prepared release of Node.js with PR #12345')
|
57
81
|
.example('git node --prepare --startLTS',
|
58
82
|
'Prepare the first LTS release');
|
59
83
|
}
|
@@ -88,17 +112,21 @@ function release(state, argv) {
|
|
88
112
|
}
|
89
113
|
|
90
114
|
async function main(state, argv, cli, dir) {
|
115
|
+
const prID = /^(?:https:\/\/github\.com\/nodejs\/node\/pull\/)?(\d+)$/.exec(argv.prid);
|
116
|
+
if (prID) {
|
117
|
+
argv.prid = Number(prID[1]);
|
118
|
+
}
|
91
119
|
if (state === PREPARE) {
|
92
|
-
const
|
120
|
+
const release = new ReleasePreparation(argv, cli, dir);
|
93
121
|
|
94
|
-
await
|
122
|
+
await release.prepareLocalBranch();
|
95
123
|
|
96
|
-
if (
|
124
|
+
if (release.warnForWrongBranch()) return;
|
97
125
|
|
98
126
|
// If the new version was automatically calculated, confirm it.
|
99
127
|
if (!argv.newVersion) {
|
100
128
|
const create = await cli.prompt(
|
101
|
-
`Create release with new version ${
|
129
|
+
`Create release with new version ${release.newVersion}?`,
|
102
130
|
{ defaultAnswer: true });
|
103
131
|
|
104
132
|
if (!create) {
|
@@ -107,8 +135,30 @@ async function main(state, argv, cli, dir) {
|
|
107
135
|
}
|
108
136
|
}
|
109
137
|
|
110
|
-
return
|
138
|
+
return release.prepare();
|
111
139
|
} else if (state === PROMOTE) {
|
112
|
-
|
140
|
+
const credentials = await auth({ github: true });
|
141
|
+
const request = new Request(credentials);
|
142
|
+
const release = new ReleasePromotion(argv, request, cli, dir);
|
143
|
+
|
144
|
+
cli.startSpinner('Verifying Releaser status');
|
145
|
+
const info = new TeamInfo(cli, request, 'nodejs', RELEASERS);
|
146
|
+
|
147
|
+
const releasers = await info.getMembers();
|
148
|
+
if (release.username === undefined) {
|
149
|
+
cli.stopSpinner('Failed to verify Releaser status');
|
150
|
+
cli.info(
|
151
|
+
'Username was undefined - do you have your .ncurc set up correctly?');
|
152
|
+
return;
|
153
|
+
} else if (releasers.every(r => r.login !== release.username)) {
|
154
|
+
cli.stopSpinner(`${release.username} is not a Releaser`, 'failed');
|
155
|
+
if (!argv.dryRun) {
|
156
|
+
throw new Error('aborted');
|
157
|
+
}
|
158
|
+
} else {
|
159
|
+
cli.stopSpinner(`${release.username} is a Releaser`);
|
160
|
+
}
|
161
|
+
|
162
|
+
return release.promote();
|
113
163
|
}
|
114
164
|
}
|
@@ -29,8 +29,8 @@ const securityOptions = {
|
|
29
29
|
type: 'string'
|
30
30
|
},
|
31
31
|
'pre-release': {
|
32
|
-
describe: 'Create the pre-release announcement',
|
33
|
-
type: '
|
32
|
+
describe: 'Create the pre-release announcement to the given nodejs.org folder',
|
33
|
+
type: 'string'
|
34
34
|
},
|
35
35
|
'notify-pre-release': {
|
36
36
|
describe: 'Notify the community about the security release',
|
@@ -41,8 +41,8 @@ const securityOptions = {
|
|
41
41
|
type: 'boolean'
|
42
42
|
},
|
43
43
|
'post-release': {
|
44
|
-
describe: 'Create the post-release announcement',
|
45
|
-
type: '
|
44
|
+
describe: 'Create the post-release announcement to the given nodejs.org folder',
|
45
|
+
type: 'string'
|
46
46
|
},
|
47
47
|
cleanup: {
|
48
48
|
describe: 'cleanup the security release.',
|
@@ -73,7 +73,7 @@ export function builder(yargs) {
|
|
73
73
|
'git node security --remove-report=H1-ID',
|
74
74
|
'Removes the Hackerone report based on ID provided from vulnerabilities.json'
|
75
75
|
).example(
|
76
|
-
'git node security --pre-release',
|
76
|
+
'git node security --pre-release="../nodejs.org/"',
|
77
77
|
'Create the pre-release announcement on the Nodejs.org repo'
|
78
78
|
).example(
|
79
79
|
'git node security --notify-pre-release',
|
@@ -83,7 +83,7 @@ export function builder(yargs) {
|
|
83
83
|
'Request CVEs for a security release of Node.js based on' +
|
84
84
|
' the next-security-release/vulnerabilities.json'
|
85
85
|
).example(
|
86
|
-
'git node security --post-release',
|
86
|
+
'git node security --post-release="../nodejs.org/"',
|
87
87
|
'Create the post-release announcement on the Nodejs.org repo'
|
88
88
|
).example(
|
89
89
|
'git node security --cleanup',
|
@@ -149,11 +149,12 @@ async function updateReleaseDate(argv) {
|
|
149
149
|
return update.updateReleaseDate(releaseDate);
|
150
150
|
}
|
151
151
|
|
152
|
-
async function createPreRelease() {
|
152
|
+
async function createPreRelease(argv) {
|
153
|
+
const nodejsOrgFolder = argv['pre-release'];
|
153
154
|
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
|
154
155
|
const cli = new CLI(logStream);
|
155
156
|
const preRelease = new SecurityBlog(cli);
|
156
|
-
return preRelease.createPreRelease();
|
157
|
+
return preRelease.createPreRelease(nodejsOrgFolder);
|
157
158
|
}
|
158
159
|
|
159
160
|
async function requestCVEs() {
|
@@ -163,11 +164,12 @@ async function requestCVEs() {
|
|
163
164
|
return hackerOneCve.requestCVEs();
|
164
165
|
}
|
165
166
|
|
166
|
-
async function createPostRelease() {
|
167
|
+
async function createPostRelease(argv) {
|
168
|
+
const nodejsOrgFolder = argv['post-release'];
|
167
169
|
const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
|
168
170
|
const cli = new CLI(logStream);
|
169
171
|
const blog = new SecurityBlog(cli);
|
170
|
-
return blog.createPostRelease();
|
172
|
+
return blog.createPostRelease(nodejsOrgFolder);
|
171
173
|
}
|
172
174
|
|
173
175
|
async function startSecurityRelease() {
|
package/components/git/v8.js
CHANGED
@@ -44,10 +44,22 @@ export function builder(yargs) {
|
|
44
44
|
describe: 'Bump V8 embedder version number or patch version',
|
45
45
|
default: true
|
46
46
|
})
|
47
|
+
.option('gpg-sign', {
|
48
|
+
alias: 'S',
|
49
|
+
type: 'boolean',
|
50
|
+
describe: 'GPG-sign commits',
|
51
|
+
default: false
|
52
|
+
})
|
53
|
+
.option('preserve-original-author', {
|
54
|
+
type: 'boolean',
|
55
|
+
describe: 'Preserve original commit author and date',
|
56
|
+
default: true
|
57
|
+
})
|
47
58
|
.option('squash', {
|
48
59
|
type: 'boolean',
|
49
60
|
describe:
|
50
|
-
'If multiple commits are backported, squash them into one'
|
61
|
+
'If multiple commits are backported, squash them into one. When ' +
|
62
|
+
'`--squash` is passed, `--preserve-original-author` will be ignored',
|
51
63
|
default: false
|
52
64
|
});
|
53
65
|
}
|
@@ -88,7 +100,7 @@ export function handler(argv) {
|
|
88
100
|
input,
|
89
101
|
spawnArgs: {
|
90
102
|
cwd: options.nodeDir,
|
91
|
-
stdio: input ? ['pipe', '
|
103
|
+
stdio: input ? ['pipe', 'inherit', 'inherit'] : 'inherit'
|
92
104
|
}
|
93
105
|
});
|
94
106
|
};
|
@@ -97,7 +109,7 @@ export function handler(argv) {
|
|
97
109
|
return forceRunAsync('git', args, {
|
98
110
|
ignoreFailure: false,
|
99
111
|
captureStdout: true,
|
100
|
-
spawnArgs: { cwd: options.v8Dir, stdio: ['ignore', 'pipe', '
|
112
|
+
spawnArgs: { cwd: options.v8Dir, stdio: ['ignore', 'pipe', 'inherit'] }
|
101
113
|
});
|
102
114
|
};
|
103
115
|
|
package/components/git/wpt.js
CHANGED
@@ -52,7 +52,7 @@ async function main(argv) {
|
|
52
52
|
if (fs.existsSync(statusFolder)) {
|
53
53
|
const jsons = fs.readdirSync(statusFolder);
|
54
54
|
supported = supported.concat(
|
55
|
-
jsons.map(item =>
|
55
|
+
jsons.map(item => path.basename(item, path.extname(item))));
|
56
56
|
} else {
|
57
57
|
cli.warn(`Please create the status JSON files in ${statusFolder}`);
|
58
58
|
}
|
package/lib/prepare_release.js
CHANGED
@@ -167,7 +167,11 @@ export default class ReleasePreparation extends Session {
|
|
167
167
|
return this.prepareSecurity();
|
168
168
|
}
|
169
169
|
|
170
|
-
|
170
|
+
const runBranchDiff = await cli.prompt(
|
171
|
+
'Do you want to check if any additional commits could be backported ' +
|
172
|
+
'(recommended except for Maintenance releases)?',
|
173
|
+
{ defaultAnswer: this.runBranchDiff });
|
174
|
+
if (runBranchDiff) {
|
171
175
|
// TODO: UPDATE re-use
|
172
176
|
// Check the branch diff to determine if the releaser
|
173
177
|
// wants to backport any more commits before proceeding.
|
@@ -363,6 +367,16 @@ export default class ReleasePreparation extends Session {
|
|
363
367
|
runSync('git', ['fetch', this.upstream, 'tag', '-n', tagName]);
|
364
368
|
this.cli.stopSpinner(`Tag fetched: ${tagName}`);
|
365
369
|
}
|
370
|
+
try {
|
371
|
+
if (runSync('git', ['describe', '--abbrev=0', '--tags']).trim() !== tagName) {
|
372
|
+
this.cli.warn(`${tagName} is not the closest tag`);
|
373
|
+
}
|
374
|
+
} catch {
|
375
|
+
this.cli.startSpinner(`${tagName} is unreachable from the current HEAD`);
|
376
|
+
runSync('git', ['fetch', '--shallow-exclude', tagName, this.upstream, this.branch]);
|
377
|
+
runSync('git', ['fetch', '--deepen=1', this.upstream, this.branch]);
|
378
|
+
this.cli.stopSpinner('Local clone unshallowed');
|
379
|
+
}
|
366
380
|
return tagName;
|
367
381
|
}
|
368
382
|
|
@@ -0,0 +1,493 @@
|
|
1
|
+
import path from 'node:path';
|
2
|
+
import fs from 'node:fs/promises';
|
3
|
+
import semver from 'semver';
|
4
|
+
import * as gst from 'git-secure-tag';
|
5
|
+
|
6
|
+
import { forceRunAsync } from './run.js';
|
7
|
+
import PRData from './pr_data.js';
|
8
|
+
import PRChecker from './pr_checker.js';
|
9
|
+
import Session from './session.js';
|
10
|
+
import { existsSync } from 'node:fs';
|
11
|
+
|
12
|
+
const dryRunMessage = 'You are running in dry-run mode, meaning NCU will not run ' +
|
13
|
+
'the `git push` commands, you would need to copy-paste the ' +
|
14
|
+
'following command in another terminal window. Alternatively, ' +
|
15
|
+
'pass `--run` flag to ask NCU to run the command for you ' +
|
16
|
+
'(might not work if you need to type a passphrase to push to the remote).';
|
17
|
+
|
18
|
+
export default class ReleasePromotion extends Session {
|
19
|
+
constructor(argv, req, cli, dir) {
|
20
|
+
super(cli, dir, argv.prid);
|
21
|
+
this.req = req;
|
22
|
+
this.dryRun = !argv.run;
|
23
|
+
this.isLTS = false;
|
24
|
+
this.ltsCodename = '';
|
25
|
+
this.date = '';
|
26
|
+
this.gpgSign = argv?.['gpg-sign']
|
27
|
+
? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
|
28
|
+
: [];
|
29
|
+
}
|
30
|
+
|
31
|
+
get branch() {
|
32
|
+
return this.defaultBranch ?? this.config.branch;
|
33
|
+
}
|
34
|
+
|
35
|
+
async getDefaultBranch() {
|
36
|
+
const { repository: { defaultBranchRef } } = await this.req.gql(
|
37
|
+
'DefaultBranchRef',
|
38
|
+
{ owner: this.owner, repo: this.repo });
|
39
|
+
return defaultBranchRef.name;
|
40
|
+
}
|
41
|
+
|
42
|
+
async promote() {
|
43
|
+
const { prid, cli } = this;
|
44
|
+
|
45
|
+
// In the promotion stage, we can pull most relevant data
|
46
|
+
// from the release commit created in the preparation stage.
|
47
|
+
// Verify that PR is ready to promote.
|
48
|
+
const {
|
49
|
+
githubCIReady,
|
50
|
+
isApproved,
|
51
|
+
jenkinsReady,
|
52
|
+
releaseCommitSha
|
53
|
+
} = await this.verifyPRAttributes();
|
54
|
+
|
55
|
+
this.releaseCommitSha = releaseCommitSha;
|
56
|
+
|
57
|
+
let localCloneIsClean = true;
|
58
|
+
const currentHEAD = await forceRunAsync('git', ['rev-parse', 'HEAD'],
|
59
|
+
{ captureStdout: true, ignoreFailure: false });
|
60
|
+
if (currentHEAD.trim() !== releaseCommitSha) {
|
61
|
+
cli.warn('Current HEAD is not the release commit');
|
62
|
+
localCloneIsClean = false;
|
63
|
+
}
|
64
|
+
try {
|
65
|
+
await forceRunAsync('git', ['--no-pager', 'diff', '--exit-code'], { ignoreFailure: false });
|
66
|
+
} catch {
|
67
|
+
cli.warn('Some local changes have not been committed');
|
68
|
+
localCloneIsClean = false;
|
69
|
+
}
|
70
|
+
if (!localCloneIsClean) {
|
71
|
+
if (await cli.prompt('Should we reset the local HEAD to be the release proposal?')) {
|
72
|
+
cli.startSpinner('Fetching the proposal upstream...');
|
73
|
+
await forceRunAsync('git', ['fetch', this.upstream, releaseCommitSha],
|
74
|
+
{ ignoreFailure: false });
|
75
|
+
await forceRunAsync('git', ['reset', releaseCommitSha, '--hard'], { ignoreFailure: false });
|
76
|
+
cli.stopSpinner('Local HEAD is now in sync with the proposal');
|
77
|
+
} else {
|
78
|
+
cli.error('Local clone is not ready');
|
79
|
+
throw new Error('Aborted');
|
80
|
+
}
|
81
|
+
}
|
82
|
+
|
83
|
+
await this.parseDataFromReleaseCommit();
|
84
|
+
|
85
|
+
const { version } = this;
|
86
|
+
cli.startSpinner('Verifying Jenkins CI status');
|
87
|
+
if (!jenkinsReady) {
|
88
|
+
cli.stopSpinner(
|
89
|
+
`Jenkins CI is failing for #${prid}`, cli.SPINNER_STATUS.FAILED);
|
90
|
+
const proceed = await cli.prompt('Do you want to proceed?');
|
91
|
+
if (!proceed) {
|
92
|
+
cli.warn(`Aborting release promotion for version ${version}`);
|
93
|
+
throw new Error('Aborted');
|
94
|
+
}
|
95
|
+
} else {
|
96
|
+
cli.stopSpinner('Jenkins CI is passing');
|
97
|
+
}
|
98
|
+
|
99
|
+
cli.startSpinner('Verifying GitHub CI status');
|
100
|
+
if (!githubCIReady) {
|
101
|
+
cli.stopSpinner(
|
102
|
+
`GitHub CI is failing for #${prid}`, cli.SPINNER_STATUS.FAILED);
|
103
|
+
const proceed = await cli.prompt('Do you want to proceed?');
|
104
|
+
if (!proceed) {
|
105
|
+
cli.warn(`Aborting release promotion for version ${version}`);
|
106
|
+
throw new Error('Aborted');
|
107
|
+
}
|
108
|
+
} else {
|
109
|
+
cli.stopSpinner('GitHub CI is passing');
|
110
|
+
}
|
111
|
+
|
112
|
+
cli.startSpinner('Verifying PR approval status');
|
113
|
+
if (!isApproved) {
|
114
|
+
cli.stopSpinner(
|
115
|
+
`#${prid} does not have sufficient approvals`,
|
116
|
+
cli.SPINNER_STATUS.FAILED);
|
117
|
+
const proceed = await cli.prompt('Do you want to proceed?');
|
118
|
+
if (!proceed) {
|
119
|
+
cli.warn(`Aborting release promotion for version ${version}`);
|
120
|
+
throw new Error('Aborted');
|
121
|
+
}
|
122
|
+
} else {
|
123
|
+
cli.stopSpinner(`#${prid} has necessary approvals`);
|
124
|
+
}
|
125
|
+
|
126
|
+
// Create and sign the release tag.
|
127
|
+
const shouldTagAndSignRelease = await cli.prompt(
|
128
|
+
'Tag and sign the release?');
|
129
|
+
if (!shouldTagAndSignRelease) {
|
130
|
+
cli.warn(`Aborting release promotion for version ${version}`);
|
131
|
+
throw new Error('Aborted');
|
132
|
+
}
|
133
|
+
await this.secureTagRelease();
|
134
|
+
|
135
|
+
// Set up for next release.
|
136
|
+
cli.startSpinner('Setting up for next release');
|
137
|
+
const workingOnNewReleaseCommit = await this.setupForNextRelease();
|
138
|
+
cli.stopSpinner('Successfully set up for next release');
|
139
|
+
|
140
|
+
// Cherry pick release commit to master.
|
141
|
+
const shouldCherryPick = await cli.prompt(
|
142
|
+
'Cherry-pick release commit to the default branch?', { defaultAnswer: true });
|
143
|
+
if (!shouldCherryPick) {
|
144
|
+
cli.warn(`Aborting release promotion for version ${version}`);
|
145
|
+
throw new Error('Aborted');
|
146
|
+
}
|
147
|
+
const appliedCleanly = await this.cherryPickToDefaultBranch();
|
148
|
+
|
149
|
+
// Ensure `node_version.h`'s `NODE_VERSION_IS_RELEASE` bit is not updated
|
150
|
+
await forceRunAsync('git', ['checkout',
|
151
|
+
appliedCleanly
|
152
|
+
? 'HEAD^' // In the absence of conflict, the top of the remote branch is the commit before.
|
153
|
+
: 'HEAD', // In case of conflict, HEAD is still the top of the remove branch.
|
154
|
+
'--', 'src/node_version.h'],
|
155
|
+
{ ignoreFailure: false });
|
156
|
+
|
157
|
+
if (appliedCleanly) {
|
158
|
+
// There were no conflicts, we have to amend the commit to revert the
|
159
|
+
// `node_version.h` changes.
|
160
|
+
await forceRunAsync('git', ['commit', ...this.gpgSign, '--amend', '--no-edit', '-n'],
|
161
|
+
{ ignoreFailure: false });
|
162
|
+
} else {
|
163
|
+
// There will be remaining cherry-pick conflicts the Releaser will
|
164
|
+
// need to resolve, so confirm they've been resolved before
|
165
|
+
// proceeding with next steps.
|
166
|
+
cli.separator();
|
167
|
+
cli.info('Resolve the conflicts and commit the result');
|
168
|
+
cli.separator();
|
169
|
+
const didResolveConflicts = await cli.prompt(
|
170
|
+
'Finished resolving cherry-pick conflicts?', { defaultAnswer: true });
|
171
|
+
if (!didResolveConflicts) {
|
172
|
+
cli.warn(`Aborting release promotion for version ${version}`);
|
173
|
+
throw new Error('Aborted');
|
174
|
+
}
|
175
|
+
}
|
176
|
+
|
177
|
+
if (existsSync('.git/CHERRY_PICK_HEAD')) {
|
178
|
+
cli.info('Cherry-pick is still in progress, attempting to continue it.');
|
179
|
+
await forceRunAsync('git', ['cherry-pick', ...this.gpgSign, '--continue'],
|
180
|
+
{ ignoreFailure: false });
|
181
|
+
}
|
182
|
+
|
183
|
+
// Validate release commit on the default branch
|
184
|
+
const releaseCommitOnDefaultBranch =
|
185
|
+
await forceRunAsync('git', ['show', 'HEAD', '--name-only', '--pretty=format:%s'],
|
186
|
+
{ captureStdout: true, ignoreFailure: false });
|
187
|
+
const [commitTitle, ...modifiedFiles] = releaseCommitOnDefaultBranch.trim().split('\n');
|
188
|
+
await this.validateReleaseCommit(commitTitle);
|
189
|
+
if (modifiedFiles.some(file => !file.endsWith('.md'))) {
|
190
|
+
cli.warn('Some modified files are not markdown, that\'s unusual.');
|
191
|
+
cli.info(`The list of modified files: ${modifiedFiles.map(f => `- ${f}`).join('\n')}`);
|
192
|
+
if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
|
193
|
+
throw new Error('Aborted');
|
194
|
+
}
|
195
|
+
}
|
196
|
+
|
197
|
+
// Push to the remote the release tag, and default, release, and staging branch.
|
198
|
+
await this.pushToRemote(workingOnNewReleaseCommit);
|
199
|
+
|
200
|
+
// Promote and sign the release builds.
|
201
|
+
await this.promoteAndSignRelease();
|
202
|
+
|
203
|
+
cli.separator();
|
204
|
+
cli.ok(`Release promotion for ${version} complete.\n`);
|
205
|
+
cli.info(
|
206
|
+
'To finish this release, you\'ll need to: \n' +
|
207
|
+
` 1. Check the release at: https://nodejs.org/dist/v${version}\n` +
|
208
|
+
' 2. Create the blog post for nodejs.org.\n' +
|
209
|
+
' 3. Create the release on GitHub.\n' +
|
210
|
+
' 4. Optionally, announce the release on your social networks.\n' +
|
211
|
+
' 5. Tag @nodejs-social-team on #nodejs-release Slack channel.\n');
|
212
|
+
|
213
|
+
cli.separator();
|
214
|
+
cli.info('Use the following command to create the GitHub release:');
|
215
|
+
cli.separator();
|
216
|
+
cli.info(
|
217
|
+
'awk \'' +
|
218
|
+
`/^## ${this.date}, Version ${this.version.replaceAll('.', '\\.')} /,` +
|
219
|
+
'/^<a id="[0-9]+\\.[0-9]+\\.[0-9]+"><\\x2fa>$/{' +
|
220
|
+
'print buf; if(firstLine == "") firstLine = $0; else buf = $0' +
|
221
|
+
`}' doc/changelogs/CHANGELOG_V${
|
222
|
+
this.versionComponents.major}.md | gh release create v${this.version} --verify-tag --latest${
|
223
|
+
this.isLTS ? '=false' : ''} --title=${JSON.stringify(this.releaseTitle)} --notes-file -`);
|
224
|
+
}
|
225
|
+
|
226
|
+
async verifyPRAttributes() {
|
227
|
+
const { cli, prid, owner, repo, req } = this;
|
228
|
+
|
229
|
+
const data = new PRData({ prid, owner, repo }, cli, req);
|
230
|
+
await data.getAll();
|
231
|
+
|
232
|
+
const checker = new PRChecker(cli, data, { prid, owner, repo }, { maxCommits: 0 });
|
233
|
+
const jenkinsReady = checker.checkJenkinsCI();
|
234
|
+
const githubCIReady = checker.checkGitHubCI();
|
235
|
+
const isApproved = checker.checkReviewsAndWait(new Date(), false);
|
236
|
+
|
237
|
+
return {
|
238
|
+
githubCIReady,
|
239
|
+
isApproved,
|
240
|
+
jenkinsReady,
|
241
|
+
releaseCommitSha: data.commits.at(-1).commit.oid
|
242
|
+
};
|
243
|
+
}
|
244
|
+
|
245
|
+
async validateReleaseCommit(releaseCommitMessage) {
|
246
|
+
const { cli } = this;
|
247
|
+
const data = {};
|
248
|
+
// Parse out release date.
|
249
|
+
if (!/^\d{4}-\d{2}-\d{2}, Version \d/.test(releaseCommitMessage)) {
|
250
|
+
cli.error(`Invalid Release commit message: ${releaseCommitMessage}`);
|
251
|
+
throw new Error('Aborted');
|
252
|
+
}
|
253
|
+
data.date = releaseCommitMessage.slice(0, 10);
|
254
|
+
const systemDate = new Date().toISOString().slice(0, 10);
|
255
|
+
if (data.date !== systemDate) {
|
256
|
+
cli.warn(
|
257
|
+
`The release date (${data.date}) does not match the system date for today (${systemDate}).`
|
258
|
+
);
|
259
|
+
if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
|
260
|
+
throw new Error('Aborted');
|
261
|
+
}
|
262
|
+
}
|
263
|
+
|
264
|
+
// Parse out release version.
|
265
|
+
data.version = releaseCommitMessage.slice(20, releaseCommitMessage.indexOf(' ', 20));
|
266
|
+
const version = semver.parse(data.version);
|
267
|
+
if (!version) {
|
268
|
+
cli.error(`Release commit contains invalid semantic version: ${data.version}`);
|
269
|
+
throw new Error('Aborted');
|
270
|
+
}
|
271
|
+
|
272
|
+
const { major, minor, patch } = version;
|
273
|
+
data.stagingBranch = `v${major}.x-staging`;
|
274
|
+
data.versionComponents = {
|
275
|
+
major,
|
276
|
+
minor,
|
277
|
+
patch
|
278
|
+
};
|
279
|
+
|
280
|
+
// Parse out LTS status and codename.
|
281
|
+
if (!releaseCommitMessage.endsWith(' (Current)')) {
|
282
|
+
const match = /'([^']+)' \(LTS\)$/.exec(releaseCommitMessage);
|
283
|
+
if (match == null) {
|
284
|
+
cli.error('Invalid release commit, it should match either Current or LTS release format');
|
285
|
+
throw new Error('Aborted');
|
286
|
+
}
|
287
|
+
data.isLTS = true;
|
288
|
+
data.ltsCodename = match[1];
|
289
|
+
}
|
290
|
+
return data;
|
291
|
+
}
|
292
|
+
|
293
|
+
async parseDataFromReleaseCommit() {
|
294
|
+
const { cli, releaseCommitSha } = this;
|
295
|
+
|
296
|
+
const releaseCommitMessage = await forceRunAsync('git', [
|
297
|
+
'--no-pager', 'log', '-1',
|
298
|
+
releaseCommitSha,
|
299
|
+
'--pretty=format:%s'], {
|
300
|
+
captureStdout: true,
|
301
|
+
ignoreFailure: false
|
302
|
+
});
|
303
|
+
|
304
|
+
const releaseCommitData = await this.validateReleaseCommit(releaseCommitMessage);
|
305
|
+
|
306
|
+
this.date = releaseCommitData.date;
|
307
|
+
this.version = releaseCommitData.version;
|
308
|
+
this.stagingBranch = releaseCommitData.stagingBranch;
|
309
|
+
this.versionComponents = releaseCommitData.versionComponents;
|
310
|
+
this.isLTS = releaseCommitData.isLTS;
|
311
|
+
this.ltsCodename = releaseCommitData.ltsCodename;
|
312
|
+
|
313
|
+
// Check if CHANGELOG show the correct releaser for the current release
|
314
|
+
const changeLogDiff = await forceRunAsync('git', [
|
315
|
+
'--no-pager', 'diff',
|
316
|
+
`${this.releaseCommitSha}^..${this.releaseCommitSha}`,
|
317
|
+
'--',
|
318
|
+
`doc/changelogs/CHANGELOG_V${this.versionComponents.major}.md`
|
319
|
+
], { captureStdout: true, ignoreFailure: false });
|
320
|
+
const headingLine = /^\+## \d{4}-\d{2}-\d{2}, Version \d.+$/m.exec(changeLogDiff);
|
321
|
+
if (headingLine == null) {
|
322
|
+
cli.error('Cannot find section for the new release in CHANGELOG');
|
323
|
+
throw new Error('Aborted');
|
324
|
+
}
|
325
|
+
this.releaseTitle = headingLine[0].slice(4);
|
326
|
+
const expectedLine = `+## ${releaseCommitMessage}, @${this.username}`;
|
327
|
+
if (headingLine[0] !== expectedLine &&
|
328
|
+
!headingLine[0].startsWith(`${expectedLine} prepared by @`)) {
|
329
|
+
cli.error(
|
330
|
+
`Invalid section heading for CHANGELOG. Expected "${
|
331
|
+
expectedLine.slice(1)
|
332
|
+
}", found "${headingLine[0].slice(1)}`
|
333
|
+
);
|
334
|
+
if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
|
335
|
+
throw new Error('Aborted');
|
336
|
+
}
|
337
|
+
}
|
338
|
+
}
|
339
|
+
|
340
|
+
async secureTagRelease() {
|
341
|
+
const { version, isLTS, ltsCodename, releaseCommitSha } = this;
|
342
|
+
|
343
|
+
const releaseInfo = isLTS ? `${ltsCodename} (LTS)` : '(Current)';
|
344
|
+
|
345
|
+
try {
|
346
|
+
await new Promise((resolve, reject) => {
|
347
|
+
const api = new gst.API(process.cwd());
|
348
|
+
api.sign(`v${version}`, releaseCommitSha, {
|
349
|
+
insecure: false,
|
350
|
+
m: `${this.date} Node.js v${version} ${releaseInfo} Release`
|
351
|
+
}, (err) => err ? reject(err) : resolve());
|
352
|
+
});
|
353
|
+
} catch (err) {
|
354
|
+
const tagCommitSHA = await forceRunAsync('git', [
|
355
|
+
'rev-parse', `refs/tags/v${version}^0`
|
356
|
+
], { captureStdout: true, ignoreFailure: false });
|
357
|
+
if (tagCommitSHA.trim() !== releaseCommitSha) {
|
358
|
+
throw new Error(
|
359
|
+
`Existing version tag points to ${tagCommitSHA.trim()} instead of ${releaseCommitSha}`,
|
360
|
+
{ cause: err }
|
361
|
+
);
|
362
|
+
}
|
363
|
+
await forceRunAsync('git', ['tag', '--verify', `v${version}`], { ignoreFailure: false });
|
364
|
+
this.cli.info('Using the existing tag');
|
365
|
+
}
|
366
|
+
}
|
367
|
+
|
368
|
+
// Set up the branch so that nightly builds are produced with the next
|
369
|
+
// version number and a pre-release tag.
|
370
|
+
async setupForNextRelease() {
|
371
|
+
const { versionComponents, prid } = this;
|
372
|
+
|
373
|
+
// Update node_version.h for next patch release.
|
374
|
+
const filePath = path.resolve('src', 'node_version.h');
|
375
|
+
const nodeVersionFile = await fs.open(filePath, 'r+');
|
376
|
+
|
377
|
+
const patchVersion = versionComponents.patch + 1;
|
378
|
+
let cursor = 0;
|
379
|
+
for await (const line of nodeVersionFile.readLines({ autoClose: false })) {
|
380
|
+
cursor += line.length + 1;
|
381
|
+
if (line === `#define NODE_PATCH_VERSION ${versionComponents.patch}`) {
|
382
|
+
await nodeVersionFile.write(`${patchVersion}`, cursor - 2, 'ascii');
|
383
|
+
} else if (line === '#define NODE_VERSION_IS_RELEASE 1') {
|
384
|
+
await nodeVersionFile.write('0', cursor - 2, 'ascii');
|
385
|
+
break;
|
386
|
+
}
|
387
|
+
}
|
388
|
+
|
389
|
+
await nodeVersionFile.close();
|
390
|
+
|
391
|
+
const workingOnVersion =
|
392
|
+
`v${versionComponents.major}.${versionComponents.minor}.${patchVersion}`;
|
393
|
+
|
394
|
+
// Create 'Working On' commit.
|
395
|
+
await forceRunAsync('git', ['add', filePath], { ignoreFailure: false });
|
396
|
+
await forceRunAsync('git', [
|
397
|
+
'commit',
|
398
|
+
...this.gpgSign,
|
399
|
+
'-m',
|
400
|
+
`Working on ${workingOnVersion}`,
|
401
|
+
'-m',
|
402
|
+
`PR-URL: https://github.com/nodejs/node/pull/${prid}`
|
403
|
+
], { ignoreFailure: false });
|
404
|
+
const workingOnNewReleaseCommit = await forceRunAsync('git', ['rev-parse', 'HEAD'],
|
405
|
+
{ ignoreFailure: false, captureStdout: true });
|
406
|
+
return workingOnNewReleaseCommit.trim();
|
407
|
+
}
|
408
|
+
|
409
|
+
async pushToRemote(workingOnNewReleaseCommit) {
|
410
|
+
const { cli, dryRun, version, versionComponents, stagingBranch } = this;
|
411
|
+
const releaseBranch = `v${versionComponents.major}.x`;
|
412
|
+
const tagVersion = `v${version}`;
|
413
|
+
|
414
|
+
this.defaultBranch ??= await this.getDefaultBranch();
|
415
|
+
|
416
|
+
let prompt = `Push release tag and commits to ${this.upstream}?`;
|
417
|
+
if (dryRun) {
|
418
|
+
cli.info(dryRunMessage);
|
419
|
+
cli.info('Run the following command to push to remote:');
|
420
|
+
cli.info(`git push ${this.upstream} ${
|
421
|
+
this.defaultBranch} ${
|
422
|
+
tagVersion} ${
|
423
|
+
workingOnNewReleaseCommit}:refs/heads/${releaseBranch} ${
|
424
|
+
workingOnNewReleaseCommit}:refs/heads/${stagingBranch}`);
|
425
|
+
cli.warn('Once pushed, you must not delete the local tag');
|
426
|
+
prompt = 'Ready to continue?';
|
427
|
+
}
|
428
|
+
|
429
|
+
const shouldPushTag = await cli.prompt(prompt, { defaultAnswer: true });
|
430
|
+
if (!shouldPushTag) {
|
431
|
+
cli.warn('Aborting release promotion');
|
432
|
+
throw new Error('Aborted');
|
433
|
+
} else if (dryRun) {
|
434
|
+
return;
|
435
|
+
}
|
436
|
+
|
437
|
+
cli.startSpinner('Pushing to remote');
|
438
|
+
await forceRunAsync('git', ['push', this.upstream, this.defaultBranch, tagVersion,
|
439
|
+
`${workingOnNewReleaseCommit}:refs/heads/${releaseBranch}`,
|
440
|
+
`${workingOnNewReleaseCommit}:refs/heads/${stagingBranch}`],
|
441
|
+
{ ignoreFailure: false });
|
442
|
+
cli.stopSpinner(`Pushed ${tagVersion}, ${this.defaultBranch}, ${
|
443
|
+
releaseBranch}, and ${stagingBranch} to remote`);
|
444
|
+
cli.warn('Now that it has been pushed, you must not delete the local tag');
|
445
|
+
}
|
446
|
+
|
447
|
+
async promoteAndSignRelease() {
|
448
|
+
const { cli, dryRun } = this;
|
449
|
+
let prompt = 'Promote and sign release builds?';
|
450
|
+
|
451
|
+
if (dryRun) {
|
452
|
+
cli.info(dryRunMessage);
|
453
|
+
cli.info('Run the following command to sign and promote the release:');
|
454
|
+
cli.info('./tools/release.sh -i <keyPath>');
|
455
|
+
prompt = 'Ready to continue?';
|
456
|
+
}
|
457
|
+
const shouldPromote = await cli.prompt(prompt, { defaultAnswer: true });
|
458
|
+
if (!shouldPromote) {
|
459
|
+
cli.warn('Aborting release promotion');
|
460
|
+
throw new Error('Aborted');
|
461
|
+
} else if (dryRun) {
|
462
|
+
return;
|
463
|
+
}
|
464
|
+
|
465
|
+
// TODO: move this to .ncurc
|
466
|
+
const defaultKeyPath = '~/.ssh/node_id_rsa';
|
467
|
+
const keyPath = await cli.prompt(
|
468
|
+
`Please enter the path to your ssh key (Default ${defaultKeyPath}): `,
|
469
|
+
{ questionType: 'input', defaultAnswer: defaultKeyPath });
|
470
|
+
|
471
|
+
cli.startSpinner('Signing and promoting the release');
|
472
|
+
await forceRunAsync('./tools/release.sh', ['-i', keyPath], { ignoreFailure: false });
|
473
|
+
cli.stopSpinner('Release has been signed and promoted');
|
474
|
+
}
|
475
|
+
|
476
|
+
async cherryPickToDefaultBranch() {
|
477
|
+
this.defaultBranch ??= await this.getDefaultBranch();
|
478
|
+
const releaseCommitSha = this.releaseCommitSha;
|
479
|
+
await forceRunAsync('git', ['checkout', this.defaultBranch], { ignoreFailure: false });
|
480
|
+
|
481
|
+
await this.tryResetBranch();
|
482
|
+
|
483
|
+
// There might be conflicts, we do not want to treat this as a hard failure,
|
484
|
+
// but we want to retain that information.
|
485
|
+
try {
|
486
|
+
await forceRunAsync('git', ['cherry-pick', ...this.gpgSign, releaseCommitSha],
|
487
|
+
{ ignoreFailure: false });
|
488
|
+
return true;
|
489
|
+
} catch {
|
490
|
+
return false;
|
491
|
+
}
|
492
|
+
}
|
493
|
+
}
|
package/lib/security_blog.js
CHANGED
@@ -11,12 +11,10 @@ import {
|
|
11
11
|
import auth from './auth.js';
|
12
12
|
import Request from './request.js';
|
13
13
|
|
14
|
-
const kChanged = Symbol('changed');
|
15
|
-
|
16
14
|
export default class SecurityBlog extends SecurityRelease {
|
17
15
|
req;
|
18
16
|
|
19
|
-
async createPreRelease() {
|
17
|
+
async createPreRelease(nodejsOrgFolder) {
|
20
18
|
const { cli } = this;
|
21
19
|
|
22
20
|
// checkout on security release branch
|
@@ -45,14 +43,32 @@ export default class SecurityBlog extends SecurityRelease {
|
|
45
43
|
};
|
46
44
|
const month = releaseDate.toLocaleString('en-US', { month: 'long' }).toLowerCase();
|
47
45
|
const year = releaseDate.getFullYear();
|
48
|
-
const fileName = `${month}-${year}-security-releases
|
46
|
+
const fileName = `${month}-${year}-security-releases`;
|
47
|
+
const fileNameExt = fileName + '.md';
|
49
48
|
const preRelease = this.buildPreRelease(template, data);
|
50
|
-
|
49
|
+
|
50
|
+
const pathToBlogPosts = 'apps/site/pages/en/blog/release';
|
51
|
+
const pathToBannerJson = 'apps/site/site.json';
|
52
|
+
|
53
|
+
const file = path.resolve(process.cwd(), nodejsOrgFolder, pathToBlogPosts, fileNameExt);
|
54
|
+
const site = path.resolve(process.cwd(), nodejsOrgFolder, pathToBannerJson);
|
55
|
+
|
56
|
+
const endDate = new Date(data.annoucementDate);
|
57
|
+
endDate.setDate(endDate.getDate() + 7);
|
58
|
+
|
59
|
+
this.updateWebsiteBanner(site, {
|
60
|
+
startDate: data.annoucementDate,
|
61
|
+
endDate: endDate.toISOString(),
|
62
|
+
text: `New security releases to be made available ${data.releaseDate}`,
|
63
|
+
link: `https://nodejs.org/en/blog/vulnerability/${fileName}`,
|
64
|
+
type: 'warning'
|
65
|
+
});
|
66
|
+
|
51
67
|
fs.writeFileSync(file, preRelease);
|
52
|
-
cli.ok(`
|
68
|
+
cli.ok(`Announcement file created and banner has been updated. Folder: ${nodejsOrgFolder}`);
|
53
69
|
}
|
54
70
|
|
55
|
-
async createPostRelease() {
|
71
|
+
async createPostRelease(nodejsOrgFolder) {
|
56
72
|
const { cli } = this;
|
57
73
|
const credentials = await auth({
|
58
74
|
github: true,
|
@@ -65,7 +81,7 @@ export default class SecurityBlog extends SecurityRelease {
|
|
65
81
|
checkoutOnSecurityReleaseBranch(cli, this.repository);
|
66
82
|
|
67
83
|
// read vulnerabilities JSON file
|
68
|
-
const content = this.readVulnerabilitiesJSON(
|
84
|
+
const content = this.readVulnerabilitiesJSON();
|
69
85
|
if (!content.releaseDate) {
|
70
86
|
cli.error('Release date is not set in vulnerabilities.json,' +
|
71
87
|
' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
|
@@ -76,47 +92,54 @@ export default class SecurityBlog extends SecurityRelease {
|
|
76
92
|
const releaseDate = new Date(content.releaseDate);
|
77
93
|
const template = this.getSecurityPostReleaseTemplate();
|
78
94
|
const data = {
|
79
|
-
|
80
|
-
annoucementDate: await this.getAnnouncementDate(cli),
|
95
|
+
annoucementDate: releaseDate.toISOString(),
|
81
96
|
releaseDate: this.formatReleaseDate(releaseDate),
|
82
97
|
affectedVersions: this.getAffectedVersions(content),
|
83
98
|
vulnerabilities: this.getVulnerabilities(content),
|
84
99
|
slug: this.getSlug(releaseDate),
|
85
|
-
author:
|
100
|
+
author: 'The Node.js Project',
|
86
101
|
dependencyUpdates: content.dependencies
|
87
102
|
};
|
88
|
-
const postReleaseContent = await this.buildPostRelease(template, data, content);
|
89
103
|
|
90
|
-
const
|
91
|
-
|
92
|
-
|
104
|
+
const pathToBlogPosts = path.resolve(nodejsOrgFolder, 'apps/site/pages/en/blog/release');
|
105
|
+
const pathToBannerJson = path.resolve(nodejsOrgFolder, 'apps/site/site.json');
|
106
|
+
|
107
|
+
const preReleasePath = path.resolve(pathToBlogPosts, data.slug + '.md');
|
108
|
+
let preReleaseContent = this.findExistingPreRelease(preReleasePath);
|
109
|
+
if (!preReleaseContent) {
|
110
|
+
cli.error(`Existing pre-release not found! Path: ${preReleasePath} `);
|
111
|
+
process.exit(1);
|
112
|
+
}
|
113
|
+
|
114
|
+
const postReleaseContent = await this.buildPostRelease(template, data, content);
|
93
115
|
// cut the part before summary
|
94
116
|
const preSummary = preReleaseContent.indexOf('# Summary');
|
95
117
|
if (preSummary !== -1) {
|
96
118
|
preReleaseContent = preReleaseContent.substring(preSummary);
|
97
119
|
}
|
98
|
-
|
99
120
|
const updatedContent = postReleaseContent + preReleaseContent;
|
100
121
|
|
101
|
-
|
102
|
-
|
122
|
+
const endDate = new Date(data.annoucementDate);
|
123
|
+
endDate.setDate(endDate.getDate() + 7);
|
124
|
+
const month = releaseDate.toLocaleString('en-US', { month: 'long' });
|
125
|
+
const capitalizedMonth = month[0].toUpperCase() + month.slice(1);
|
103
126
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
127
|
+
this.updateWebsiteBanner(pathToBannerJson, {
|
128
|
+
startDate: releaseDate,
|
129
|
+
endDate,
|
130
|
+
text: `${capitalizedMonth} Security Release is available`
|
131
|
+
});
|
108
132
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
questionType: 'input',
|
113
|
-
defaultAnswer: ''
|
114
|
-
});
|
133
|
+
fs.writeFileSync(preReleasePath, updatedContent);
|
134
|
+
cli.ok(`Announcement file and banner has been updated. Folder: ${nodejsOrgFolder}`);
|
135
|
+
}
|
115
136
|
|
116
|
-
|
117
|
-
|
137
|
+
findExistingPreRelease(filepath) {
|
138
|
+
if (!fs.existsSync(filepath)) {
|
139
|
+
return null;
|
118
140
|
}
|
119
|
-
|
141
|
+
|
142
|
+
return fs.readFileSync(filepath, 'utf-8');
|
120
143
|
}
|
121
144
|
|
122
145
|
promptAuthor(cli) {
|
@@ -127,6 +150,20 @@ export default class SecurityBlog extends SecurityRelease {
|
|
127
150
|
});
|
128
151
|
}
|
129
152
|
|
153
|
+
updateWebsiteBanner(siteJsonPath, content) {
|
154
|
+
const siteJson = JSON.parse(fs.readFileSync(siteJsonPath));
|
155
|
+
|
156
|
+
const currentValue = siteJson.websiteBanners.index;
|
157
|
+
siteJson.websiteBanners.index = {
|
158
|
+
startDate: content.startDate ?? currentValue.startDate,
|
159
|
+
endDate: content.endDate ?? currentValue.endDate,
|
160
|
+
text: content.text ?? currentValue.text,
|
161
|
+
link: content.link ?? currentValue.link,
|
162
|
+
type: content.type ?? currentValue.type
|
163
|
+
};
|
164
|
+
fs.writeFileSync(siteJsonPath, JSON.stringify(siteJson, null, 2));
|
165
|
+
}
|
166
|
+
|
130
167
|
formatReleaseDate(releaseDate) {
|
131
168
|
const options = {
|
132
169
|
weekday: 'long',
|
package/lib/session.js
CHANGED
@@ -9,6 +9,7 @@ import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer';
|
|
9
9
|
import { shortSha } from '../utils.js';
|
10
10
|
|
11
11
|
import { getCurrentV8Version } from './common.js';
|
12
|
+
import { forceRunAsync } from '../run.js';
|
12
13
|
|
13
14
|
export async function checkOptions(options) {
|
14
15
|
if (options.sha.length > 1 && options.squash) {
|
@@ -41,6 +42,8 @@ export function doBackport(options) {
|
|
41
42
|
}
|
42
43
|
}
|
43
44
|
todo.push(commitSquashedBackport());
|
45
|
+
} else if (options.preserveOriginalAuthor) {
|
46
|
+
todo.push(cherryPickV8Commits(options));
|
44
47
|
} else {
|
45
48
|
todo.push(applyAndCommitPatches());
|
46
49
|
}
|
@@ -76,18 +79,47 @@ function commitSquashedBackport() {
|
|
76
79
|
};
|
77
80
|
};
|
78
81
|
|
79
|
-
|
82
|
+
const commitTask = (patch, extraArgs, trailers) => async(ctx) => {
|
83
|
+
const messageTitle = formatMessageTitle([patch]);
|
84
|
+
const messageBody = formatMessageBody(patch, false, trailers);
|
85
|
+
await ctx.execGitNode('add', ['deps/v8']);
|
86
|
+
await ctx.execGitNode('commit', [
|
87
|
+
...ctx.gpgSign, ...extraArgs,
|
88
|
+
'-m', messageTitle, '-m', messageBody
|
89
|
+
]);
|
90
|
+
};
|
91
|
+
|
92
|
+
function amendHEAD(patch) {
|
80
93
|
return {
|
81
|
-
title: '
|
94
|
+
title: 'Amend/commit',
|
82
95
|
task: async(ctx) => {
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
96
|
+
let coAuthor;
|
97
|
+
if (patch.hadConflicts) {
|
98
|
+
const getGitConfigEntry = async(configKey) => {
|
99
|
+
const output = await forceRunAsync('git', ['config', configKey], {
|
100
|
+
ignoreFailure: false,
|
101
|
+
captureStdout: true,
|
102
|
+
spawnArgs: { cwd: ctx.nodeDir }
|
103
|
+
});
|
104
|
+
return output.trim();
|
105
|
+
};
|
106
|
+
await ctx.execGitNode('am', [...ctx.gpgSign, '--continue']);
|
107
|
+
coAuthor = `\nCo-authored-by: ${
|
108
|
+
await getGitConfigEntry('user.name')} <${
|
109
|
+
await getGitConfigEntry('user.email')}>`;
|
110
|
+
}
|
111
|
+
await commitTask(patch, ['--amend'], coAuthor)(ctx);
|
87
112
|
}
|
88
113
|
};
|
89
114
|
}
|
90
115
|
|
116
|
+
function commitPatch(patch) {
|
117
|
+
return {
|
118
|
+
title: 'Commit patch',
|
119
|
+
task: commitTask(patch)
|
120
|
+
};
|
121
|
+
}
|
122
|
+
|
91
123
|
function formatMessageTitle(patches) {
|
92
124
|
const action =
|
93
125
|
patches.some(patch => patch.hadConflicts) ? 'backport' : 'cherry-pick';
|
@@ -106,12 +138,12 @@ function formatMessageTitle(patches) {
|
|
106
138
|
}
|
107
139
|
}
|
108
140
|
|
109
|
-
function formatMessageBody(patch, prefixTitle) {
|
141
|
+
function formatMessageBody(patch, prefixTitle, trailers = '') {
|
110
142
|
const indentedMessage = patch.message.replace(/\n/g, '\n ');
|
111
143
|
const body =
|
112
144
|
'Original commit message:\n\n' +
|
113
145
|
` ${indentedMessage}\n\n` +
|
114
|
-
`Refs: https://github.com/v8/v8/commit/${patch.sha}`;
|
146
|
+
`Refs: https://github.com/v8/v8/commit/${patch.sha}${trailers}`;
|
115
147
|
|
116
148
|
if (prefixTitle) {
|
117
149
|
const action = patch.hadConflicts ? 'Backport' : 'Cherry-pick';
|
@@ -167,6 +199,15 @@ function applyAndCommitPatches() {
|
|
167
199
|
};
|
168
200
|
}
|
169
201
|
|
202
|
+
function cherryPickV8Commits() {
|
203
|
+
return {
|
204
|
+
title: 'Cherry-pick commit from V8 clone to deps/v8',
|
205
|
+
task: (ctx, task) => {
|
206
|
+
return task.newListr(ctx.patches.map(cherryPickV8CommitTask));
|
207
|
+
}
|
208
|
+
};
|
209
|
+
}
|
210
|
+
|
170
211
|
function applyPatchTask(patch) {
|
171
212
|
return {
|
172
213
|
title: `Commit ${shortSha(patch.sha)}`,
|
@@ -190,10 +231,33 @@ function applyPatchTask(patch) {
|
|
190
231
|
};
|
191
232
|
}
|
192
233
|
|
193
|
-
|
234
|
+
function cherryPickV8CommitTask(patch) {
|
235
|
+
return {
|
236
|
+
title: `Commit ${shortSha(patch.sha)}`,
|
237
|
+
task: (ctx, task) => {
|
238
|
+
const todo = [
|
239
|
+
{
|
240
|
+
title: 'Cherry-pick',
|
241
|
+
task: (ctx, task) => applyPatch(ctx, task, patch, 'am')
|
242
|
+
}
|
243
|
+
];
|
244
|
+
if (ctx.bump !== false) {
|
245
|
+
if (ctx.nodeMajorVersion < 9) {
|
246
|
+
todo.push(incrementV8Version());
|
247
|
+
} else {
|
248
|
+
todo.push(incrementEmbedderVersion());
|
249
|
+
}
|
250
|
+
}
|
251
|
+
todo.push(amendHEAD(patch));
|
252
|
+
return task.newListr(todo);
|
253
|
+
}
|
254
|
+
};
|
255
|
+
}
|
256
|
+
|
257
|
+
async function applyPatch(ctx, task, patch, method = 'apply') {
|
194
258
|
try {
|
195
259
|
await ctx.execGitNode(
|
196
|
-
|
260
|
+
method,
|
197
261
|
['-p1', '--3way', '--directory=deps/v8'],
|
198
262
|
patch.data /* input */
|
199
263
|
);
|
package/lib/update-v8/index.js
CHANGED
@@ -26,6 +26,7 @@ export function minor(options) {
|
|
26
26
|
export async function backport(options) {
|
27
27
|
const shouldStop = await checkOptions(options);
|
28
28
|
if (shouldStop) return;
|
29
|
+
options.gpgSign = options.gpgSign ? ['-S'] : [];
|
29
30
|
const tasks = new Listr(
|
30
31
|
[updateV8Clone(), doBackport(options)],
|
31
32
|
getOptions(options)
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@node-core/utils",
|
3
|
-
"version": "5.
|
3
|
+
"version": "5.8.0",
|
4
4
|
"description": "Utilities for Node.js core collaborators",
|
5
5
|
"type": "module",
|
6
6
|
"engines": {
|
@@ -46,6 +46,7 @@
|
|
46
46
|
"core-validate-commit": "^4.1.0",
|
47
47
|
"figures": "^6.1.0",
|
48
48
|
"ghauth": "^6.0.7",
|
49
|
+
"git-secure-tag": "^2.3.1",
|
49
50
|
"js-yaml": "^4.1.0",
|
50
51
|
"listr2": "^8.2.4",
|
51
52
|
"lodash": "^4.17.21",
|