@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
@@ -4,11 +4,13 @@ import {
4
4
  commitAndPushVulnerabilitiesJSON,
5
5
  validateDate,
6
6
  pickReport,
7
+ getAffectedVersionLines,
7
8
  getReportSeverity,
8
9
  getSummary,
10
+ confirmSecurityStep,
11
+ writeSecurityFile,
9
12
  SecurityRelease
10
13
  } from './security-release/security-release.js';
11
- import fs from 'node:fs';
12
14
  import auth from './auth.js';
13
15
  import Request from './request.js';
14
16
  import nv from '@pkgjs/nv';
@@ -16,7 +18,7 @@ import semver from 'semver';
16
18
 
17
19
  export default class UpdateSecurityRelease extends SecurityRelease {
18
20
  async sync() {
19
- checkRemote(this.cli, this.repository);
21
+ await checkRemote(this.cli, this.repository);
20
22
 
21
23
  const content = this.readVulnerabilitiesJSON();
22
24
  const credentials = await auth({
@@ -46,10 +48,15 @@ export default class UpdateSecurityRelease extends SecurityRelease {
46
48
  };
47
49
  }
48
50
  const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
49
- fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
51
+ await writeSecurityFile(
52
+ this.cli,
53
+ vulnerabilitiesJSONPath,
54
+ JSON.stringify(content, null, 2),
55
+ 'This writes synchronized HackerOne report data to vulnerabilities.json.'
56
+ );
50
57
 
51
58
  const commitMessage = 'chore: git node security --sync';
52
- commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
59
+ await commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
53
60
  commitMessage, { cli: this.cli, repository: this.repository });
54
61
  this.cli.ok('Synced vulnerabilities.json with HackerOne');
55
62
  }
@@ -59,19 +66,19 @@ export default class UpdateSecurityRelease extends SecurityRelease {
59
66
 
60
67
  try {
61
68
  validateDate(releaseDate);
62
- } catch (error) {
69
+ } catch {
63
70
  cli.error('Invalid date format. Please use the format yyyy/mm/dd.');
64
71
  process.exit(1);
65
72
  }
66
73
 
67
74
  // checkout on the next-security-release branch
68
- checkoutOnSecurityReleaseBranch(cli, this.repository);
75
+ await checkoutOnSecurityReleaseBranch(cli, this.repository);
69
76
 
70
77
  // update the release date in the vulnerabilities.json file
71
78
  const updatedVulnerabilitiesFiles = await this.updateJSONReleaseDate(releaseDate, { cli });
72
79
 
73
80
  const commitMessage = `chore: update the release date to ${releaseDate}`;
74
- commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles,
81
+ await commitAndPushVulnerabilitiesJSON(updatedVulnerabilitiesFiles,
75
82
  commitMessage, { cli, repository: this.repository });
76
83
  cli.ok('Done!');
77
84
  }
@@ -81,7 +88,12 @@ export default class UpdateSecurityRelease extends SecurityRelease {
81
88
  const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
82
89
  content.releaseDate = releaseDate;
83
90
 
84
- fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
91
+ await writeSecurityFile(
92
+ this.cli,
93
+ vulnerabilitiesJSONPath,
94
+ JSON.stringify(content, null, 2),
95
+ `This sets releaseDate to ${releaseDate} in vulnerabilities.json.`
96
+ );
85
97
 
86
98
  this.cli.ok(`Updated the release date in vulnerabilities.json: ${releaseDate}`);
87
99
  return [vulnerabilitiesJSONPath];
@@ -95,7 +107,7 @@ export default class UpdateSecurityRelease extends SecurityRelease {
95
107
 
96
108
  const req = new Request(credentials);
97
109
  // checkout on the next-security-release branch
98
- checkoutOnSecurityReleaseBranch(this.cli, this.repository);
110
+ await checkoutOnSecurityReleaseBranch(this.cli, this.repository);
99
111
 
100
112
  // get h1 report
101
113
  const { data: report } = await req.getReport(reportId);
@@ -104,18 +116,23 @@ export default class UpdateSecurityRelease extends SecurityRelease {
104
116
  const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
105
117
  const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
106
118
  content.reports.push(entry);
107
- fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
119
+ await writeSecurityFile(
120
+ this.cli,
121
+ vulnerabilitiesJSONPath,
122
+ JSON.stringify(content, null, 2),
123
+ `This appends HackerOne report ${entry.id} to vulnerabilities.json.`
124
+ );
108
125
  this.cli.ok(`Updated vulnerabilities.json with the report: ${entry.id}`);
109
126
  const commitMessage = `chore: added report ${entry.id} to vulnerabilities.json`;
110
- commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
127
+ await commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
111
128
  commitMessage, { cli: this.cli, repository: this.repository });
112
129
  this.cli.ok('Done!');
113
130
  }
114
131
 
115
- removeReport(reportId) {
132
+ async removeReport(reportId) {
116
133
  const { cli } = this;
117
134
  // checkout on the next-security-release branch
118
- checkoutOnSecurityReleaseBranch(cli, this.repository);
135
+ await checkoutOnSecurityReleaseBranch(cli, this.repository);
119
136
  const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
120
137
  const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
121
138
  const found = content.reports.some((report) => report.id === reportId);
@@ -124,11 +141,16 @@ export default class UpdateSecurityRelease extends SecurityRelease {
124
141
  process.exit(1);
125
142
  }
126
143
  content.reports = content.reports.filter((report) => report.id !== reportId);
127
- fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
144
+ await writeSecurityFile(
145
+ this.cli,
146
+ vulnerabilitiesJSONPath,
147
+ JSON.stringify(content, null, 2),
148
+ `This removes HackerOne report ${reportId} from vulnerabilities.json.`
149
+ );
128
150
  this.cli.ok(`Updated vulnerabilities.json with the report: ${reportId}`);
129
151
 
130
152
  const commitMessage = `chore: remove report ${reportId} from vulnerabilities.json`;
131
- commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
153
+ await commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
132
154
  commitMessage, { cli, repository: this.repository });
133
155
  cli.ok('Done!');
134
156
  }
@@ -169,6 +191,16 @@ export default class UpdateSecurityRelease extends SecurityRelease {
169
191
  }
170
192
 
171
193
  async updateHackonerReportCve(req, report) {
194
+ const { id, cveIds } = report;
195
+ await confirmSecurityStep(
196
+ this.cli,
197
+ `update HackerOne report \`${id}\` with CVEs ${cveIds}`,
198
+ 'This writes the assigned CVE IDs back to the HackerOne report.'
199
+ );
200
+ return this.updateHackonerReportCveWithoutConfirmation(req, report);
201
+ }
202
+
203
+ async updateHackonerReportCveWithoutConfirmation(req, report) {
172
204
  const { id, cveIds } = report;
173
205
  this.cli.startSpinner(`Updating report ${id} with CVEs ${cveIds}..`);
174
206
  const body = {
@@ -179,90 +211,286 @@ export default class UpdateSecurityRelease extends SecurityRelease {
179
211
  }
180
212
  }
181
213
  };
182
- const response = await req.updateReportCVE(id, body);
183
- if (response.errors) {
184
- this.cli.error(`Error updating report ${id}`);
185
- this.cli.error(JSON.stringify(response.errors, null, 2));
214
+ try {
215
+ const response = await req.updateReportCVE(id, body);
216
+ if (response.errors) {
217
+ this.cli.stopSpinner(
218
+ `Error updating report ${id}`,
219
+ this.cli.SPINNER_STATUS.FAILED
220
+ );
221
+ this.cli.error(`Error updating report ${id}`);
222
+ this.cli.error(JSON.stringify(response.errors, null, 2));
223
+ return false;
224
+ }
225
+ this.cli.stopSpinner(`Done updating report ${id} with CVEs ${cveIds}..`);
226
+ return true;
227
+ } catch (error) {
228
+ this.cli.stopSpinner(
229
+ `Error updating report ${id}`,
230
+ this.cli.SPINNER_STATUS.FAILED
231
+ );
232
+ this.cli.error(error);
233
+ return false;
186
234
  }
187
- this.cli.stopSpinner(`Done updating report ${id} with CVEs ${cveIds}..`);
188
235
  }
189
236
 
190
237
  async promptCVECreation(req, reports, programId, content) {
238
+ const reportsWithoutCVE = reports.filter((report) => !report.cveIds?.length);
239
+ const requestAll = await this.promptRequestAllCVEs(reportsWithoutCVE);
240
+
241
+ if (requestAll) {
242
+ await confirmSecurityStep(
243
+ this.cli,
244
+ `request CVEs for ${reportsWithoutCVE.length} HackerOne reports`,
245
+ 'This submits CVE requests to HackerOne for every report without a CVE.'
246
+ );
247
+ }
248
+
191
249
  const supportedVersions = (await nv('supported'));
192
250
  const eolVersions = (await nv('eol'));
193
- for (const report of reports) {
194
- const { id, summary, title, affectedVersions, cveIds, link } = report;
195
- // skip if already has a CVE
196
- // risky because the CVE associated might be
197
- // mentioned in the report and not requested by Node
198
- if (cveIds?.length) continue;
251
+ const versionCache = new Map();
252
+ const successfulReports = await this.collectSuccessfulCVERequests({
253
+ req,
254
+ reports,
255
+ programId,
256
+ supportedVersions,
257
+ eolVersions,
258
+ versionCache,
259
+ requestAll
260
+ });
199
261
 
200
- let severity = report.severity;
262
+ if (!successfulReports.length) {
263
+ this.cli.warn('No CVE requests succeeded.');
264
+ return;
265
+ }
201
266
 
202
- if (!severity.cvss_vector_string || !severity.weakness_id) {
203
- try {
204
- const h1Report = await req.getReport(id);
205
- if (!h1Report.data.relationships.severity?.data.attributes.cvss_vector_string) {
206
- throw new Error('No severity found');
207
- }
208
- severity = {
209
- weakness_id: h1Report.data.relationships.weakness?.data.id,
210
- cvss_vector_string:
211
- h1Report.data.relationships.severity?.data.attributes.cvss_vector_string,
212
- rating: h1Report.data.relationships.severity?.data.attributes.rating
213
- };
214
- } catch (error) {
215
- this.cli.error(`Couldnt not retrieve severity from report ${id}, skipping...`);
216
- continue;
267
+ await this.applySuccessfulCVEUpdates(req, content, successfulReports);
268
+ }
269
+
270
+ showCVERequestSummary(reports) {
271
+ this.cli.info(`Reports selected for CVE requests (${reports.length}):`);
272
+ for (const { id, severity, title } of reports) {
273
+ const rating = severity?.rating || 'unknown';
274
+ this.cli.info(`- ${id} [${rating}] ${title}`);
275
+ }
276
+ }
277
+
278
+ promptRequestAllCVEs(reportsWithoutCVE) {
279
+ if (reportsWithoutCVE.length <= 1) return false;
280
+ this.showCVERequestSummary(reportsWithoutCVE);
281
+ return this.cli.prompt(
282
+ 'Request CVEs for all reports without prompting for each report?',
283
+ { defaultAnswer: false }
284
+ );
285
+ }
286
+
287
+ async collectSuccessfulCVERequests({
288
+ req,
289
+ reports,
290
+ programId,
291
+ supportedVersions,
292
+ eolVersions,
293
+ versionCache,
294
+ requestAll
295
+ }) {
296
+ const successfulReports = [];
297
+ try {
298
+ for (const report of reports) {
299
+ const requested = await this.requestReportCVE({
300
+ req,
301
+ report,
302
+ programId,
303
+ supportedVersions,
304
+ eolVersions,
305
+ versionCache,
306
+ requestAll
307
+ });
308
+ if (requested) successfulReports.push(report);
309
+ }
310
+ } catch (error) {
311
+ if (!this.isPromptInterrupted(error)) throw error;
312
+ this.cli.warn('CVE request interrupted. Finalizing successful requests.');
313
+ }
314
+ return successfulReports;
315
+ }
316
+
317
+ isPromptInterrupted(error) {
318
+ return error?.name === 'ExitPromptError' ||
319
+ error?.name === 'AbortPromptError' ||
320
+ error?.message?.includes('User force closed');
321
+ }
322
+
323
+ async requestReportCVE({
324
+ req,
325
+ report,
326
+ programId,
327
+ supportedVersions,
328
+ eolVersions,
329
+ versionCache,
330
+ requestAll
331
+ }) {
332
+ const { id, summary, title, affectedVersions, cveIds, link } = report;
333
+ const affectedVersionLines = getAffectedVersionLines(affectedVersions);
334
+ // skip if already has a CVE
335
+ // risky because the CVE associated might be
336
+ // mentioned in the report and not requested by Node
337
+ if (cveIds?.length) return false;
338
+
339
+ let severity = report.severity;
340
+
341
+ if (!severity.cvss_vector_string || !severity.weakness_id) {
342
+ try {
343
+ const h1Report = await req.getReport(id);
344
+ if (!h1Report.data.relationships.severity?.data.attributes.cvss_vector_string) {
345
+ throw new Error('No severity found');
217
346
  }
347
+ severity = {
348
+ weakness_id: h1Report.data.relationships.weakness?.data.id,
349
+ cvss_vector_string:
350
+ h1Report.data.relationships.severity?.data.attributes.cvss_vector_string,
351
+ rating: h1Report.data.relationships.severity?.data.attributes.rating
352
+ };
353
+ } catch {
354
+ this.cli.error(`Couldnt not retrieve severity from report ${id}, skipping...`);
355
+ return false;
218
356
  }
357
+ }
219
358
 
220
- const { cvss_vector_string, weakness_id } = severity;
359
+ const { cvss_vector_string, weakness_id } = severity;
221
360
 
222
- const create = await this.cli.prompt(
223
- `Request a CVE for: \n
361
+ const create = requestAll || await this.cli.prompt(
362
+ `Request a CVE for: \n
224
363
  Title: ${title}\n
225
364
  Link: ${link}\n
226
- Affected versions: ${affectedVersions.join(', ')}\n
365
+ Affected versions: ${affectedVersionLines.join(', ')}\n
227
366
  Vector: ${cvss_vector_string}\n
228
367
  Summary: ${summary}\n`,
229
- { defaultAnswer: true });
230
-
231
- if (!create) continue;
232
-
233
- const { h1AffectedVersions, patchedVersions } =
234
- await this.calculateVersions(affectedVersions, supportedVersions, eolVersions);
235
- const body = {
236
- data: {
237
- type: 'cve-request',
238
- attributes: {
239
- team_handle: 'nodejs-team',
240
- versions: h1AffectedVersions,
241
- metrics: [
242
- {
243
- vectorString: cvss_vector_string
244
- }
245
- ],
246
- auto_submit_on_publicly_disclosing_report: true,
247
- references: ['https://nodejs.org/en/blog/vulnerability'],
248
- report_id: report.id,
249
- weakness_id: Number(weakness_id),
250
- description: report.summary,
251
- vulnerability_discovered_at: new Date().toISOString()
368
+ { defaultAnswer: true });
369
+
370
+ if (!create) return false;
371
+
372
+ const { h1AffectedVersions, patchedVersions } =
373
+ await this.calculateVersions(
374
+ affectedVersionLines,
375
+ supportedVersions,
376
+ eolVersions,
377
+ versionCache
378
+ );
379
+ const body = {
380
+ data: {
381
+ type: 'cve-request',
382
+ attributes: {
383
+ team_handle: 'nodejs-team',
384
+ versions: h1AffectedVersions,
385
+ metrics: [
386
+ {
387
+ vectorString: cvss_vector_string
388
+ }
389
+ ],
390
+ auto_submit_on_publicly_disclosing_report: true,
391
+ references: ['https://nodejs.org/en/blog/vulnerability'],
392
+ report_id: report.id,
393
+ weakness_id: Number(weakness_id),
394
+ description: report.summary,
395
+ vulnerability_discovered_at: new Date().toISOString()
396
+ }
397
+ }
398
+ };
399
+ if (!requestAll) {
400
+ await confirmSecurityStep(
401
+ this.cli,
402
+ `request CVE for HackerOne report \`${report.id}\``,
403
+ 'This submits a CVE request to HackerOne for the selected report.'
404
+ );
405
+ }
406
+ this.cli.startSpinner(`Requesting CVE for report ${id}...`);
407
+ let response;
408
+ try {
409
+ response = await req.requestCVE(programId, body);
410
+ } catch (error) {
411
+ this.cli.stopSpinner(
412
+ `Error requesting CVE for report ${id}`,
413
+ this.cli.SPINNER_STATUS.FAILED
414
+ );
415
+ this.cli.error(error);
416
+ return false;
417
+ }
418
+ if (response.errors) {
419
+ this.cli.stopSpinner(
420
+ `Error requesting CVE for report ${id}`,
421
+ this.cli.SPINNER_STATUS.FAILED
422
+ );
423
+ this.cli.error(`Error requesting CVE for report ${id}`);
424
+ this.cli.error(JSON.stringify(response.errors, null, 2));
425
+ return false;
426
+ }
427
+ this.cli.stopSpinner(`Requested CVE for report ${id}`);
428
+ const { cve_identifier } = response.data.attributes;
429
+ report.cveIds = [cve_identifier];
430
+ report.patchedVersions = patchedVersions;
431
+ return true;
432
+ }
433
+
434
+ async applySuccessfulCVEUpdates(req, content, successfulReports) {
435
+ this.cli.info('CVE requests succeeded:');
436
+ for (const { id, cveIds } of successfulReports) {
437
+ this.cli.info(`- ${id}: ${cveIds.join(', ')}`);
438
+ }
439
+ const updateVulnerabilitiesJSON = await this.cli.prompt(
440
+ 'Update vulnerabilities.json with the successful CVE requests?',
441
+ { defaultAnswer: true }
442
+ );
443
+ if (!updateVulnerabilitiesJSON) {
444
+ this.cli.warn(
445
+ 'Skipping HackerOne updates because vulnerabilities.json was not updated.'
446
+ );
447
+ return;
448
+ }
449
+
450
+ const vulnerabilitiesUpdated = await this.updateVulnerabilitiesJSON(content);
451
+ if (!vulnerabilitiesUpdated) {
452
+ this.cli.warn('Skipping HackerOne updates because vulnerabilities.json update failed.');
453
+ return;
454
+ }
455
+
456
+ const updateHackerOne = await this.cli.prompt(
457
+ 'Update HackerOne reports with the successful CVE IDs?',
458
+ { defaultAnswer: true }
459
+ );
460
+ if (updateHackerOne) {
461
+ await confirmSecurityStep(
462
+ this.cli,
463
+ `update ${successfulReports.length} HackerOne reports with CVE IDs`,
464
+ 'This writes the assigned CVE IDs back to all successful HackerOne reports.'
465
+ );
466
+ const h1Succeeded = [];
467
+ const h1Failed = [];
468
+ for (const report of successfulReports) {
469
+ try {
470
+ const updated = await this.updateHackonerReportCveWithoutConfirmation(req, report);
471
+ if (updated) {
472
+ h1Succeeded.push(report);
473
+ } else {
474
+ h1Failed.push(report);
252
475
  }
476
+ } catch (error) {
477
+ this.cli.error(error);
478
+ h1Failed.push(report);
479
+ }
480
+ }
481
+
482
+ if (h1Succeeded.length) {
483
+ this.cli.info('HackerOne reports updated:');
484
+ for (const { id, cveIds } of h1Succeeded) {
485
+ this.cli.info(`- ${id}: ${cveIds.join(', ')}`);
486
+ }
487
+ }
488
+ if (h1Failed.length) {
489
+ this.cli.warn('HackerOne reports not updated:');
490
+ for (const { id, cveIds } of h1Failed) {
491
+ this.cli.warn(`- ${id}: ${cveIds.join(', ')}`);
253
492
  }
254
- };
255
- const response = await req.requestCVE(programId, body);
256
- if (response.errors) {
257
- this.cli.error(`Error requesting CVE for report ${id}`);
258
- this.cli.error(JSON.stringify(response.errors, null, 2));
259
- continue;
260
493
  }
261
- const { cve_identifier } = response.data.attributes;
262
- report.cveIds = [cve_identifier];
263
- report.patchedVersions = patchedVersions;
264
- this.updateVulnerabilitiesJSON(content);
265
- await this.updateHackonerReportCve(req, report);
266
494
  }
267
495
  }
268
496
 
@@ -277,39 +505,53 @@ Summary: ${summary}\n`,
277
505
  }
278
506
  }
279
507
 
280
- async calculateVersions(affectedVersions, supportedVersions, eolVersions) {
508
+ async calculateVersions(
509
+ affectedVersions,
510
+ supportedVersions,
511
+ eolVersions,
512
+ versionCache = new Map()
513
+ ) {
514
+ affectedVersions = getAffectedVersionLines(affectedVersions);
281
515
  const h1AffectedVersions = [];
282
516
  const patchedVersions = [];
283
517
  let isPatchRelease = true;
284
518
  for (const affectedVersion of affectedVersions) {
285
- const affectedMajor = affectedVersion.split('.')[0];
286
- const latest = supportedVersions.find((v) => v.major === Number(affectedMajor)).version;
287
- const version = await this.cli.prompt(
288
- `What is the affected version (<=) for release line ${affectedVersion}?`,
289
- { questionType: 'input', defaultAnswer: latest });
290
-
291
- const nextPatchVersion = semver.inc(version, 'patch');
292
- const nextMinorVersion = semver.inc(version, 'minor');
293
- const patchedVersion = await this.cli.prompt(
294
- `What is the patched version (>=) for release line ${affectedVersion}?`,
295
- {
296
- questionType: 'input',
297
- defaultAnswer: isPatchRelease ? nextPatchVersion : nextMinorVersion
298
- });
519
+ if (!versionCache.has(affectedVersion)) {
520
+ const affectedMajor = affectedVersion.split('.')[0];
521
+ const latest = supportedVersions.find((v) => v.major === Number(affectedMajor)).version;
522
+ const version = await this.cli.prompt(
523
+ `What is the affected version (<=) for release line ${affectedVersion}?`,
524
+ { questionType: 'input', defaultAnswer: latest });
525
+
526
+ const nextPatchVersion = semver.inc(version, 'patch');
527
+ const nextMinorVersion = semver.inc(version, 'minor');
528
+ const patchedVersion = await this.cli.promptRadio(
529
+ `What is the patched version (>=) for release line ${affectedVersion}?`,
530
+ [nextPatchVersion, nextMinorVersion],
531
+ {
532
+ defaultAnswer: isPatchRelease ? nextPatchVersion : nextMinorVersion
533
+ });
534
+
535
+ if (patchedVersion !== nextPatchVersion) {
536
+ isPatchRelease = false; // is a minor release
537
+ }
299
538
 
300
- if (patchedVersion !== nextPatchVersion) {
301
- isPatchRelease = false; // is a minor release
539
+ versionCache.set(affectedVersion, {
540
+ patchedVersion,
541
+ h1AffectedVersion: {
542
+ vendor: 'nodejs',
543
+ product: 'node',
544
+ func: '<=',
545
+ version,
546
+ versionType: 'semver',
547
+ affected: true
548
+ }
549
+ });
302
550
  }
303
551
 
552
+ const { h1AffectedVersion, patchedVersion } = versionCache.get(affectedVersion);
304
553
  patchedVersions.push(patchedVersion);
305
- h1AffectedVersions.push({
306
- vendor: 'nodejs',
307
- product: 'node',
308
- func: '<=',
309
- version,
310
- versionType: 'semver',
311
- affected: true
312
- });
554
+ h1AffectedVersions.push(h1AffectedVersion);
313
555
  }
314
556
 
315
557
  // All EOL versions are affected since they no longer receive security patches
package/lib/utils.js CHANGED
@@ -6,12 +6,12 @@ import { forceRunAsync } from './run.js';
6
6
  export function ascending(a, b) {
7
7
  if (a === b) return 0;
8
8
  return a < b ? -1 : 1;
9
- };
9
+ }
10
10
 
11
11
  export function descending(a, b) {
12
12
  if (a === b) return 0;
13
13
  return a > b ? -1 : 1;
14
- };
14
+ }
15
15
 
16
16
  export function flatten(arr) {
17
17
  let result = [];
@@ -27,7 +27,7 @@ export function flatten(arr) {
27
27
 
28
28
  export function shortSha(sha) {
29
29
  return sha.slice(0, 12);
30
- };
30
+ }
31
31
 
32
32
  let isGhAvailableCache;
33
33
  export function isGhAvailable() {
@@ -35,7 +35,7 @@ export function isGhAvailable() {
35
35
  isGhAvailableCache = which.sync('gh', { nothrow: true }) !== null;
36
36
  }
37
37
  return isGhAvailableCache;
38
- };
38
+ }
39
39
 
40
40
  /**
41
41
  * Returns the user's preferred text editor command.
@@ -65,4 +65,4 @@ export async function getEditor(options = {}) {
65
65
 
66
66
  return process.env.VISUAL || process.env.EDITOR ||
67
67
  (platform() === 'win32' ? 'notepad.exe' : 'vi');
68
- };
68
+ }
package/lib/verbosity.js CHANGED
@@ -11,7 +11,7 @@ export let verbosity = VERBOSITY.NONE;
11
11
 
12
12
  export function isDebugVerbosity() {
13
13
  return verbosity === VERBOSITY.DEBUG;
14
- };
14
+ }
15
15
 
16
16
  export function setVerbosityFromEnv() {
17
17
  const env = (process.env.NCU_VERBOSITY || '').toUpperCase();
@@ -21,9 +21,9 @@ export function setVerbosityFromEnv() {
21
21
  if (!isDebugVerbosity()) {
22
22
  Error.stackTraceLimit = 0;
23
23
  }
24
- };
24
+ }
25
25
 
26
26
  export function debuglog(...args) {
27
27
  // Prepend a line break in case it's logged while the spinner is running
28
28
  console.error(chalk.green(util.format('\n[DEBUG]', ...args)));
29
- };
29
+ }