@node-core/utils 5.4.0 → 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.
- package/components/git/release.js +2 -0
- package/components/git/security.js +17 -0
- package/lib/ci/build-types/citgm_comparison_build.js +15 -1
- package/lib/landing_session.js +1 -1
- package/lib/prepare_release.js +133 -73
- package/lib/prepare_security.js +73 -9
- package/lib/request.js +59 -0
- package/lib/security-release/security-release.js +61 -0
- package/lib/security_blog.js +4 -42
- package/lib/update-v8/constants.js +12 -0
- package/lib/update_security_release.js +6 -41
- package/package.json +1 -1
@@ -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() {
|
package/lib/landing_session.js
CHANGED
@@ -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/prepare_release.js
CHANGED
@@ -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 {
|
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
|
-
|
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
|
-
|
35
|
-
|
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 ${
|
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
|
-
|
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
|
342
|
+
newVersion.major++;
|
343
|
+
newVersion.minor = 0;
|
344
|
+
newVersion.patch = 0;
|
386
345
|
} else if (changelog.includes('SEMVER-MINOR') || this.isLTSTransition) {
|
387
|
-
newVersion
|
346
|
+
newVersion.minor++;
|
347
|
+
newVersion.patch = 0;
|
388
348
|
} else {
|
389
|
-
newVersion
|
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
|
-
|
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 {
|
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
|
-
|
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
|
-
|
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,
|
package/lib/prepare_security.js
CHANGED
@@ -5,22 +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
8
|
checkoutOnSecurityReleaseBranch,
|
10
9
|
commitAndPushVulnerabilitiesJSON,
|
11
10
|
validateDate,
|
12
11
|
promptDependencies,
|
13
12
|
getSupportedVersions,
|
14
|
-
pickReport
|
13
|
+
pickReport,
|
14
|
+
SecurityRelease
|
15
15
|
} from './security-release/security-release.js';
|
16
16
|
import _ from 'lodash';
|
17
17
|
|
18
|
-
export default class PrepareSecurityRelease {
|
19
|
-
repository = NEXT_SECURITY_RELEASE_REPOSITORY;
|
18
|
+
export default class PrepareSecurityRelease extends SecurityRelease {
|
20
19
|
title = 'Next Security Release';
|
21
|
-
constructor(cli) {
|
22
|
-
this.cli = cli;
|
23
|
-
}
|
24
20
|
|
25
21
|
async start() {
|
26
22
|
const credentials = await auth({
|
@@ -44,6 +40,40 @@ export default class PrepareSecurityRelease {
|
|
44
40
|
this.cli.ok('Done!');
|
45
41
|
}
|
46
42
|
|
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));
|
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`);
|
74
|
+
this.cli.ok('Done!');
|
75
|
+
}
|
76
|
+
|
47
77
|
async startVulnerabilitiesJSONCreation(releaseDate, content) {
|
48
78
|
// checkout on the next-security-release branch
|
49
79
|
checkoutOnSecurityReleaseBranch(this.cli, this.repository);
|
@@ -163,9 +193,9 @@ export default class PrepareSecurityRelease {
|
|
163
193
|
|
164
194
|
const folderPath = path.join(process.cwd(), NEXT_SECURITY_RELEASE_FOLDER);
|
165
195
|
try {
|
166
|
-
|
196
|
+
fs.accessSync(folderPath);
|
167
197
|
} catch (error) {
|
168
|
-
|
198
|
+
fs.mkdirSync(folderPath, { recursive: true });
|
169
199
|
}
|
170
200
|
|
171
201
|
const fullPath = path.join(folderPath, 'vulnerabilities.json');
|
@@ -254,4 +284,38 @@ export default class PrepareSecurityRelease {
|
|
254
284
|
}
|
255
285
|
return deps;
|
256
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
|
+
}
|
257
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
|
+
}
|
package/lib/security_blog.js
CHANGED
@@ -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
|
-
|
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 =
|
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 =
|
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:', {
|
@@ -324,21 +301,6 @@ export default class SecurityBlog {
|
|
324
301
|
return text.join('\n');
|
325
302
|
}
|
326
303
|
|
327
|
-
getAffectedVersions(content) {
|
328
|
-
const affectedVersions = new Set();
|
329
|
-
for (const report of Object.values(content.reports)) {
|
330
|
-
for (const affectedVersion of report.affectedVersions) {
|
331
|
-
affectedVersions.add(affectedVersion);
|
332
|
-
}
|
333
|
-
}
|
334
|
-
const parseToNumber = str => +(str.match(/[\d.]+/g)[0]);
|
335
|
-
return Array.from(affectedVersions)
|
336
|
-
.sort((a, b) => {
|
337
|
-
return parseToNumber(a) > parseToNumber(b) ? -1 : 1;
|
338
|
-
})
|
339
|
-
.join(', ');
|
340
|
-
}
|
341
|
-
|
342
304
|
getSecurityPreReleaseTemplate() {
|
343
305
|
return fs.readFileSync(
|
344
306
|
new URL(
|
@@ -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,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
|
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
|
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 = [];
|