@node-core/utils 6.4.0 → 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 +25 -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 +312 -46
- 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 +5 -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
|
@@ -11,10 +11,12 @@ import {
|
|
|
11
11
|
promptDependencies,
|
|
12
12
|
getSupportedVersions,
|
|
13
13
|
getReportSeverity,
|
|
14
|
+
getSummary,
|
|
14
15
|
pickReport,
|
|
16
|
+
confirmSecurityStep,
|
|
17
|
+
writeSecurityFile,
|
|
15
18
|
SecurityRelease
|
|
16
19
|
} from './security-release/security-release.js';
|
|
17
|
-
import _ from 'lodash';
|
|
18
20
|
|
|
19
21
|
function relativeDate(date) {
|
|
20
22
|
const days = Math.floor((Date.now() - date) / (1000 * 60 * 60 * 24));
|
|
@@ -23,6 +25,131 @@ function relativeDate(date) {
|
|
|
23
25
|
return months === 1 ? '1 month ago' : `${months} months ago`;
|
|
24
26
|
}
|
|
25
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
|
+
}
|
|
152
|
+
|
|
26
153
|
export default class PrepareSecurityRelease extends SecurityRelease {
|
|
27
154
|
title = 'Next Security Release';
|
|
28
155
|
|
|
@@ -35,22 +162,23 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
35
162
|
this.req = new Request(credentials);
|
|
36
163
|
|
|
37
164
|
let excludedReports = [];
|
|
38
|
-
const showTriaged = await this.promptShowTriagedWithoutPR();
|
|
39
|
-
if (showTriaged) {
|
|
40
|
-
excludedReports = await this.showTriagedReportsWithoutPR();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
165
|
const releaseDate = await this.promptReleaseDate();
|
|
44
166
|
if (releaseDate !== 'TBD') {
|
|
45
167
|
validateDate(releaseDate);
|
|
46
168
|
}
|
|
47
169
|
const createVulnerabilitiesJSON = await this.promptVulnerabilitiesJSON();
|
|
48
170
|
|
|
49
|
-
|
|
50
|
-
const content = await this.buildDescription(releaseDate, securityReleasePRUrl);
|
|
171
|
+
const content = await this.buildDescription(releaseDate);
|
|
51
172
|
if (createVulnerabilitiesJSON) {
|
|
52
|
-
|
|
53
|
-
|
|
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);
|
|
54
182
|
}
|
|
55
183
|
|
|
56
184
|
this.cli.ok('Done!');
|
|
@@ -73,6 +201,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
73
201
|
|
|
74
202
|
if (vulnerabilityJSON.buildIssue) {
|
|
75
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
|
+
);
|
|
76
209
|
await this.req.commentIssue(
|
|
77
210
|
vulnerabilityJSON.buildIssue,
|
|
78
211
|
'Security release is out'
|
|
@@ -81,6 +214,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
81
214
|
|
|
82
215
|
if (vulnerabilityJSON.dockerIssue) {
|
|
83
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
|
+
);
|
|
84
222
|
await this.req.commentIssue(
|
|
85
223
|
vulnerabilityJSON.dockerIssue,
|
|
86
224
|
'Security release is out'
|
|
@@ -92,11 +230,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
92
230
|
vulnerabilityJSON.releaseDate}?`,
|
|
93
231
|
{ defaultAnswer: true });
|
|
94
232
|
if (updateFolder) {
|
|
95
|
-
this.updateReleaseFolder(
|
|
233
|
+
await this.updateReleaseFolder(
|
|
96
234
|
vulnerabilityJSON.releaseDate.replaceAll('/', '-')
|
|
97
235
|
);
|
|
98
236
|
const securityReleaseFolder = path.join(process.cwd(), 'security-release');
|
|
99
|
-
commitAndPushVulnerabilitiesJSON(
|
|
237
|
+
await commitAndPushVulnerabilitiesJSON(
|
|
100
238
|
securityReleaseFolder,
|
|
101
239
|
'chore: change next-security-release folder',
|
|
102
240
|
{ cli: this.cli, repository: this.repository }
|
|
@@ -109,14 +247,20 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
109
247
|
this.cli.ok('Done!');
|
|
110
248
|
}
|
|
111
249
|
|
|
112
|
-
async startVulnerabilitiesJSONCreation(
|
|
250
|
+
async startVulnerabilitiesJSONCreation(
|
|
251
|
+
releaseDate,
|
|
252
|
+
content,
|
|
253
|
+
excludedReports = [],
|
|
254
|
+
reportSelectionMode = 'review'
|
|
255
|
+
) {
|
|
113
256
|
// checkout on the next-security-release branch
|
|
114
|
-
checkoutOnSecurityReleaseBranch(this.cli, this.repository);
|
|
257
|
+
await checkoutOnSecurityReleaseBranch(this.cli, this.repository);
|
|
115
258
|
|
|
116
259
|
// choose the reports to include in the security release
|
|
117
|
-
const reports =
|
|
118
|
-
|
|
119
|
-
|
|
260
|
+
const reports = reportSelectionMode === 'include-all'
|
|
261
|
+
? await this.includeAllTriagedReports(excludedReports)
|
|
262
|
+
: await this.chooseReports(excludedReports);
|
|
263
|
+
const deps = await this.getDependencyUpdates();
|
|
120
264
|
|
|
121
265
|
// create the vulnerabilities.json file in the security-release repo
|
|
122
266
|
const filePath = await this.createVulnerabilitiesJSON(reports, deps, releaseDate);
|
|
@@ -131,11 +275,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
131
275
|
- git push -u origin ${NEXT_SECURITY_RELEASE_BRANCH}
|
|
132
276
|
- open a PR on ${this.repository.owner}/${this.repository.repo}`);
|
|
133
277
|
return;
|
|
134
|
-
}
|
|
278
|
+
}
|
|
135
279
|
|
|
136
280
|
// commit and push the vulnerabilities.json file
|
|
137
281
|
const commitMessage = 'chore: create vulnerabilities.json for next security release';
|
|
138
|
-
commitAndPushVulnerabilitiesJSON(filePath,
|
|
282
|
+
await commitAndPushVulnerabilitiesJSON(filePath,
|
|
139
283
|
commitMessage,
|
|
140
284
|
{ cli: this.cli, repository: this.repository });
|
|
141
285
|
|
|
@@ -165,21 +309,18 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
165
309
|
return body.substring(index);
|
|
166
310
|
}
|
|
167
311
|
return body;
|
|
168
|
-
} catch
|
|
312
|
+
} catch {
|
|
169
313
|
this.cli.error(`Could not retrieve the security issue template from ${url}`);
|
|
170
314
|
}
|
|
171
315
|
}
|
|
172
316
|
|
|
173
317
|
async promptReleaseDate() {
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
questionType: 'input',
|
|
181
|
-
defaultAnswer: formattedDate
|
|
182
|
-
});
|
|
318
|
+
const choices = getNextTuesdayReleaseDateChoices();
|
|
319
|
+
return this.cli.promptSelect(
|
|
320
|
+
'Select target release date:',
|
|
321
|
+
choices,
|
|
322
|
+
{ defaultAnswer: choices[0].value }
|
|
323
|
+
);
|
|
183
324
|
}
|
|
184
325
|
|
|
185
326
|
async promptVulnerabilitiesJSON() {
|
|
@@ -188,6 +329,25 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
188
329
|
{ defaultAnswer: true });
|
|
189
330
|
}
|
|
190
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
|
+
|
|
191
351
|
async promptCreateRelaseIssue() {
|
|
192
352
|
return this.cli.prompt(
|
|
193
353
|
'Create the Next Security Release issue?',
|
|
@@ -262,6 +422,80 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
262
422
|
return selectedReports;
|
|
263
423
|
}
|
|
264
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
|
+
|
|
265
499
|
async createVulnerabilitiesJSON(reports, dependencies, releaseDate) {
|
|
266
500
|
this.cli.startSpinner('Creating vulnerabilities.json...');
|
|
267
501
|
const fileContent = JSON.stringify({
|
|
@@ -271,10 +505,19 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
271
505
|
}, null, 2) + '\n';
|
|
272
506
|
|
|
273
507
|
const folderPath = path.resolve(NEXT_SECURITY_RELEASE_FOLDER);
|
|
274
|
-
await fs.promises.mkdir(folderPath, { recursive: true });
|
|
275
|
-
|
|
276
508
|
const fullPath = path.join(folderPath, 'vulnerabilities.json');
|
|
277
|
-
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
|
+
);
|
|
278
521
|
this.cli.stopSpinner(`Created ${fullPath}`);
|
|
279
522
|
|
|
280
523
|
return fullPath;
|
|
@@ -282,6 +525,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
282
525
|
|
|
283
526
|
async createPullRequest(content) {
|
|
284
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
|
+
);
|
|
285
533
|
const response = await this.req.createPullRequest(
|
|
286
534
|
this.title,
|
|
287
535
|
content ?? 'List of vulnerabilities to be included in the next security release',
|
|
@@ -308,10 +556,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
308
556
|
process.exit(1);
|
|
309
557
|
}
|
|
310
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>' } } }
|
|
311
562
|
async getDependencyUpdates() {
|
|
312
|
-
const deps =
|
|
313
|
-
this.cli.log('\n');
|
|
314
|
-
this.cli.separator('Dependency Updates');
|
|
563
|
+
const deps = {};
|
|
315
564
|
const updates = await this.cli.prompt('Are there dependency updates in this security release?',
|
|
316
565
|
{
|
|
317
566
|
defaultAnswer: true,
|
|
@@ -322,11 +571,9 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
322
571
|
|
|
323
572
|
const supportedVersions = await getSupportedVersions();
|
|
324
573
|
|
|
325
|
-
|
|
326
|
-
while (asking) {
|
|
574
|
+
while (true) {
|
|
327
575
|
const dep = await promptDependencies(this.cli);
|
|
328
576
|
if (!dep) {
|
|
329
|
-
asking = false;
|
|
330
577
|
break;
|
|
331
578
|
}
|
|
332
579
|
|
|
@@ -337,20 +584,24 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
337
584
|
});
|
|
338
585
|
|
|
339
586
|
const versions = await this.cli.prompt(
|
|
340
|
-
'Which release line does this dependency update affect?'
|
|
587
|
+
'Which release line(s) does this dependency update PR affect? ' +
|
|
588
|
+
'(comma-separated)', {
|
|
341
589
|
defaultAnswer: supportedVersions,
|
|
342
590
|
questionType: 'input'
|
|
343
591
|
});
|
|
344
592
|
|
|
345
593
|
try {
|
|
346
594
|
const res = await this.req.getPullRequest(dep);
|
|
347
|
-
const { html_url
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
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
|
+
}
|
|
354
605
|
this.cli.separator();
|
|
355
606
|
} catch (error) {
|
|
356
607
|
this.cli.error('Invalid PR url. Please provide a valid PR url.');
|
|
@@ -364,6 +615,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
364
615
|
this.cli.startSpinner('Closing HackerOne reports');
|
|
365
616
|
for (const report of jsonReports) {
|
|
366
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
|
+
);
|
|
367
623
|
await this.req.updateReportState(
|
|
368
624
|
report.id,
|
|
369
625
|
'resolved',
|
|
@@ -371,6 +627,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
371
627
|
);
|
|
372
628
|
|
|
373
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
|
+
);
|
|
374
635
|
await this.req.requestDisclosure(report.id);
|
|
375
636
|
}
|
|
376
637
|
this.cli.stopSpinner('Done closing H1 Reports and requesting disclosure');
|
|
@@ -388,6 +649,11 @@ export default class PrepareSecurityRelease extends SecurityRelease {
|
|
|
388
649
|
for (const pr of prs) {
|
|
389
650
|
if (pr.labels.some((l) => labels.includes(l.name))) {
|
|
390
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
|
+
);
|
|
391
657
|
await this.req.closePullRequest(pr.number,
|
|
392
658
|
{ owner: 'nodejs-private', repo: 'node-private' });
|
|
393
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
|
+
}
|
package/lib/queries/PR.gql
CHANGED
package/lib/request.js
CHANGED
|
@@ -100,6 +100,47 @@ export default class Request {
|
|
|
100
100
|
return this.json(prUrl);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
async * getPullRequestFiles({ owner, repo, prid }) {
|
|
104
|
+
let page = 1;
|
|
105
|
+
for (;;) {
|
|
106
|
+
const url =
|
|
107
|
+
`/repos/${owner}/${repo}/pulls/${prid}/files?per_page=100&page=${page}`;
|
|
108
|
+
const batch = await this.json(url);
|
|
109
|
+
if (!Array.isArray(batch) || batch.length === 0) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
yield * batch;
|
|
113
|
+
if (batch.length < 100) {
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
page++;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async listDirectory({ owner, repo, path, ref }) {
|
|
121
|
+
let url = `/repos/${owner}/${repo}/contents/${path}`;
|
|
122
|
+
if (ref) {
|
|
123
|
+
url += `?ref=${encodeURIComponent(ref)}`;
|
|
124
|
+
}
|
|
125
|
+
return this.json(url);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async dispatchWorkflow(workflowId, { owner, repo, ref, inputs }) {
|
|
129
|
+
const url =
|
|
130
|
+
`/repos/${owner}/${repo}/actions/workflows/${workflowId}/dispatches`;
|
|
131
|
+
const res = await this.fetch(url, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
body: JSON.stringify({ ref, inputs })
|
|
134
|
+
});
|
|
135
|
+
if (!res.ok) {
|
|
136
|
+
const text = await res.text();
|
|
137
|
+
throw new Error(
|
|
138
|
+
`Failed to dispatch workflow ${workflowId} on ${owner}/${repo}: ` +
|
|
139
|
+
`${res.status} ${res.statusText}${text ? `\n${text}` : ''}`);
|
|
140
|
+
}
|
|
141
|
+
return res;
|
|
142
|
+
}
|
|
143
|
+
|
|
103
144
|
async createPullRequest(title, body, { owner, repo, head, base }) {
|
|
104
145
|
const url = `/repos/${owner}/${repo}/pulls`;
|
|
105
146
|
const options = {
|