@node-core/utils 6.0.0 → 6.1.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.
@@ -50,8 +50,14 @@ const releaseOptions = {
50
50
  type: 'boolean'
51
51
  },
52
52
  security: {
53
- describe: 'Demarcate the new security release as a security release',
54
- type: 'boolean'
53
+ describe: 'Demarcate the new security release as a security release. ' +
54
+ 'Optionally provide path to security-release repository for CVE auto-population',
55
+ type: 'string',
56
+ coerce: (arg) => {
57
+ // If --security=path is used, return the path
58
+ if (arg === '' || arg === true) return true;
59
+ return arg;
60
+ }
55
61
  },
56
62
  skipBranchDiff: {
57
63
  describe: 'Skips the initial branch-diff check when preparing releases',
@@ -14,20 +14,30 @@ export default class CherryPick {
14
14
  upstream,
15
15
  gpgSign,
16
16
  lint,
17
- includeCVE
17
+ includeCVE,
18
+ cveIds,
19
+ vulnCveMap
18
20
  } = {}) {
19
21
  this.prid = prid;
20
22
  this.cli = cli;
21
23
  this.dir = dir;
22
24
  this.upstream = upstream;
23
25
  this.gpgSign = gpgSign;
24
- this.options = { owner, repo, lint, includeCVE };
26
+ this.options = { owner, repo, lint, includeCVE, cveIds, vulnCveMap };
25
27
  }
26
28
 
27
29
  get includeCVE() {
28
30
  return this.options.includeCVE ?? false;
29
31
  }
30
32
 
33
+ get cveIds() {
34
+ return this.options.cveIds ?? null;
35
+ }
36
+
37
+ get vulnCveMap() {
38
+ return this.options.vulnCveMap ?? null;
39
+ }
40
+
31
41
  get owner() {
32
42
  return this.options.owner || 'nodejs';
33
43
  }
@@ -345,12 +345,39 @@ export default class LandingSession extends Session {
345
345
  }
346
346
 
347
347
  if (!containCVETrailer && this.includeCVE) {
348
- const cveID = await cli.prompt(
349
- 'Git found no CVE-ID trailer in the original commit message. ' +
350
- 'Please, provide the CVE-ID',
351
- { questionType: 'input', defaultAnswer: 'CVE-2023-XXXXX' }
352
- );
353
- amended.push('CVE-ID: ' + cveID);
348
+ let cveID;
349
+ if (this.cveIds && this.cveIds.length > 0) {
350
+ cveID = this.cveIds.join(', ');
351
+ cli.ok(`Using CVE-ID from vulnerabilities.json: ${cveID}`);
352
+ } else {
353
+ // Fallback: check if the original commit has a PR-URL trailer
354
+ // and use it to look up CVE-IDs from the vulnerabilities map
355
+ if (this.vulnCveMap) {
356
+ const prUrlMatch = original.match(PR_RE);
357
+ if (prUrlMatch) {
358
+ const prUrl = prUrlMatch[1];
359
+ const cveIds = this.vulnCveMap.get(prUrl);
360
+ if (cveIds && cveIds.length > 0) {
361
+ cveID = cveIds.join(', ');
362
+ cli.ok(`Using CVE-ID from backport PR-URL (${prUrl}): ${cveID}`);
363
+ }
364
+ }
365
+ }
366
+
367
+ // Fall back to prompt if still not found
368
+ if (!cveID) {
369
+ cveID = await cli.prompt(
370
+ 'Git found no CVE-ID trailer in the original commit message. ' +
371
+ 'Please, provide the CVE-ID or leave it empty',
372
+ { questionType: 'input', defaultAnswer: 'CVE-2026-XXXXX' }
373
+ );
374
+ }
375
+ }
376
+ // Some commits might not address a vulnerability, but it is necessary
377
+ // for the security release to happen.
378
+ if (cveID !== '') {
379
+ amended.push('CVE-ID: ' + cveID);
380
+ }
354
381
  }
355
382
 
356
383
  const message = amended.join('\n');
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { promises as fs } from 'node:fs';
2
+ import { promises as fs, existsSync, readFileSync } from 'node:fs';
3
3
 
4
4
  import semver from 'semver';
5
5
  import { replaceInFile } from 'replace-in-file';
