@node-core/utils 5.3.1 → 5.5.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.
@@ -78,6 +78,8 @@ async function main(state, argv, cli, dir) {
78
78
  if (state === PREPARE) {
79
79
  const prep = new ReleasePreparation(argv, cli, dir);
80
80
 
81
+ await prep.prepareLocalBranch();
82
+
81
83
  if (prep.warnForWrongBranch()) return;
82
84
 
83
85
  // If the new version was automatically calculated, confirm it.
@@ -43,6 +43,10 @@ const securityOptions = {
43
43
  'post-release': {
44
44
  describe: 'Create the post-release announcement',
45
45
  type: 'boolean'
46
+ },
47
+ cleanup: {
48
+ describe: 'cleanup the security release.',
49
+ type: 'boolean'
46
50
  }
47
51
  };
48
52
 
@@ -81,6 +85,9 @@ export function builder(yargs) {
81
85
  ).example(
82
86
  'git node security --post-release',
83
87
  'Create the post-release announcement on the Nodejs.org repo'
88
+ ).example(
89
+ 'git node security --cleanup',
90
+ 'Cleanup the security release. Merge the PR and close H1 reports'
84
91
  );
85
92
  }
86
93
 
@@ -112,6 +119,9 @@ export function handler(argv) {
112
119
  if (argv['post-release']) {
113
120
  return createPostRelease(argv);
114
121
  }
122
+ if (argv.cleanup) {
123
+ return cleanupSecurityRelease(argv);
124
+ }
115
125
  yargsInstance.showHelp();
116
126
  }
117
127
 
@@ -167,6 +177,13 @@ async function startSecurityRelease() {
167
177
  return release.start();
168
178
  }
169
179
 
180
+ async function cleanupSecurityRelease() {
181
+ const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
182
+ const cli = new CLI(logStream);
183
+ const release = new PrepareSecurityRelease(cli);
184
+ return release.cleanup();
185
+ }
186
+
170
187
  async function syncSecurityRelease(argv) {
171
188
  const logStream = process.stdout.isTTY ? process.stdout : process.stderr;
172
189
  const cli = new CLI(logStream);
@@ -44,6 +44,7 @@ export class CITGMComparisonBuild {
44
44
  const { failures: comparisonFailures } = comparisonBuild.results;
45
45
 
46
46
  const failures = {};
47
+ let allPlatformFailures;
47
48
  for (const platform in comparisonFailures) {
48
49
  // Account for no failure on this platform, or different platform.
49
50
  if (!Object.prototype.hasOwnProperty.call(baseFailures, platform)) {
@@ -66,11 +67,18 @@ export class CITGMComparisonBuild {
66
67
  if (newFailures.length !== 0) {
67
68
  result = statusType.FAILURE;
68
69
  }
69
-
70
+ if (allPlatformFailures === undefined) {
71
+ allPlatformFailures = newFailures;
72
+ } else if (allPlatformFailures.length > 0) {
73
+ allPlatformFailures = allPlatformFailures.filter(f => {
74
+ return newFailures.includes(f);
75
+ });
76
+ }
70
77
  failures[platform] = newFailures;
71
78
  }
72
79
 
73
80
  this.results.failures = failures;
81
+ this.results.allPlatformFailures = allPlatformFailures;
74
82
  this.result = result;
75
83
 
76
84
  return result;
@@ -124,6 +132,12 @@ export class CITGMComparisonBuild {
124
132
  const str = `${totalFailures} failures in ${cID} not present in ${bID}`;
125
133
  cli.log(`${statusType.FAILURE}: ${str}\n\n`);
126
134
  console.table(output);
135
+ if (
136
+ results.allPlatformFailures &&
137
+ results.allPlatformFailures.length) {
138
+ const failures = results.allPlatformFailures.join(', ');
139
+ console.warn(`These modules failed in all platforms: ${failures}`);
140
+ }
127
141
  }
128
142
 
129
143
  formatAsJson() {
@@ -21,7 +21,7 @@ export default class LandingSession extends Session {
21
21
  prid, backport, lint, autorebase, fixupAll,
22
22
  checkCI, oneCommitMax, ...argv
23
23
  } = {}) {
24
- super(cli, dir, prid);
24
+ super(cli, dir, prid, argv);
25
25
  this.req = req;
26
26
  this.backport = backport;
27
27
  this.lint = lint;
package/lib/pr_checker.js CHANGED
@@ -29,6 +29,7 @@ const GITHUB_SUCCESS_CONCLUSIONS = ['SUCCESS', 'NEUTRAL', 'SKIPPED'];
29
29
  const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to approve\.$/;
30
30
  const FAST_TRACK_MIN_APPROVALS = 2;
31
31
  const GIT_CONFIG_GUIDE_URL = 'https://github.com/nodejs/node/blob/99b1ada/doc/guides/contributing/pull-requests.md#step-1-fork';
32
+ const IGNORED_CHECK_SLUGS = ['dependabot', 'codecov'];
32
33
 
33
34
  // eslint-disable-next-line no-extend-native
34
35
  Array.prototype.findLastIndex ??= function findLastIndex(fn) {
@@ -373,9 +374,9 @@ export default class PRChecker {
373
374
 
374
375
  // GitHub new Check API
375
376
  for (const { status, conclusion, app } of checkSuites.nodes) {
376
- if (app && app.slug === 'dependabot') {
377
- // Ignore Dependabot check suites. They are expected to show up
378
- // sometimes and never complete.
377
+ if (app && IGNORED_CHECK_SLUGS.includes(app.slug)) {
378
+ // Ignore Dependabot and Codecov check suites.
379
+ // They are expected to show up sometimes and never complete.
379
380
  continue;
380
381
  }
381
382
 
@@ -4,8 +4,7 @@ import { promises as fs } from 'node:fs';
4
4
  import semver from 'semver';
5
5
  import { replaceInFile } from 'replace-in-file';
6
6
 
7
- import { getMergedConfig } from './config.js';
8
- import { runAsync, runSync } from './run.js';
7
+ import { forceRunAsync, runAsync, runSync } from './run.js';
9
8
  import { writeJson, readJson } from './file.js';
10
9
  import Request from './request.js';
11
10
  import auth from './auth.js';
@@ -15,58 +14,25 @@ import {
15
14
  updateTestProcessRelease
16
15
  } from './release/utils.js';
17
16
  import CherryPick from './cherry_pick.js';
17
+ import Session from './session.js';
18
18
 
19
19
  const isWindows = process.platform === 'win32';
20
20
 
21
- export default class ReleasePreparation {
21
+ export default class ReleasePreparation extends Session {
22
22
  constructor(argv, cli, dir) {
23
- this.cli = cli;
24
- this.dir = dir;
23
+ super(cli, dir);
25
24
  this.isSecurityRelease = argv.security;
26
25
  this.isLTS = false;
27
26
  this.isLTSTransition = argv.startLTS;
28
27
  this.runBranchDiff = !argv.skipBranchDiff;
29
28
  this.ltsCodename = '';
30
29
  this.date = '';
31
- this.config = getMergedConfig(this.dir);
32
30
  this.filterLabels = argv.filterLabel && argv.filterLabel.split(',');
31
+ this.newVersion = argv.newVersion;
32
+ }
33
33
 
34
- // Ensure the preparer has set an upstream and username.
35
- if (this.warnForMissing()) {
36
- cli.error('Failed to begin the release preparation process.');
37
- return;
38
- }
39
-
40
- // Allow passing optional new version.
41
- if (argv.newVersion) {
42
- const newVersion = semver.clean(argv.newVersion);
43
- if (!semver.valid(newVersion)) {
44
- cli.warn(`${newVersion} is not a valid semantic version.`);
45
- return;
46
- }
47
- this.newVersion = newVersion;
48
- } else {
49
- this.newVersion = this.calculateNewVersion();
50
- }
51
-
52
- const { upstream, owner, repo, newVersion } = this;
53
-
54
- this.versionComponents = {
55
- major: semver.major(newVersion),
56
- minor: semver.minor(newVersion),
57
- patch: semver.patch(newVersion)
58
- };
59
-
60
- this.stagingBranch = `v${this.versionComponents.major}.x-staging`;
61
- this.releaseBranch = `v${this.versionComponents.major}.x`;
62
-
63
- const upstreamHref = runSync('git', [
64
- 'config', '--get',
65
- `remote.${upstream}.url`]).trim();
66
- if (!new RegExp(`${owner}/${repo}(?:.git)?$`).test(upstreamHref)) {
67
- cli.warn('Remote repository URL does not point to the expected ' +
68
- `repository ${owner}/${repo}`);
69
- }
34
+ get branch() {
35
+ return this.stagingBranch;
70
36
  }
71
37
 
72
38
  warnForNonMergeablePR(pr) {
@@ -205,7 +171,7 @@ export default class ReleasePreparation {
205
171
  // Check the branch diff to determine if the releaser
206
172
  // wants to backport any more commits before proceeding.
207
173
  cli.startSpinner('Fetching branch-diff');
208
- const raw = this.getBranchDiff({
174
+ const raw = await this.getBranchDiff({
209
175
  onlyNotableChanges: false,
210
176
  comparisonBranch: newVersion
211
177
  });
@@ -215,10 +181,9 @@ export default class ReleasePreparation {
215
181
 
216
182
  const outstandingCommits = diff.length - 1;
217
183
  if (outstandingCommits !== 0) {
218
- const staging = `v${semver.major(newVersion)}.x-staging`;
219
184
  const proceed = await cli.prompt(
220
185
  `There are ${outstandingCommits} commits that may be ` +
221
- `backported to ${staging} - do you still want to proceed?`,
186
+ `backported to ${this.stagingBranch} - do you still want to proceed?`,
222
187
  { defaultAnswer: false });
223
188
 
224
189
  if (!proceed) {
@@ -369,24 +334,19 @@ export default class ReleasePreparation {
369
334
  return missing;
370
335
  }
371
336
 
372
- calculateNewVersion() {
373
- let newVersion;
374
-
375
- const lastTagVersion = semver.clean(this.getLastRef());
376
- const lastTag = {
377
- major: semver.major(lastTagVersion),
378
- minor: semver.minor(lastTagVersion),
379
- patch: semver.patch(lastTagVersion)
380
- };
381
-
382
- const changelog = this.getChangelog();
337
+ async calculateNewVersion({ tagName, major, minor, patch }) {
338
+ const changelog = this.getChangelog(tagName);
383
339
 
340
+ const newVersion = { major, minor, patch };
384
341
  if (changelog.includes('SEMVER-MAJOR')) {
385
- newVersion = `${lastTag.major + 1}.0.0`;
342
+ newVersion.major++;
343
+ newVersion.minor = 0;
344
+ newVersion.patch = 0;
386
345
  } else if (changelog.includes('SEMVER-MINOR') || this.isLTSTransition) {
387
- newVersion = `${lastTag.major}.${lastTag.minor + 1}.0`;
346
+ newVersion.minor++;
347
+ newVersion.patch = 0;
388
348
  } else {
389
- newVersion = `${lastTag.major}.${lastTag.minor}.${lastTag.patch + 1}`;
349
+ newVersion.patch++;
390
350
  }
391
351
 
392
352
  return newVersion;
@@ -396,11 +356,22 @@ export default class ReleasePreparation {
396
356
  return runSync('git', ['rev-parse', '--abbrev-ref', 'HEAD']).trim();
397
357
  }
398
358
 
399
- getLastRef() {
400
- return runSync('git', ['describe', '--abbrev=0', '--tags']).trim();
359
+ getLastRef(tagName) {
360
+ if (!tagName) {
361
+ return runSync('git', ['describe', '--abbrev=0', '--tags']).trim();
362
+ }
363
+
364
+ try {
365
+ runSync('git', ['rev-parse', tagName]);
366
+ } catch {
367
+ this.cli.startSpinner(`Error parsing git ref ${tagName}, attempting fetching it as a tag`);
368
+ runSync('git', ['fetch', this.upstream, 'tag', '-n', tagName]);
369
+ this.cli.stopSpinner(`Tag fetched: ${tagName}`);
370
+ }
371
+ return tagName;
401
372
  }
402
373
 
403
- getChangelog() {
374
+ getChangelog(tagName) {
404
375
  const changelogMaker = new URL(
405
376
  '../node_modules/.bin/changelog-maker' + (isWindows ? '.cmd' : ''),
406
377
  import.meta.url
@@ -411,7 +382,7 @@ export default class ReleasePreparation {
411
382
  '--markdown',
412
383
  '--filter-release',
413
384
  '--start-ref',
414
- this.getLastRef()
385
+ this.getLastRef(tagName)
415
386
  ]).trim();
416
387
  }
417
388
 
@@ -496,7 +467,7 @@ export default class ReleasePreparation {
496
467
  const data = await fs.readFile(majorChangelogPath, 'utf8');
497
468
  const arr = data.split('\n');
498
469
  const allCommits = this.getChangelog();
499
- const notableChanges = this.getBranchDiff({ onlyNotableChanges: true });
470
+ const notableChanges = await this.getBranchDiff({ onlyNotableChanges: true });
500
471
  let releaseHeader = `## ${date}, Version ${newVersion}` +
501
472
  ` ${releaseInfo}, @${username}\n`;
502
473
  if (isSecurityRelease) {
@@ -550,14 +521,14 @@ export default class ReleasePreparation {
550
521
  }
551
522
 
552
523
  async createProposalBranch(base = this.stagingBranch) {
553
- const { upstream, newVersion } = this;
524
+ const { newVersion } = this;
554
525
  const proposalBranch = `v${newVersion}-proposal`;
555
526
 
556
527
  await runAsync('git', [
557
528
  'checkout',
558
529
  '-b',
559
530
  proposalBranch,
560
- `${upstream}/${base}`
531
+ base
561
532
  ]);
562
533
  return proposalBranch;
563
534
  }
@@ -632,7 +603,7 @@ export default class ReleasePreparation {
632
603
  messageBody.push('This is a security release.\n\n');
633
604
  }
634
605
 
635
- const notableChanges = this.getBranchDiff({
606
+ const notableChanges = await this.getBranchDiff({
636
607
  onlyNotableChanges: true,
637
608
  format: 'plaintext'
638
609
  });
@@ -659,8 +630,9 @@ export default class ReleasePreparation {
659
630
  return useMessage;
660
631
  }
661
632
 
662
- getBranchDiff(opts) {
633
+ async getBranchDiff(opts) {
663
634
  const {
635
+ cli,
664
636
  versionComponents = {},
665
637
  upstream,
666
638
  newVersion,
@@ -688,6 +660,10 @@ export default class ReleasePreparation {
688
660
  'semver-minor'
689
661
  ];
690
662
 
663
+ await forceRunAsync('git', ['remote', 'set-branches', '--add', upstream, releaseBranch], {
664
+ ignoreFailures: false
665
+ });
666
+ await forceRunAsync('git', ['fetch', upstream, releaseBranch], { ignoreFailures: false });
691
667
  branchDiffOptions = [
692
668
  `${upstream}/${releaseBranch}`,
693
669
  proposalBranch,
@@ -706,20 +682,43 @@ export default class ReleasePreparation {
706
682
  'baking-for-lts'
707
683
  ];
708
684
 
709
- let comparisonBranch = 'main';
685
+ let comparisonBranch = this.config.branch || 'main';
710
686
  const isSemverMinor = versionComponents.patch === 0;
711
687
  if (isLTS) {
688
+ const res = await fetch('https://nodejs.org/dist/index.json');
689
+ if (!res.ok) throw new Error('Failed to fetch', { cause: res });
690
+ const [latest] = await res.json();
712
691
  // Assume Current branch matches tag with highest semver value.
713
- const tags = runSync('git',
714
- ['tag', '-l', '--sort', '-version:refname']).trim();
715
- const highestVersionTag = tags.split('\n')[0];
716
- comparisonBranch = `v${semver.coerce(highestVersionTag).major}.x`;
692
+ comparisonBranch = `v${semver.coerce(latest.version).major}.x`;
717
693
 
718
694
  if (!isSemverMinor) {
719
695
  excludeLabels.push('semver-minor');
720
696
  }
721
697
  }
722
698
 
699
+ await forceRunAsync('git', ['fetch', upstream, comparisonBranch], { ignoreFailures: false });
700
+ const commits = await forceRunAsync('git', ['rev-parse', 'FETCH_HEAD', comparisonBranch], {
701
+ captureStdout: 'lines',
702
+ ignoreFailures: true
703
+ });
704
+ if (commits == null) {
705
+ const shouldCreateCompareBranch = await cli.prompt(
706
+ `No local branch ${comparisonBranch}, do you want to create it?`);
707
+ if (shouldCreateCompareBranch) {
708
+ await forceRunAsync('git', ['branch', comparisonBranch, 'FETCH_HEAD'], {
709
+ ignoreFailures: false
710
+ });
711
+ }
712
+ } else if (commits[0] !== commits[1]) {
713
+ const shouldUpBranch = cli.prompt(`Local ${comparisonBranch} branch is not in sync with ${
714
+ upstream}/${comparisonBranch}, do you want to update it?`);
715
+ if (shouldUpBranch) {
716
+ await forceRunAsync('git', ['branch', '-f', comparisonBranch, 'FETCH_HEAD'], {
717
+ ignoreFailures: false
718
+ });
719
+ }
720
+ }
721
+
723
722
  branchDiffOptions = [
724
723
  stagingBranch,
725
724
  comparisonBranch,
@@ -736,6 +735,67 @@ export default class ReleasePreparation {
736
735
  return runSync(branchDiff, branchDiffOptions);
737
736
  }
738
737
 
738
+ async getLastRelease(major) {
739
+ const { cli } = this;
740
+
741
+ cli.startSpinner(`Parsing CHANGELOG for most recent release of v${major}.x`);
742
+ const data = await fs.readFile(
743
+ path.resolve(`doc/changelogs/CHANGELOG_V${major}.md`),
744
+ 'utf8'
745
+ );
746
+ const [,, minor, patch] = /<a href="#(\d+)\.(\d+)\.(\d+)">\1\.\2\.\3<\/a><br\/>/.exec(data);
747
+ this.isLTS = data.includes('<th>LTS ');
748
+
749
+ cli.stopSpinner(`Latest release on ${major}.x line is ${major}.${minor}.${patch}${
750
+ this.isLTS ? ' (LTS)' : ''
751
+ }`);
752
+
753
+ return {
754
+ tagName: await this.getLastRef(`v${major}.${minor}.${patch}`),
755
+ major, minor, patch
756
+ };
757
+ }
758
+
759
+ async prepareLocalBranch() {
760
+ const { cli } = this;
761
+ if (this.newVersion) {
762
+ // If the CLI asked for a specific version:
763
+ const newVersion = semver.parse(this.newVersion);
764
+ if (!newVersion) {
765
+ cli.warn(`${this.newVersion} is not a valid semantic version.`);
766
+ return;
767
+ }
768
+ this.newVersion = newVersion.version;
769
+ this.versionComponents = {
770
+ major: newVersion.major,
771
+ minor: newVersion.minor,
772
+ patch: newVersion.patch
773
+ };
774
+ this.stagingBranch = `v${newVersion.major}.x-staging`;
775
+ this.releaseBranch = `v${newVersion.major}.x`;
776
+ await this.tryResetBranch();
777
+ await this.getLastRelease(newVersion.major);
778
+ return;
779
+ }
780
+
781
+ // Otherwise, we need to figure out what's the next version number for the
782
+ // release line of the branch that's currently checked out.
783
+ const currentBranch = this.getCurrentBranch();
784
+ const match = /^v(\d+)\.x-staging$/.exec(currentBranch);
785
+
786
+ if (!match) {
787
+ cli.warn(`Cannot prepare a release from ${currentBranch
788
+ }. Switch to a staging branch before proceeding.`);
789
+ return;
790
+ }
791
+ this.stagingBranch = currentBranch;
792
+ await this.tryResetBranch();
793
+ this.versionComponents = await this.calculateNewVersion(await this.getLastRelease(match[1]));
794
+ const { major, minor, patch } = this.versionComponents;
795
+ this.newVersion = `${major}.${minor}.${patch}`;
796
+ this.releaseBranch = `v${major}.x`;
797
+ }
798
+
739
799
  warnForWrongBranch() {
740
800
  const {
741
801
  cli,
@@ -5,23 +5,18 @@ import Request from './request.js';
5
5
  import {
6
6
  NEXT_SECURITY_RELEASE_BRANCH,
7
7
  NEXT_SECURITY_RELEASE_FOLDER,
8
- NEXT_SECURITY_RELEASE_REPOSITORY,
9
- PLACEHOLDERS,
10
8
  checkoutOnSecurityReleaseBranch,
11
9
  commitAndPushVulnerabilitiesJSON,
12
10
  validateDate,
13
11
  promptDependencies,
14
12
  getSupportedVersions,
15
- pickReport
13
+ pickReport,
14
+ SecurityRelease
16
15
  } from './security-release/security-release.js';
17
16
  import _ from 'lodash';
18
17
 
19
- export default class PrepareSecurityRelease {
20
- repository = NEXT_SECURITY_RELEASE_REPOSITORY;
18
+ export default class PrepareSecurityRelease extends SecurityRelease {
21
19
  title = 'Next Security Release';
22
- constructor(cli) {
23
- this.cli = cli;
24
- }
25
20
 
26
21
  async start() {
27
22
  const credentials = await auth({
@@ -37,22 +32,49 @@ export default class PrepareSecurityRelease {
37
32
  const createVulnerabilitiesJSON = await this.promptVulnerabilitiesJSON();
38
33
 
39
34
  let securityReleasePRUrl;
35
+ const content = await this.buildDescription(releaseDate, securityReleasePRUrl);
40
36
  if (createVulnerabilitiesJSON) {
41
- securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate);
37
+ securityReleasePRUrl = await this.startVulnerabilitiesJSONCreation(releaseDate, content);
42
38
  }
43
39
 
44
- const createIssue = await this.promptCreateRelaseIssue();
40
+ this.cli.ok('Done!');
41
+ }
45
42
 
46
- if (createIssue) {
47
- const content = await this.buildIssue(releaseDate, securityReleasePRUrl);
48
- await createIssue(
49
- this.title, content, this.repository, { cli: this.cli, repository: this.repository });
50
- };
43
+ async cleanup() {
44
+ const credentials = await auth({
45
+ github: true,
46
+ h1: true
47
+ });
48
+
49
+ this.req = new Request(credentials);
50
+ const vulnerabilityJSON = this.readVulnerabilitiesJSON();
51
+ this.cli.info('Closing and request disclosure to HackerOne reports');
52
+ await this.closeAndRequestDisclosure(vulnerabilityJSON.reports);
53
+
54
+ this.cli.info('Closing pull requests');
55
+ // For now, close the ones with vN.x label
56
+ await this.closePRWithLabel(this.getAffectedVersions(vulnerabilityJSON));
51
57
 
58
+ const updateFolder = this.cli.prompt(
59
+ // eslint-disable-next-line max-len
60
+ `Would you like to update the next-security-release folder to ${vulnerabilityJSON.releaseDate}?`,
61
+ { defaultAnswer: true });
62
+ if (updateFolder) {
63
+ const newFolder = this.updateReleaseFolder(vulnerabilityJSON.releaseDate);
64
+ commitAndPushVulnerabilitiesJSON(
65
+ newFolder,
66
+ 'chore: change next-security-release folder',
67
+ { cli: this.cli, repository: this.repository }
68
+ );
69
+ }
70
+ this.cli.info(`Merge pull request with:
71
+ - git checkout main
72
+ - git merge --squash ${NEXT_SECURITY_RELEASE_BRANCH}
73
+ - git push origin main`);
52
74
  this.cli.ok('Done!');
53
75
  }
54
76
 
55
- async startVulnerabilitiesJSONCreation(releaseDate) {
77
+ async startVulnerabilitiesJSONCreation(releaseDate, content) {
56
78
  // checkout on the next-security-release branch
57
79
  checkoutOnSecurityReleaseBranch(this.cli, this.repository);
58
80
 
@@ -87,7 +109,7 @@ export default class PrepareSecurityRelease {
87
109
  if (!createPr) return;
88
110
 
89
111
  // create pr on the security-release repo
90
- return this.createPullRequest();
112
+ return this.createPullRequest(content);
91
113
  }
92
114
 
93
115
  promptCreatePR() {
@@ -143,11 +165,9 @@ export default class PrepareSecurityRelease {
143
165
  { defaultAnswer: true });
144
166
  }
145
167
 
146
- async buildIssue(releaseDate, securityReleasePRUrl = PLACEHOLDERS.vulnerabilitiesPRURL) {
168
+ async buildDescription() {
147
169
  const template = await this.getSecurityIssueTemplate();
148
- const content = template.replace(PLACEHOLDERS.releaseDate, releaseDate)
149
- .replace(PLACEHOLDERS.vulnerabilitiesPRURL, securityReleasePRUrl);
150
- return content;
170
+ return template;
151
171
  }
152
172
 
153
173
  async chooseReports() {
@@ -173,9 +193,9 @@ export default class PrepareSecurityRelease {
173
193
 
174
194
  const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
175
195
  try {
176
- await fs.accessSync(folderPath);
196
+ fs.accessSync(folderPath);
177
197
  } catch (error) {
178
- await fs.mkdirSync(folderPath, { recursive: true });
198
+ fs.mkdirSync(folderPath, { recursive: true });
179
199
  }
180
200
 
181
201
  const fullPath = path.join(folderPath, 'vulnerabilities.json');
@@ -185,11 +205,11 @@ export default class PrepareSecurityRelease {
185
205
  return fullPath;
186
206
  }
187
207
 
188
- async createPullRequest() {
208
+ async createPullRequest(content) {
189
209
  const { owner, repo } = this.repository;
190
210
  const response = await this.req.createPullRequest(
191
211
  this.title,
192
- 'List of vulnerabilities to be included in the next security release',
212
+ content ?? 'List of vulnerabilities to be included in the next security release',
193
213
  {
194
214
  owner,
195
215
  repo,
@@ -264,4 +284,38 @@ export default class PrepareSecurityRelease {
264
284
  }
265
285
  return deps;
266
286
  }
287
+
288
+ async closeAndRequestDisclosure(jsonReports) {
289
+ this.cli.startSpinner('Closing HackerOne reports');
290
+ for (const report of jsonReports) {
291
+ this.cli.updateSpinner(`Closing report ${report.id}...`);
292
+ await this.req.updateReportState(
293
+ report.id,
294
+ 'resolved',
295
+ 'Closing as resolved'
296
+ );
297
+
298
+ this.cli.updateSpinner(`Requesting disclosure to report ${report.id}...`);
299
+ await this.req.requestDisclosure(report.id);
300
+ }
301
+ this.cli.stopSpinner('Done closing H1 Reports and requesting disclosure');
302
+ }
303
+
304
+ async closePRWithLabel(labels) {
305
+ if (typeof labels === 'string') {
306
+ labels = [labels];
307
+ }
308
+
309
+ const url = 'https://github.com/nodejs-private/node-private/pulls';
310
+ this.cli.startSpinner('Closing GitHub Pull Requests...');
311
+ // At this point, GitHub does not provide filters through their REST API
312
+ const prs = this.req.getPullRequest(url);
313
+ for (const pr of prs) {
314
+ if (pr.labels.some((l) => labels.includes(l))) {
315
+ this.cli.updateSpinner(`Closing Pull Request: ${pr.id}`);
316
+ await this.req.closePullRequest(pr.id);
317
+ }
318
+ }
319
+ this.cli.startSpinner('Closed GitHub Pull Requests.');
320
+ }
267
321
  }
package/lib/request.js CHANGED
@@ -109,6 +109,22 @@ export default class Request {
109
109
  return this.json(url, options);
110
110
  }
111
111
 
112
+ async closePullRequest({ owner, repo }) {
113
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls`;
114
+ const options = {
115
+ method: 'POST',
116
+ headers: {
117
+ Authorization: `Basic ${this.credentials.github}`,
118
+ 'User-Agent': 'node-core-utils',
119
+ Accept: 'application/vnd.github+json'
120
+ },
121
+ body: JSON.stringify({
122
+ state: 'closed'
123
+ })
124
+ };
125
+ return this.json(url, options);
126
+ }
127
+
112
128
  async gql(name, variables, path) {
113
129
  const query = this.loadQuery(name);
114
130
  if (path) {
@@ -201,6 +217,49 @@ export default class Request {
201
217
  return this.json(url, options);
202
218
  }
203
219
 
220
+ async updateReportState(reportId, state, message) {
221
+ const url = `https://api.hackerone.com/v1/reports/${reportId}/state_changes`;
222
+ const options = {
223
+ method: 'POST',
224
+ headers: {
225
+ Authorization: `Basic ${this.credentials.h1}`,
226
+ 'User-Agent': 'node-core-utils',
227
+ Accept: 'application/json'
228
+ },
229
+ body: JSON.stringify({
230
+ data: {
231
+ type: 'state-change',
232
+ attributes: {
233
+ message,
234
+ state
235
+ }
236
+ }
237
+ })
238
+ };
239
+ return this.json(url, options);
240
+ }
241
+
242
+ async requestDisclosure(reportId) {
243
+ const url = `https://api.hackerone.com/v1/reports/${reportId}/disclosure_requests`;
244
+ const options = {
245
+ method: 'POST',
246
+ headers: {
247
+ Authorization: `Basic ${this.credentials.h1}`,
248
+ 'User-Agent': 'node-core-utils',
249
+ Accept: 'application/json'
250
+ },
251
+ body: JSON.stringify({
252
+ data: {
253
+ attributes: {
254
+ // default to limited version
255
+ substate: 'no-content'
256
+ }
257
+ }
258
+ })
259
+ };
260
+ return this.json(url, options);
261
+ }
262
+
204
263
  // This is for github v4 API queries, for other types of queries
205
264
  // use .text or .json
206
265
  async query(query, variables) {
@@ -210,3 +210,64 @@ export async function pickReport(report, { cli, req }) {
210
210
  reporter: reporter.data.attributes.username
211
211
  };
212
212
  }
213
+
214
+ export class SecurityRelease {
215
+ constructor(cli, repository = NEXT_SECURITY_RELEASE_REPOSITORY) {
216
+ this.cli = cli;
217
+ this.repository = repository;
218
+ }
219
+
220
+ readVulnerabilitiesJSON(vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath()) {
221
+ const exists = fs.existsSync(vulnerabilitiesJSONPath);
222
+
223
+ if (!exists) {
224
+ this.cli.error(`The file vulnerabilities.json does not exist at ${vulnerabilitiesJSONPath}`);
225
+ process.exit(1);
226
+ }
227
+
228
+ return JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf8'));
229
+ }
230
+
231
+ getVulnerabilitiesJSONPath() {
232
+ return path.join(process.cwd(),
233
+ NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
234
+ }
235
+
236
+ updateReleaseFolder(releaseDate) {
237
+ const folder = path.join(process.cwd(),
238
+ NEXT_SECURITY_RELEASE_FOLDER);
239
+ const newFolder = path.join(process.cwd(), releaseDate);
240
+ fs.renameSync(folder, newFolder);
241
+ return newFolder;
242
+ }
243
+
244
+ updateVulnerabilitiesJSON(content) {
245
+ try {
246
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
247
+ this.cli.startSpinner(`Updating vulnerabilities.json from ${vulnerabilitiesJSONPath}...`);
248
+ fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
249
+ commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
250
+ 'chore: updated vulnerabilities.json',
251
+ { cli: this.cli, repository: this.repository });
252
+ this.cli.stopSpinner(`Done updating vulnerabilities.json from ${vulnerabilitiesJSONPath}`);
253
+ } catch (error) {
254
+ this.cli.error('Error updating vulnerabilities.json');
255
+ this.cli.error(error);
256
+ }
257
+ }
258
+
259
+ getAffectedVersions(content) {
260
+ const affectedVersions = new Set();
261
+ for (const report of Object.values(content.reports)) {
262
+ for (const affectedVersion of report.affectedVersions) {
263
+ affectedVersions.add(affectedVersion);
264
+ }
265
+ }
266
+ const parseToNumber = str => +(str.match(/[\d.]+/g)[0]);
267
+ return Array.from(affectedVersions)
268
+ .sort((a, b) => {
269
+ return parseToNumber(a) > parseToNumber(b) ? -1 : 1;
270
+ })
271
+ .join(', ');
272
+ }
273
+ }
@@ -4,24 +4,17 @@ import _ from 'lodash';
4
4
  import nv from '@pkgjs/nv';
5
5
  import {
6
6
  PLACEHOLDERS,
7
- getVulnerabilitiesJSON,
8
7
  checkoutOnSecurityReleaseBranch,
9
- NEXT_SECURITY_RELEASE_REPOSITORY,
10
8
  validateDate,
11
- commitAndPushVulnerabilitiesJSON,
12
- NEXT_SECURITY_RELEASE_FOLDER
9
+ SecurityRelease
13
10
  } from './security-release/security-release.js';
14
11
  import auth from './auth.js';
15
12
  import Request from './request.js';
16
13
 
17
14
  const kChanged = Symbol('changed');
18
15
 
19
- export default class SecurityBlog {
20
- repository = NEXT_SECURITY_RELEASE_REPOSITORY;
16
+ export default class SecurityBlog extends SecurityRelease {
21
17
  req;
22
- constructor(cli) {
23
- this.cli = cli;
24
- }
25
18
 
26
19
  async createPreRelease() {
27
20
  const { cli } = this;
@@ -30,7 +23,7 @@ export default class SecurityBlog {
30
23
  checkoutOnSecurityReleaseBranch(cli, this.repository);
31
24
 
32
25
  // read vulnerabilities JSON file
33
- const content = getVulnerabilitiesJSON(cli);
26
+ const content = this.readVulnerabilitiesJSON();
34
27
  // validate the release date read from vulnerabilities JSON
35
28
  if (!content.releaseDate) {
36
29
  cli.error('Release date is not set in vulnerabilities.json,' +
@@ -72,7 +65,7 @@ export default class SecurityBlog {
72
65
  checkoutOnSecurityReleaseBranch(cli, this.repository);
73
66
 
74
67
  // read vulnerabilities JSON file
75
- const content = getVulnerabilitiesJSON(cli);
68
+ const content = this.readVulnerabilitiesJSON(cli);
76
69
  if (!content.releaseDate) {
77
70
  cli.error('Release date is not set in vulnerabilities.json,' +
78
71
  ' run `git node security --update-date=YYYY/MM/DD` to set the release date.');
@@ -113,22 +106,6 @@ export default class SecurityBlog {
113
106
  this.updateVulnerabilitiesJSON(content);
114
107
  }
115
108
 
116
- updateVulnerabilitiesJSON(content) {
117
- try {
118
- this.cli.info('Updating vulnerabilities.json');
119
- const vulnerabilitiesJSONPath = path.join(process.cwd(),
120
- NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
121
- fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
122
- const commitMessage = 'chore: updated vulnerabilities.json';
123
- commitAndPushVulnerabilitiesJSON(vulnerabilitiesJSONPath,
124
- commitMessage,
125
- { cli: this.cli, repository: this.repository });
126
- } catch (error) {
127
- this.cli.error('Error updating vulnerabilities.json');
128
- this.cli.error(error);
129
- }
130
- }
131
-
132
109
  async promptExistingPreRelease(cli) {
133
110
  const pathPreRelease = await cli.prompt(
134
111
  'Please provide the path of the existing pre-release announcement:', {
@@ -232,9 +209,10 @@ export default class SecurityBlog {
232
209
  }
233
210
 
234
211
  getDependencyUpdatesTemplate(dependencyUpdates) {
235
- if (!dependencyUpdates) return '';
236
- let template = 'This security release includes the following dependency' +
237
- ' updates to address public vulnerabilities:\n\n';
212
+ if (typeof dependencyUpdates !== 'object') return '';
213
+ if (Object.keys(dependencyUpdates).length === 0) return '';
214
+ let template = '\nThis security release includes the following dependency' +
215
+ ' updates to address public vulnerabilities:\n';
238
216
  for (const dependencyUpdate of Object.values(dependencyUpdates)) {
239
217
  for (const dependency of dependencyUpdate) {
240
218
  const title = dependency.title.substring(dependency.title.indexOf(':') + ':'.length).trim();
@@ -323,16 +301,6 @@ export default class SecurityBlog {
323
301
  return text.join('\n');
324
302
  }
325
303
 
326
- getAffectedVersions(content) {
327
- const affectedVersions = new Set();
328
- for (const report of Object.values(content.reports)) {
329
- for (const affectedVersion of report.affectedVersions) {
330
- affectedVersions.add(affectedVersion);
331
- }
332
- }
333
- return Array.from(affectedVersions).join(', ');
334
- }
335
-
336
304
  getSecurityPreReleaseTemplate() {
337
305
  return fs.readFileSync(
338
306
  new URL(
@@ -1,7 +1,5 @@
1
1
  import path from 'node:path';
2
2
 
3
- import { Listr } from 'listr2';
4
-
5
3
  import {
6
4
  getNodeV8Version,
7
5
  filterForVersion,
@@ -19,10 +17,10 @@ const nodeChanges = [
19
17
  export default function applyNodeChanges() {
20
18
  return {
21
19
  title: 'Apply Node-specific changes',
22
- task: async(ctx) => {
20
+ task: async(ctx, task) => {
23
21
  const v8Version = await getNodeV8Version(ctx.nodeDir);
24
22
  const list = filterForVersion(nodeChanges, v8Version);
25
- return new Listr(list.map((change) => change.task()));
23
+ return task.newListr(list.map((change) => change.task()));
26
24
  }
27
25
  };
28
26
  }
@@ -4,7 +4,6 @@ import {
4
4
  } from 'node:fs';
5
5
 
6
6
  import inquirer from 'inquirer';
7
- import { Listr } from 'listr2';
8
7
  import { ListrEnquirerPromptAdapter } from '@listr2/prompt-adapter-enquirer';
9
8
 
10
9
  import { shortSha } from '../utils.js';
@@ -50,8 +49,8 @@ export function doBackport(options) {
50
49
 
51
50
  return {
52
51
  title: 'V8 commit backport',
53
- task: () => {
54
- return new Listr(todo);
52
+ task: (ctx, task) => {
53
+ return task.newListr(todo);
55
54
  }
56
55
  };
57
56
  };
@@ -164,8 +163,8 @@ function applyPatches() {
164
163
  function applyAndCommitPatches() {
165
164
  return {
166
165
  title: 'Apply and commit patches to deps/v8',
167
- task: (ctx) => {
168
- return new Listr(ctx.patches.map(applyPatchTask));
166
+ task: (ctx, task) => {
167
+ return task.newListr(ctx.patches.map(applyPatchTask));
169
168
  }
170
169
  };
171
170
  }
@@ -173,7 +172,7 @@ function applyAndCommitPatches() {
173
172
  function applyPatchTask(patch) {
174
173
  return {
175
174
  title: `Commit ${shortSha(patch.sha)}`,
176
- task: (ctx) => {
175
+ task: (ctx, task) => {
177
176
  const todo = [
178
177
  {
179
178
  title: 'Apply patch',
@@ -188,7 +187,7 @@ function applyPatchTask(patch) {
188
187
  }
189
188
  }
190
189
  todo.push(commitPatch(patch));
191
- return new Listr(todo);
190
+ return task.newListr(todo);
192
191
  }
193
192
  };
194
193
  }
@@ -38,6 +38,9 @@ const fp16Ignore = `!/third_party/fp16
38
38
  /third_party/fp16/src/*
39
39
  !/third_party/fp16/src/include`;
40
40
 
41
+ const fastFloatReplace = `/third_party/fast_float/src/*
42
+ !/third_party/fast_float/src/include`;
43
+
41
44
  export const v8Deps = [
42
45
  {
43
46
  name: 'trace_event',
@@ -103,5 +106,14 @@ export const v8Deps = [
103
106
  repo: 'third_party/fp16/src',
104
107
  gitignore: fp16Ignore,
105
108
  since: 124
109
+ },
110
+ {
111
+ name: 'fast_float',
112
+ repo: 'third_party/fast_float/src',
113
+ gitignore: {
114
+ match: '/third_party/fast_float/src',
115
+ replace: fastFloatReplace
116
+ },
117
+ since: 130
106
118
  }
107
119
  ];
@@ -1,8 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import { promises as fs } from 'node:fs';
3
3
 
4
- import { Listr } from 'listr2';
5
-
6
4
  import { getCurrentV8Version } from './common.js';
7
5
  import {
8
6
  getNodeV8Version,
@@ -19,8 +17,8 @@ import { forceRunAsync } from '../run.js';
19
17
  export default function majorUpdate() {
20
18
  return {
21
19
  title: 'Major V8 update',
22
- task: () => {
23
- return new Listr([
20
+ task: (ctx, task) => {
21
+ return task.newListr([
24
22
  getCurrentV8Version(),
25
23
  checkoutBranch(),
26
24
  removeDepsV8(),
@@ -2,8 +2,6 @@ import { spawn } from 'node:child_process';
2
2
  import path from 'node:path';
3
3
  import { promises as fs } from 'node:fs';
4
4
 
5
- import { Listr } from 'listr2';
6
-
7
5
  import { getCurrentV8Version } from './common.js';
8
6
  import { isVersionString } from './util.js';
9
7
  import { forceRunAsync } from '../run.js';
@@ -11,8 +9,8 @@ import { forceRunAsync } from '../run.js';
11
9
  export default function minorUpdate() {
12
10
  return {
13
11
  title: 'Minor V8 update',
14
- task: () => {
15
- return new Listr([
12
+ task: (ctx, task) => {
13
+ return task.newListr([
16
14
  getCurrentV8Version(),
17
15
  getLatestV8Version(),
18
16
  doMinorUpdate()
@@ -1,15 +1,13 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
 
3
- import { Listr } from 'listr2';
4
-
5
3
  import { v8Git } from './constants.js';
6
4
  import { forceRunAsync } from '../run.js';
7
5
 
8
6
  export default function updateV8Clone() {
9
7
  return {
10
8
  title: 'Update local V8 clone',
11
- task: () => {
12
- return new Listr([fetchOrigin(), createClone()]);
9
+ task: (ctx, task) => {
10
+ return task.newListr([fetchOrigin(), createClone()]);
13
11
  }
14
12
  };
15
13
  };
@@ -1,15 +1,13 @@
1
1
  import path from 'node:path';
2
2
  import { promises as fs } from 'node:fs';
3
3
 
4
- import { Listr } from 'listr2';
5
-
6
4
  import { getNodeV8Version } from './util.js';
7
5
 
8
6
  export default function updateVersionNumbers() {
9
7
  return {
10
8
  title: 'Update version numbers',
11
- task: () => {
12
- return new Listr([resetEmbedderString(), bumpNodeModule()]);
9
+ task: (ctx, task) => {
10
+ return task.newListr([resetEmbedderString(), bumpNodeModule()]);
13
11
  }
14
12
  };
15
13
  };
@@ -1,31 +1,23 @@
1
1
  import {
2
- NEXT_SECURITY_RELEASE_FOLDER,
3
- NEXT_SECURITY_RELEASE_REPOSITORY,
4
2
  checkoutOnSecurityReleaseBranch,
5
3
  checkRemote,
6
4
  commitAndPushVulnerabilitiesJSON,
7
5
  validateDate,
8
6
  pickReport,
9
7
  getReportSeverity,
10
- getSummary
8
+ getSummary,
9
+ SecurityRelease
11
10
  } from './security-release/security-release.js';
12
11
  import fs from 'node:fs';
13
- import path from 'node:path';
14
12
  import auth from './auth.js';
15
13
  import Request from './request.js';
16
14
  import nv from '@pkgjs/nv';
17
15
 
18
- export default class UpdateSecurityRelease {
19
- repository = NEXT_SECURITY_RELEASE_REPOSITORY;
20
- constructor(cli) {
21
- this.cli = cli;
22
- }
23
-
16
+ export default class UpdateSecurityRelease extends SecurityRelease {
24
17
  async sync() {
25
18
  checkRemote(this.cli, this.repository);
26
19
 
27
- const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
28
- const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
20
+ const content = this.readVulnerabilitiesJSON();
29
21
  const credentials = await auth({
30
22
  github: true,
31
23
  h1: true
@@ -52,6 +44,7 @@ export default class UpdateSecurityRelease {
52
44
  prURL
53
45
  };
54
46
  }
47
+ const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
55
48
  fs.writeFileSync(vulnerabilitiesJSONPath, JSON.stringify(content, null, 2));
56
49
  this.cli.ok('Synced vulnerabilities.json with HackerOne');
57
50
  }
@@ -78,22 +71,6 @@ export default class UpdateSecurityRelease {
78
71
  cli.ok('Done!');
79
72
  }
80
73
 
81
- readVulnerabilitiesJSON(vulnerabilitiesJSONPath) {
82
- const exists = fs.existsSync(vulnerabilitiesJSONPath);
83
-
84
- if (!exists) {
85
- this.cli.error(`The file vulnerabilities.json does not exist at ${vulnerabilitiesJSONPath}`);
86
- process.exit(1);
87
- }
88
-
89
- return JSON.parse(fs.readFileSync(vulnerabilitiesJSONPath, 'utf8'));
90
- }
91
-
92
- getVulnerabilitiesJSONPath() {
93
- return path.join(process.cwd(),
94
- NEXT_SECURITY_RELEASE_FOLDER, 'vulnerabilities.json');
95
- }
96
-
97
74
  async updateJSONReleaseDate(releaseDate) {
98
75
  const vulnerabilitiesJSONPath = this.getVulnerabilitiesJSONPath();
99
76
  const content = this.readVulnerabilitiesJSON(vulnerabilitiesJSONPath);
@@ -163,7 +140,7 @@ export default class UpdateSecurityRelease {
163
140
  const programId = await this.getNodeProgramId(req);
164
141
  const cves = await this.promptCVECreation(req, reports, programId);
165
142
  this.assignCVEtoReport(cves, reports);
166
- this.updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath);
143
+ this.updateVulnerabilitiesJSON(content);
167
144
  this.updateHackonerReportCve(req, reports);
168
145
  }
169
146
 
@@ -195,18 +172,6 @@ export default class UpdateSecurityRelease {
195
172
  }
196
173
  }
197
174
 
198
- updateVulnerabilitiesJSON(content, vulnerabilitiesJSONPath) {
199
- this.cli.startSpinner(`Updating vulnerabilities.json from\
200
- ${vulnerabilitiesJSONPath}..`);
201
- const filePath = path.resolve(vulnerabilitiesJSONPath);
202
- fs.writeFileSync(filePath, JSON.stringify(content, null, 2));
203
- // push the changes to the repository
204
- commitAndPushVulnerabilitiesJSON(filePath,
205
- 'chore: updated vulnerabilities.json with CVEs',
206
- { cli: this.cli, repository: this.repository });
207
- this.cli.stopSpinner(`Done updating vulnerabilities.json from ${filePath}`);
208
- }
209
-
210
175
  async promptCVECreation(req, reports, programId) {
211
176
  const supportedVersions = (await nv('supported'));
212
177
  const cves = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "5.3.1",
3
+ "version": "5.5.0",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {