@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
@@ -10,10 +10,145 @@ import {
10
10
  validateDate,
11
11
  promptDependencies,
12
12
  getSupportedVersions,
13
+ getReportSeverity,
14
+ getSummary,
13
15
  pickReport,
16
+ confirmSecurityStep,
17
+ writeSecurityFile,
14
18
  SecurityRelease
15
19
  } from './security-release/security-release.js';
16
- import _ from 'lodash';
20
+
21
+ function relativeDate(date) {
22
+ const days = Math.floor((Date.now() - date) / (1000 * 60 * 60 * 24));
23
+ if (days < 30) return days === 1 ? '1 day ago' : `${days} days ago`;
24
+ const months = Math.floor(days / 30);
25
+ return months === 1 ? '1 month ago' : `${months} months ago`;
26
+ }
27
+
28
+ function formatDate(date) {
29
+ const year = date.getFullYear();
30
+ const month = String(date.getMonth() + 1).padStart(2, '0');
31
+ const day = String(date.getDate()).padStart(2, '0');
32
+ return `${year}/${month}/${day}`;
33
+ }
34
+
35
+ function formatDaysFromNow(days) {
36
+ return days === 1 ? 'in 1 day' : `in ${days} days`;
37
+ }
38
+
39
+ function getNextTuesdayReleaseDateObjects(fromDate = new Date(), count = 4) {
40
+ const TUESDAY = 2;
41
+ const start = new Date(
42
+ fromDate.getFullYear(),
43
+ fromDate.getMonth(),
44
+ fromDate.getDate()
45
+ );
46
+ const daysUntilTuesday = (TUESDAY - start.getDay() + 7) % 7 || 7;
47
+ start.setDate(start.getDate() + daysUntilTuesday);
48
+
49
+ return Array.from({ length: count }, (_, index) => {
50
+ const date = new Date(start);
51
+ date.setDate(start.getDate() + (index * 7));
52
+ return {
53
+ date,
54
+ daysFromNow: daysUntilTuesday + (index * 7)
55
+ };
56
+ });
57
+ }
58
+
59
+ export function getNextTuesdayReleaseDates(fromDate = new Date(), count = 4) {
60
+ return getNextTuesdayReleaseDateObjects(fromDate, count)
61
+ .map(({ date }) => formatDate(date));
62
+ }
63
+
64
+ export function getNextTuesdayReleaseDateChoices(fromDate = new Date(), count = 4) {
65
+ const choices = getNextTuesdayReleaseDateObjects(fromDate, count)
66
+ .map(({ date, daysFromNow }) => {
67
+ const formattedDate = formatDate(date);
68
+ return {
69
+ name: formattedDate,
70
+ value: formattedDate,
71
+ description: `Tuesday, ${formatDaysFromNow(daysFromNow)}`
72
+ };
73
+ });
74
+
75
+ choices.push({
76
+ name: 'TBD',
77
+ value: 'TBD',
78
+ description: 'Release date not defined yet'
79
+ });
80
+
81
+ return choices;
82
+ }
83
+
84
+ function getReportPRURL(report) {
85
+ const customFieldValues = report.relationships.custom_field_values?.data ?? [];
86
+ return customFieldValues[0]?.attributes?.value ?? '';
87
+ }
88
+
89
+ export function buildIncludedTriagedReport(report, options = {}) {
90
+ const {
91
+ affectedVersions = '',
92
+ patchAuthors = [],
93
+ prURL = getReportPRURL(report)
94
+ } = options;
95
+ const {
96
+ id,
97
+ attributes: { title, cve_ids = [] },
98
+ relationships: { reporter }
99
+ } = report;
100
+ const link = `https://hackerone.com/reports/${id}`;
101
+ const summaryContent = getSummary(report);
102
+
103
+ return {
104
+ id,
105
+ title,
106
+ cveIds: cve_ids,
107
+ severity: getReportSeverity(report),
108
+ summary: summaryContent ?? '',
109
+ patchAuthors,
110
+ prURL,
111
+ affectedVersions: affectedVersions
112
+ .split(',')
113
+ .map((v) => v.replace('v', '').trim())
114
+ .filter(Boolean),
115
+ link,
116
+ reporter: reporter?.data?.attributes?.username ?? ''
117
+ };
118
+ }
119
+
120
+ export function getMissingReportInformation(report) {
121
+ const missing = [];
122
+
123
+ if (!report.severity?.rating) missing.push('severity rating');
124
+ if (!report.severity?.cvss_vector_string) missing.push('CVSS vector');
125
+ if (!report.severity?.weakness_id) missing.push('weakness ID');
126
+ if (!report.summary) missing.push('team summary');
127
+ if (!report.prURL) missing.push('PR URL');
128
+ if (!report.patchAuthors?.length) missing.push('patch authors');
129
+ if (!report.affectedVersions?.length) missing.push('affected versions');
130
+
131
+ return missing;
132
+ }
133
+
134
+ export function groupMissingReportInformation(reports) {
135
+ const grouped = new Map();
136
+
137
+ for (const report of reports) {
138
+ for (const field of report.missing) {
139
+ const current = grouped.get(field) ?? [];
140
+ current.push(report);
141
+ grouped.set(field, current);
142
+ }
143
+ }
144
+
145
+ return Array.from(grouped.entries())
146
+ .map(([field, fieldReports]) => ({
147
+ field,
148
+ reports: fieldReports
149
+ }))
150
+ .sort((a, b) => b.reports.length - a.reports.length);
151
+ }
17
152
 
