@node-core/utils 5.14.0 → 5.15.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 CHANGED
@@ -89,6 +89,14 @@ After the token is generated, create an rc file with the following content:
89
89
  Note: you could use `ncu-config` to configure these variables, but it's not
90
90
  recommended to leave your tokens in your command line history.
91
91
 
92
+ If you have `gpg` installed and setup on your local machine, it is recommended
93
+ to store an encrypted version of this file:
94
+
95
+ ```console
96
+ $ gpg --default-recipient-self --encrypt ~/.ncurc
97
+ $ rm ~/.ncurc
98
+ ```
99
+
92
100
  ### Setting up Jenkins credentials
93
101
 
94
102
  The `git-node` and `ncu-ci` commands need to query the Node.js Jenkins API for
@@ -104,8 +112,9 @@ To obtain the Jenkins API token
104
112
  3. Enter an identifiable name (for example, `node-core-utils`) for this
105
113
  token in the inbox that appears, and click `GENERATE`.
106
114
  4. Copy the generated token.
107
- 5. Add it into your `ncurc` file (`~/.ncurc` or `$XDG_CONFIG_HOME/ncurc`)
108
- with `jenkins_token` as key, like this:
115
+ 5. Add it into your `ncurc` file (`~/.ncurc` or `$XDG_CONFIG_HOME/ncurc`, or
116
+ `~/.ncurc.gpg` or `$XDG_CONFIG_HOME/ncurc.gpg`) with `jenkins_token` as key,
117
+ like this:
109
118
 
110
119
  ```json
111
120
  {
@@ -125,6 +134,7 @@ Put the following entries into your
125
134
  ```
126
135
  # node-core-utils configuration file
127
136
  .ncurc
137
+ .ncurc.gpg
128
138
  # node-core-utils working directory
129
139
  .ncu
