@node-core/utils 6.3.3 → 7.0.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.
Files changed (56) hide show
  1. package/README.md +2 -5
  2. package/bin/ncu-ci.js +3 -19
  3. package/components/git/benchmark.js +112 -0
  4. package/components/git/land.js +3 -3
  5. package/components/git/metadata.js +34 -1
  6. package/components/git/release.js +3 -5
  7. package/components/git/security.js +258 -27
  8. package/components/git/v8.js +6 -3
  9. package/components/metadata.js +75 -7
  10. package/lib/auth.js +3 -4
  11. package/lib/backport_session.js +3 -3
  12. package/lib/benchmark.js +243 -0
  13. package/lib/cherry_pick.js +32 -11
  14. package/lib/ci/failure_aggregator.js +16 -20
  15. package/lib/ci/run_ci.js +1 -1
  16. package/lib/cli.js +33 -1
  17. package/lib/config.js +7 -7
  18. package/lib/file.js +5 -5
  19. package/lib/landing_session.js +65 -24
  20. package/lib/links.js +4 -4
  21. package/lib/pr_checker.js +163 -31
  22. package/lib/pr_data.js +1 -1
  23. package/lib/pr_summary.js +1 -1
  24. package/lib/prepare_release.js +270 -85
  25. package/lib/prepare_security.js +367 -41
  26. package/lib/promote_release.js +19 -16
  27. package/lib/queries/ListSecurityReleasePRs.gql +19 -0
  28. package/lib/queries/PR.gql +4 -1
  29. package/lib/request.js +41 -0
  30. package/lib/run.js +5 -5
  31. package/lib/security-announcement.js +10 -5
  32. package/lib/security-release/security-release.js +160 -20
  33. package/lib/security_blog.js +130 -65
  34. package/lib/session.js +4 -3
  35. package/lib/team_info.js +1 -1
  36. package/lib/update-v8/applyNodeChanges.js +1 -3
  37. package/lib/update-v8/backport.js +6 -6
  38. package/lib/update-v8/commitUpdate.js +3 -6
  39. package/lib/update-v8/common.js +30 -6
  40. package/lib/update-v8/constants.js +22 -0
  41. package/lib/update-v8/deps.js +3 -5
  42. package/lib/update-v8/index.js +14 -5
  43. package/lib/update-v8/majorUpdate.js +5 -4
  44. package/lib/update-v8/minorUpdate.js +4 -4
  45. package/lib/update-v8/updateCrates.js +192 -0
  46. package/lib/update-v8/updateV8Clone.js +1 -1
  47. package/lib/update-v8/updateVersionNumbers.js +3 -6
  48. package/lib/update-v8/util.js +3 -3
  49. package/lib/update_security_release.js +347 -105
  50. package/lib/utils.js +5 -5
  51. package/lib/verbosity.js +3 -3
  52. package/lib/wpt/index.js +1 -23
  53. package/package.json +18 -22
  54. package/bin/get-metadata.js +0 -11
  55. package/lib/wpt/templates/README.md +0 -16
  56. package/npm-shrinkwrap.json +0 -20380
package/README.md CHANGED
@@ -29,9 +29,6 @@ CLI tools for Node.js Core collaborators.
29
29
  to use.
30
30
  - [`ncu-team`](./docs/ncu-team.md): Listing members of a team, synchronizing
31
31
  special blocks in files with the list of members.
32
- - [`get-metadata`](./docs/get-metadata.md): Retrieving metadata for a Pull Request.
33
- **DEPRECATED**: use [`git node metadata`](./docs/git-node.md#git-node-metadata)
34
- instead.
35
32
  - [`ncu-ci`](./docs/ncu-ci.md): Parse the results of a Jenkins CI run and display a summary for all the failures.
36
33
 
37
34
  ## Usage
@@ -66,8 +63,8 @@ to create the token.
66
63
 
67
64
  When creating the token, the following boxes need to be checked:
68
65
 
69
- - `user:email`: Used by `git-node` and `get-metadata` to read the email of the
70
- PR author in order to check if it matches the email of the commit author.
66
+ - `user:email`: Used by `git-node` to read the email of the PR author in order
67
+ to check if it matches the email of the commit author.
71
68
  - `read:org`: Used by `ncu-team` to read the list of team members.
72
69
 
73
70
  Optionally, if you want to grant write access so `git-node` can write comments:
package/bin/ncu-ci.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import yargs from 'yargs';
4
- import clipboardy from 'clipboardy';
5
4
 
6
5
  import {
7
6
  JobParser,
@@ -222,11 +221,6 @@ const args = yargs(hideBin(process.argv))
222
221
  handler
223
222
  })
