Skip to content

Commit 9ccbe8e

Browse files
committed
fix: adjust changelog on security releases
1 parent e828716 commit 9ccbe8e

4 files changed

Lines changed: 165 additions & 17 deletions

File tree

‎lib/landing_session.js‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -398,14 +398,14 @@ export default class LandingSession extends Session {
398398
if (!cveID) {
399399
cveID = await cli.prompt(
400400
'Git found no CVE-ID trailer in the original commit message. ' +
401-
'Please, provide the CVE-ID or leave it empty',
402-
{ questionType: 'input', defaultAnswer: 'CVE-2026-XXXXX' }
401+
'Please, provide the CVE-ID (e.g. CVE-2026-12345) or leave it empty',
402+
{ questionType: 'input', defaultAnswer: '' }
403403
);
404404
}
405405
}
406406
// Some commits might not address a vulnerability, but it is necessary
407407
// for the security release to happen.
408-
if (cveID !== '') {
408+
if (cveID) {
409409
amended.push('CVE-ID: ' + cveID);
410410
}
411411
}

‎lib/prepare_release.js‎

Lines changed: 92 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import CherryPick from './cherry_pick.js';
1515
import Session from './session.js';
1616
import {
1717
getAffectedVersionLines,
18-
getDependencyUpdates
18+
getDependencyUpdates,
19+
SEVERITY_RANKS
1920
} from './security-release/security-release.js';
2021

2122
const isWindows = process.platform === 'win32';
@@ -51,6 +52,34 @@ export function getPullRequestURLForLine(affectedVersions, line, legacyPrURL) {
5152
return null;
5253
}
5354

