@node-core/utils 5.0.2 → 5.2.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.
@@ -12,6 +12,10 @@ const securityOptions = {
12
12
  describe: 'Start security release process',
13
13
  type: 'boolean'
14
14
  },
15
+ sync: {
16
+ describe: 'Synchronize an ongoing security release with HackerOne',
17
+ type: 'boolean'
18
+ },
15
19
  'update-date': {
16
20
  describe: 'Updates the target date of the security release',
17
21
  type: 'string'
@@ -46,6 +50,10 @@ export function builder(yargs) {
46
50
  .example(
47
51
  'git node security --start',
48
52
  'Prepare a security release of Node.js')
53
+ .example(
54
+ 'git node security --sync',
55
+ 'Synchronize an ongoing security release with HackerOne'
56
+ )
49
57
  .example(
50
58
  'git node security --update-date=YYYY/MM/DD',
51
59
  'Updates the target date of the security release'
@@ -76,6 +84,9 @@ export function handler(argv) {
76
84
  if (argv.start) {
77
85
  return startSecurityRelease(argv);
78
86
  }
87
+ if (argv.sync) {
88
+ return syncSecurityRelease(argv);
89
+ }
79
90
  if (argv['update-date']) {
80
91
  return updateReleaseDate(argv);
81
92
  }
@@ -142,6 +153,13 @@ async function startSecurityRelease(argv) {
142
153
  return release.start();
143
154
  }
144
155
 
156
+ async function syncSecurityRelease(argv) {
157
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
158
+ const cli = new CLI(logStream);
159
+ const release = new UpdateSecurityRelease(cli);
160
+ return release.sync();
161
+ }
162
+
145
163
  async function notifyPreRelease() {
146
164
  const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
147
165
  const cli = new CLI(logStream);
@@ -81,6 +81,7 @@ export function handler(argv) {
81
81
  options.execGitNode = function execGitNode(cmd, args, input) {
82
82
  args.unshift(cmd);
83
83
  return forceRunAsync('git', args, {
84
+ ignoreFailure: false,
84
85
  input,
85
86
  spawnArgs: {
86
87
  cwd: options.nodeDir,
@@ -91,6 +92,7 @@ export function handler(argv) {
91
92
 
92
93
  options.execGitV8 = function execGitV8(...args) {
93
94
  return forceRunAsync('git', args, {
95
+ ignoreFailure: false,
94
96
  captureStdout: true,
95
97
  spawnArgs: { cwd: options.v8Dir, stdio: ['ignore', 'pipe', 'ignore'] }
96
98
  });
package/lib/ci/run_ci.js CHANGED
@@ -27,7 +27,7 @@ export class RunPRJob {
27
27
  this.certifySafe =
28
28
  certifySafe ||
29
29
  Promise.all([this.prData.getReviews(), this.prData.getPR()]).then(() =>
30
- new PRChecker(cli, this.prData, request, {}).checkCommitsAfterReview()
30
+ new PRChecker(cli, this.prData, request, {}).checkCommitsAfterReviewOrLabel()
31
31
  );
32
32
  }
33
33
 
@@ -18,6 +18,10 @@ releases lines on or shortly after, %RELEASE_DATE% in order to address:
18
18
 
19
19
  %IMPACT%
20
20
 
21
+ It's important to note that End-of-Life versions are always affected when a security release occurs.
22
+ To ensure your system's security, please use an up-to-date version as outlined in our
23
+ [Release Schedule](https://github.com/nodejs/release#release-schedule).
24
+
21
25
  ## Release timing
22
26
 
23
27
  Releases will be available on, or shortly after, %RELEASE_DATE%.
package/lib/pr_checker.js CHANGED
@@ -523,6 +523,32 @@ export default class PRChecker {
523
523
  return true;
524
524
  }
525
525
 
526
+ async checkCommitsAfterReviewOrLabel() {
527
+ if (this.checkCommitsAfterReview()) return true;
528
+
529
+ await Promise.all([this.data.getLabeledEvents(), this.data.getCollaborators()]);
530
+
531
+ const {
532
+ cli, data, pr
533
+ } = this;
534
+
535
+ const { updatedAt } = pr.timelineItems;
536
+ const requestCiLabels = data.labeledEvents.findLast(
537
+ ({ createdAt, label: { name } }) => name === 'request-ci' && createdAt > updatedAt
538
+ );
539
+ if (requestCiLabels == null) return false;
540
+
541
+ const { actor: { login } } = requestCiLabels;
542
+ const collaborators = Array.from(data.collaborators.values(),
543
+ (c) => c.login.toLowerCase());
544
+ if (collaborators.includes(login.toLowerCase())) {
545
+ cli.info('request-ci label was added by a Collaborator after the last push event.');
546
+ return true;
547
+ }
548
+
549
+ return false;
550
+ }
551
+
526
552
  checkCommitsAfterReview() {
527
553
  const {
528
554
  commits, reviews, cli, argv
package/lib/pr_data.js CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  } from './user_status.js';
6
6
 
7
7
  // lib/queries/*.gql file names
8
+ const LABELED_EVENTS_QUERY = 'PRLabeledEvents';
8
9
  const PR_QUERY = 'PR';
9
10
  const REVIEWS_QUERY = 'Reviews';
10
11
  const COMMENTS_QUERY = 'PRComments';
@@ -33,6 +34,7 @@ export default class PRData {
33
34
  this.comments = [];
34
35
  this.commits = [];
35
36
  this.reviewers = [];
37
+ this.labeledEvents = [];
36
38
  }
37
39
 
38
40
  getThread() {
@@ -90,6 +92,14 @@ export default class PRData {
90
92
  ]);
91
93
  }
92
94
 
95
+ async getLabeledEvents() {
96
+ const { prid, owner, repo, cli, request, prStr } = this;
97
+ const vars = { prid, owner, repo };
98
+ cli.updateSpinner(`Getting labels from ${prStr}`);
99
+ this.labeledEvents = (await request.gql(LABELED_EVENTS_QUERY, vars))
100
+ .repository.pullRequest.timelineItems.nodes;
101
+ }
102
+
93
103
  async getComments() {
94
104
  const { prid, owner, repo, cli, request, prStr } = this;
95
105
  const vars = { prid, owner, repo };
@@ -248,8 +248,7 @@ export default class PrepareSecurityRelease {
248
248
  });
249
249
 
250
250
  try {
251
- const prUrl = dep.replace('https://github.com/', 'https://api.github.com/repos/').replace('pull', 'pulls');
252
- const res = await this.req.getPullRequest(prUrl);
251
+ const res = await this.req.getPullRequest(dep);
253
252
  const { html_url, title } = res;
254
253
  deps.push({
255
254
  name,
@@ -0,0 +1,19 @@
1
+ query PRLabeledEvents($prid: Int!, $owner: String!, $repo: String!, $after: String) {
2
+ repository(owner: $owner, name: $repo) {
3
+ pullRequest(number: $prid) {
4
+ timelineItems(itemTypes: LABELED_EVENT, after: $after, last: 100) {
5
+ nodes {
6
+ ... on LabeledEvent {
7
+ actor {
8
+ login
9
+ }
10
+ label {
11
+ name
12
+ }
13
+ createdAt
14
+ }
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
package/lib/request.js CHANGED
@@ -77,7 +77,8 @@ export default class Request {
77
77
  return this.json(url, options);
78
78
  }
79
79
 
80
- async getPullRequest(url) {
80
+ async getPullRequest(fullUrl) {
81
+ const prUrl = fullUrl.replace('https://github.com/', 'https://api.github.com/repos/').replace('pull', 'pulls');
81
82
  const options = {
82
83
  method: 'GET',
83
84
  headers: {
@@ -86,7 +87,7 @@ export default class Request {
86
87
  Accept: 'application/vnd.github+json'
87
88
  }
88
89
  };
89
- return this.json(url, options);
90
+ return this.json(prUrl, options);
90
91
  }
91
92
 
92
93
  async createPullRequest(title, body, { owner, repo, head, base }) {
@@ -87,8 +87,8 @@ export async function getSupportedVersions() {
87
87
  return supportedVersions;
88
88
  }
89
89
 
90
- export async function getSummary(reportId, req) {
91
- const { data } = await req.getReport(reportId);
90
+ export function getSummary(report) {
91
+ const { data } = report;
92
92
  const summaryList = data?.relationships?.summaries?.data;
93
93
  if (!summaryList?.length) return;
94
94
  const summaries = summaryList.filter((summary) => summary?.attributes?.category === 'team');
@@ -139,17 +139,25 @@ export async function createIssue(title, content, repository, { cli, req }) {
139
139
  }
140
140
  }
141
141
 
142
- export async function pickReport(report, { cli, req }) {
142
+ export function getReportSeverity(report) {
143
143
  const {
144
- id, attributes: { title, cve_ids },
145
- relationships: { severity, weakness, reporter }
144
+ relationships: { severity, weakness }
146
145
  } = report;
147
- const link = `https://hackerone.com/reports/${id}`;
148
146
  const reportSeverity = {
149
147
  rating: severity?.data?.attributes?.rating || '',
150
148
  cvss_vector_string: severity?.data?.attributes?.cvss_vector_string || '',
151
149
  weakness_id: weakness?.data?.id || ''
152
150
  };
151
+ return reportSeverity;
152
+ }
153
+
154
+ export async function pickReport(report, { cli, req }) {
155
+ const {
156
+ id, attributes: { title, cve_ids },
157
+ relationships: { reporter, custom_field_values }
158
+ } = report;
159
+ const link = `https://hackerone.com/reports/${id}`;
160
+ const reportSeverity = getReportSeverity(report);
153
161
 
154
162
  cli.separator();
155
163
  cli.info(`Report: ${link} - ${title} (${reportSeverity?.rating})`);
@@ -165,19 +173,27 @@ export async function pickReport(report, { cli, req }) {
165
173
  defaultAnswer: await getSupportedVersions()
166
174
  });
167
175
 
168
- let patchAuthors = await cli.prompt(
169
- 'Add github username of the authors of the patch (split by comma if multiple)', {
170
- questionType: 'input',
171
- defaultAnswer: ''
172
- });
173
-
174
- if (!patchAuthors) {
175
- patchAuthors = [];
176
+ let prURL = '';
177
+ let patchAuthors = [];
178
+ if (custom_field_values.data.length) {
179
+ prURL = custom_field_values.data[0].attributes.value;
180
+ const { user } = await req.getPullRequest(prURL);
181
+ patchAuthors = [user.login];
176
182
  } else {
177
- patchAuthors = patchAuthors.split(',').map((p) => p.trim());
183
+ patchAuthors = await cli.prompt(
184
+ 'Add github username of the authors of the patch (split by comma if multiple)', {
185
+ questionType: 'input',
186
+ defaultAnswer: ''
187
+ });
188
+
189
+ if (!patchAuthors) {
190
+ patchAuthors = [];
191
+ } else {
192
+ patchAuthors = patchAuthors.split(',').map((p) => p.trim());
193
+ }
178
194
  }
179
195
 
180
- const summaryContent = await getSummary(id, req);
196
+ const summaryContent = getSummary(report);
181
197
 
182
198
  return {
183
199
  id,
@@ -186,6 +202,7 @@ export async function pickReport(report, { cli, req }) {
186
202
  severity: reportSeverity,
187
203
  summary: summaryContent ?? '',
188
204
  patchAuthors,
205
+ prURL,
189
206
  affectedVersions: versions.split(',').map((v) => v.replace('v', '').trim()),
190
207
  link,
191
208
  reporter: reporter.data.attributes.username
@@ -14,7 +14,7 @@ import {
14
14
  } from './util.js';
15
15
  import applyNodeChanges from './applyNodeChanges.js';
16
16
  import { chromiumGit, v8Deps } from './constants.js';
17
- import { runAsync } from '../run.js';
17
+ import { forceRunAsync } from '../run.js';
18
18
 
19
19
  export default function majorUpdate() {
20
20
  return {
@@ -83,7 +83,8 @@ function cloneLocalV8() {
83
83
  return {
84
84
  title: 'Clone branch to deps/v8',
85
85
  task: (ctx) =>
86
- runAsync('git', ['clone', '-b', ctx.branch, ctx.v8Dir, 'deps/v8'], {
86
+ forceRunAsync('git', ['clone', '-b', ctx.branch, ctx.v8Dir, 'deps/v8'], {
87
+ ignoreFailure: false,
87
88
  spawnArgs: { cwd: ctx.nodeDir, stdio: 'ignore' }
88
89
  })
89
90
  };
@@ -101,7 +102,8 @@ function addDepsV8() {
101
102
  title: 'Track all files in deps/v8',
102
103
  // Add all V8 files with --force before updating DEPS. We have to do this
103
104
  // because some files are checked in by V8 despite .gitignore rules.
104
- task: (ctx) => runAsync('git', ['add', '--force', 'deps/v8'], {
105
+ task: (ctx) => forceRunAsync('git', ['add', '--force', 'deps/v8'], {
106
+ ignoreFailure: false,
105
107
  spawnArgs: { cwd: ctx.nodeDir, stdio: 'ignore' }
106
108
  })
107
109
  };
@@ -164,6 +166,9 @@ async function fetchFromGit(cwd, repo, commit) {
164
166
  await removeDirectory(path.join(cwd, '.git'));
165
167
 
166
168
  function exec(...options) {
167
- return runAsync('git', options, { spawnArgs: { cwd, stdio: 'ignore' } });
169
+ return forceRunAsync('git', options, {
170
+ ignoreFailure: false,
171
+ spawnArgs: { cwd, stdio: 'ignore' }
172
+ });
168
173
  }
169
174
  }
@@ -6,7 +6,7 @@ import { Listr } from 'listr2';
6
6
 
7
7
  import { getCurrentV8Version } from './common.js';
8
8
  import { isVersionString } from './util.js';
9
- import { runAsync } from '../run.js';
9
+ import { forceRunAsync } from '../run.js';
10
10
 
11
11
  export default function minorUpdate() {
12
12
  return {
@@ -27,7 +27,8 @@ function getLatestV8Version() {
27
27
  task: async(ctx) => {
28
28
  const version = ctx.currentVersion;
29
29
  const currentV8Tag = `${version.major}.${version.minor}.${version.build}`;
30
- const result = await runAsync('git', ['tag', '-l', `${currentV8Tag}.*`], {
30
+ const result = await forceRunAsync('git', ['tag', '-l', `${currentV8Tag}.*`], {
31
+ ignoreFailure: false,
31
32
  captureStdout: true,
32
33
  spawnArgs: {
33
34
  cwd: ctx.v8Dir,
@@ -68,7 +69,8 @@ async function applyPatch(ctx, latestStr) {
68
69
  { cwd: ctx.v8Dir, stdio: ['ignore', 'pipe', 'ignore'] }
69
70
  );
70
71
  try {
71
- await runAsync('git', ['apply', '--directory', 'deps/v8'], {
72
+ await forceRunAsync('git', ['apply', '--directory', 'deps/v8'], {
73
+ ignoreFailure: false,
72
74
  spawnArgs: {
73
75
  cwd: ctx.nodeDir,
74
76
  stdio: [diff.stdout, 'ignore', 'ignore']
@@ -20,6 +20,7 @@ function fetchOrigin() {
20
20
  task: async(ctx, task) => {
21
21
  try {
22
22
  await forceRunAsync('git', ['fetch', 'origin'], {
23
+ ignoreFailure: false,
23
24
  spawnArgs: { cwd: ctx.v8Dir, stdio: 'ignore' }
24
25
  });
25
26
  } catch (e) {
@@ -40,6 +41,7 @@ function createClone() {
40
41
  task: async(ctx) => {
41
42
  await fs.mkdir(ctx.baseDir, { recursive: true });
42
43
  await forceRunAsync('git', ['clone', v8Git, ctx.v8Dir], {
44
+ ignoreFailure: false,
43
45
  spawnArgs: { stdio: 'ignore' }
44
46
  });
45
47
  },
@@ -2,9 +2,12 @@ import {
2
2
  NEXT_SECURITY_RELEASE_FOLDER,
3
3
  NEXT_SECURITY_RELEASE_REPOSITORY,
4
4
  checkoutOnSecurityReleaseBranch,
5
+ checkRemote,
5
6
  commitAndPushVulnerabilitiesJSON,
6
7
  validateDate,
7
- pickReport
8
+ pickReport,
9
+ getReportSeverity,
10
+ getSummary
8
11
  } from './security-release/security-release.js';
9
12
  import fs from 'node:fs';
10
13
  import path from 'node:path';
@@ -18,6 +21,41 @@ export default class UpdateSecurityRelease {
18
21
  this.cli = cli;
19
22
  }
20
23
 
24
+ async sync() {
25
+ checkRemote(this.cli, this.repository);
26
+
27
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
28
+ const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
29
+ const credentials = await auth({
30
+ github: true,
31
+ h1: true
32
+ });
33
+ const req = new Request(credentials);
34
+ for (let i = 0; i < content.reports.length; ++i) {
35
+ let report = content.reports[i];
36
+ const { data } = await req.getReport(report.id);
37
+ const reportSeverity = getReportSeverity(data);
38
+ const summaryContent = getSummary(data);
39
+ const link = `https://hackerone.com/reports/${report.id}`;
40
+ let prURL = report.prURL;
41
+ if (data.relationships.custom_field_values.data.length) {
42
+ prURL = data.relationships.custom_field_values.data[0].attributes.value;
43
+ }
44
+
45
+ report = {
46
+ ...report,
47
+ title: data.attributes.title,
48
+ cveIds: data.attributes.cve_ids,
49
+ severity: reportSeverity,
50
+ summary: summaryContent ?? report.summary,
51
+ link,
52
+ prURL
53
+ };
54
+ }
55
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
56
+ this.cli.ok('Synced vulnerabilities.json with HackerOne');
57
+ }
58
+
21
59
  async updateReleaseDate(releaseDate) {
22
60
  const { cli } = this;
23
61
 
@@ -114,7 +114,7 @@ export default class VotingSession extends Session {
114
114
  const body = 'I would like to close this vote, and for this effect, I\'m revealing my ' +
115
115
  `key part:\n\n${'```'}\n${keyPart}\n${'```'}\n`;
116
116
  if (this.postComment) {
117
- const { html_url } = await this.req.json(`https://api.github.com/repos/${this.owner}/${this.repo}/issues/${this.prid}/comments`, {
117
+ const { message, html_url } = await this.req.json(`https://api.github.com/repos/${this.owner}/${this.repo}/issues/${this.prid}/comments`, {
118
118
  agent: this.req.proxyAgent,
119
119
  method: 'POST',
120
120
  headers: {
@@ -124,13 +124,23 @@ export default class VotingSession extends Session {
124
124
  },
125
125
  body: JSON.stringify({ body })
126
126
  });
127
- this.cli.log('Comment posted at:', html_url);
128
- } else if (isGhAvailable()) {
127
+ if (html_url) {
128
+ this.cli.log(`Comment posted at: ${html_url}`);
129
+ return;
130
+ } else {
131
+ this.cli.warn(message);
132
+ this.cli.error('Failed to post comment');
133
+ }
134
+ }
135
+ if (isGhAvailable()) {
129
136
  this.cli.log('\nRun the following command to post the comment:\n');
130
137
  this.cli.log(
131
138
  `gh pr comment ${this.prid} --repo ${this.owner}/${this.repo} ` +
132
139
  `--body-file - <<'EOF'\n${body}\nEOF`
133
140
  );
141
+ } else {
142
+ this.cli.log('\nPost the following comment on the PR thread:\n');
143
+ this.cli.log(body);
134
144
  }
135
145
  }
136
146
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "5.0.2",
3
+ "version": "5.2.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {
@@ -34,36 +34,36 @@
34
34
  ],
35
35
  "license": "MIT",
36
36
  "dependencies": {
37
- "@listr2/prompt-adapter-enquirer": "^2.0.1",
38
- "@node-core/caritat": "^1.3.0",
37
+ "@listr2/prompt-adapter-enquirer": "^2.0.8",
38
+ "@node-core/caritat": "^1.3.1",
39
39
  "@pkgjs/nv": "^0.2.2",
40
- "branch-diff": "^3.0.2",
40
+ "branch-diff": "^3.0.4",
41
41
  "chalk": "^5.3.0",
42
- "changelog-maker": "^4.0.1",
42
+ "changelog-maker": "^4.1.1",
43
43
  "cheerio": "^1.0.0-rc.12",
44
44
  "clipboardy": "^4.0.0",
45
45
  "core-validate-commit": "^4.0.0",
46
- "figures": "^6.0.1",
47
- "ghauth": "^6.0.1",
48
- "inquirer": "^9.2.12",
46
+ "figures": "^6.1.0",
47
+ "ghauth": "^6.0.4",
48
+ "inquirer": "^9.2.22",
49
49
  "js-yaml": "^4.1.0",
50
- "listr2": "^8.0.1",
50
+ "listr2": "^8.2.1",
51
51
  "lodash": "^4.17.21",
52
52
  "log-symbols": "^6.0.0",
53
53
  "ora": "^8.0.1",
54
54
  "replace-in-file": "^7.1.0",
55
- "undici": "^6.3.0",
55
+ "undici": "^6.18.0",
56
56
  "which": "^4.0.0",
57
57
  "yargs": "^17.7.2"
58
58
  },
59
59
  "devDependencies": {
60
- "@reporters/github": "^1.5.4",
61
- "c8": "^9.0.0",
62
- "eslint": "^8.56.0",
60
+ "@reporters/github": "^1.7.0",
61
+ "c8": "^9.1.0",
62
+ "eslint": "^8.57.0",
63
63
  "eslint-config-standard": "^17.1.0",
64
64
  "eslint-plugin-import": "^2.29.1",
65
- "eslint-plugin-n": "^16.6.1",
65
+ "eslint-plugin-n": "^16.6.2",
66
66
  "eslint-plugin-promise": "^6.1.1",
67
- "sinon": "^17.0.1"
67
+ "sinon": "^18.0.0"
68
68
  }
69
69
  }