130
140
  ```
package/bin/ncu-ci.js CHANGED
@@ -111,8 +111,8 @@ const args = yargs(hideBin(process.argv))
111
111
  builder: (yargs) => {
112
112
  yargs
113
113
  .positional('prid', {
114
- describe: 'ID of the PR',
115
- type: 'number'
114
+ describe: 'ID of the PR or URL to the PR or its head commit',
115
+ type: 'string'
116
116
  })
117
117
  .option('certify-safe', {
118
118
  describe: 'SHA of the commit that is expected to be at the tip of the PR head. ' +
@@ -574,6 +574,15 @@ async function main(command, argv) {
574
574
  // Prepare queue.
575
575
  switch (command) {
576
576
  case 'run': {
577
+ const maybeURL = URL.parse(argv.prid);
578
+ if (maybeURL?.host === 'github.com') {
579
+ const [, owner, repo, , prid, , commit_sha] = maybeURL.pathname.split('/');
580
+ argv.owner ||= owner;
581
+ argv.repo ||= repo;
582
+ argv.certifySafe ||= commit_sha;
583
+ argv.prid = prid;
584
+ }
585
+ argv.prid = Number(argv.prid);
577
586
  const jobRunner = new RunPRJobCommand(cli, request, argv);
578
587
  return jobRunner.start();
579
588
  }
@@ -11,12 +11,16 @@ export default class CherryPick {
11
11
  constructor(prid, dir, cli, {
12
12
  owner,
13
13
  repo,
14
+ upstream,
15
+ gpgSign,
14
16
  lint,
15
17
  includeCVE
16
18
  } = {}) {
17
19
  this.prid = prid;
18
20
  this.cli = cli;
19
21
  this.dir = dir;
22
+ this.upstream = upstream;
23
+ this.gpgSign = gpgSign;
20
24
  this.options = { owner, repo, lint, includeCVE };
21
25
  }
22
26
 
@@ -88,14 +92,15 @@ export default class CherryPick {
88
92
  } else if (cleanLint === LINT_RESULTS.SUCCESS) {
89
93
  cli.ok('Lint passed cleanly');
90
94
  }
91
- return this.amend(metadata.metadata, commitInfo);
95
+ this.metadata = metadata.metadata;
96
+ return this.amend(commitInfo);
92
97
  } catch (e) {
93
98
  cli.error(e.message);
94
99
  return false;
95
100
  }
96
101
  }
97
102
 
98
- async amend(metadata, commitInfo) {
103
+ async amend(commitInfo) {
99
104
  const { cli } = this;
100
105
  const subjects = await runAsync('git',
101
106
  ['log', '--pretty=format:%s', `${commitInfo.base}..${commitInfo.head}`],
@@ -116,7 +121,7 @@ export default class CherryPick {
116
121
  await runAsync('git', ['commit', '--amend', '--no-edit']);
117
122
  }
118
123
 
119
- return LandingSession.prototype.amend.call(this, metadata);
124
+ return LandingSession.prototype.amend.call(this);
120
125
  }
121
126
 
122
127
  readyToAmend() {
package/lib/config.js CHANGED
@@ -2,6 +2,8 @@ import path from 'node:path';
2
2
  import os from 'node:os';
3
3
 
4
4
  import { readJson, writeJson } from './file.js';
5
+ import { existsSync } from 'node:fs';
6
+ import { spawnSync } from 'node:child_process';
5
7
 
6
8
  export const GLOBAL_CONFIG = Symbol('globalConfig');
7
9
  export const PROJECT_CONFIG = Symbol('projectConfig');
@@ -25,6 +27,14 @@ export function getMergedConfig(dir, home) {
25
27
 
26
28
  export function getConfig(configType, dir) {
27
29
  const configPath = getConfigPath(configType, dir);
30
+ const encryptedConfigPath = configPath + '.gpg';
31
+ if (existsSync(encryptedConfigPath)) {
32
+ const { status, stdout } =
33
+ spawnSync('gpg', ['--decrypt', encryptedConfigPath]);
34
+ if (status === 0) {
35
+ return JSON.parse(stdout.toString('utf-8'));
36
+ }
37
+ }
28
38
  try {
29
39
  return readJson(configPath);
30
40
  } catch (cause) {
@@ -7,6 +7,7 @@ import Session from './session.js';
7
7
  import {
8
8
  shortSha, isGhAvailable, getEditor
9
9
  } from './utils.js';
10
+ import { debuglog, isDebugVerbosity } from './verbosity.js';
10
11
 
11
12
  const isWindows = process.platform === 'win32';
12
13
 
@@ -27,9 +28,6 @@ export default class LandingSession extends Session {
27
28
  this.lint = lint;
28
29
  this.autorebase = autorebase;
29
30
  this.fixupAll = fixupAll;
30
- this.gpgSign = argv?.['gpg-sign']
31
- ? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
32
- : [];
33
31
  this.oneCommitMax = oneCommitMax;
34
32
  this.expectedCommitShas = [];
35
33
  this.checkCI = !!checkCI;
@@ -120,6 +118,9 @@ export default class LandingSession extends Session {
120
118
  ['cherry-pick', '--allow-empty', ...this.gpgSign, `${base}..${head}`],
121
119
  { ignoreFailure: false });
122
120
  } catch (ex) {
121
+ if (isDebugVerbosity()) {
122
+ debuglog('[LandingSession] Got error', ex);
123
+ }
123
124
  cli.error('Failed to apply patches');
124
125
  process.exit(1);
125
126
  }
@@ -70,6 +70,8 @@ export default class ReleasePreparation extends Session {
70
70
  const cp = new CherryPick(pr.number, this.dir, cli, {
71
71
  owner: this.owner,
72
72
  repo: this.repo,
73
+ gpgSign: this.gpgSign,
74
+ upstream: this.isSecurityRelease ? `https://${this.username}:${this.config.token}@github.com/${this.owner}/${this.repo}.git` : this.upstream,
73
75
  lint: false,
74
76
  includeCVE: true
75
77
  });
@@ -17,13 +17,10 @@ const dryRunMessage = 'You are running in dry-run mode, meaning NCU will not run
17
17
 