55+
// Format the notable changes of a security release, one entry per commit:
56+
// `* (CVE-ID) subsystem: title (Author) – Severity`, sorted from highest to
57+
// lowest severity, then by CVE-ID. Commits without a CVE-ID trailer
58+
// (e.g. dependency updates) are listed last.
59+
export function formatSecurityNotableChanges(commits, severityByCVE) {
60+
const rank = rating => SEVERITY_RANKS.indexOf((rating || '').toUpperCase());
61+
const entries = commits
62+
.filter(({ subject }) => subject)
63+
.map(({ subject, author, cveIds = [] }) => ({
64+
subject,
65+
author,
66+
cveIds,
67+
rating: cveIds.map(id => severityByCVE.get(id)).find(Boolean) || ''
68+
}))
69+
.sort((a, b) => (rank(b.rating) - rank(a.rating)) ||
70+
(b.cveIds.length - a.cveIds.length) ||
71+
(a.cveIds[0] || '').localeCompare(b.cveIds[0] || '', 'en', { numeric: true }));
72+
73+
const lines = entries.map(({ subject, author, cveIds, rating }) => {
74+
const cve = cveIds.length ? `(${cveIds.join(', ')}) ` : '';
75+
const severity = rating
76+
? ` – ${rating[0].toUpperCase()}${rating.slice(1).toLowerCase()}`
77+
: '';
78+
return `* ${cve}${subject} (${author})${severity}`;
79+
});
80+
return lines.length ? `${lines.join('\n')}\n` : '';
81+
}
82+
5483
export default class ReleasePreparation extends Session {
5584
constructor(argv, cli, dir) {
5685
super(cli, dir);
@@ -137,7 +166,12 @@ export default class ReleasePreparation extends Session {
137166
const url = getPullRequestURLForLine(
138167
dep.affectedVersions, line, dep.prURL);
139168
if (url) {
140-
targets.push({ url, cveIds: null, label: `dependency: ${dep.name}` });
169+
targets.push({
170+
url,
171+
cveIds: null,
172+
label: `dependency: ${dep.name}`,
173+
isDependency: true
174+
});
141175
}
142176
}
143177

@@ -197,7 +231,7 @@ export default class ReleasePreparation extends Session {
197231
amendAll = answer === 'all';
198232
}
199233

200-
if (!target.cveIds) {
234+
if (!target.cveIds && !target.isDependency) {
201235
cli.warn(`No CVE-IDs found in vulnerabilities.json for ${target.url}`);
202236
}
203237

@@ -208,7 +242,7 @@ export default class ReleasePreparation extends Session {
208242
gpgSign: this.gpgSign,
209243
upstream: this.upstreamForPR(pr),
210244
lint: false,
211-
includeCVE: true,
245+
includeCVE: !target.isDependency,
212246
cveIds: target.cveIds,
213247
promptAmend: false,
214248
skipMessagePrompt: amendAll
@@ -553,10 +587,12 @@ export default class ReleasePreparation extends Session {
553587
const data = await fs.readFile(majorChangelogPath, 'utf8');
554588
const arr = data.split('\n');
555589
const allCommits = this.getChangelog();
556-
const notableChanges = await this.getBranchDiff({
557-
onlyNotableChanges: true,
558-
format: isSecurityRelease ? 'messageonly' : 'markdown',
559-
});
590+
const notableChanges = isSecurityRelease
591+
? await this.getSecurityNotableChanges()
592+
: await this.getBranchDiff({
593+
onlyNotableChanges: true,
594+
format: 'markdown',
595+
});
560596
let releaseHeader = `## ${date}, Version ${newVersion}` +
561597
` ${releaseInfo}, @${username}\n`;
562598
if (isSecurityRelease) {
@@ -692,10 +728,12 @@ export default class ReleasePreparation extends Session {
692728
messageBody.push('This is a security release.\n\n');
693729
}
694730

695-
const notableChanges = await this.getBranchDiff({
696-
onlyNotableChanges: true,
697-
format: isSecurityRelease ? 'messageonly' : 'plaintext'
698-
});
731+
const notableChanges = isSecurityRelease
732+
? await this.getSecurityNotableChanges()
733+
: await this.getBranchDiff({
734+
onlyNotableChanges: true,
735+
format: 'plaintext'
736+
});
699737
messageBody.push('Notable changes:\n\n');
700738
if (isLTSTransition) {
701739
messageBody.push(`${getStartLTSBlurb(this)}\n\n`);
@@ -718,6 +756,48 @@ export default class ReleasePreparation extends Session {
718756
return useMessage;
719757
}
720758

759+
// Build the notable changes of a security release from the commits
760+
// cherry-picked onto the proposal branch and the severity ratings in
761+
// vulnerabilities.json.
762+
async getSecurityNotableChanges() {
763+
const { upstream, versionComponents } = this;
764+
const releaseBranch = `v${versionComponents.major}.x`;
765+
766+
await forceRunAsync('git', ['remote', 'set-branches', '--add', upstream, releaseBranch], {
767+
ignoreFailures: false
768+
});
769+
await forceRunAsync('git', ['fetch', upstream, releaseBranch], { ignoreFailures: false });
770+
771+
const severityByCVE = new Map();
772+
const vulnPath = this.getVulnerabilitiesJSONPath();
773+
if (vulnPath && existsSync(vulnPath)) {
774+
const { reports } = JSON.parse(readFileSync(vulnPath, 'utf-8'));
775+
for (const report of reports ?? []) {
776+
for (const cveId of report.cveIds ?? []) {
777+
severityByCVE.set(cveId, report.severity?.rating);
778+
}
779+
}
780+
}
781+
782+
const log = runSync('git', [
783+
'log',
784+
'--format=%s%x1f%an%x1f%(trailers:key=CVE-ID,valueonly,separator=%x2C)%x1e',
785+
`${upstream}/${releaseBranch}..HEAD`
786+
]);
787+
const commits = log.split('\x1e').map(record => {
788+
const [subject, author, cveIds] = record.trim().split('\x1f');
789+
return {
790+
subject,
791+
author,
792+
// Ignore placeholder trailers such as CVE-2026-XXXXX.
793+
cveIds: (cveIds || '').split(',')
794+
.map(id => id.trim())
795+
.filter(id => /^CVE-\d{4}-\d+$/.test(id))
796+
};
797+
});
798+
return formatSecurityNotableChanges(commits, severityByCVE);
799+
}
800+
721801
async getBranchDiff(opts) {
722802
const {
723803
cli,

‎lib/security-release/security-release.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const NEXT_SECURITY_RELEASE_REPOSITORY = {
1111
repo: 'security-release'
1212
};
1313

14-
const SEVERITY_RANKS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
14+
export const SEVERITY_RANKS = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL'];
1515
const RELEASE_LINE_RE = /^v?(\d+)(?:\.x)?$/;
1616
const SEMVER_RE = /^v?(\d+)\.\d+\.\d+/;
1717

‎test/unit/prepare_release.test.js‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { readFileSync } from 'node:fs';
55
import * as utils from '../../lib/release/utils.js';
66
import {
77
parsePullRequestURL,
8-
getPullRequestURLForLine
8+
getPullRequestURLForLine,
9+
formatSecurityNotableChanges
910
} from '../../lib/prepare_release.js';
1011

1112
describe('prepare_release: utils.getEOLDate', () => {
@@ -136,3 +137,70 @@ describe('prepare_release: getPullRequestURLForLine', () => {
136137
assert.strictEqual(getPullRequestURLForLine(null, '22.x'), null);
137138
});
138139
});
140+
141+
describe('prepare_release: formatSecurityNotableChanges', () => {
142+
it('sorts by severity then CVE-ID and formats each entry', () => {
143+
const severityByCVE = new Map([
144+
['CVE-2026-48933', 'high'],
145+
['CVE-2026-48618', 'high'],
146+
['CVE-2026-48615', 'medium'],
147+
['CVE-2026-48617', 'low']
148+
]);
149+
const commits = [
150+
{
151+
subject: 'permission: handle process.chdir on writereport',
152+
author: 'RafaelGSS',
153+
cveIds: ['CVE-2026-48617']
154+
},
155+
{
156+
subject: 'lib,test: redact proxy credentials in tunnel errors',
157+
author: 'Matteo Collina',
158+
cveIds: ['CVE-2026-48615']
159+
},
160+
{
161+
subject: 'crypto: guard WebCrypto cipher output length',
162+
author: 'Filip Skokan',
163+
cveIds: ['CVE-2026-48933']
164+
},
165+
{
166+
subject: 'tls: normalize hostname for server identity checks',
167+
author: 'Matteo Collina',
168+
cveIds: ['CVE-2026-48618']
169+
}
170+
];
171+
172+
assert.strictEqual(
173+
formatSecurityNotableChanges(commits, severityByCVE),
174+
'* (CVE-2026-48618) tls: normalize hostname for server identity ' +
175+
'checks (Matteo Collina) – High\n' +
176+
'* (CVE-2026-48933) crypto: guard WebCrypto cipher output length ' +
177+
'(Filip Skokan) – High\n' +
178+
'* (CVE-2026-48615) lib,test: redact proxy credentials in tunnel ' +
179+
'errors (Matteo Collina) – Medium\n' +
180+
'* (CVE-2026-48617) permission: handle process.chdir on writereport ' +
181+
'(RafaelGSS) – Low\n'
182+
);
183+
});
184+
185+
it('lists commits without a CVE or severity last, without annotations', () => {
186+
const severityByCVE = new Map([['CVE-2026-48618', 'HIGH']]);
187+
const commits = [
188+
{ subject: 'deps: update undici to 6.21.2', author: 'Node.js GitHub Bot', cveIds: [] },
189+
{ subject: 'tls: some fix', author: 'A Contributor', cveIds: ['CVE-2026-1'] },
190+
{
191+
subject: 'tls: normalize hostname for server identity checks',
192+
author: 'Matteo Collina',
193+
cveIds: ['CVE-2026-48618']
194+
}
195+
];
196+
197+
assert.strictEqual(
198+
formatSecurityNotableChanges(commits, severityByCVE),
199+
'* (CVE-2026-48618) tls: normalize hostname for server identity ' +
200+
'checks (Matteo Collina) – High\n' +
201+
'* (CVE-2026-1) tls: some fix (A Contributor)\n' +
202+
'* deps: update undici to 6.21.2 (Node.js GitHub Bot)\n'
203+
);
204+
assert.strictEqual(formatSecurityNotableChanges([], severityByCVE), '');
205+
});
206+
});

0 commit comments

Comments
 (0)