@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.
- package/README.md +2 -5
- package/bin/ncu-ci.js +3 -19
- package/components/git/benchmark.js +112 -0
- package/components/git/land.js +3 -3
- package/components/git/metadata.js +34 -1
- package/components/git/release.js +3 -5
- package/components/git/security.js +258 -27
- package/components/git/v8.js +6 -3
- package/components/metadata.js +75 -7
- package/lib/auth.js +3 -4
- package/lib/backport_session.js +3 -3
- package/lib/benchmark.js +243 -0
- package/lib/cherry_pick.js +32 -11
- package/lib/ci/failure_aggregator.js +16 -20
- package/lib/ci/run_ci.js +1 -1
- package/lib/cli.js +33 -1
- package/lib/config.js +7 -7
- package/lib/file.js +5 -5
- package/lib/landing_session.js +65 -24
- package/lib/links.js +4 -4
- package/lib/pr_checker.js +163 -31
- package/lib/pr_data.js +1 -1
- package/lib/pr_summary.js +1 -1
- package/lib/prepare_release.js +270 -85
- package/lib/prepare_security.js +367 -41
- package/lib/promote_release.js +19 -16
- package/lib/queries/ListSecurityReleasePRs.gql +19 -0
- package/lib/queries/PR.gql +4 -1
- package/lib/request.js +41 -0
- package/lib/run.js +5 -5
- package/lib/security-announcement.js +10 -5
- package/lib/security-release/security-release.js +160 -20
- package/lib/security_blog.js +130 -65
- package/lib/session.js +4 -3
- package/lib/team_info.js +1 -1
- package/lib/update-v8/applyNodeChanges.js +1 -3
- package/lib/update-v8/backport.js +6 -6
- package/lib/update-v8/commitUpdate.js +3 -6
- package/lib/update-v8/common.js +30 -6
- package/lib/update-v8/constants.js +22 -0
- package/lib/update-v8/deps.js +3 -5
- package/lib/update-v8/index.js +14 -5
- package/lib/update-v8/majorUpdate.js +5 -4
- package/lib/update-v8/minorUpdate.js +4 -4
- package/lib/update-v8/updateCrates.js +192 -0
- package/lib/update-v8/updateV8Clone.js +1 -1
- package/lib/update-v8/updateVersionNumbers.js +3 -6
- package/lib/update-v8/util.js +3 -3
- package/lib/update_security_release.js +347 -105
- package/lib/utils.js +5 -5
- package/lib/verbosity.js +3 -3
- package/lib/wpt/index.js +1 -23
- package/package.json +18 -22
- package/bin/get-metadata.js +0 -11
- package/lib/wpt/templates/README.md +0 -16
- package/npm-shrinkwrap.json +0 -20380
package/lib/prepare_security.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
35
|
-
const content = await this.buildDescription(releaseDate, securityReleasePRUrl);
|
|
171
|
+
const content = await this.buildDescription(releaseDate);
|
|
36
172
|
if (createVulnerabilitiesJSON) {
|
|
37
|
-
|
|
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(
|
|
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 =
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
}
|
package/lib/promote_release.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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 (
|
|
549
|
-
|
|
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
|
+
}
|