224
223
  .demandCommand(1, 'must provide a valid command')
225
- .option('copy', {
226
- describe: 'Write the results as markdown to clipboard',
227
- default: false,
228
- type: 'boolean'
229
- })
230
224
  .option('skip-more-than', {
231
225
  describe: 'Skip jobs that fail more than <limit> builds, when --stat is true, default to 10.',
232
226
  type: 'number',
@@ -397,7 +391,7 @@ class CICommand {
397
391
  this.json = this.json.concat(json);
398
392
  }
399
393
 
400
- if ((argv.copy || argv.markdown) && !argv.stats) {
394
+ if (argv.markdown && !argv.stats) {
401
395
  this.markdown += build.formatAsMarkdown();
402
396
  }
403
397
  }
@@ -408,16 +402,6 @@ class CICommand {
408
402
  async serialize() {
409
403
  const { argv, cli } = this;
410
404
 
411
- if (argv.copy) {
412
- if (this.markdown) {
413
- clipboardy.writeSync(this.markdown);
414
- cli.separator('');
415
- cli.log('Written markdown to clipboard');
416
- } else {
417
- cli.error('No markdown generated');
418
- }
419
- }
420
-
421
405
  if (argv.markdown) {
422
406
  if (this.markdown) {
423
407
  writeFile(argv.markdown, this.markdown);
@@ -483,7 +467,7 @@ class WalkCommand extends CICommand {
483
467
  cli.log('');
484
468
  aggregator.display();
485
469
 
486
- if (argv.markdown || argv.copy) {
470
+ if (argv.markdown) {
487
471
  this.markdown = aggregator.formatAsMarkdown();
488
472
  }
489
473
  }
@@ -564,7 +548,7 @@ class DailyCommand extends CICommand {
564
548
  cli.log('');
565
549
  aggregator.display();
566
550
 
567
- if (argv.markdown || argv.copy) {
551
+ if (argv.markdown) {
568
552
  this.markdown = aggregator.formatAsMarkdown();
569
553
  }
570
554
  }
@@ -0,0 +1,112 @@
1
+ import auth from '../../lib/auth.js';
2
+ import { parsePRFromURL } from '../../lib/links.js';
3
+ import CLI from '../../lib/cli.js';
4
+ import Request from '../../lib/request.js';
5
+ import { runPromise } from '../../lib/run.js';
6
+ import BenchmarkSession from '../../lib/benchmark.js';
7
+
8
+ export const command = 'benchmark <identifier>';
9
+ export const describe =
10
+ 'Trigger the benchmark GitHub Actions workflow for a pull request';
11
+
12
+ const options = {
13
+ owner: {
14
+ alias: 'o',
15
+ describe: 'GitHub owner of the PR repository',
16
+ default: 'nodejs',
17
+ type: 'string'
18
+ },
19
+ repo: {
20
+ alias: 'r',
21
+ describe: 'GitHub repository of the PR',
22
+ default: 'node',
23
+ type: 'string'
24
+ },
25
+ 'workflow-owner': {
26
+ describe: 'GitHub owner of the repository hosting the benchmark workflow ' +
27
+ '(defaults to the PR owner)',
28
+ type: 'string'
29
+ },
30
+ 'workflow-repo': {
31
+ describe: 'GitHub repository hosting the benchmark workflow ' +
32
+ '(defaults to the PR repository)',
33
+ type: 'string'
34
+ },
35
+ workflow: {
36
+ describe: 'The workflow file to dispatch',
37
+ default: 'benchmark.yml',
38
+ type: 'string'
39
+ },
40
+ ref: {
41
+ describe: 'The git ref of the workflow repository to run the workflow from',
42
+ default: 'main',
43
+ type: 'string'
44
+ },
45
+ runs: {
46
+ describe: 'How many times to repeat each benchmark',
47
+ type: 'number'
48
+ }
49
+ };
50
+
51
+ let yargsInstance;
52
+
53
+ export function builder(yargs) {
54
+ yargsInstance = yargs;
55
+ return yargs
56
+ .options(options)
57
+ .positional('identifier', {
58
+ type: 'string',
59
+ describe: 'ID or URL of the pull request. A commit URL ' +
60
+ '(…/pull/<id>/commits/<sha>) benchmarks that commit instead of the ' +
61
+ 'PR head.'
62
+ })
63
+ .example('git node benchmark 12344',
64
+ 'Pick benchmark categories to run on ' +
65
+ 'https://github.com/nodejs/node/pull/12344')
66
+ .example('git node benchmark https://github.com/nodejs/node/pull/12344',
67
+ 'Pick benchmark categories to run on ' +
68
+ 'https://github.com/nodejs/node/pull/12344')
69
+ .example(
70
+ 'git node benchmark ' +
71
+ 'https://github.com/nodejs/node/pull/12344/commits/abc1234',
72
+ 'Benchmark commit abc1234 rather than the current PR head')
73
+ .wrap(90);
74
+ }
75
+
76
+ const COMMIT_URL_RE = /\/pull\/\d+\/commits\/([0-9a-f]{7,40})/i;
77
+
78
+ // Parse the positional identifier into { prid, owner?, repo?, commit? }.
79
+ // A PR commit URL (…/pull/<id>/commits/<sha>) also yields the commit SHA.
80
+ export function parseIdentifier(identifier) {
81
+ const prid = Number.parseInt(identifier);
82
+ if (!Number.isNaN(prid)) {
83
+ return { prid };
84
+ }
85
+ const parsed = parsePRFromURL(identifier);
86
+ if (!parsed) {
87
+ return undefined;
88
+ }
89
+ const commit = COMMIT_URL_RE.exec(identifier);
90
+ if (commit) {
91
+ parsed.commit = commit[1];
92
+ }
93
+ return parsed;
94
+ }
95
+
96
+ export function handler(argv) {
97
+ const parsed = parseIdentifier(argv.identifier);
98
+ if (!parsed) {
99
+ return yargsInstance.showHelp();
100
+ }
101
+ Object.assign(argv, parsed);
102
+
103
+ return runPromise(main(argv));
104
+ }
105
+
106
+ async function main(argv) {
107
+ const cli = new CLI(process.stderr);
108
+ const credentials = await auth({ github: true });
109
+ const request = new Request(credentials);
110
+ const session = new BenchmarkSession(cli, request, argv);
111
+ return session.start();
112
+ }
@@ -48,8 +48,8 @@ const landActions = {
48
48
  type: 'boolean'
49
49
  },
50
50
  fixupAll: {
51
- describe: 'Automatically fixup all commits to the first one dismissing ' +
52
- 'other commit messages',
51
+ describe: 'Automatically fixup ("squash") all commits to the first one ' +
52
+ 'dismissing other commit messages',
53
53
  default: false,
54
54
  type: 'boolean'
55
55
  },
@@ -181,7 +181,7 @@ async function main(state, argv, cli, dir) {
181
181
  }
182
182
  try {
183
183
  session.restore();
184
- } catch (err) { // JSON error?
184
+ } catch { // JSON error?
185
185
  if (state === ABORT) {
186
186
  await session.abort();
187
187
  return;
@@ -36,6 +36,10 @@ const options = {
36
36
  describe: 'Check for \'LGTM\' in comments',
37
37
  type: 'boolean'
38
38
  },
39
+ json: {
40
+ describe: 'Print metadata and PR readiness result as JSON',
41
+ type: 'boolean'
42
+ },
39
43
  'max-commits': {
40
44
  describe: 'Number of commits to warn',
41
45
  type: 'number',
@@ -45,6 +49,29 @@ const options = {
45
49
 
46
50
  let yargsInstance;
47
51
 
52
+ function writeStdout(text) {
53
+ return new Promise((resolve, reject) => {
54
+ process.stdout.write(text, (error) => {
55
+ if (error) {
56
+ reject(error);
57
+ } else {
58
+ resolve();
59
+ }
60
+ });
61
+ });
62
+ }
63
+
64
+ export async function writeMetadataJsonResult(metadataPromise) {
65
+ try {
66
+ const { json } = await metadataPromise;
67
+ await writeStdout(`${JSON.stringify(json, null, 2)}\n`);
68
+ process.exitCode = json.exitCode;
69
+ } catch (error) {
70
+ console.error(error);
71
+ process.exitCode = 1;
72
+ }
73
+ }
74
+
48
75
  export function builder(yargs) {
49
76
  yargsInstance = yargs;
50
77
  return yargs
@@ -81,10 +108,16 @@ export function handler(argv) {
81
108
  return yargsInstance.showHelp();
82
109
  }
83
110
 
84
- const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
111
+ const logStream = argv.json || !process.stdout.isTTY
112
+ ? process.stderr
113
+ : process.stdout;
85
114
  const cli = new CLI(logStream);
86
115
 
87
116
  const merged = Object.assign({}, argv, parsed, config);
117
+ if (argv.json) {
118
+ return writeMetadataJsonResult(getMetadata(merged, false, cli));
119
+ }
120
+
88
121
  return runPromise(getMetadata(merged, false, cli)
89
122
  .then(({ status }) => {
90
123
  if (status === false) {
@@ -14,10 +14,6 @@ const PROMOTE = 'promote';
14
14
  const RELEASERS = 'releasers';
15
15
 
16
16
  const releaseOptions = {
17
- filterLabel: {
18
- describe: 'Labels separated by "," to filter security PRs',
19
- type: 'string'
20
- },
21
17
  'gpg-sign': {
22
18
  describe: 'GPG-sign commits, will be passed to the git process',
23
19
  alias: 'S'
@@ -51,7 +47,9 @@ const releaseOptions = {
51
47
  },
52
48
  security: {
53
49
  describe: 'Demarcate the new security release as a security release. ' +
54
- 'Optionally provide path to security-release repository for CVE auto-population',
50
+ 'Optionally provide the path to vulnerabilities.json (or the ' +
51
+ 'security-release repository) to cherry-pick the reports and ' +
52
+ 'dependency updates for this release line and auto-populate CVEs',
55
53
  type: 'string',
56
54
  coerce: (arg) => {
57
55
  // If --security=path is used, return the path
@@ -1,17 +1,37 @@
1
+ import auth from '../../lib/auth.js';
2
+ import Request from '../../lib/request.js';
3
+ import LandingSession from '../../lib/landing_session.js';
4
+ import Session from '../../lib/session.js';
1
5
  import CLI from '../../lib/cli.js';
6
+ import { getMetadata } from '../metadata.js';
7
+ import { checkCwd } from '../../lib/update-v8/common.js';
8
+ import { parsePRFromURL } from '../../lib/links.js';
2
9
  import PrepareSecurityRelease from '../../lib/prepare_security.js';
3
10
  import UpdateSecurityRelease from '../../lib/update_security_release.js';
4
11
  import SecurityBlog from '../../lib/security_blog.js';
5
12
  import SecurityAnnouncement from '../../lib/security-announcement.js';
13
+ import { forceRunAsync } from '../../lib/run.js';
14
+ import { getAffectedVersionLines } from '../../lib/security-release/security-release.js';
15
+ import { getPullRequestURLForLine } from '../../lib/prepare_release.js';
6
16
 
7
17
  export const command = 'security [options]';
8
18
  export const describe = 'Manage an in-progress security release or start a new one.';
9
19
 
20
+ const SECURITY_REPO = {
21
+ owner: 'nodejs-private',
22
+ repo: 'node-private',
23
+ };
24
+
10
25
  const securityOptions = {
11
26
  start: {
12
27
  describe: 'Start security release process',
13
28
  type: 'boolean'
14
29
  },
30
+ 'apply-patches': {
31
+ describe: 'Start an interactive session to make local HEAD ready to create ' +
32
+ 'a security release proposal',
33
+ type: 'boolean'
34
+ },
15
35
  sync: {
16
36
  describe: 'Synchronize an ongoing security release with HackerOne',
17
37
  type: 'boolean'
@@ -52,6 +72,26 @@ const securityOptions = {
52
72
 
53
73
  let yargsInstance;
54
74
 
75
+ function isPromptInterrupted(error) {
76
+ return error?.name === 'ExitPromptError' ||
77
+ error?.name === 'AbortPromptError' ||
78
+ error?.message?.includes('User force closed');
79
+ }
80
+
81
+ function runSecurityAction(cli, action) {
82
+ return Promise.resolve(action).catch((error) => {
83
+ if (!isPromptInterrupted(error)) {
84
+ throw error;
85
+ }
86
+
87
+ if (cli.spinner.isSpinning) {
88
+ cli.spinner.stop();
89
+ }
90
+ cli.warn('Aborted.');
91
+ cli.setExitCode(1);
92
+ });
93
+ }
94
+
55
95
  export function builder(yargs) {
56
96
  yargsInstance = yargs;
57
97
  return yargs.options(securityOptions)
@@ -59,6 +99,10 @@ export function builder(yargs) {
59
99
  'git node security --start',
60
100
  'Prepare a security release of Node.js'
61
101
  )
102
+ .example(
103
+ 'git node security --apply-patches',
104
+ 'Fetch all the patches for an upcoming security release'
105
+ )
62
106
  .example(
63
107
  'git node security --sync',
64
108
  'Synchronize an ongoing security release with HackerOne'
@@ -94,37 +138,36 @@ export function builder(yargs) {
94
138
  export function handler(argv) {
95
139
  const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
96
140
  const cli = new CLI(logStream);
141
+ let action;
97
142
 
98
143
  if (argv.start) {
99
- return startSecurityRelease(cli, argv);
100
- }
101
- if (argv.sync) {
102
- return syncSecurityRelease(cli, argv);
103
- }
104
- if (argv['update-date']) {
105
- return updateReleaseDate(cli, argv);
144
+ action = startSecurityRelease(cli, argv);
145
+ } else if (argv['apply-patches']) {
146
+ action = applySecurityPatches(cli, argv);
147
+ } else if (argv.sync) {
148
+ action = syncSecurityRelease(cli, argv);
149
+ } else if (argv['update-date']) {
150
+ action = updateReleaseDate(cli, argv);
151
+ } else if (argv['pre-release']) {
152
+ action = createPreRelease(cli, argv);
153
+ } else if (argv['add-report']) {
154
+ action = addReport(cli, argv);
155
+ } else if (argv['remove-report']) {
156
+ action = removeReport(cli, argv);
157
+ } else if (argv['notify-pre-release']) {
158
+ action = notifyPreRelease(cli, argv);
159
+ } else if (argv['request-cve']) {
160
+ action = requestCVEs(cli, argv);
161
+ } else if (argv['post-release']) {
162
+ action = createPostRelease(cli, argv);
163
+ } else if (argv.cleanup) {
164
+ action = cleanupSecurityRelease(cli, argv);
106
165
  }
107
- if (argv['pre-release']) {
108
- return createPreRelease(cli, argv);
109
- }
110
- if (argv['add-report']) {
111
- return addReport(cli, argv);
112
- }
113
- if (argv['remove-report']) {
114
- return removeReport(cli, argv);
115
- }
116
- if (argv['notify-pre-release']) {
117
- return notifyPreRelease(cli, argv);
118
- }
119
- if (argv['request-cve']) {
120
- return requestCVEs(cli, argv);
121
- }
122
- if (argv['post-release']) {
123
- return createPostRelease(cli, argv);
124
- }
125
- if (argv.cleanup) {
126
- return cleanupSecurityRelease(cli, argv);
166
+
167
+ if (action) {
168
+ return runSecurityAction(cli, action);
127
169
  }
170
+
128
171
  yargsInstance.showHelp();
129
172
  }
130
173
 
@@ -168,6 +211,194 @@ async function startSecurityRelease(cli) {
168
211
  return release.start();
169
212
  }
170
213
 
214
+ async function fetchVulnerabilitiesDotJSON(cli, req) {
215
+ const { owner } = SECURITY_REPO;
216
+ const repo = 'security-release';
217
+
218
+ cli.startSpinner(`Looking for Security Release PR on ${owner}/${repo}`);
219
+ const { repository: { pullRequests: { nodes: { length, 0: pr } } } } =
220
+ await req.gql('ListSecurityReleasePRs', { owner, repo });
221
+ if (length !== 1) {
222
+ cli.stopSpinner('Expected exactly one open Pull Request on the ' +
223
+ `${owner}/${repo} repository, found ${length}`,
224
+ cli.SPINNER_STATUS.FAILED);
225
+ cli.setExitCode(1);
226
+ return;
227
+ }
228
+ if (pr.files.nodes.length !== 1 || !pr.files.nodes[0].path.endsWith('vulnerabilities.json')) {
229
+ cli.stopSpinner(
230
+ `${owner}/${repo}#${pr.number} does not contain only vulnerabilities.json`,
231
+ cli.SPINNER_STATUS.FAILED
232
+ );
233
+ cli.setExitCode(1);
234
+ return;
235
+ }
236
+ cli.stopSpinner(`Found ${owner}/${repo}#${pr.number} by @${pr.author.login}`);
237
+ cli.startSpinner('Fetching vulnerabilities.json...');
238
+ const result = await req.json(
239
+ `/repos/${owner}/${repo}/contents/${pr.files.nodes[0].path}?ref=${pr.headRefOid}`,
240
+ { headers: { Accept: 'application/vnd.github.raw+json' } }
241
+ );
242
+ cli.stopSpinner('Fetched vulnerabilities.json');
243
+ return result;
244
+ }
245
+
246
+ async function skipIfExisting(cli, prURL) {
247
+ const existingCommit = await forceRunAsync('git',
248
+ ['--no-pager', 'log', 'HEAD', '--grep', `^PR-URL: ${prURL}$`, '--format=%h %s'],
249
+ { ignoreFailure: false, captureStdout: true });
250
+ if (existingCommit.trim()) {
251
+ cli.info(`${prURL} seems to already be on the current tree: ${existingCommit}`);
252
+ return await cli.prompt('Do you want to skip it?', { defaultAnswer: true });
253
+ }
254
+ return false;
255
+ }
256
+ async function landingSession(cli, req, prURL, argv, cveIds = undefined) {
257
+ const response = await cli.prompt('Do you want to land it on the current HEAD?',
258
+ { defaultAnswer: true });
259
+ if (!response) {
260
+ cli.info('Skipping');
261
+ cli.warn('The resulting HEAD will not be ready for a release proposal');
262
+ return true;
263
+ }
264
+
265
+ if (!cli.hasDetachedHEAD) {
266
+ // Moving to a detached HEAD, we don't want security patches to be pushed to the public repo.
267
+ await forceRunAsync('git', ['checkout', '--detach'], { ignoreFailure: false });
268
+ }
269
+ cli.hasDetachedHEAD = (await forceRunAsync(
270
+ 'git', ['rev-parse', 'HEAD'],
271
+ { ignoreFailure: false, captureStdout: true })).trim();
272
+
273
+ const session = new LandingSession(cli, req, process.cwd(), {
274
+ autorebase: true, oneCommitMax: false, ...argv
275
+ });
276
+ Object.defineProperty(session, 'tryResetBranch', {
277
+ __proto__: null,
278
+ value: Function.prototype,
279
+ configurable: true,
280
+ });
281
+ const metadata = await getMetadata(session.argv, true, cli);
282
+ if (argv?.backport) {
283
+ metadata.metadata += `PR-URL: ${prURL}\n`;
284
+ }
285
+ if (cveIds?.length) {
286
+ metadata.metadata += cveIds.map(cve => `CVE-ID: ${cve}\n`).join('');
287
+ }
288
+ await session.start(metadata);
289
+ return false;
290
+ }
291
+ async function applySecurityPatches(cli) {
292
+ const { nodeMajorVersion } = await checkCwd({ nodeDir: process.cwd() });
293
+ const branch = `v${nodeMajorVersion}.x`;
294
+ const credentials = await auth({
295
+ github: true
296
+ });
297
+ const req = new Request(credentials);
298
+
299
+ cli.info('N.B.: if there are commits on the staging branch that need to be included in the ' +
300
+ 'security release, please rebase them manually and answer no to the following question');
301
+ // Try reset to the public upstream
302
+ const session = new Session(cli, process.cwd(), undefined, { branch });
303
+ await session.tryResetBranch();
304
+
305
+ const { owner, repo } = SECURITY_REPO;
306
+ const { releaseDate, reports, dependencies } = await fetchVulnerabilitiesDotJSON(cli, req);
307
+
308
+ let patchedVersion;
309
+ const releaseLine = `${nodeMajorVersion}.x`;
310
+ for (const [name, dep] of Object.entries(dependencies ?? {})) {
311
+ const prURL = getPullRequestURLForLine(
312
+ dep.affectedVersions, releaseLine, dep.prURL);
313
+ if (!prURL) continue;
314
+ cli.separator(`Taking care of ${name}...`);
315
+ if (await skipIfExisting(cli, prURL)) continue;
316
+
317
+ const argv = parsePRFromURL(prURL);
318
+ if (argv.owner === SECURITY_REPO.owner && argv.repo === SECURITY_REPO.repo) {
319
+ await landingSession(cli, req, prURL, argv);
320
+ continue;
321
+ }
322
+
323
+ const existingCommits = (await forceRunAsync('git',
324
+ ['--no-pager', 'log', `${session.upstream}/v${nodeMajorVersion}.x-staging`,
325
+ '--grep', `^PR-URL: ${prURL}$`,
326
+ '--format=%H %h %s'],
327
+ { ignoreFailure: false, captureStdout: true })).trim().split('\n');
328
+ if (existingCommits[0] === '') {
329
+ cli.error(`${prURL} was not found on ${session.upstream}/v${nodeMajorVersion}.x-staging.`);
330
+ cli.info('Please cherry-pick the adequate commits to the public staging branch.');
331
+ cli.info('Skipping');
332
+ cli.warn('The resulting HEAD will not be ready for a release proposal');
333
+ continue;
334
+ }
335
+ cli.info(`The following commit(s) have been found on ${session.upstream}/v${nodeMajorVersion
336
+ }.x-staging:\n${existingCommits.map(c => ` - ${c.slice(41)}`).join('\n')}`);
337
+ if (!await cli.prompt('Cherry-pick those for the current proposal?')) {
338
+ cli.info('Skipping');
339
+ cli.warn('The resulting HEAD will not be ready for a release proposal');
340
+ continue;
341
+ }
342
+ await forceRunAsync('git',
343
+ ['cherry-pick', ...existingCommits.map(c => c.slice(0, 40))],
344
+ { ignoreFailure: false });
345
+ }
346
+
347
+ cli.startSpinner(`Fetching open PRs on ${owner}/${repo}...`);
348
+ const { repository: { pullRequests: { nodes } } } = await req.gql('PRs', {
349
+ owner, repo, labels: [branch],
350
+ });
351
+ cli.stopSpinner(`Fetched all PRs labeled for v${nodeMajorVersion}.x`);
352
+
353
+ for (const { affectedVersions, prURL, cveIds, patchedVersions } of reports) {
354
+ if (!getAffectedVersionLines(affectedVersions).includes(`${nodeMajorVersion}.x`)) continue;
355
+ patchedVersion ??= patchedVersions?.find(v => v.startsWith(`${nodeMajorVersion}.`));
356
+ cli.separator(`Taking care of ${cveIds.join(', ')}...`);
357
+
358
+ if (await skipIfExisting(cli, prURL)) continue;
359
+
360
+ let pr = nodes.find(({ url }) => url === prURL);
361
+ if (!pr) {
362
+ cli.info(
363
+ `${prURL} is not labelled for v${nodeMajorVersion}.x, there might be a backport PR.`
364
+ );
365
+
366
+ cli.startSpinner('Fetching PR title to find a match...');
367
+ const { title } = await req.getPullRequest(prURL);
368
+ pr = nodes.find((pr) => pr.title.endsWith(title));
369
+ if (pr) {
370
+ cli.stopSpinner(`Found ${pr.url}`);
371
+ } else {
372
+ cli.stopSpinner(`Did not find a match for "${title}"`, cli.SPINNER_STATUS.WARN);
373
+ const prID = await cli.prompt(
374
+ 'Please enter the PR number to use:',
375
+ { questionType: cli.QUESTION_TYPE.NUMBER, defaultAnswer: NaN }
376
+ );
377
+ pr = nodes.find(({ number }) => number === prID);
378
+ if (!pr) {
379
+ cli.error(`${prID} is not in the list of PRs labelled for v${nodeMajorVersion}.x`);
380
+ cli.info('The list of labelled PRs and vulnerabilities.json are fetched ' +
381
+ 'once at the start of the session; to refresh those, start a new NCU session');
382
+ const response = await cli.prompt('Do you want to skip that CVE?',
383
+ { defaultAnswer: false });
384
+ if (response) continue;
385
+ throw new Error(`Found no patch for ${cveIds}`);
386
+ }
387
+ }
388
+ }
389
+ cli.ok(`${pr.url} is labelled for v${nodeMajorVersion}.x.`);
390
+
391
+ const backport = prURL !== pr.url;
392
+
393
+ await landingSession(cli, req, prURL, { prid: pr.number, backport, ...SECURITY_REPO }, cveIds);
394
+ }
395
+ cli.ok('All patches are on the local HEAD!');
396
+ cli.info('You can now build and test, and create a proposal with the following commands:');
397
+ cli.info(`git switch -C v${nodeMajorVersion}.x HEAD`);
398
+ cli.info(`git node release --prepare --security --newVersion=${patchedVersion} ` +
399
+ `--releaseDate=${releaseDate.replaceAll('/', '-')} --skipBranchDiff`);
400
+ }
401
+
171
402
  async function cleanupSecurityRelease(cli) {
172
403
  const release = new PrepareSecurityRelease(cli);
173
404
  return release.cleanup();
@@ -1,11 +1,10 @@
1
1
  import path from 'node:path';
2
2
 
3
- import logSymbols from 'log-symbols';
4
-
5
3
  import { minor, major, backport, deps } from '../../lib/update-v8/index.js';
6
4
  import { defaultBaseDir } from '../../lib/update-v8/constants.js';
7
5
  import { checkCwd } from '../../lib/update-v8/common.js';
8
6
  import { forceRunAsync } from '../../lib/run.js';
7
+ import { error } from '../../lib/figures.js';
9
8
 
10
9
  export const command = 'v8 [major|minor|backport|deps]';
11
10
  export const describe = 'Update or patch the V8 engine';
@@ -31,6 +30,10 @@ export function builder(yargs) {
31
30
  describe: 'Update dependencies concurrently',
32
31
  default: true,
33
32
  });
33
+ yargs.option('cargo', {
34
+ describe: 'The cargo binary that will be executed when updating the Rust crates',
35
+ default: 'cargo',
36
+ });
34
37
  }
35
38
  })
36
39
  .command({
@@ -150,7 +153,7 @@ export function handler(argv) {
150
153
  })
151
154
  .catch((err) => {
152
155
  console.error(
153
- logSymbols.error,
156
+ error,
154
157
  options.verbose ? err.stack : err.message
155
158
  );
156
159
  process.exitCode = 1;