@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 CHANGED
@@ -115,9 +115,9 @@ const args = yargs(hideBin(process.argv))
115
115
  type: 'number'
116
116
  })
117
117
  .positional('certify-safe', {
118
- describe: 'If not provided, the command will reject PRs that have ' +
119
- 'been pushed since the last review',
120
- type: 'boolean'
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\/node\/pull\/)?(\d+)$/.exec(argv.prid);
115
+ const prID = /^(?:https:\/\/github\.com\/nodejs(-private)?\/node\1\/pull\/)?(\d+)$/.exec(argv.prid);
116
116
  if (prID) {
117
- argv.prid = Number(prID[1]);
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, {}).checkCommitsAfterReviewOrLabel()
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
- async checkCommitsAfterReviewOrLabel() {
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 reviewDate = reviews[reviewIndex].publishedAt;
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 (totalCommits > 0) {
550
+ if (reviewedCommitIndex !== commits.length - 1) {
589
551
  cli.warn('Commits were pushed since the last approving review:');
590
- const sliceLength = maxCommits === 0 ? totalCommits : -maxCommits;
591
- afterCommits.slice(sliceLength)
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
- `--max-commits ${totalCommits}` +
599
- '` to see the full list of commits)';
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 true;
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 };
@@ -92,9 +92,9 @@ export default class ReleasePreparation extends Session {
92
92
  } = this;
93
93
 
94
94
  // Create new proposal branch.
95
- cli.startSpinner(`Creating new proposal branch for ${newVersion}`);
95
+ cli.startSpinner(`Switching to proposal branch for ${newVersion}`);
96
96
  const proposalBranch = await this.createProposalBranch(releaseBranch);
97
- cli.stopSpinner(`Created new proposal branch for ${newVersion}`);
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(`Creating new proposal branch for ${newVersion}`);
203
+ cli.startSpinner(`Switching to proposal branch for ${newVersion}`);
204
204
  await this.createProposalBranch();
205
- cli.stopSpinner(`Created new proposal branch for ${newVersion}`);
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({ onlyNotableChanges: true });
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
- '-b',
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) {
@@ -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 [needle, haystack] = await Promise.all([forceRunAsync(
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
- 'log', '-1',
232
- `refs/tags/v${version}`,
233
- '--format=* **%an** <<%ae>>\n `%GF`'
234
- ], { captureStdout: true }), fs.readFile('README.md')]);
235
- if (haystack.includes(needle)) {
236
- return;
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.warn('Tag was signed with an undocumented identity/key pair!');
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/nodejs/node/pull/${prid}`
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 });
@@ -23,9 +23,6 @@ query PR($prid: Int!, $owner: String!, $repo: String!) {
23
23
  path
24
24
  }
25
25
  },
26
- timelineItems(itemTypes: [HEAD_REF_FORCE_PUSHED_EVENT, PULL_REQUEST_COMMIT]) {
27
- updatedAt
28
- },
29
26
  title,
30
27
  baseRefName,
31
28
  headRefName,
@@ -13,6 +13,9 @@ query Reviews($prid: Int!, $owner: String!, $repo: String!, $after: String) {
13
13
  author {
14
14
  login
15
15
  }
16
+ commit {
17
+ oid
18
+ }
16
19
  authorCanPushToRepository
17
20
  url
18
21
  publishedAt
package/lib/reviews.js CHANGED
@@ -29,6 +29,7 @@ export class Review {
29
29
  * @property {string} bodyText
30
30
  * @property {string} state
31
31
  * @property {{login: string}} author
32
+ * @property {{oid:string}} commit
32
33
  * @property {string} url
33
34
  * @property {string} publishedAt
34
35
  *
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 = [input == null ? 'inherit' : 'pipe', captureStdout ? 'pipe' : 'inherit', 'inherit'];
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-core/utils",
3
- "version": "5.11.0",
3
+ "version": "5.12.1",
4
4
  "description": "Utilities for Node.js core collaborators",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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
- }