@@ -21,7 +21,11 @@ const isWindows = process.platform === 'win32';
21
21
  export default class ReleasePreparation extends Session {
22
22
  constructor(argv, cli, dir) {
23
23
  super(cli, dir);
24
- this.isSecurityRelease = argv.security;
24
+ // argv.security can be either:
25
+ // - true (boolean) if --security was used without parameter
26
+ // - string if --security=path was used
27
+ this.isSecurityRelease = !!argv.security;
28
+ this.securityReleaseRepo = typeof argv.security === 'string' ? argv.security : null;
25
29
  this.isLTS = false;
26
30
  this.isLTSTransition = argv.startLTS;
27
31
  this.runBranchDiff = !argv.skipBranchDiff;
@@ -63,17 +67,62 @@ export default class ReleasePreparation extends Session {
63
67
  return false;
64
68
  }
65
69
 
70
+ const vulnCveMap = new Map();
71
+ if (this.isSecurityRelease && this.securityReleaseRepo) {
72
+ const vulnPath = path.join(
73
+ this.securityReleaseRepo,
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
+ }
102
+ }
103
+
66
104
  for (const pr of prs) {
67
105
  if (pr.mergeable !== 'MERGEABLE') {
68
106
  this.warnForNonMergeablePR(pr);
69
107
  }
108
+
109
+ // Look up CVE-IDs from vulnerabilities.json
110
+ const prUrl = `https://github.com/${this.owner}/${this.repo}/pull/${pr.number}`;
111
+ const cveIds = vulnCveMap.get(prUrl);
112
+
113
+ if (!cveIds || cveIds.length === 0) {
114
+ cli.warn(`No CVE-IDs found in vulnerabilities.json for ${prUrl}`);
115
+ }
116
+
70
117
  const cp = new CherryPick(pr.number, this.dir, cli, {
71
118
  owner: this.owner,
72
119
  repo: this.repo,
73
120
  gpgSign: this.gpgSign,
74
121
  upstream: this.isSecurityRelease ? `https://${this.username}:${this.config.token}@github.com/${this.owner}/${this.repo}.git` : this.upstream,
75
122
  lint: false,
76
- includeCVE: true
123
+ includeCVE: true,
124
+ cveIds: cveIds || null,
125
+ vulnCveMap
77
126
  });
78
127
  const success = await cp.start();
79
128
  if (!success) {
@@ -12,6 +12,7 @@ import fs from 'node:fs';
12
12
  import auth from './auth.js';
13
13
  import Request from './request.js';
14
14
  import nv from '@pkgjs/nv';
15
+ import semver from 'semver';
15
16
 
16
17
  export default class UpdateSecurityRelease extends SecurityRelease {
17
18
  async sync() {
@@ -268,17 +269,26 @@ Summary: ${summary}\n`,
268
269
  async calculateVersions(affectedVersions, supportedVersions) {
269
270
  const h1AffectedVersions = [];
270
271
  const patchedVersions = [];
272
+ let isPatchRelease = true;
271
273
  for (const affectedVersion of affectedVersions) {
272
- const major = affectedVersion.split('.')[0];
273
- const latest = supportedVersions.find((v) => v.major === Number(major)).version;
274
+ const affectedMajor = affectedVersion.split('.')[0];
275
+ const latest = supportedVersions.find((v) => v.major === Number(affectedMajor)).version;
274
276
  const version = await this.cli.prompt(
275
277
  `What is the affected version (<=) for release line ${affectedVersion}?`,
276
278
  { questionType: 'input', defaultAnswer: latest });
277
279
 
278
- const nextPatchVersion = parseInt(version.split('.')[2]) + 1;
280
+ const nextPatchVersion = semver.inc(version, 'patch');
281
+ const nextMinorVersion = semver.inc(version, 'minor');
279
282
  const patchedVersion = await this.cli.prompt(
280
283
  `What is the patched version (>=) for release line ${affectedVersion}?`,
281
- { questionType: 'input', defaultAnswer: nextPatchVersion });
284
+ {
285
+ questionType: 'input',
286
+ defaultAnswer: isPatchRelease ? nextPatchVersion : nextMinorVersion
287
+ });
288
+
289
+ if (patchedVersion !== nextPatchVersion) {
290
+ isPatchRelease = false; // is a minor release
291
+ }
282
292
 
283
293
  patchedVersions.push(patchedVersion);
284
294
  h1AffectedVersions.push({
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@node-core/utils",
9
- "version": "6.0.0",
9
+ "version": "6.1.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@inquirer/prompts": "^7.4.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {