@node-core/utils 5.16.2 → 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.
@@ -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) {
@@ -1,5 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { pipeline } from 'node:stream/promises';
3
5
  import semver from 'semver';
4
6
  import * as gst from 'git-secure-tag';
5
7
 
@@ -196,31 +198,44 @@ export default class ReleasePromotion extends Session {
196
198
 
197
199
  async verifyTagSignature(version) {
198
200
  const { cli } = this;
199
- const verifyTagPattern = /gpg:[^\n]+\ngpg:\s+using \w+ key ([^\n]+)\ngpg:\s+issuer "([^"]+)"\ngpg:\s+Good signature from (?:"[^"]+"(?: \[ultimate\])?\ngpg:\s+aka )*"([^<]+) <\2>"/;
200
- const [verifyTagOutput, haystack] = await Promise.all([forceRunAsync(
201
- 'git', ['--no-pager',
202
- 'verify-tag',
203
- `v${version}`
204
- ], { ignoreFailure: false, captureStderr: true }), fs.readFile('README.md')]);
205
- const match = verifyTagPattern.exec(verifyTagOutput);
206
- if (match == null) {
207
- cli.warn('git was not able to verify the tag:');
208
- cli.info(verifyTagOutput);
209
- } else {
210
- const [, keyID, email, name] = match;
211
- const needle = `* **${name}** <<${email}>>\n ${'`'}${keyID}${'`'}`;
212
- if (haystack.includes(needle)) {
213
- return;
201
+
202
+ cli.startSpinner('Downloading active releasers keyring from nodejs/release-keys...');
203
+ const [keyRingStream, [GNUPGHOME, keyRingFd]] = await Promise.all([
204
+ fetch('https://github.com/nodejs/release-keys/raw/HEAD/gpg-only-active-keys/pubring.kbx'),
205
+ fs.mkdtemp(path.join(tmpdir(), 'ncu-'))
206
+ .then(async d => [d, await fs.open(path.join(d, 'pubring.kbx'), 'w')]),
207
+ ]);
208
+ if (!keyRingStream.ok) throw new Error('Failed to download keyring', { cause: keyRingStream });
209
+ await pipeline(keyRingStream.body, keyRingFd.createWriteStream());
210
+ cli.stopSpinner('Active releasers keyring stored in temp directory');
211
+
212
+ try {
213
+ await forceRunAsync(
214
+ 'git', ['--no-pager',
215
+ 'verify-tag',
216
+ `v${version}`
217
+ ], {
218
+ ignoreFailure: false,
219
+ spawnArgs: { env: { ...process.env, GNUPGHOME } },
220
+ });
221
+ cli.ok('git tag signature verified');
222
+ } catch (cause) {
223
+ cli.error('git was not able to verify the tag');
224
+ cli.warn('This means that either the tag was signed with the wrong key,');
225
+ cli.warn('or that nodejs/release-keys contains outdated information.');
226
+ cli.warn('The release should not proceed.');
227
+ if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
228
+ if (await cli.prompt('Do you want to delete the local tag?')) {
229
+ await forceRunAsync('git', ['tag', '-d', `v${version}`]);
230
+ } else {
231
+ cli.info(`Run 'git tag -d v${version}' to remove the local tag.`);
232
+ }
233
+ throw new Error('Aborted', { cause });
214
234
  }
215
- cli.warn('Tag was signed with an undocumented identity/key pair!');
216
- cli.info('Expected to find the following entry in the README:');
217
- cli.info(needle);
218
- cli.info('If you are using a subkey, it might be OK.');
219
- }
220
- cli.info(`If that doesn't sound right, consider removing the tag (git tag -d v${version
221
- }), check your local config, and start the process over.`);
222
- if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
223
- throw new Error('Aborted');
235
+ } finally {
236
+ cli.startSpinner('Cleaning up temp files');
237
+ await fs.rm(GNUPGHOME, { force: true, recursive: true });
238
+ cli.stopSpinner('Temp files removed');
224
239
  }
225
240
  }
226
241
 