18
153
  export default class PrepareSecurityRelease extends SecurityRelease {
19
154
  title = 'Next Security Release';
@@ -25,16 +160,25 @@ export default class PrepareSecurityRelease extends SecurityRelease {
25
160
  });
26
161
 
27
162
  this.req = new Request(credentials);
163
+
164
+ let excludedReports = [];
28
165
  const releaseDate = await this.promptReleaseDate();
29
166
  if (releaseDate !== 'TBD') {
30
167
  validateDate(releaseDate);
31
168
  }
32
169
  const createVulnerabilitiesJSON = await this.promptVulnerabilitiesJSON();
33
170
 
34
- let securityReleasePRUrl;
35
- const content = await this.buildDescription(releaseDate, securityReleasePRUrl);
171
+ const content = await this.buildDescription(releaseDate);
36
172
  if (createVulnerabilitiesJSON) {
37
- securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate, content);
173
+ const reportSelectionMode = await this.promptReportSelectionMode();
174
+ if (reportSelectionMode === 'review') {
175
+ const showTriaged = await this.promptShowTriagedWithoutPR();
176
+ if (showTriaged) {
177
+ excludedReports = await this.showTriagedReportsWithoutPR();
178
+ }
179
+ }
180
+ await this.startVulnerabilitiesJSONCreation(
181
+ releaseDate, content, excludedReports, reportSelectionMode);
38
182
  }
39
183
 
40
184
  this.cli.ok('Done!');
@@ -57,6 +201,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
57
201
 
