@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_release.js
CHANGED
|
@@ -2,12 +2,9 @@ import path from 'node:path';
|
|
|
2
2
|
import { promises as fs, existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
4
|
import semver from 'semver';
|
|
5
|
-
import { replaceInFile } from 'replace-in-file';
|
|
6
5
|
|
|
7
6
|
import { forceRunAsync, runAsync, runSync } from './run.js';
|
|
8
7
|
import { writeJson, readJson } from './file.js';
|
|
9
|
-
import Request from './request.js';
|
|
10
|
-
import auth from './auth.js';
|
|
11
8
|
import {
|
|
12
9
|
getEOLDate,
|
|
13
10
|
getStartLTSBlurb,
|
|
@@ -15,9 +12,73 @@ import {
|
|
|
15
12
|
} from './release/utils.js';
|
|
16
13
|
import CherryPick from './cherry_pick.js';
|
|
17
14
|
import Session from './session.js';
|
|
15
|
+
import {
|
|
16
|
+
getAffectedVersionLines,
|
|
17
|
+
getDependencyUpdates,
|
|
18
|
+
SEVERITY_RANKS
|
|
19
|
+
} from './security-release/security-release.js';
|
|
18
20
|
|
|
19
21
|
const isWindows = process.platform === 'win32';
|
|
20
22
|
|
|
23
|
+
// Parse a GitHub Pull Request URL into its owner, repo and number.
|
|
24
|
+
// Returns null when the URL does not look like a PR URL.
|
|
25
|
+
export function parsePullRequestURL(url) {
|
|
26
|
+
const match = /github\.com\/([^/]+)\/([^/]+)\/pull\/(\d+)/.exec(url || '');
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
return { owner: match[1], repo: match[2], number: Number(match[3]) };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Resolve the PR URL that fixes a given release line from a report or
|
|
32
|
+
// dependency `affectedVersions` entry.
|
|
33
|
+
// Supports the current schema, where `affectedVersions` maps a release line to
|
|
34
|
+
// its PR URL (e.g. `{ '22.x': 'https://.../pull/1' }`), and the legacy schema,
|
|
35
|
+
// where `affectedVersions` is an array of lines paired with a single `prURL`.
|
|
36
|
+
export function getPullRequestURLForLine(affectedVersions, line, legacyPrURL) {
|
|
37
|
+
if (
|
|
38
|
+
affectedVersions &&
|
|
39
|
+
typeof affectedVersions === 'object' &&
|
|
40
|
+
!Array.isArray(affectedVersions)
|
|
41
|
+
) {
|
|
42
|
+
return affectedVersions[line] || null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (Array.isArray(affectedVersions) && legacyPrURL) {
|
|
46
|
+
return getAffectedVersionLines(affectedVersions).includes(line)
|
|
47
|
+
? legacyPrURL
|
|
48
|
+
: null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Format the notable changes of a security release, one entry per commit:
|
|
55
|
+
// `* (CVE-ID) subsystem: title (Author) – Severity`, sorted from highest to
|
|
56
|
+
// lowest severity, then by CVE-ID. Commits without a CVE-ID trailer
|
|
57
|
+
// (e.g. dependency updates) are listed last.
|
|
58
|
+
export function formatSecurityNotableChanges(commits, severityByCVE) {
|
|
59
|
+
const rank = rating => SEVERITY_RANKS.indexOf((rating || '').toUpperCase());
|
|
60
|
+
const entries = commits
|
|
61
|
+
.filter(({ subject }) => subject)
|
|
62
|
+
.map(({ subject, author, cveIds = [] }) => ({
|
|
63
|
+
subject,
|
|
64
|
+
author,
|
|
65
|
+
cveIds,
|
|
66
|
+
rating: cveIds.map(id => severityByCVE.get(id)).find(Boolean) || ''
|
|
67
|
+
}))
|
|
68
|
+
.sort((a, b) => (rank(b.rating) - rank(a.rating)) ||
|
|
69
|
+
(b.cveIds.length - a.cveIds.length) ||
|
|
70
|
+
(a.cveIds[0] || '').localeCompare(b.cveIds[0] || '', 'en', { numeric: true }));
|
|
71
|
+
|
|
72
|
+
const lines = entries.map(({ subject, author, cveIds, rating }) => {
|
|
73
|
+
const cve = cveIds.length ? `(${cveIds.join(', ')}) ` : '';
|
|
74
|
+
const severity = rating
|
|
75
|
+
? ` – ${rating[0].toUpperCase()}${rating.slice(1).toLowerCase()}`
|
|
76
|
+
: '';
|
|
77
|
+
return `* ${cve}${subject} (${author})${severity}`;
|
|
78
|
+
});
|
|
79
|
+
return lines.length ? `${lines.join('\n')}\n` : '';
|
|
80
|
+
}
|
|
81
|
+
|
|
21
82
|
export default class ReleasePreparation extends Session {
|
|
22
83
|
constructor(argv, cli, dir) {
|
|
23
84
|
super(cli, dir);
|
|
@@ -32,101 +93,162 @@ export default class ReleasePreparation extends Session {
|
|
|
32
93
|
this.defaultReleaseDate = argv.releaseDate ?? new Date().toISOString().slice(0, 10);
|
|
33
94
|
this.ltsCodename = '';
|
|
34
95
|
this.date = '';
|
|
35
|
-
this.filterLabels = argv.filterLabel && argv.filterLabel.split(',');
|
|
36
96
|
this.newVersion = argv.newVersion;
|
|
37
97
|
}
|
|
38
98
|
|
|
39
99
|
get branch() {
|
|
40
|
-
return this.stagingBranch;
|
|
100
|
+
return this.isSecurityRelease ? this.releaseBranch : this.stagingBranch;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Resolve the path to the security release vulnerabilities.json.
|
|
104
|
+
// `--security` may point either directly at a vulnerabilities.json file or at
|
|
105
|
+
// the root of the security-release repository, in which case the well-known
|
|
106
|
+
// sub-path is appended.
|
|
107
|
+
getVulnerabilitiesJSONPath() {
|
|
108
|
+
if (!this.securityReleaseRepo) return null;
|
|
109
|
+
if (this.securityReleaseRepo.endsWith('.json')) {
|
|
110
|
+
return this.securityReleaseRepo;
|
|
111
|
+
}
|
|
112
|
+
return path.join(
|
|
113
|
+
this.securityReleaseRepo,
|
|
114
|
+
'security-release',
|
|
115
|
+
'next-security-release',
|
|
116
|
+
'vulnerabilities.json'
|
|
117
|
+
);
|
|
41
118
|
}
|
|
42
119
|
|
|
43
|
-
|
|
120
|
+
loadVulnerabilitiesJSON() {
|
|
44
121
|
const { cli } = this;
|
|
122
|
+
const vulnPath = this.getVulnerabilitiesJSONPath();
|
|
123
|
+
|
|
124
|
+
if (!vulnPath) {
|
|
125
|
+
cli.error('No vulnerabilities.json path provided. Pass the path to ' +
|
|
126
|
+
'vulnerabilities.json (or the security-release repository) via ' +
|
|
127
|
+
'`--security=<path>`.');
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
45
130
|
|
|
46
|
-
|
|
47
|
-
|
|
131
|
+
if (!existsSync(vulnPath)) {
|
|
132
|
+
cli.error(`vulnerabilities.json not found at ${vulnPath}.`);
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
cli.startSpinner(`Reading vulnerabilities.json from ${vulnPath}..`);
|
|
138
|
+
const vulnData = JSON.parse(readFileSync(vulnPath, 'utf-8'));
|
|
139
|
+
cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnPath}`);
|
|
140
|
+
return vulnData;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
cli.error(`Failed to read vulnerabilities.json: ${err.message}`);
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
48
145
|
}
|
|
49
146
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
147
|
+
// Build the list of PRs to cherry-pick for a given release line from both the
|
|
148
|
+
// reports and the dependency updates, using their `affectedVersions` mapping.
|
|
149
|
+
getSecurityCherryPickTargets(vulnData, line) {
|
|
150
|
+
const targets = [];
|
|
151
|
+
|
|
152
|
+
for (const report of vulnData.reports ?? []) {
|
|
153
|
+
const url = getPullRequestURLForLine(
|
|
154
|
+
report.affectedVersions, line, report.prURL);
|
|
155
|
+
if (url) {
|
|
156
|
+
targets.push({
|
|
157
|
+
url,
|
|
158
|
+
cveIds: report.cveIds?.length ? report.cveIds : null,
|
|
159
|
+
label: report.title || report.id
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
for (const dep of getDependencyUpdates(vulnData.dependencies)) {
|
|
165
|
+
const url = getPullRequestURLForLine(
|
|
166
|
+
dep.affectedVersions, line, dep.prURL);
|
|
167
|
+
if (url) {
|
|
168
|
+
targets.push({
|
|
169
|
+
url,
|
|
170
|
+
cveIds: null,
|
|
171
|
+
label: `dependency: ${dep.name}`,
|
|
172
|
+
isDependency: true
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return targets;
|
|
59
178
|
}
|
|
60
179
|
|
|
61
|
-
|
|
180
|
+
upstreamForPR({ owner, repo }) {
|
|
181
|
+
if (owner === 'nodejs-private') {
|
|
182
|
+
return `https://${this.username}:${this.config.token}` +
|
|
183
|
+
`@github.com/${owner}/${repo}.git`;
|
|
184
|
+
}
|
|
185
|
+
return `https://github.com/${owner}/${repo}.git`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async cherryPickSecurityPRs() {
|
|
62
189
|
const { cli } = this;
|
|
190
|
+
const line = `${this.versionComponents.major}.x`;
|
|
63
191
|
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
cli.
|
|
192
|
+
const vulnData = this.loadVulnerabilitiesJSON();
|
|
193
|
+
if (!vulnData) {
|
|
194
|
+
cli.error('Cannot proceed with a security release without a valid ' +
|
|
195
|
+
'vulnerabilities.json.');
|
|
67
196
|
return false;
|
|
68
197
|
}
|
|
69
198
|
|
|
70
|
-
const
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'security-release',
|
|
75
|
-
'next-security-release',
|
|
76
|
-
'vulnerabilities.json'
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
if (!existsSync(vulnPath)) {
|
|
80
|
-
cli.error(`vulnerabilities.json not found at ${vulnPath}. ` +
|
|
81
|
-
'Skipping CVE auto-population.');
|
|
82
|
-
cli.warn('PRs will require manual CVE-ID entry.');
|
|
83
|
-
} else {
|
|
84
|
-
try {
|
|
85
|
-
cli.startSpinner(`Reading vulnerabilities.json from ${vulnPath}..`);
|
|
86
|
-
const vulnData = JSON.parse(readFileSync(vulnPath, 'utf-8'));
|
|
87
|
-
cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnPath}`);
|
|
88
|
-
|
|
89
|
-
if (vulnData.reports && Array.isArray(vulnData.reports)) {
|
|
90
|
-
vulnData.reports.forEach(report => {
|
|
91
|
-
if (report.prURL && report.cveIds && report.cveIds.length > 0) {
|
|
92
|
-
vulnCveMap.set(report.prURL, report.cveIds);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
cli.ok(`Loaded ${vulnCveMap.size} CVE mappings from vulnerabilities.json`);
|
|
97
|
-
} catch (err) {
|
|
98
|
-
cli.error(`Failed to read vulnerabilities.json: ${err.message}`);
|
|
99
|
-
cli.warn('Continuing without CVE auto-population.');
|
|
100
|
-
}
|
|
101
|
-
}
|
|
199
|
+
const targets = this.getSecurityCherryPickTargets(vulnData, line);
|
|
200
|
+
if (targets.length === 0) {
|
|
201
|
+
cli.warn(`No PRs found in vulnerabilities.json for release line ${line}.`);
|
|
202
|
+
return false;
|
|
102
203
|
}
|
|
103
204
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
205
|
+
cli.info(`Cherry-picking ${targets.length} PR(s) for ${line} from ` +
|
|
206
|
+
'vulnerabilities.json');
|
|
207
|
+
|
|
208
|
+
let amendAll = false;
|
|
209
|
+
|
|
210
|
+
for (const target of targets) {
|
|
211
|
+
const pr = parsePullRequestURL(target.url);
|
|
212
|
+
if (!pr) {
|
|
213
|
+
cli.warn(`Skipping invalid PR URL for ${target.label}: ${target.url}`);
|
|
214
|
+
continue;
|
|
107
215
|
}
|
|
108
216
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
217
|
+
if (!amendAll) {
|
|
218
|
+
const answer = await cli.promptSelect(
|
|
219
|
+
`Would you like to amend ${target.label} (${target.url}) to the proposal?`,
|
|
220
|
+
[
|
|
221
|
+
{ name: 'Yes', value: 'yes' },
|
|
222
|
+
{ name: 'No, skip this PR', value: 'no' },
|
|
223
|
+
{
|
|
224
|
+
name: 'Yes to all remaining PRs (no further prompts)',
|
|
225
|
+
value: 'all'
|
|
226
|
+
}
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
if (answer === 'no') continue;
|
|
230
|
+
amendAll = answer === 'all';
|
|
231
|
+
}
|
|
112
232
|
|
|
113
|
-
if (!cveIds
|
|
114
|
-
cli.warn(`No CVE-IDs found in vulnerabilities.json for ${
|
|
233
|
+
if (!target.cveIds && !target.isDependency) {
|
|
234
|
+
cli.warn(`No CVE-IDs found in vulnerabilities.json for ${target.url}`);
|
|
115
235
|
}
|
|
116
236
|
|
|
237
|
+
cli.info(`Cherry-picking ${target.label} (${target.url})`);
|
|
117
238
|
const cp = new CherryPick(pr.number, this.dir, cli, {
|
|
118
|
-
owner:
|
|
119
|
-
repo:
|
|
239
|
+
owner: pr.owner,
|
|
240
|
+
repo: pr.repo,
|
|
120
241
|
gpgSign: this.gpgSign,
|
|
121
|
-
upstream: this.
|
|
242
|
+
upstream: this.upstreamForPR(pr),
|
|
122
243
|
lint: false,
|
|
123
|
-
includeCVE:
|
|
124
|
-
cveIds: cveIds
|
|
125
|
-
|
|
244
|
+
includeCVE: !target.isDependency,
|
|
245
|
+
cveIds: target.cveIds,
|
|
246
|
+
promptAmend: false,
|
|
247
|
+
skipMessagePrompt: amendAll
|
|
126
248
|
});
|
|
127
249
|
const success = await cp.start();
|
|
128
250
|
if (!success) {
|
|
129
|
-
cli.warn(
|
|
251
|
+
cli.warn(`The cherry-pick has failed. PR: ${target.url}`);
|
|
130
252
|
return false;
|
|
131
253
|
}
|
|
132
254
|
}
|
|
@@ -147,7 +269,7 @@ export default class ReleasePreparation extends Session {
|
|
|
147
269
|
{ defaultAnswer: this.runBranchDiff });
|
|
148
270
|
if (runBranchDiff) {
|
|
149
271
|
if (isSecurityRelease) {
|
|
150
|
-
const success = await this.cherryPickSecurityPRs(
|
|
272
|
+
const success = await this.cherryPickSecurityPRs();
|
|
151
273
|
if (!success) {
|
|
152
274
|
cli.error('Aborting security release preparation.');
|
|
153
275
|
return;
|
|
@@ -395,11 +517,24 @@ export default class ReleasePreparation extends Session {
|
|
|
395
517
|
async updateREPLACEMEs() {
|
|
396
518
|
const { newVersion } = this;
|
|
397
519
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
520
|
+
const promises = [];
|
|
521
|
+
for await (const file of fs.glob('doc/api/*.md')) {
|
|
522
|
+
promises.push(fs.open(file, 'r+').then(async fd => {
|
|
523
|
+
try {
|
|
524
|
+
const fileContent = await fd.readFile('utf-8');
|
|
525
|
+
if (fileContent.includes('REPLACEME')) {
|
|
526
|
+
const { bytesWritten } =
|
|
527
|
+
await fd.write(fileContent.replaceAll('REPLACEME', `v${newVersion}`), 0, 'utf-8');
|
|
528
|
+
await fd.truncate(bytesWritten);
|
|
529
|
+
}
|
|
530
|
+
// eslint-disable-next-line promise/always-return
|
|
531
|
+
} finally {
|
|
532
|
+
// TODO: replace with `using` syntax when dropping support for Node.js 22
|
|
533
|
+
await fd.close();
|
|
534
|
+
}
|
|
535
|
+
}));
|
|
536
|
+
}
|
|
537
|
+
await Promise.all(promises);
|
|
403
538
|
}
|
|
404
539
|
|
|
405
540
|
async updateMainChangelog() {
|
|
@@ -436,7 +571,7 @@ export default class ReleasePreparation extends Session {
|
|
|
436
571
|
arr.splice(idx, 1, `<b>${newRefLink}</b><br/>`, `${lastRefLink}<br/>`);
|
|
437
572
|
break;
|
|
438
573
|
}
|
|
439
|
-
}
|
|
574
|
+
}
|
|
440
575
|
|
|
441
576
|
await fs.writeFile(mainChangelogPath, arr.join('\n'));
|
|
442
577
|
}
|
|
@@ -464,10 +599,12 @@ export default class ReleasePreparation extends Session {
|
|
|
464
599
|
const data = await fs.readFile(majorChangelogPath, 'utf8');
|
|
465
600
|
const arr = data.split('\n');
|
|
466
601
|
const allCommits = this.getChangelog();
|
|
467
|
-
const notableChanges =
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
602
|
+
const notableChanges = isSecurityRelease
|
|
603
|
+
? await this.getSecurityNotableChanges()
|
|
604
|
+
: await this.getBranchDiff({
|
|
605
|
+
onlyNotableChanges: true,
|
|
606
|
+
format: 'markdown',
|
|
607
|
+
});
|
|
471
608
|
let releaseHeader = `## ${date}, Version ${newVersion}` +
|
|
472
609
|
` ${releaseInfo}, @${username}\n`;
|
|
473
610
|
if (isSecurityRelease) {
|
|
@@ -515,12 +652,12 @@ export default class ReleasePreparation extends Session {
|
|
|
515
652
|
arr.splice(idx, 0, ...toAppend);
|
|
516
653
|
break;
|
|
517
654
|
}
|
|
518
|
-
}
|
|
655
|
+
}
|
|
519
656
|
|
|
520
657
|
await fs.writeFile(majorChangelogPath, arr.join('\n'));
|
|
521
658
|
}
|
|
522
659
|
|
|
523
|
-
async createProposalBranch(base = this.
|
|
660
|
+
async createProposalBranch(base = this.branch) {
|
|
524
661
|
const { newVersion } = this;
|
|
525
662
|
const proposalBranch = `v${newVersion}-proposal`;
|
|
526
663
|
|
|
@@ -603,10 +740,12 @@ export default class ReleasePreparation extends Session {
|
|
|
603
740
|
messageBody.push('This is a security release.\n\n');
|
|
604
741
|
}
|
|
605
742
|
|
|
606
|
-
const notableChanges =
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
743
|
+
const notableChanges = isSecurityRelease
|
|
744
|
+
? await this.getSecurityNotableChanges()
|
|
745
|
+
: await this.getBranchDiff({
|
|
746
|
+
onlyNotableChanges: true,
|
|
747
|
+
format: 'plaintext'
|
|
748
|
+
});
|
|
610
749
|
messageBody.push('Notable changes:\n\n');
|
|
611
750
|
if (isLTSTransition) {
|
|
612
751
|
messageBody.push(`${getStartLTSBlurb(this)}\n\n`);
|
|
@@ -629,6 +768,48 @@ export default class ReleasePreparation extends Session {
|
|
|
629
768
|
return useMessage;
|
|
630
769
|
}
|
|
631
770
|
|
|
771
|
+
// Build the notable changes of a security release from the commits
|
|
772
|
+
// cherry-picked onto the proposal branch and the severity ratings in
|
|
773
|
+
// vulnerabilities.json.
|
|
774
|
+
async getSecurityNotableChanges() {
|
|
775
|
+
const { upstream, versionComponents } = this;
|
|
776
|
+
const releaseBranch = `v${versionComponents.major}.x`;
|
|
777
|
+
|
|
778
|
+
await forceRunAsync('git', ['remote', 'set-branches', '--add', upstream, releaseBranch], {
|
|
779
|
+
ignoreFailures: false
|
|
780
|
+
});
|
|
781
|
+
await forceRunAsync('git', ['fetch', upstream, releaseBranch], { ignoreFailures: false });
|
|
782
|
+
|
|
783
|
+
const severityByCVE = new Map();
|
|
784
|
+
const vulnPath = this.getVulnerabilitiesJSONPath();
|
|
785
|
+
if (vulnPath && existsSync(vulnPath)) {
|
|
786
|
+
const { reports } = JSON.parse(readFileSync(vulnPath, 'utf-8'));
|
|
787
|
+
for (const report of reports ?? []) {
|
|
788
|
+
for (const cveId of report.cveIds ?? []) {
|
|
789
|
+
severityByCVE.set(cveId, report.severity?.rating);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const log = runSync('git', [
|
|
795
|
+
'log',
|
|
796
|
+
'--format=%s%x1f%an%x1f%(trailers:key=CVE-ID,valueonly,separator=%x2C)%x1e',
|
|
797
|
+
`${upstream}/${releaseBranch}..HEAD`
|
|
798
|
+
]);
|
|
799
|
+
const commits = log.split('\x1e').map(record => {
|
|
800
|
+
const [subject, author, cveIds] = record.trim().split('\x1f');
|
|
801
|
+
return {
|
|
802
|
+
subject,
|
|
803
|
+
author,
|
|
804
|
+
// Ignore placeholder trailers such as CVE-2026-XXXXX.
|
|
805
|
+
cveIds: (cveIds || '').split(',')
|
|
806
|
+
.map(id => id.trim())
|
|
807
|
+
.filter(id => /^CVE-\d{4}-\d+$/.test(id))
|
|
808
|
+
};
|
|
809
|
+
});
|
|
810
|
+
return formatSecurityNotableChanges(commits, severityByCVE);
|
|
811
|
+
}
|
|
812
|
+
|
|
632
813
|
async getBranchDiff(opts) {
|
|
633
814
|
const {
|
|
634
815
|
cli,
|
|
@@ -796,6 +977,10 @@ export default class ReleasePreparation extends Session {
|
|
|
796
977
|
return;
|
|
797
978
|
}
|
|
798
979
|
this.stagingBranch = currentBranch;
|
|
980
|
+
// On security releases `branch` resolves to `releaseBranch`, which
|
|
981
|
+
// tryResetBranch() needs. Derive it from the branch that is checked out:
|
|
982
|
+
// the new version is only calculated below, and it may bump the major.
|
|
983
|
+
this.releaseBranch = `v${match[1]}.x`;
|
|
799
984
|
await this.tryResetBranch();
|
|
800
985
|
this.versionComponents = await this.calculateNewVersion(await this.getLastRelease(match[1]));
|
|
801
986
|
const { major, minor, patch } = this.versionComponents;
|