@node-core/utils 5.11.0 → 5.12.1
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/bin/ncu-ci.js +3 -3
- package/components/git/release.js +3 -2
- package/lib/ci/run_ci.js +2 -1
- package/lib/pr_checker.js +19 -53
- package/lib/pr_data.js +0 -10
- package/lib/prepare_release.js +10 -7
- package/lib/promote_release.js +26 -15
- package/lib/queries/PR.gql +0 -3
- package/lib/queries/Reviews.gql +3 -0
- package/lib/reviews.js +1 -0
- package/lib/run.js +14 -3
- package/package.json +1 -1
- package/lib/queries/PRLabeledEvents.gql +0 -19
package/bin/ncu-ci.js
CHANGED
@@ -115,9 +115,9 @@ const args = yargs(hideBin(process.argv))
|
|
115
115
|
type: 'number'
|
116
116
|
})
|
117
117
|
.positional('certify-safe', {
|
118
|
-
describe: '
|
119
|
-
'
|
120
|
-
type: '
|
118
|
+
describe: 'SHA of the commit that is expected to be at the tip of the PR head. ' +
|
119
|
+
'If not provided, the command will use the SHA of the last approved commit.',
|
120
|
+
type: 'string'
|
121
121
|
})
|
122
122
|
.option('owner', {
|
123
123
|
default: '',
|
@@ -112,9 +112,10 @@ function release(state, argv) {
|
|
112
112
|
}
|
113
113
|
|
114
114
|
async function main(state, argv, cli, dir) {
|
115
|
-
const prID = /^(?:https:\/\/github\.com\/nodejs
|
115
|
+
const prID = /^(?:https:\/\/github\.com\/nodejs(-private)?\/node\1\/pull\/)?(\d+)$/.exec(argv.prid);
|
116
116
|
if (prID) {
|
117
|
-
|
117
|
+
if (prID[1]) argv.security = true;
|
118
|
+
argv.prid = Number(prID[2]);
|
118
119
|
}
|
119
120
|
if (state === PREPARE) {
|
120
121
|
const release = new ReleasePreparation(argv, cli, dir);
|
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, {}).
|
30
|
+
(this.certifySafe = new PRChecker(cli, this.prData, request, {}).getApprovedTipOfHead())
|
31
31
|
);
|
32
32
|
}
|
33
33
|
|
@@ -45,6 +45,7 @@ export class RunPRJob {
|
|
45
45
|
payload.append('json', JSON.stringify({
|
46
46
|
parameter: [
|
47
47
|
{ name: 'CERTIFY_SAFE', value: 'on' },
|
48
|
+
{ name: 'COMMIT_SHA_CHECK', value: this.certifySafe },
|
48
49
|
{ name: 'TARGET_GITHUB_ORG', value: this.owner },
|
49
50
|
{ name: 'TARGET_REPO_NAME', value: this.repo },
|
50
51
|
{ name: 'PR_ID', value: this.prid },
|
package/lib/pr_checker.js
CHANGED
@@ -524,38 +524,17 @@ export default class PRChecker {
|
|
524
524
|
return true;
|
525
525
|
}
|
526
526
|
|
527
|
-
|
528
|
-
if (this.checkCommitsAfterReview()) return true;
|
529
|
-
|
530
|
-
await Promise.all([this.data.getLabeledEvents(), this.data.getCollaborators()]);
|
531
|
-
|
532
|
-
const {
|
533
|
-
cli, data, pr
|
534
|
-
} = this;
|
535
|
-
|
536
|
-
const { updatedAt } = pr.timelineItems;
|
537
|
-
const requestCiLabels = data.labeledEvents.findLast(
|
538
|
-
({ createdAt, label: { name } }) => name === 'request-ci' && createdAt > updatedAt
|
539
|
-
);
|
540
|
-
if (requestCiLabels == null) return false;
|
541
|
-
|
542
|
-
const { actor: { login } } = requestCiLabels;
|
543
|
-
const collaborators = Array.from(data.collaborators.values(),
|
544
|
-
(c) => c.login.toLowerCase());
|
545
|
-
if (collaborators.includes(login.toLowerCase())) {
|
546
|
-
cli.info('request-ci label was added by a Collaborator after the last push event.');
|
547
|
-
return true;
|
548
|
-
}
|
549
|
-
|
550
|
-
return false;
|
551
|
-
}
|
552
|
-
|
553
|
-
checkCommitsAfterReview() {
|
527
|
+
getApprovedTipOfHead() {
|
554
528
|
const {
|
555
529
|
commits, reviews, cli, argv
|
556
530
|
} = this;
|
557
531
|
const { maxCommits } = argv;
|
558
532
|
|
533
|
+
if (commits.length === 0) {
|
534
|
+
cli.warn('No commits found');
|
535
|
+
return false;
|
536
|
+
}
|
537
|
+
|
559
538
|
const reviewIndex = reviews.findLastIndex(
|
560
539
|
review => review.authorCanPushToRepository && review.state === 'APPROVED'
|
561
540
|
);
|
@@ -565,45 +544,32 @@ export default class PRChecker {
|
|
565
544
|
return false;
|
566
545
|
}
|
567
546
|
|
568
|
-
const
|
569
|
-
|
570
|
-
const afterCommits = [];
|
571
|
-
commits.forEach((commit) => {
|
572
|
-
commit = commit.commit;
|
573
|
-
if (commit.committedDate > reviewDate) {
|
574
|
-
afterCommits.push(commit);
|
575
|
-
}
|
576
|
-
});
|
577
|
-
|
578
|
-
const totalCommits = afterCommits.length;
|
579
|
-
if (totalCommits === 0 && this.pr.timelineItems.updatedAt > reviewDate) {
|
580
|
-
// Some commits were pushed, but all the commits have a commit date prior
|
581
|
-
// to the last review. It means that either that a long time elapsed
|
582
|
-
// between the commit and the push, or that the clock on the dev machine
|
583
|
-
// is wrong, or the commit date was forged.
|
584
|
-
cli.warn('Something was pushed to the Pull Request branch since the last approving review.');
|
585
|
-
return false;
|
586
|
-
}
|
547
|
+
const reviewedCommitIndex = commits
|
548
|
+
.findLastIndex(({ commit }) => commit.oid === reviews[reviewIndex].commit.oid);
|
587
549
|
|
588
|
-
if (
|
550
|
+
if (reviewedCommitIndex !== commits.length - 1) {
|
589
551
|
cli.warn('Commits were pushed since the last approving review:');
|
590
|
-
|
591
|
-
|
592
|
-
.forEach(commit => {
|
552
|
+
commits.slice(Math.max(reviewedCommitIndex + 1, commits.length - maxCommits))
|
553
|
+
.forEach(({ commit }) => {
|
593
554
|
cli.warn(`- ${commit.messageHeadline}`);
|
594
555
|
});
|
595
556
|
|
557
|
+
const totalCommits = commits.length - reviewedCommitIndex - 1;
|
596
558
|
if (totalCommits > maxCommits) {
|
597
559
|
const infoMsg = '...(use `' +
|
598
|
-
|
599
|
-
|
560
|
+
`--max-commits ${totalCommits}` +
|
561
|
+
'` to see the full list of commits)';
|
600
562
|
cli.warn(infoMsg);
|
601
563
|
}
|
602
564
|
|
603
565
|
return false;
|
604
566
|
}
|
605
567
|
|
606
|
-
return
|
568
|
+
return reviews[reviewIndex].commit.oid;
|
569
|
+
}
|
570
|
+
|
571
|
+
checkCommitsAfterReview() {
|
572
|
+
return !!this.getApprovedTipOfHead();
|
607
573
|
}
|
608
574
|
|
609
575
|
checkMergeableState() {
|
package/lib/pr_data.js
CHANGED
@@ -5,7 +5,6 @@ import {
|
|
5
5
|
} from './user_status.js';
|
6
6
|
|
7
7
|
// lib/queries/*.gql file names
|
8
|
-
const LABELED_EVENTS_QUERY = 'PRLabeledEvents';
|
9
8
|
const PR_QUERY = 'PR';
|
10
9
|
const REVIEWS_QUERY = 'Reviews';
|
11
10
|
const COMMENTS_QUERY = 'PRComments';
|
@@ -34,7 +33,6 @@ export default class PRData {
|
|
34
33
|
this.comments = [];
|
35
34
|
this.commits = [];
|
36
35
|
this.reviewers = [];
|
37
|
-
this.labeledEvents = [];
|
38
36
|
}
|
39
37
|
|
40
38
|
getThread() {
|
@@ -92,14 +90,6 @@ export default class PRData {
|
|
92
90
|
]);
|
93
91
|
}
|
94
92
|
|
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
|
-
|
103
93
|
async getComments() {
|
104
94
|
const { prid, owner, repo, cli, request, prStr } = this;
|
105
95
|
const vars = { prid, owner, repo };
|
package/lib/prepare_release.js
CHANGED
@@ -92,9 +92,9 @@ export default class ReleasePreparation extends Session {
|
|
92
92
|
} = this;
|
93
93
|
|
94
94
|
// Create new proposal branch.
|
95
|
-
cli.startSpinner(`
|
95
|
+
cli.startSpinner(`Switching to proposal branch for ${newVersion}`);
|
96
96
|
const proposalBranch = await this.createProposalBranch(releaseBranch);
|
97
|
-
cli.stopSpinner(`
|
97
|
+
cli.stopSpinner(`Switched to proposal branch for ${newVersion}`);
|
98
98
|
|
99
99
|
const success = await this.cherryPickSecurityPRs(filterLabels);
|
100
100
|
if (!success) {
|
@@ -200,9 +200,9 @@ export default class ReleasePreparation extends Session {
|
|
200
200
|
}
|
201
201
|
|
202
202
|
// Create new proposal branch.
|
203
|
-
cli.startSpinner(`
|
203
|
+
cli.startSpinner(`Switching to proposal branch for ${newVersion}`);
|
204
204
|
await this.createProposalBranch();
|
205
|
-
cli.stopSpinner(`
|
205
|
+
cli.stopSpinner(`Switched to proposal branch for ${newVersion}`);
|
206
206
|
|
207
207
|
if (this.isLTSTransition) {
|
208
208
|
// For releases transitioning into LTS, fetch the new code name.
|
@@ -481,7 +481,10 @@ export default class ReleasePreparation extends Session {
|
|
481
481
|
const data = await fs.readFile(majorChangelogPath, 'utf8');
|
482
482
|
const arr = data.split('\n');
|
483
483
|
const allCommits = this.getChangelog();
|
484
|
-
const notableChanges = await this.getBranchDiff({
|
484
|
+
const notableChanges = await this.getBranchDiff({
|
485
|
+
onlyNotableChanges: true,
|
486
|
+
format: isSecurityRelease ? 'messageonly' : 'markdown',
|
487
|
+
});
|
485
488
|
let releaseHeader = `## ${date}, Version ${newVersion}` +
|
486
489
|
` ${releaseInfo}, @${username}\n`;
|
487
490
|
if (isSecurityRelease) {
|
@@ -540,7 +543,7 @@ export default class ReleasePreparation extends Session {
|
|
540
543
|
|
541
544
|
await runAsync('git', [
|
542
545
|
'checkout',
|
543
|
-
'-
|
546
|
+
'-B',
|
544
547
|
proposalBranch,
|
545
548
|
base
|
546
549
|
]);
|
@@ -619,7 +622,7 @@ export default class ReleasePreparation extends Session {
|
|
619
622
|
|
620
623
|
const notableChanges = await this.getBranchDiff({
|
621
624
|
onlyNotableChanges: true,
|
622
|
-
format: 'plaintext'
|
625
|
+
format: isSecurityRelease ? 'messageonly' : 'plaintext'
|
623
626
|
});
|
624
627
|
messageBody.push('Notable changes:\n\n');
|
625
628
|
if (isLTSTransition) {
|
package/lib/promote_release.js
CHANGED
@@ -19,6 +19,10 @@ export default class ReleasePromotion extends Session {
|
|
19
19
|
constructor(argv, req, cli, dir) {
|
20
20
|
super(cli, dir, argv.prid);
|
21
21
|
this.req = req;
|
22
|
+
if (argv.security) {
|
23
|
+
this.config.owner = 'nodejs-private';
|
24
|
+
this.config.repo = 'node-private';
|
25
|
+
}
|
22
26
|
this.dryRun = !argv.run;
|
23
27
|
this.isLTS = false;
|
24
28
|
this.ltsCodename = '';
|
@@ -226,20 +230,28 @@ export default class ReleasePromotion extends Session {
|
|
226
230
|
|
227
231
|
async verifyTagSignature() {
|
228
232
|
const { cli, version } = this;
|
229
|
-
const [
|
233
|
+
const verifyTagPattern = /gpg:[^\n]+\ngpg:\s+using RSA key ([^\n]+)\ngpg:\s+issuer "([^"]+)"\ngpg:\s+Good signature from "([^<]+) <\2>"/;
|
234
|
+
const [verifyTagOutput, haystack] = await Promise.all([forceRunAsync(
|
230
235
|
'git', ['--no-pager',
|
231
|
-
'
|
232
|
-
`
|
233
|
-
|
234
|
-
|
235
|
-
if (
|
236
|
-
|
236
|
+
'verify-tag',
|
237
|
+
`v${version}`
|
238
|
+
], { ignoreFailure: false, captureStderr: true }), fs.readFile('README.md')]);
|
239
|
+
const match = verifyTagPattern.exec(verifyTagOutput);
|
240
|
+
if (match == null) {
|
241
|
+
cli.warn('git was not able to verify the tag:');
|
242
|
+
cli.info(verifyTagOutput);
|
243
|
+
} else {
|
244
|
+
const [, keyID, email, name] = match;
|
245
|
+
const needle = `* **${name}** <<${email}>>\n ${'`'}${keyID}${'`'}`;
|
246
|
+
if (haystack.includes(needle)) {
|
247
|
+
return;
|
248
|
+
}
|
249
|
+
cli.warn('Tag was signed with an undocumented identity/key pair!');
|
250
|
+
cli.info('Expected to find the following entry in the README:');
|
251
|
+
cli.info(needle);
|
252
|
+
cli.info('If you are using a subkey, it might be OK.');
|
237
253
|
}
|
238
|
-
cli.
|
239
|
-
cli.info('Expected to find the following entry in the README:');
|
240
|
-
cli.info(needle);
|
241
|
-
cli.info('If you are using a subkey, it might be OK.');
|
242
|
-
cli.info(`Otherwise consider removing the tag (git tag -d v${version
|
254
|
+
cli.info(`If that doesn't sound right, consider removing the tag (git tag -d v${version
|
243
255
|
}), check your local config, and start the process over.`);
|
244
256
|
if (!await cli.prompt('Do you want to proceed anyway?', { defaultAnswer: false })) {
|
245
257
|
throw new Error('Aborted');
|
@@ -383,7 +395,6 @@ export default class ReleasePromotion extends Session {
|
|
383
395
|
{ cause: err }
|
384
396
|
);
|
385
397
|
}
|
386
|
-
await forceRunAsync('git', ['tag', '--verify', `v${version}`], { ignoreFailure: false });
|
387
398
|
this.cli.info('Using the existing tag');
|
388
399
|
}
|
389
400
|
}
|
@@ -391,7 +402,7 @@ export default class ReleasePromotion extends Session {
|
|
391
402
|
// Set up the branch so that nightly builds are produced with the next
|
392
403
|
// version number and a pre-release tag.
|
393
404
|
async setupForNextRelease() {
|
394
|
-
const { versionComponents, prid } = this;
|
405
|
+
const { versionComponents, prid, owner, repo } = this;
|
395
406
|
|
396
407
|
// Update node_version.h for next patch release.
|
397
408
|
const filePath = path.resolve('src', 'node_version.h');
|
@@ -422,7 +433,7 @@ export default class ReleasePromotion extends Session {
|
|
422
433
|
'-m',
|
423
434
|
`Working on ${workingOnVersion}`,
|
424
435
|
'-m',
|
425
|
-
`PR-URL: https://github.com/
|
436
|
+
`PR-URL: https://github.com/${owner}/${repo}/pull/${prid}`
|
426
437
|
], { ignoreFailure: false });
|
427
438
|
const workingOnNewReleaseCommit = await forceRunAsync('git', ['rev-parse', 'HEAD'],
|
428
439
|
{ ignoreFailure: false, captureStdout: true });
|
package/lib/queries/PR.gql
CHANGED
package/lib/queries/Reviews.gql
CHANGED
package/lib/reviews.js
CHANGED
package/lib/run.js
CHANGED
@@ -12,14 +12,19 @@ function runAsyncBase(cmd, args, {
|
|
12
12
|
ignoreFailure = true,
|
13
13
|
spawnArgs,
|
14
14
|
input,
|
15
|
+
captureStderr = false,
|
15
16
|
captureStdout = false
|
16
17
|
} = {}) {
|
17
18
|
if (cmd instanceof URL) {
|
18
19
|
cmd = fileURLToPath(cmd);
|
19
20
|
}
|
20
21
|
let stdio = 'inherit';
|
21
|
-
if (captureStdout || input != null) {
|
22
|
-
stdio = [
|
22
|
+
if (captureStderr || captureStdout || input != null) {
|
23
|
+
stdio = [
|
24
|
+
input == null ? 'inherit' : 'pipe',
|
25
|
+
captureStdout ? 'pipe' : 'inherit',
|
26
|
+
captureStderr ? 'pipe' : 'inherit'
|
27
|
+
];
|
23
28
|
}
|
24
29
|
return new Promise((resolve, reject) => {
|
25
30
|
const opt = Object.assign({
|
@@ -30,6 +35,12 @@ function runAsyncBase(cmd, args, {
|
|
30
35
|
debuglog('[Spawn]', `${cmd} ${(args || []).join(' ')}`, opt);
|
31
36
|
}
|
32
37
|
const child = spawn(cmd, args, opt);
|
38
|
+
let stderr;
|
39
|
+
if (!captureStdout && captureStderr) {
|
40
|
+
stderr = '';
|
41
|
+
child.stderr.setEncoding('utf8');
|
42
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
43
|
+
}
|
33
44
|
let stdout;
|
34
45
|
if (captureStdout) {
|
35
46
|
stdout = '';
|
@@ -51,7 +62,7 @@ function runAsyncBase(cmd, args, {
|
|
51
62
|
stdout = stdout.split(/\r?\n/g);
|
52
63
|
if (stdout[stdout.length - 1] === '') stdout.pop();
|
53
64
|
}
|
54
|
-
return resolve(stdout);
|
65
|
+
return resolve(stdout ?? stderr);
|
55
66
|
});
|
56
67
|
if (input != null) child.stdin.end(input);
|
57
68
|
});
|
package/package.json
CHANGED
@@ -1,19 +0,0 @@
|
|
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
|
-
}
|