58
202
  if (vulnerabilityJSON.buildIssue) {
59
203
  this.cli.info('Commenting on nodejs/build issue');
204
+ await confirmSecurityStep(
205
+ this.cli,
206
+ `comment on GitHub issue \`${vulnerabilityJSON.buildIssue}\``,
207
+ 'This posts that the security release is out on the build tracking issue.'
208
+ );
60
209
  await this.req.commentIssue(
61
210
  vulnerabilityJSON.buildIssue,
62
211
  'Security release is out'
@@ -65,6 +214,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
65
214
 
66
215
  if (vulnerabilityJSON.dockerIssue) {
67
216
  this.cli.info('Commenting on nodejs/docker-node issue');
217
+ await confirmSecurityStep(
218
+ this.cli,
219
+ `comment on GitHub issue \`${vulnerabilityJSON.dockerIssue}\``,
220
+ 'This posts that the security release is out on the docker-node tracking issue.'
221
+ );
68
222
  await this.req.commentIssue(
69
223
  vulnerabilityJSON.dockerIssue,
70
224
  'Security release is out'
@@ -76,11 +230,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
76
230
  vulnerabilityJSON.releaseDate}?`,
77
231
  { defaultAnswer: true });
78
232
  if (updateFolder) {
79
- this.updateReleaseFolder(
233
+ await this.updateReleaseFolder(
80
234
  vulnerabilityJSON.releaseDate.replaceAll('/', '-')
81
235
  );
82
236
  const securityReleaseFolder = path.join(process.cwd(), 'security-release');
83
- commitAndPushVulnerabilitiesJSON(
237
+ await commitAndPushVulnerabilitiesJSON(
84
238
  securityReleaseFolder,
85
239
  'chore: change next-security-release folder',
86
240
  { cli: this.cli, repository: this.repository }
@@ -93,14 +247,20 @@ export default class PrepareSecurityRelease extends SecurityRelease {
93
247
  this.cli.ok('Done!');
94
248
  }
95
249
 
96
- async startVulnerabilitiesJSONCreation(releaseDate, content) {
250
+ async startVulnerabilitiesJSONCreation(
251
+ releaseDate,
252
+ content,
253
+ excludedReports = [],
254
+ reportSelectionMode = 'review'
255
+ ) {
97
256
  // checkout on the next-security-release branch
98
- checkoutOnSecurityReleaseBranch(this.cli, this.repository);
257
+ await checkoutOnSecurityReleaseBranch(this.cli, this.repository);
99
258
 
100
259
  // choose the reports to include in the security release
101
- const reports = await this.chooseReports();
102
- const depUpdates = await this.getDependencyUpdates();
103
- const deps = _.groupBy(depUpdates, 'name');
260
+ const reports = reportSelectionMode === 'include-all'
261
+ ? await this.includeAllTriagedReports(excludedReports)
262
+ : await this.chooseReports(excludedReports);
263
+ const deps = await this.getDependencyUpdates();
104
264
 
105
265
  // create the vulnerabilities.json file in the security-release repo
106
266
  const filePath = await this.createVulnerabilitiesJSON(reports, deps, releaseDate);
@@ -115,11 +275,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
115
275
  - git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
116
276
  - open a PR on ${this.repository.owner}/${this.repository.repo}`);
117
277
  return;
118
- };
278
+ }
119
279
 
120
280
  // commit and push the vulnerabilities.json file
121
281
  const commitMessage = 'chore: create vulnerabilities.json for next security release';
122
- commitAndPushVulnerabilitiesJSON(filePath,
282
+ await commitAndPushVulnerabilitiesJSON(filePath,
123
283
  commitMessage,
124
284
  { cli: this.cli, repository: this.repository });
125
285
 
@@ -149,21 +309,18 @@ export default class PrepareSecurityRelease extends SecurityRelease {
149
309
  return body.substring(index);
150
310
  }
151
311
  return body;
152
- } catch (error) {
312
+ } catch {
153
313
  this.cli.error(`Could not retrieve the security issue template from ${url}`);
154
314
  }
155
315
  }
156
316
 
157
317
  async promptReleaseDate() {
158
- const nextWeekDate = new Date();
159
- nextWeekDate.setDate(nextWeekDate.getDate() + 7);
160
- // Format the date as YYYY/MM/DD
161
- const formattedDate = nextWeekDate.toISOString().slice(0, 10).replace(/-/g, '/');
162
- return this.cli.prompt(
163
- 'Enter target release date in YYYY/MM/DD format (TBD if not defined yet):', {
164
- questionType: 'input',
165
- defaultAnswer: formattedDate
166
- });
318
+ const choices = getNextTuesdayReleaseDateChoices();
319
+ return this.cli.promptSelect(
320
+ 'Select target release date:',
321
+ choices,
322
+ { defaultAnswer: choices[0].value }
323
+ );
167
324
  }
168
325
 
169
326
  async promptVulnerabilitiesJSON() {
@@ -172,6 +329,25 @@ export default class PrepareSecurityRelease extends SecurityRelease {
172
329
  { defaultAnswer: true });
173
330
  }
174
331
 