package/lib/session.js CHANGED
@@ -19,7 +19,7 @@ export default class Session {
19
19
  this.cli = cli;
20
20
  this.dir = dir;
21
21
  this.prid = prid;
22
- this.config = { ...getMergedConfig(this.dir), ...argv };
22
+ this.config = getMergedConfig(this.dir, undefined, argv);
23
23
  this.gpgSign = argv?.['gpg-sign']
24
24
  ? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
25
25
  : [];
@@ -126,7 +126,12 @@ export default class Session {
126
126
  writeJson(this.sessionPath, {
127
127
  state: STARTED,
128
128
  prid: this.prid,
129
- config: this.config
129
+ // Filter out getters, those are likely encrypted data we don't need on the session.
130
+ config: Object.entries(Object.getOwnPropertyDescriptors(this.config))
131
+ .reduce((pv, [key, desc]) => {
132
+ if (Object.hasOwn(desc, 'value')) pv[key] = desc.value;
133
+ return pv;
134
+ }, { __proto__: null }),
130
135
  });
131
136
  }
132
137
 
@@ -382,12 +387,19 @@ export default class Session {
382
387
  cli.setExitCode(1);
383
388
  }
384
389
  if (!upstream) {
385
- cli.warn('You have not told git-node the remote you want to sync with.');
390
+ cli.warn('You have not told git-node what remote name you are trying to land commits on.');
386
391
  cli.separator();
387
- cli.info(
388
- 'For example, if your remote pointing to nodejs/node is' +
389
- ' `remote-upstream`, you can run:\n\n' +
390
- ' $ ncu-config set upstream remote-upstream');
392
+ const remoteName = runSync('git', ['remote', '-v'])
393
+ .match(/^(\S+)\s+(?:https:\/\/github\.com\/|git@github\.com:)nodejs\/node\.git \(\S+\)\r?$/m)?.[1];
394
+ cli.info(remoteName
395
+ ? `You likely want to run the following:\n\n $ ncu-config set upstream ${remoteName}`
396
+ : 'The expected repository does not seem to appear in your local config.\n' +
397
+ '1. First, add the Node.js core repository as a remote:\n' +
398
+ ' $ git remote add upstream https://github.com/nodejs/node.git\n\n' +
399
+ '2. Then, tell git-node to use this remote for syncing:\n' +
400
+ ' $ ncu-config set upstream upstream\n\n' +
401
+ 'Note: Using "upstream" is recommended, but you can use any remote name.\n' +
402
+ 'For security reasons, you need to add the remote manually.');
391
403
  cli.separator();
392
404
  cli.setExitCode(1);
393
405
  }
@@ -0,0 +1,88 @@
1
+ import path from 'node:path';
2
+ import { promises as fs } from 'node:fs';
3
+
4
+ import { chromiumGit, v8Deps } from './constants.js';
5
+ import { forceRunAsync } from '../run.js';
6
+ import {
7
+ addToGitignore,
8
+ filterForVersion,
9
+ getNodeV8Version,
10
+ removeDirectory,
11
+ replaceGitignore,
12
+ } from './util.js';
13
+
14
+ async function fetchFromGit(cwd, repo, commit) {
15
+ await removeDirectory(cwd);
16
+ await fs.mkdir(cwd, { recursive: true });
17
+ await exec('init');
18
+ await exec('remote', 'add', 'origin', repo);
19
+ await exec('fetch', 'origin', commit);
20
+ await exec('reset', '--hard', 'FETCH_HEAD');
21
+ await removeDirectory(path.join(cwd, '.git'));
22
+
23
+ function exec(...options) {
24
+ return forceRunAsync('git', options, {
25
+ ignoreFailure: false,
26
+ spawnArgs: { cwd, stdio: 'ignore' }
27
+ });
28
+ }
29
+ }
30
+
31
+ async function readDeps(nodeDir) {
32
+ const depsStr = await fs.readFile(path.join(nodeDir, 'deps/v8/DEPS'), 'utf8');
33
+ const start = depsStr.indexOf('deps = {');
34
+ const end = depsStr.indexOf('\n}', start) + 2;
35
+ const depsDeclaration = depsStr.substring(start, end).replace(/^ *#.*/gm, '');
36
+ const Var = () => chromiumGit; // eslint-disable-line no-unused-vars
37
+ let deps;
38
+ eval(depsDeclaration); // eslint-disable-line no-eval
39
+ return deps;
40
+ }
41
+
42
+ async function lookupDep(depsTable, depName) {
43
+ const dep = depsTable[depName];
44
+ if (!dep) {
45
+ throw new Error(`V8 dep "${depName}" not found in DEPS file`);
46
+ }
47
+ if (typeof dep === 'object') {
48
+ return dep.url.split('@');
49
+ }
50
+ return dep.split('@');
51
+ }
52
+
53
+ export default function updateV8Deps() {
54
+ return {
55
+ title: 'Update V8 DEPS',
56
+ task: async(ctx, task) => {
57
+ const newV8Version = await getNodeV8Version(ctx.nodeDir);
58
+ const repoPrefix = newV8Version.majorMinor >= 86 ? '' : 'v8/';
59
+ const deps = filterForVersion(v8Deps.map((v8Dep) => ({
60
+ ...v8Dep,
61
+ repo: `${repoPrefix}${v8Dep.repo}`,
62
+ path: v8Dep.repo
63
+ })), newV8Version);
64
+ if (deps.length === 0) return;
65
+ const depsTable = await readDeps(ctx.nodeDir);
66
+ const subtasks = [];
67
+ for (const dep of deps) {
68
+ // Update .gitignore sequentially to avoid races
69
+ if (dep.gitignore) {
70
+ if (typeof dep.gitignore === 'string') {
71
+ await addToGitignore(ctx.nodeDir, dep.gitignore);
72
+ } else {
73
+ await replaceGitignore(ctx.nodeDir, dep.gitignore);
74
+ }
75
+ }
76
+ subtasks.push({
77
+ title: `Update ${dep.path}`,
78
+ task: async(ctx) => {
79
+ const [repo, commit] = await lookupDep(depsTable, dep.repo);
80
+ const thePath = path.join(ctx.nodeDir, 'deps/v8', dep.path);
81
+ await fetchFromGit(thePath, repo, commit);
82
+ }
83
+ });
84
+ }
85
+ return task.newListr(subtasks, { concurrent: ctx.concurrent });
86
+ }
87
+ };
88
+ };
@@ -5,6 +5,7 @@ import updateVersionNumbers from './updateVersionNumbers.js';
5
5
  import commitUpdate from './commitUpdate.js';
6
6
  import majorUpdate from './majorUpdate.js';
7
7
  import minorUpdate from './minorUpdate.js';
8
+ import updateDeps from './deps.js';
8
9
  import updateV8Clone from './updateV8Clone.js';
9
10
 
10
11
  export function major(options) {
@@ -34,6 +35,14 @@ export async function backport(options) {
34
35
  return tasks.run(options);
35
36
  };
36
37
 
38
+ export async function deps(options) {
39
+ const tasks = new Listr(
40
+ [updateDeps()],
41
+ getOptions(options)
42
+ );
43
+ return tasks.run(options);
44
+ };
45
+
37
46
  /**
38
47
  * Get the listr2 options.
39
48
  * @param {{ verbose?: boolean }} options The original options.
@@ -1,17 +1,12 @@
1
1
  import path from 'node:path';
2
- import { promises as fs } from 'node:fs';
3
2
 
4
3
  import { getCurrentV8Version } from './common.js';
4
+ import updateV8Deps from './deps.js';
5
5
  import {
6
- getNodeV8Version,
7
- filterForVersion,
8
- addToGitignore,
9
- replaceGitignore,
10
6
  removeDirectory,
11
7
  isVersionString
12
8
  } from './util.js';
13
9
  import applyNodeChanges from './applyNodeChanges.js';
14
- import { chromiumGit, v8Deps } from './constants.js';
15
10
  import { forceRunAsync } from '../run.js';
16
11
 
17
12
  export default function majorUpdate() {
@@ -106,65 +101,3 @@ function addDepsV8() {
106
101
  })
107
102
  };
108
103
  }
109
-
110
- function updateV8Deps() {
111
- return {
112
- title: 'Update V8 DEPS',
113
- task: async(ctx) => {
114
- const newV8Version = await getNodeV8Version(ctx.nodeDir);
115
- const repoPrefix = newV8Version.majorMinor >= 86 ? '' : 'v8/';
116
- const deps = filterForVersion(v8Deps.map((v8Dep) => ({
117
- ...v8Dep,
118
- repo: `${repoPrefix}${v8Dep.repo}`,
119
- path: v8Dep.repo
120
- })), newV8Version);
121
- if (deps.length === 0) return;
122
- for (const dep of deps) {
123
- if (dep.gitignore) {
124
- if (typeof dep.gitignore === 'string') {
125
- await addToGitignore(ctx.nodeDir, dep.gitignore);
126
- } else {
127
- await replaceGitignore(ctx.nodeDir, dep.gitignore);
128
- }
129
- }
130
- const [repo, commit] = await readDeps(ctx.nodeDir, dep.repo);
131
- const thePath = path.join(ctx.nodeDir, 'deps/v8', dep.path);
132
- await fetchFromGit(thePath, repo, commit);
133
- }
134
- }
135
- };
136
- }
137
-
138
- async function readDeps(nodeDir, depName) {
139
- const depsStr = await fs.readFile(path.join(nodeDir, 'deps/v8/DEPS'), 'utf8');
140
- const start = depsStr.indexOf('deps = {');
141
- const end = depsStr.indexOf('\n}', start) + 2;
142
- const depsDeclaration = depsStr.substring(start, end).replace(/^ *#.*/gm, '');
143
- const Var = () => chromiumGit; // eslint-disable-line no-unused-vars
144
- let deps;
145
- eval(depsDeclaration); // eslint-disable-line no-eval
146
- const dep = deps[depName];
147
- if (!dep) {
148
- throw new Error(`V8 dep "${depName}" not found in DEPS file`);
149
- }
150
- if (typeof dep === 'object') {
151
- return dep.url.split('@');
152
- }
153
- return dep.split('@');
154
- }
155
-
156
- async function fetchFromGit(cwd, repo, commit) {
157
- await fs.mkdir(cwd, { recursive: true });
158
- await exec('init');
159
- await exec('remote', 'add', 'origin', repo);
160
- await exec('fetch', 'origin', commit);
161
- await exec('reset', '--hard', 'FETCH_HEAD');
162
- await removeDirectory(path.join(cwd, '.git'));
163
-
164
- function exec(...options) {
165
- return forceRunAsync('git', options, {
166
- ignoreFailure: false,
167
- spawnArgs: { cwd, stdio: 'ignore' }
168
- });
169
- }
170
- }
@@ -37,13 +37,18 @@ export function filterForVersion(list, version) {
37
37
 
38
38
  export async function addToGitignore(nodeDir, value) {
39
39
  const gitignorePath = path.join(nodeDir, 'deps/v8/.gitignore');
40
- await fs.appendFile(gitignorePath, `${value}\n`);
40
+ const gitignore = await fs.readFile(gitignorePath, 'utf8');
41
+ if (!gitignore.includes(value)) {
42
+ await fs.appendFile(gitignorePath, `${value}\n`);
43
+ }
41
44
  }
42
45
 
43
46
  export async function replaceGitignore(nodeDir, options) {
44
47
  const gitignorePath = path.join(nodeDir, 'deps/v8/.gitignore');
45
48
  let gitignore = await fs.readFile(gitignorePath, 'utf8');
46
- gitignore = gitignore.replace(options.match, options.replace);
49
+ if (!gitignore.includes(options.replace)) {
50
+ gitignore = gitignore.replace(options.match, options.replace);
51
+ }
47
52
  await fs.writeFile(gitignorePath, gitignore);
48
53
  }
49
54
 
@@ -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({
package/lib/verbosity.js CHANGED
@@ -18,6 +18,9 @@ export function setVerbosityFromEnv() {
18
18
  if (Object.keys(VERBOSITY).includes(env)) {
19
19
  verbosity = VERBOSITY[env];
20
20
  }
21
+ if (!isDebugVerbosity()) {
22
+ Error.stackTraceLimit = 0;
23
+ }
21
24
  };
22
25
 
23
26
  export function debuglog(...args) {
@@ -10,7 +10,6 @@ import {
10
10
  getEditor, isGhAvailable
11
11
  } from './utils.js';
12
12
 
13
- // eslint-disable-next-line import/no-unresolved
14
13
  import voteUsingGit from '@node-core/caritat/voteUsingGit';
15
14
  import * as yaml from 'js-yaml';
16
15