18
18
  export default class ReleasePromotion extends Session {
19
19
  constructor(argv, req, cli, dir) {
20
- super(cli, dir);
20
+ super(cli, dir, null, argv);
21
21
  this.req = req;
22
22
  this.dryRun = !argv.run;
23
23
  this.proposalUpstreamRemote = argv.fetchFrom ?? this.upstream;
24
- this.gpgSign = argv?.['gpg-sign']
25
- ? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
26
- : [];
27
24
  }
28
25
 
29
26
  get branch() {
package/lib/request.js CHANGED
@@ -43,7 +43,11 @@ export default class Request {
43
43
  }
44
44
 
45
45
  async text(url, options = {}) {
46
- return this.fetch(url, options).then(res => res.text());
46
+ const res = await this.fetch(url, options);
47
+ if (isDebugVerbosity()) {
48
+ debuglog('[Request] Got response from', url, ':\n', res.status, ' ', res.statusText);
49
+ }
50
+ return res.text();
47
51
  }
48
52
 
49
53
  async json(url, options = {}) {
@@ -1,9 +1,12 @@
1
+ import fs from 'node:fs';
1
2
  import {
2
3
  NEXT_SECURITY_RELEASE_REPOSITORY,
3
4
  checkoutOnSecurityReleaseBranch,
4
5
  getVulnerabilitiesJSON,
6
+ getVulnerabilitiesJSONPath,
5
7
  validateDate,
6
8
  formatDateToYYYYMMDD,
9
+ commitAndPushVulnerabilitiesJSON,
7
10
  createIssue
8
11
  } from './security-release/security-release.js';
9
12
  import auth from './auth.js';
@@ -40,10 +43,21 @@ export default class SecurityAnnouncement {
40
43
  validateDate(content.releaseDate);
41
44
  const releaseDate = new Date(content.releaseDate);
42
45
 
43
- await Promise.all([
46
+ const [dockerIssue, buildIssue] = await Promise.all([
44
47
  this.createDockerNodeIssue(releaseDate),
45
48
  this.createBuildWGIssue(releaseDate)
46
49
  ]);
50
+
51
+ content.buildIssue = buildIssue;
52
+ content.dockerIssue = dockerIssue;
53
+
54
+ const vulnerabilitiesJSONPath = getVulnerabilitiesJSONPath();
55
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
56
+ const commitMessage = 'chore: add build and docker issue link';
57
+ commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
58
+ commitMessage, { cli: this.cli, repository: this.repository });
59
+
60
+ this.cli.ok('Added docker and build issue in vulnerabilities.json');
47
61
  }
48
62
 
49
63
  async createBuildWGIssue(releaseDate) {
@@ -53,7 +67,7 @@ export default class SecurityAnnouncement {
53
67
  };
54
68
 
55
69
  const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'build');
56
- await createIssue(title, content, repository, { cli: this.cli, req: this.req });
70
+ return createIssue(title, content, repository, { cli: this.cli, req: this.req });
57
71
  }
58
72
 
59
73
  createPreleaseAnnouncementIssue(releaseDate, team) {
@@ -71,6 +85,6 @@ export default class SecurityAnnouncement {
71
85
  };
72
86
 
73
87
  const { title, content } = this.createPreleaseAnnouncementIssue(releaseDate, 'docker');
74
- await createIssue(title, content, repository, { cli: this.cli, req: this.req });
88
+ return createIssue(title, content, repository, { cli: this.cli, req: this.req });
75
89
  }
76
90
  }
@@ -107,6 +107,12 @@ export function getVulnerabilitiesJSON(cli) {
107
107
  return file;
108
108
  }
109
109
 
110
+ export function getVulnerabilitiesJSONPath() {
111
+ const vulnerabilitiesJSONPath = path.join(process.cwd(),
112
+ NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
113
+ return vulnerabilitiesJSONPath;
114
+ }
115
+
110
116
  export function validateDate(releaseDate) {
111
117
  const value = new Date(releaseDate).valueOf();
112
118
  if (Number.isNaN(value) || value < 0) {
@@ -135,6 +141,7 @@ export async function createIssue(title, content, repository, { cli, req }) {
135
141
  const data = await req.createIssue(title, content, repository);
136
142
  if (data.html_url) {
137
143
  cli.ok(`Created: ${data.html_url}`);
144
+ return data.html_url;
138
145
  } else {
139
146
  cli.error(data);
140
147
  process.exit(1);
@@ -6,7 +6,8 @@ import {
6
6
  PLACEHOLDERS,
7
7
  checkoutOnSecurityReleaseBranch,
8
8
  validateDate,
9
- SecurityRelease
9
+ SecurityRelease,
10
+ commitAndPushVulnerabilitiesJSON,
10
11
  } from './security-release/security-release.js';
11
12
  import auth from './auth.js';
12
13
  import Request from './request.js';
@@ -56,16 +57,41 @@ export default class SecurityBlog extends SecurityRelease {
56
57
  const endDate = new Date(data.annoucementDate);
57
58
  endDate.setDate(endDate.getDate() + 7);
58
59
 
60
+ const link = `https://nodejs.org/en/blog/vulnerability/${fileName}`;
59
61
  this.updateWebsiteBanner(site, {
60
62
  startDate: data.annoucementDate,
61
63
  endDate: endDate.toISOString(),
62
64
  text: `New security releases to be made available ${data.releaseDate}`,
63
- link: `https://nodejs.org/en/blog/vulnerability/${fileName}`,
65
+ link,
64
66
  type: 'warning'
65
67
  });
66
-
67
68
  fs.writeFileSync(file, preRelease);
69
+
68
70
  cli.ok(`Announcement file created and banner has been updated. Folder: ${nodejsOrgFolder}`);
71
+ await this.updateAnnouncementLink(link);
72
+ }
73
+
74
+ async updateAnnouncementLink(link) {
75
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
76
+ const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
77
+ let shouldCommit = false;
78
+ for (let i = 0; i < content.reports.length; ++i) {
79
+ if (content.reports[i].announcement !== link) {
80
+ content.reports[i].announcement = link;
81
+ shouldCommit = true;
82
+ }
83
+ };
84
+
85
+ if (shouldCommit) {
86
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
87
+ const commitMessage = 'chore: add announcement link';
88
+ commitAndPushVulnerabilitiesJSON([vulnerabilitiesJSONPath],
89
+ commitMessage, { cli: this.cli, repository: this.repository });
90
+
91
+ this.cli.ok('Updated the announcement link in vulnerabilities.json');
92
+ }
93
+
94
+ return [vulnerabilitiesJSONPath];
69
95
  }
70
96
 
71
97
  async createPostRelease(nodejsOrgFolder) {
package/lib/session.js CHANGED
@@ -20,6 +20,9 @@ export default class Session {
20
20
  this.dir = dir;
21
21
  this.prid = prid;
22
22
  this.config = { ...getMergedConfig(this.dir), ...argv };
23
+ this.gpgSign = argv?.['gpg-sign']
24
+ ? (argv['gpg-sign'] === true ? ['-S'] : ['-S', argv['gpg-sign']])
25
+ : [];
23
26
 
24
27
  if (warnForMissing) {
25
28
  const { upstream, owner, repo } = this;
@@ -152,6 +152,7 @@ export default class UpdateSecurityRelease extends SecurityRelease {
152
152
  for (const cve of cves) {
153
153
  const report = reports.find(report => report.id === cve.reportId);
154
154
  report.cveIds = [cve.cve_identifier];
155
+ report.patchedVersions = cve.patchedVersions;
155
156
  }
156
157
  }
157
158
 
@@ -219,12 +220,14 @@ Summary: ${summary}\n`,
219
220
 
220
221
  if (!create) continue;
221
222
 
223
+ const { h1AffectedVersions, patchedVersions } =
224
+ await this.calculateVersions(affectedVersions, supportedVersions);
222
225
  const body = {
223
226
  data: {
224
227
  type: 'cve-request',
225
228
  attributes: {
226
229
  team_handle: 'nodejs-team',
227
- versions: await this.formatAffected(affectedVersions, supportedVersions),
230
+ versions: h1AffectedVersions,
228
231
  metrics: [
229
232
  {
230
233
  vectorString: cvss_vector_string
@@ -246,7 +249,7 @@ Summary: ${summary}\n`,
246
249
  continue;
247
250
  }
248
251
  const { cve_identifier } = data.attributes;
249
- cves.push({ cve_identifier, reportId: id });
252
+ cves.push({ cve_identifier, reportId: id, patchedVersions });
250
253
  }
251
254
  return cves;
252
255
  }
@@ -262,15 +265,23 @@ Summary: ${summary}\n`,
262
265
  }
263
266
  }
264
267
 
265
- async formatAffected(affectedVersions, supportedVersions) {
266
- const result = [];
268
+ async calculateVersions(affectedVersions, supportedVersions) {
269
+ const h1AffectedVersions = [];
270
+ const patchedVersions = [];
267
271
  for (const affectedVersion of affectedVersions) {
268
272
  const major = affectedVersion.split('.')[0];
269
273
  const latest = supportedVersions.find((v) => v.major === Number(major)).version;
270
274
  const version = await this.cli.prompt(
271
275
  `What is the affected version (<=) for release line ${affectedVersion}?`,
272
276
  { questionType: 'input', defaultAnswer: latest });
273
- result.push({
277
+
278
+ const nextPatchVersion = parseInt(version.split('.')[2]) + 1;
279
+ const patchedVersion = await this.cli.prompt(
280
+ `What is the patched version (>=) for release line ${affectedVersion}?`,
281
+ { questionType: 'input', defaultAnswer: nextPatchVersion });
282
+
283
+ patchedVersions.push(patchedVersion);
284
+ h1AffectedVersions.push({
274
285
  vendor: 'nodejs',
275
286
  product: 'node',
276
287
  func: '<=',
@@ -279,6 +290,6 @@ Summary: ${summary}\n`,
279
290
  affected: true
280
291
  });
281
292
  }
282
- return result;
293
+ return { h1AffectedVersions, patchedVersions };
283
294
  }
284
295
  }
@@ -111,7 +111,7 @@ export default class VotingSession extends Session {
111
111
  out.toString('base64') +
112
112
  '\n-----END SHAMIR KEY PART-----';
113
113
  this.cli.log('Your key part is:');
114
- this.cli.log(keyPart);
114
+ console.log(keyPart); // Using `console.log` so this gets output to stdout, not stderr.
115
115
  const body = 'I would like to close this vote, and for this effect, I\'m revealing my ' +
116
116
  `key part:\n\n${'```'}\n${keyPart}\n${'```'}\n`;
117
117
  if (this.postComment) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "5.14.0",
3
+ "version": "5.15.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {
@@ -68,6 +68,6 @@
68
68
  "eslint-plugin-promise": "^7.2.1",
69
69
  "globals": "^16.0.0",
70
70
  "neostandard": "^0.12.1",
71
- "sinon": "^20.0.0"
71
+ "sinon": "^21.0.0"
72
72
  }
73
73
  }