Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions components/git/release.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,14 @@ const releaseOptions = {
type: 'boolean'
},
security: {
describe: 'Demarcate the new security release as a security release',
type: 'boolean'
describe: 'Demarcate the new security release as a security release. ' +
'Optionally provide path to security-release repository for CVE auto-population',
type: 'string',
coerce: (arg) => {
// If --security=path is used, return the path
if (arg === '' || arg === true) return true;
return arg;
}
},
skipBranchDiff: {
describe: 'Skips the initial branch-diff check when preparing releases',
Expand Down
14 changes: 12 additions & 2 deletions lib/cherry_pick.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,30 @@ export default class CherryPick {
upstream,
gpgSign,
lint,
includeCVE
includeCVE,
cveIds,
vulnCveMap
} = {}) {
this.prid = prid;
this.cli = cli;
this.dir = dir;
this.upstream = upstream;
this.gpgSign = gpgSign;
this.options = { owner, repo, lint, includeCVE };
this.options = { owner, repo, lint, includeCVE, cveIds, vulnCveMap };
}

get includeCVE() {
return this.options.includeCVE ?? false;
}

get cveIds() {
return this.options.cveIds ?? null;
}

get vulnCveMap() {
return this.options.vulnCveMap ?? null;
}

get owner() {
return this.options.owner || 'nodejs';
}
Expand Down
39 changes: 33 additions & 6 deletions lib/landing_session.js
Original file line number Diff line number Diff line change
Expand Up @@ -345,12 +345,39 @@ export default class LandingSession extends Session {
}

if (!containCVETrailer && this.includeCVE) {
const cveID = await cli.prompt(
'Git found no CVE-ID trailer in the original commit message. ' +
'Please, provide the CVE-ID',
{ questionType: 'input', defaultAnswer: 'CVE-2023-XXXXX' }
);
amended.push('CVE-ID: ' + cveID);
let cveID;
if (this.cveIds && this.cveIds.length > 0) {
cveID = this.cveIds.join(', ');
cli.ok(`Using CVE-ID from vulnerabilities.json: ${cveID}`);
} else {
// Fallback: check if the original commit has a PR-URL trailer
// and use it to look up CVE-IDs from the vulnerabilities map
if (this.vulnCveMap) {
const prUrlMatch = original.match(PR_RE);
if (prUrlMatch) {
const prUrl = prUrlMatch[1];
const cveIds = this.vulnCveMap.get(prUrl);
if (cveIds && cveIds.length > 0) {
cveID = cveIds.join(', ');
cli.ok(`Using CVE-ID from backport PR-URL (${prUrl}): ${cveID}`);
}
}
}

// Fall back to prompt if still not found
if (!cveID) {
cveID = await cli.prompt(
'Git found no CVE-ID trailer in the original commit message. ' +
'Please, provide the CVE-ID or leave it empty',
{ questionType: 'input', defaultAnswer: 'CVE-2026-XXXXX' }
);
}
}
// Some commits might not address a vulnerability, but it is necessary
// for the security release to happen.
if (cveID !== '') {
amended.push('CVE-ID: ' + cveID);
}
}

const message = amended.join('\n');
Expand Down
55 changes: 52 additions & 3 deletions lib/prepare_release.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from 'node:path';
import { promises as fs } from 'node:fs';
import { promises as fs, existsSync, readFileSync } from 'node:fs';

import semver from 'semver';
import { replaceInFile } from 'replace-in-file';
Expand All @@ -21,7 +21,11 @@ const isWindows = process.platform === 'win32';
export default class ReleasePreparation extends Session {
constructor(argv, cli, dir) {
super(cli, dir);
this.isSecurityRelease = argv.security;
// argv.security can be either:
// - true (boolean) if --security was used without parameter
// - string if --security=path was used
this.isSecurityRelease = !!argv.security;
this.securityReleaseRepo = typeof argv.security === 'string' ? argv.security : null;
this.isLTS = false;
this.isLTSTransition = argv.startLTS;
this.runBranchDiff = !argv.skipBranchDiff;
Expand Down Expand Up @@ -63,17 +67,62 @@ export default class ReleasePreparation extends Session {
return false;
}

const vulnCveMap = new Map();
if (this.isSecurityRelease && this.securityReleaseRepo) {
const vulnPath = path.join(
this.securityReleaseRepo,
'security-release',
'next-security-release',
'vulnerabilities.json'
);

if (!existsSync(vulnPath)) {
cli.error(`vulnerabilities.json not found at ${vulnPath}. ` +
'Skipping CVE auto-population.');
cli.warn('PRs will require manual CVE-ID entry.');
} else {
try {
cli.startSpinner(`Reading vulnerabilities.json from ${vulnPath}..`);
const vulnData = JSON.parse(readFileSync(vulnPath, 'utf-8'));
cli.stopSpinner(`Done reading vulnerabilities.json from ${vulnPath}`);

if (vulnData.reports && Array.isArray(vulnData.reports)) {
vulnData.reports.forEach(report => {
if (report.prURL && report.cveIds && report.cveIds.length > 0) {
vulnCveMap.set(report.prURL, report.cveIds);
}
});
}
cli.ok(`Loaded ${vulnCveMap.size} CVE mappings from vulnerabilities.json`);
} catch (err) {
cli.error(`Failed to read vulnerabilities.json: ${err.message}`);
cli.warn('Continuing without CVE auto-population.');
}
}
}

for (const pr of prs) {
if (pr.mergeable !== 'MERGEABLE') {
this.warnForNonMergeablePR(pr);
}

// Look up CVE-IDs from vulnerabilities.json
const prUrl = `https://github.com/${this.owner}/${this.repo}/pull/${pr.number}`;
const cveIds = vulnCveMap.get(prUrl);

if (!cveIds || cveIds.length === 0) {
cli.warn(`No CVE-IDs found in vulnerabilities.json for ${prUrl}`);
}

const cp = new CherryPick(pr.number, this.dir, cli, {
owner: this.owner,
repo: this.repo,
gpgSign: this.gpgSign,
upstream: this.isSecurityRelease ? `https://${this.username}:${this.config.token}@github.com/${this.owner}/${this.repo}.git` : this.upstream,
lint: false,
includeCVE: true
includeCVE: true,
cveIds: cveIds || null,
vulnCveMap
});
const success = await cp.start();
if (!success) {
Expand Down
Loading