332
+ async promptReportSelectionMode() {
333
+ return this.cli.promptSelect(
334
+ 'How would you like to choose reports for the next security release?',
335
+ [
336
+ {
337
+ name: 'Review each triaged report',
338
+ value: 'review',
339
+ description: 'Iterate over reports and fill missing details interactively'
340
+ },
341
+ {
342
+ name: 'Include all triaged reports',
343
+ value: 'include-all',
344
+ description: 'Add every triaged report and summarize missing information'
345
+ }
346
+ ],
347
+ { defaultAnswer: 'review' }
348
+ );
349
+ }
350
+
175
351
  async promptCreateRelaseIssue() {
176
352
  return this.cli.prompt(
177
353
  'Create the Next Security Release issue?',
@@ -184,17 +360,61 @@ export default class PrepareSecurityRelease extends SecurityRelease {
184
360
  { defaultAnswer: true });
185
361
  }
186
362
 
363
+ async promptShowTriagedWithoutPR() {
364
+ return this.cli.prompt(
365
+ 'Do you want to see which reports are triaged but have no PR URL?',
366
+ { defaultAnswer: true });
367
+ }
368
+
369
+ async showTriagedReportsWithoutPR() {
370
+ this.cli.info('Fetching triaged reports without PR URL...');
371
+ const reports = await this.req.getTriagedReports();
372
+ const reportsWithoutPR = reports.data.filter(
373
+ (report) => !report.relationships.custom_field_values.data.length
374
+ );
375
+ if (!reportsWithoutPR.length) {
376
+ this.cli.ok('All triaged reports have a PR URL.');
377
+ return [];
378
+ }
379
+ const severityRank = { critical: 0, high: 1, medium: 2, low: 3 };
380
+ const choices = reportsWithoutPR
381
+ .sort((a, b) => {
382
+ const dateA = new Date(a.attributes.created_at);
383
+ const dateB = new Date(b.attributes.created_at);
384
+ if (dateB - dateA !== 0) return dateB - dateA;
385
+ const rankA = severityRank[getReportSeverity(a).rating] ?? 4;
386
+ const rankB = severityRank[getReportSeverity(b).rating] ?? 4;
387
+ return rankA - rankB;
388
+ })
389
+ .map((report) => {
390
+ const { id, attributes: { title, created_at } } = report;
391
+ const { rating } = getReportSeverity(report);
392
+ const openedDate = relativeDate(new Date(created_at));
393
+ const link = `https://hackerone.com/reports/${id}`;
394
+ return {
395
+ name: `[${openedDate}] (${rating}) ${title} - ${link}`,
396
+ value: id,
397
+ checked: true
398
+ };
399
+ });
400
+ return this.cli.promptCheckbox(
401
+ 'Select reports to exclude from the upcoming security release:',
402
+ choices
403
+ );
404
+ }
405
+
187
406
  async buildDescription() {
188
407
  const template = await this.getSecurityIssueTemplate();
189
408
  return template;
190
409
  }
191
410
 
192
- async chooseReports() {
411
+ async chooseReports(excludedReports = []) {
193
412
  this.cli.info('Getting triaged H1 reports...');
194
413
  const reports = await this.req.getTriagedReports();
195
414
  const selectedReports = [];
196
415
 
197
416
  for (const report of reports.data) {
417
+ if (excludedReports.includes(report.id)) continue;
198
418
  const rep = await pickReport(report, { cli: this.cli, req: this.req });
199
419
  if (!rep) continue;
200
420
  selectedReports.push(rep);
@@ -202,6 +422,80 @@ export default class PrepareSecurityRelease extends SecurityRelease {
202
422
  return selectedReports;
203
423
  }
204
424
 
425
+ async includeAllTriagedReports(excludedReports = []) {
426
+ this.cli.info('Getting triaged H1 reports...');
427
+ const reports = await this.req.getTriagedReports();
428
+ const supportedVersions = await getSupportedVersions();
429
+ const selectedReports = [];
430
+ const missingInformation = [];
431
+
432
+ for (const report of reports.data) {
433
+ if (excludedReports.includes(report.id)) continue;
434
+
435
+ const reportData = await this.buildIncludedTriagedReport(
436
+ report,
437
+ supportedVersions
438
+ );
439
+ selectedReports.push(reportData);
440
+
441
+ const missing = getMissingReportInformation(reportData);
442
+ if (missing.length) {
443
+ missingInformation.push({
444
+ id: reportData.id,
445
+ title: reportData.title,
446
+ link: reportData.link,
447
+ missing
448
+ });
449
+ }
450
+ }
451
+
452
+ this.displayMissingReportInformationSummary(missingInformation);
453
+ return selectedReports;
454
+ }
455
+
456
+ async buildIncludedTriagedReport(report, supportedVersions) {
457
+ const prURL = getReportPRURL(report);
458
+ const patchAuthors = await this.getPatchAuthorsFromPR(prURL);
459
+ return buildIncludedTriagedReport(report, {
460
+ affectedVersions: supportedVersions,
461
+ patchAuthors,
462
+ prURL
463
+ });
464
+ }
465
+
466
+ async getPatchAuthorsFromPR(prURL) {
467
+ if (!prURL) return [];
468
+
469
+ try {
470
+ const { user } = await this.req.getPullRequest(prURL);
471
+ return user?.login ? [user.login] : [];
472
+ } catch (error) {
473
+ this.cli.warn(`Could not fetch patch author from ${prURL}`);
474
+ this.cli.error(error);
475
+ return [];
476
+ }
477
+ }
478
+
479
+ displayMissingReportInformationSummary(reports) {
480
+ if (!reports.length) {
481
+ this.cli.ok('All included reports have the expected information.');
482
+ return;
483
+ }
484
+
485
+ const grouped = groupMissingReportInformation(reports);
486
+ this.cli.warn(
487
+ `${reports.length} included reports are missing information:`
488
+ );
489
+ for (const { field, reports: fieldReports } of grouped) {
490
+ const reportList = fieldReports
491
+ .map(({ id }) => `H1 #${id}`)
492
+ .join(', ');
493
+ this.cli.info(
494
+ `- ${field} (${fieldReports.length}): ${reportList}`
495
+ );
496
+ }
497
+ }
498
+
205
499
  async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
206
500
  this.cli.startSpinner('Creating vulnerabilities.json...');
207
501
  const fileContent = JSON.stringify({
@@ -211,10 +505,19 @@ export default class PrepareSecurityRelease extends SecurityRelease {
211
505
  }, null, 2) + '\n';
212
506
 
213
507
  const folderPath = path.resolve(NEXT_SECURITY_RELEASE_FOLDER);
214
- await fs.promises.mkdir(folderPath, { recursive: true });
215
-
216
508
  const fullPath = path.join(folderPath, 'vulnerabilities.json');
217
- await fs.promises.writeFile(fullPath, fileContent);
509
+ await confirmSecurityStep(
510
+ this.cli,
511
+ `create directory \`${folderPath}\``,
512
+ 'This creates the security release folder if it does not already exist.'
513
+ );
514
+ await fs.promises.mkdir(folderPath, { recursive: true });
515
+ await writeSecurityFile(
516
+ this.cli,
517
+ fullPath,
518
+ fileContent,
519
+ 'This creates vulnerabilities.json for the next security release.'
520
+ );
218
521
  this.cli.stopSpinner(`Created ${fullPath}`);
219
522
 
220
523
  return fullPath;
@@ -222,6 +525,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
222
525
 
223
526
  async createPullRequest(content) {
224
527
  const { owner, repo } = this.repository;
528
+ await confirmSecurityStep(
529
+ this.cli,
530
+ `create GitHub pull request \`${owner}/${repo}: ${this.title}\``,
531
+ `This opens a pull request from ${NEXT_SECURITY_RELEASE_BRANCH} to main.`
532
+ );
225
533
  const response = await this.req.createPullRequest(
226
534
  this.title,
227
535
  content ?? 'List of vulnerabilities to be included in the next security release',
@@ -248,10 +556,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
248
556
  process.exit(1);
249
557
  }
250
558
 
559
+ // Collect dependency updates using the same schema as the reports: each
560
+ // dependency maps every affected release line to the PR that lands it, e.g.
561
+ // { undici: { affectedVersions: { '22.x': '<prURL>', '24.x': '<prURL>' } } }
251
562
  async getDependencyUpdates() {
252
- const deps = [];
253
- this.cli.log('\n');
254
- this.cli.separator('Dependency Updates');
563
+ const deps = {};
255
564
  const updates = await this.cli.prompt('Are there dependency updates in this security release?',
256
565
  {
257
566
  defaultAnswer: true,
@@ -262,11 +571,9 @@ export default class PrepareSecurityRelease extends SecurityRelease {
262
571
 
263
572
  const supportedVersions = await getSupportedVersions();
264
573
 
265
- let asking = true;
266
- while (asking) {
574
+ while (true) {
267
575
  const dep = await promptDependencies(this.cli);
268
576
  if (!dep) {
269
- asking = false;
270
577
  break;
271
578
  }
272
579
 
@@ -277,20 +584,24 @@ export default class PrepareSecurityRelease extends SecurityRelease {
277
584
  });
278
585
 
279
586
  const versions = await this.cli.prompt(
280
- 'Which release line does this dependency update affect?', {
587
+ 'Which release line(s) does this dependency update PR affect? ' +
588
+ '(comma-separated)', {
281
589
  defaultAnswer: supportedVersions,
282
590
  questionType: 'input'
283
591
  });
284
592
 
285
593
  try {
286
594
  const res = await this.req.getPullRequest(dep);
287
- const { html_url, title } = res;
288
- deps.push({
289
- name,
290
- url: html_url,
291
- title,
292
- affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim())
293
- });
595
+ const { html_url: prURL } = res;
596
+ const affectedVersions = versions
597
+ .split(',')
598
+ .map((v) => v.replace('v', '').trim())
599
+ .filter(Boolean);
600
+
601
+ deps[name] ??= { affectedVersions: {} };
602
+ for (const line of affectedVersions) {
603
+ deps[name].affectedVersions[line] = prURL;
604
+ }
294
605
  this.cli.separator();
295
606
  } catch (error) {
296
607
  this.cli.error('Invalid PR url. Please provide a valid PR url.');
@@ -304,6 +615,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
304
615
  this.cli.startSpinner('Closing HackerOne reports');
305
616
  for (const report of jsonReports) {
306
617
  this.cli.updateSpinner(`Closing report ${report.id}...`);
618
+ await confirmSecurityStep(
619
+ this.cli,
620
+ `resolve HackerOne report \`${report.id}\``,
621
+ 'This marks the HackerOne report as resolved.'
622
+ );
307
623
  await this.req.updateReportState(
308
624
  report.id,
309
625
  'resolved',
@@ -311,6 +627,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
311
627
  );
312
628
 
313
629
  this.cli.updateSpinner(`Requesting disclosure to report ${report.id}...`);
630
+ await confirmSecurityStep(
631
+ this.cli,
632
+ `request disclosure for HackerOne report \`${report.id}\``,
633
+ 'This asks HackerOne to disclose the resolved report.'
634
+ );
314
635
  await this.req.requestDisclosure(report.id);
315
636
  }
316
637
  this.cli.stopSpinner('Done closing H1 Reports and requesting disclosure');
@@ -328,6 +649,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
328
649
  for (const pr of prs) {
329
650
  if (pr.labels.some((l) => labels.includes(l.name))) {
330
651
  this.cli.updateSpinner(`Closing Pull Request: ${pr.number}`);
652
+ await confirmSecurityStep(
653
+ this.cli,
654
+ `close GitHub pull request \`nodejs-private/node-private#${pr.number}\``,
655
+ 'This closes a pull request labeled for the security release.'
656
+ );
331
657
  await this.req.closePullRequest(pr.number,
332
658
  { owner: 'nodejs-private', repo: 'node-private' });
333
659
  }
@@ -5,7 +5,7 @@ import { pipeline } from 'node:stream/promises';
5
5
  import semver from 'semver';
6
6
  import * as gst from 'git-secure-tag';
7
7
 
8
- import { forceRunAsync } from './run.js';
8
+ import { forceRunAsync, runSync } from './run.js';
9
9
  import PRData from './pr_data.js';
10
10
  import PRChecker from './pr_checker.js';
11
11
  import Session from './session.js';
@@ -59,7 +59,8 @@ export default class ReleasePromotion extends Session {
59
59
  if (!localCloneIsClean) {
60
60
  if (await cli.prompt('Should we checkout the release proposal?')) {
61
61
  cli.startSpinner('Fetching the proposal upstream...');
62
- await forceRunAsync('git', ['fetch', proposalUpstreamRemote, releaseCommitSha],
62
+ // We need a sync spawn here to avoid the spinner getting in the way of SSH key passphrase
63
+ runSync('git', ['fetch', proposalUpstreamRemote, releaseCommitSha],
63
64
  { ignoreFailure: false });
64
65
  await forceRunAsync('git', ['checkout', releaseCommitSha], { ignoreFailure: false });
65
66
  cli.stopSpinner('Local HEAD is now in sync with the proposal');
@@ -523,11 +524,7 @@ export default class ReleasePromotion extends Session {
523
524
  cli.separator();
524
525
  cli.info('Resolve the conflicts and commit the result');
525
526
  cli.separator();
526
- const didResolveConflicts = await cli.prompt(
527
- 'Finished resolving cherry-pick conflicts?', { defaultAnswer: true });
528
- if (!didResolveConflicts) {
529
- throw new Error('Aborted');
530
- }
527
+ await cli.prompt('Ready to continue?', { defaultAnswer: true });
531
528
  }
532
529
 
533
530
  if (existsSync('.git/CHERRY_PICK_HEAD')) {
@@ -537,16 +534,22 @@ export default class ReleasePromotion extends Session {
537
534
  }
538
535
 
539
536
  // Validate release commit on the default branch
540
- const releaseCommitOnDefaultBranch =
541
- await forceRunAsync('git', ['show', 'HEAD', '--name-only', '--pretty=format:%s'],
542
- { captureStdout: true, ignoreFailure: false });
543
- const [commitTitle, ...modifiedFiles] = releaseCommitOnDefaultBranch.trim().split('\n');
544
- await this.validateReleaseCommit(commitTitle);
545
- if (modifiedFiles.some(file => !file.endsWith('.md'))) {
546
- cli.warn('Some modified files are not markdown, that\'s unusual.');
537
+ while (true) {
538
+ const releaseCommitOnDefaultBranch =
539
+ await forceRunAsync('git', ['show', 'HEAD', '--name-only', '--pretty=format:%s'],
540
+ { captureStdout: true, ignoreFailure: false });
541
+ const [commitTitle, ...modifiedFiles] = releaseCommitOnDefaultBranch.trim().split('\n');
542
+ await this.validateReleaseCommit(commitTitle);
543
+ if (!modifiedFiles.some(file => !file.endsWith('.md'))) break;
544
+
545
+ cli.warn(
546
+ 'Some modified files are not markdown, that\'s unusual. Consider amending the commit.'
547
+ );
547
548
  cli.info(`The list of modified files: ${modifiedFiles.map(f => `- ${f}`).join('\n')}`);
548
- if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
549
- throw new Error('Aborted');
549
+ if (await cli.prompt('Ignore and continue anyway?', {
550
+ defaultAnswer: false
551
+ })) {
552
+ break;
550
553
  }
551
554
  }
552
555
  }
@@ -0,0 +1,19 @@
1
+ query PR($owner: String!, $repo: String!) {
2
+ repository(owner: $owner, name: $repo) {
3
+ pullRequests(states: OPEN, first: 2, headRefName: "next-security-release", orderBy: {field: CREATED_AT, direction: DESC}) {
4
+ nodes {
5
+ number
6
+ headRefOid
7
+ author {
8
+ login
9
+ }
10
+
11
+ files(first: 2) {
12
+ nodes {
13
+ path
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
@@ -31,7 +31,10 @@ query PR($prid: Int!, $owner: String!, $repo: String!) {
31
31
  closed,
32
32
  closedAt,
33
33
  merged,
34
- mergedAt
34
+ mergedAt,
35
+ mergeCommit {
36
+ oid
37
+ }
35
38
  }
36
39
  }
37
40
  }