@node-core/utils 6.3.3 → 7.0.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.
Files changed (56) hide show
  1. package/README.md +2 -5
  2. package/bin/ncu-ci.js +3 -19
  3. package/components/git/benchmark.js +112 -0
  4. package/components/git/land.js +3 -3
  5. package/components/git/metadata.js +34 -1
  6. package/components/git/release.js +3 -5
  7. package/components/git/security.js +258 -27
  8. package/components/git/v8.js +6 -3
  9. package/components/metadata.js +75 -7
  10. package/lib/auth.js +3 -4
  11. package/lib/backport_session.js +3 -3
  12. package/lib/benchmark.js +243 -0
  13. package/lib/cherry_pick.js +32 -11
  14. package/lib/ci/failure_aggregator.js +16 -20
  15. package/lib/ci/run_ci.js +1 -1
  16. package/lib/cli.js +33 -1
  17. package/lib/config.js +7 -7
  18. package/lib/file.js +5 -5
  19. package/lib/landing_session.js +65 -24
  20. package/lib/links.js +4 -4
  21. package/lib/pr_checker.js +163 -31
  22. package/lib/pr_data.js +1 -1
  23. package/lib/pr_summary.js +1 -1
  24. package/lib/prepare_release.js +270 -85
  25. package/lib/prepare_security.js +367 -41
  26. package/lib/promote_release.js +19 -16
  27. package/lib/queries/ListSecurityReleasePRs.gql +19 -0
  28. package/lib/queries/PR.gql +4 -1
  29. package/lib/request.js +41 -0
  30. package/lib/run.js +5 -5
  31. package/lib/security-announcement.js +10 -5
  32. package/lib/security-release/security-release.js +160 -20
  33. package/lib/security_blog.js +130 -65
  34. package/lib/session.js +4 -3
  35. package/lib/team_info.js +1 -1
  36. package/lib/update-v8/applyNodeChanges.js +1 -3
  37. package/lib/update-v8/backport.js +6 -6
  38. package/lib/update-v8/commitUpdate.js +3 -6
  39. package/lib/update-v8/common.js +30 -6
  40. package/lib/update-v8/constants.js +22 -0
  41. package/lib/update-v8/deps.js +3 -5
  42. package/lib/update-v8/index.js +14 -5
  43. package/lib/update-v8/majorUpdate.js +5 -4
  44. package/lib/update-v8/minorUpdate.js +4 -4
  45. package/lib/update-v8/updateCrates.js +192 -0
  46. package/lib/update-v8/updateV8Clone.js +1 -1
  47. package/lib/update-v8/updateVersionNumbers.js +3 -6
  48. package/lib/update-v8/util.js +3 -3
  49. package/lib/update_security_release.js +347 -105
  50. package/lib/utils.js +5 -5
  51. package/lib/verbosity.js +3 -3
  52. package/lib/wpt/index.js +1 -23
  53. package/package.json +18 -22
  54. package/bin/get-metadata.js +0 -11
  55. package/lib/wpt/templates/README.md +0 -16
  56. package/npm-shrinkwrap.json +0 -20380
package/lib/file.js CHANGED
@@ -8,7 +8,7 @@ export function appendFile(file, content) {
8
8
  }
9
9
  // TODO(joyeecheung): what if the file is a dir?
10
10
  fs.appendFileSync(file, content, 'utf8');
11
- };
11
+ }
12
12
 
13
13
  export function writeFile(file, content) {
14
14
  const parts = path.parse(file);
@@ -17,18 +17,18 @@ export function writeFile(file, content) {
17
17
  }
18
18
  // TODO(joyeecheung): what if the file is a dir?
19
19
  fs.writeFileSync(file, content, 'utf8');
20
- };
20
+ }
21
21
 
22
22
  export function writeJson(file, obj) {
23
23
  writeFile(file, `${JSON.stringify(obj, null, 2)}\n`);
24
- };
24
+ }
25
25
 
26
26
  export function readFile(file) {
27
27
  if (fs.existsSync(file)) {
28
28
  return fs.readFileSync(file, 'utf8');
29
29
  }
30
30
  return '';
31
- };
31
+ }
32
32
 
33
33
  export function readJson(file) {
34
34
  const content = readFile(file);
@@ -36,7 +36,7 @@ export function readJson(file) {
36
36
  return JSON.parse(content);
37
37
  }
38
38
  return {};
39
- };
39
+ }
40
40
 
41
41
  export function removeDirectory(directory) {
42
42
  return fs.promises.rm(directory, { recursive: true, force: true });
@@ -82,7 +82,9 @@ export default class LandingSession extends Session {
82
82
  }
83
83
 
84
84
  async downloadAndPatch() {
85
- const { cli, upstream, prid, expectedCommitShas, crossRepoPR } = this;
85
+ const {
86
+ cli, upstream, prid, expectedCommitShas, crossRepoPR, landedCommitSha
87
+ } = this;
86
88
 
87
89
  if (crossRepoPR) {
88
90
  const { owner, repo } = this;
@@ -107,6 +109,12 @@ export default class LandingSession extends Session {
107
109
  { ignoreFailure: false });
108
110
  }
109
111
  } while (!isHeadAMergeCommit());
112
+ } else if (landedCommitSha) {
113
+ // The PR already landed (e.g. a security fix released publicly), so
114
+ // its merge ref is gone - fetch the commits from the base branch.
115
+ cli.startSpinner(`Downloading landed commits for ${prid}`);
116
+ await forceRunAsync('git', ['fetch', upstream, landedCommitSha],
117
+ { ignoreFailure: false });
110
118
  } else {
111
119
  cli.startSpinner(`Downloading patch for ${prid}`);
112
120
  await runAsync('git', [
@@ -114,29 +122,60 @@ export default class LandingSession extends Session {
114
122
  `refs/pull/${prid}/merge`]);
115
123
  }
116
124
 
117
- // We fetched the commit that would result if we used `git merge`.
118
- // ^1 and ^2 refer to the PR base and the PR head, respectively.
119
- const [base, head] = await runAsync('git',
120
- ['rev-parse', 'FETCH_HEAD^1', 'FETCH_HEAD^2'],
121
- { captureStdout: 'lines' });
125
+ let base, head, rebaseHead;
126
+ if (landedCommitSha && !crossRepoPR) {
127
+ // FETCH_HEAD is the last commit of the PR as landed on the base branch.
128
+ // Walk back while the PR-URL trailer still points to this PR to find
129
+ // the base of the landed series (landing may have squashed commits).
130
+ const prUrlRe = new RegExp(`^PR-URL: \\S+/pull/${prid}$`, 'm');
131
+ let count = 0;
132
+ while (count < expectedCommitShas.length) {
133
+ const message = await runAsync('git',
134
+ ['log', '-1', '--format=%B', `FETCH_HEAD~${count}`],
135
+ { captureStdout: true });
136
+ if (!prUrlRe.test(message)) break;
137
+ count++;
138
+ }
139
+ if (count === 0) {
140
+ cli.error(`Landed commit ${landedCommitSha} does not have a ` +
141
+ `PR-URL trailer pointing to pull request ${prid}`);
142
+ process.exit(1);
143
+ }
144
+ [base, head, rebaseHead] = await runAsync('git',
145
+ ['rev-parse', `FETCH_HEAD~${count}`, 'FETCH_HEAD', 'HEAD'],
146
+ { captureStdout: 'lines' });
147
+ } else {
148
+ // We fetched the commit that would result if we used `git merge`.
149
+ // ^1 and ^2 refer to the PR base and the PR head, respectively.
150
+ [base, head, rebaseHead] = await runAsync('git',
151
+ ['rev-parse', 'FETCH_HEAD^1', 'FETCH_HEAD^2', 'HEAD'],
152
+ { captureStdout: 'lines' });
153
+ }
122
154
  const commitShas = await runAsync('git',
123
155
  ['rev-list', `${base}..${head}`],
124
156
  { captureStdout: 'lines' });
125
157
  cli.stopSpinner(`Fetched commits as ${shortSha(base)}..${shortSha(head)}`);
126
158
  cli.separator();
127
159
 
128
- const mismatchedCommits = [
129
- ...commitShas.filter((sha) => !expectedCommitShas.includes(sha))
130
- .map((sha) => `Unexpected commit ${sha}`),
131
- ...expectedCommitShas.filter((sha) => !commitShas.includes(sha))
132
- .map((sha) => `Missing commit ${sha}`)
133
- ].join('\n');
134
- if (mismatchedCommits.length > 0) {
135
- cli.error(`Mismatched commits:\n${mismatchedCommits}`);
136
- process.exit(1);
160
+ if (landedCommitSha && !crossRepoPR) {
161
+ // The landed commits have different SHAs from the ones in the PR, so
162
+ // they cannot be compared - they were validated by their PR-URL
163
+ // trailers above.
164
+ cli.ok(`Using ${commitShas.length} commit(s) as landed for PR ${prid}`);
165
+ } else {
166
+ const mismatchedCommits = [
167
+ ...commitShas.filter((sha) => !expectedCommitShas.includes(sha))
168
+ .map((sha) => `Unexpected commit ${sha}`),
169
+ ...expectedCommitShas.filter((sha) => !commitShas.includes(sha))
170
+ .map((sha) => `Missing commit ${sha}`)
171
+ ].join('\n');
172
+ if (mismatchedCommits.length > 0) {
173
+ cli.error(`Mismatched commits:\n${mismatchedCommits}`);
174
+ process.exit(1);
175
+ }
137
176
  }
138
177
 
139
- const commitInfo = { base, head, shas: commitShas };
178
+ const commitInfo = { base, head, shas: commitShas, rebaseHead };
140
179
  this.saveCommitInfo(commitInfo);
141
180
 
142
181
  try {
@@ -233,12 +272,11 @@ export default class LandingSession extends Session {
233
272
  // so that it will perform everything automatically.
234
273
  cli.log(`There are ${subjects.length} commits in the PR. ` +
235
274
  'Attempting autorebase.');
236
- const { upstream, branch } = this;
237
275
  const assumeYes = this.cli.assumeYes ? '--yes' : '';
238
276
  const msgAmend = `-x "git node land --amend ${assumeYes}"`;
239
277
  try {
240
278
  await forceRunAsync('git',
241
- ['rebase', ...this.gpgSign, `${upstream}/${branch}`,
279
+ ['rebase', ...this.gpgSign, commitInfo.rebaseHead,
242
280
  '--no-keep-empty', '-i', '--autosquash', msgAmend],
243
281
  {
244
282
  ignoreFailure: false,
@@ -248,7 +286,7 @@ export default class LandingSession extends Session {
248
286
  }
249
287
  });
250
288
  return this.final();
251
- } catch (e) {
289
+ } catch {
252
290
  await runAsync('git', ['rebase', '--abort']);
253
291
  const count = subjects.length;
254
292
  cli.log(`Couldn't rebase ${count} commits in the PR automatically`);
@@ -399,14 +437,14 @@ export default class LandingSession extends Session {
399
437
  if (!cveID) {
400
438
  cveID = await cli.prompt(
401
439
  'Git found no CVE-ID trailer in the original commit message. ' +
402
- 'Please, provide the CVE-ID or leave it empty',
403
- { questionType: 'input', defaultAnswer: 'CVE-2026-XXXXX' }
440
+ 'Please, provide the CVE-ID (e.g. CVE-2026-12345) or leave it empty',
441
+ { questionType: 'input', defaultAnswer: '' }
404
442
  );
405
443
  }
406
444
  }
407
445
  // Some commits might not address a vulnerability, but it is necessary
408
446
  // for the security release to happen.
409
- if (cveID !== '') {
447
+ if (cveID) {
410
448
  amended.push('CVE-ID: ' + cveID);
411
449
  }
412
450
  }
@@ -431,7 +469,10 @@ export default class LandingSession extends Session {
431
469
  const messageFile = this.saveMessage(rev, message);
432
470
  cli.separator('New Message');
433
471
  cli.log(message.trim());
434
- const takeMessage = await cli.prompt('Use this message?');
472
+ // `skipMessagePrompt` is only set by callers that already got a blanket
473
+ // confirmation; it is undefined on a regular landing session.
474
+ const takeMessage = this.skipMessagePrompt ||
475
+ await cli.prompt('Use this message?');
435
476
  if (takeMessage) {
436
477
  await runAsync('git', ['commit', '--amend', '-F', messageFile, ...this.gpgSign]);
437
478
  return true;
@@ -469,7 +510,7 @@ export default class LandingSession extends Session {
469
510
  cli.log('A git rebase/am is in progress.' +
470
511
  ' Please complete it before running git node land --final');
471
512
  return;
472
- };
513
+ }
473
514
 
474
515
  const stray = this.getStrayCommits();
475
516
  if (stray.length > 1) {
package/lib/links.js CHANGED
@@ -100,15 +100,15 @@ export function parsePRFromURL(url) {
100
100
  };
101
101
  }
102
102
  return undefined;
103
- };
103
+ }
104
104
 
105
105
  export function getPrURL({ owner, repo, prid }) {
106
106
  return `https://github.com/${owner}/${repo}/pull/${prid}`;
107
- };
107
+ }
108
108
 
109
109
  export function getMachineUrl(machine) {
110
110
  return `[${machine.hostname}](${machine.url})`;
111
- };
111
+ }
112
112
 
113
113
  const PR_URL_RE = /PR-URL: https:\/\/github.com\/.+/;
114
114
  export function parsePrURL(text) {
@@ -120,4 +120,4 @@ export function parsePrURL(text) {
120
120
  return undefined;
121
121
  }
122
122
  return parsePRFromURL(match[0]);
123
- };
123
+ }
package/lib/pr_checker.js CHANGED
@@ -29,6 +29,30 @@ const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to
29
29
  const FAST_TRACK_MIN_APPROVALS = 2;
30
30
  const GIT_CONFIG_GUIDE_URL = 'https://github.com/nodejs/node/blob/99b1ada/doc/guides/contributing/pull-requests.md#step-1-fork';
31
31
 
32
+ export const PR_CHECK_REASON_CODES = Object.freeze({
33
+ CANCELLED_GITHUB_CI: 'cancelled-github-ci',
34
+ CLOSED: 'closed',
35
+ CONFLICT: 'conflict',
36
+ FAILED_GITHUB_CI: 'failed-github-ci',
37
+ FAILED_JENKINS_CI: 'failed-jenkins-ci',
38
+ INVALID_CI_TYPE: 'invalid-ci-type',
39
+ MERGED: 'merged',
40
+ MISSING_APPROVAL: 'missing-approval',
41
+ MISSING_FAST_TRACK_COMMENT: 'missing-fast-track-comment',
42
+ MISSING_FULL_JENKINS_CI: 'missing-full-jenkins-ci',
43
+ MISSING_GITHUB_CI: 'missing-github-ci',
44
+ MISSING_JENKINS_CI: 'missing-jenkins-ci',
45
+ MISSING_TSC_APPROVAL: 'missing-tsc-approval',
46
+ NEW_CONTRIBUTOR: 'new-contributor',
47
+ NO_COMMITS: 'no-commits',
48
+ PENDING_GITHUB_CI: 'pending-github-ci',
49
+ PENDING_JENKINS_CI: 'pending-jenkins-ci',
50
+ REQUESTED_CHANGES: 'requested-changes',
51
+ STALE_CI: 'stale-ci',
52
+ STALE_REVIEW: 'stale-review',
53
+ WAIT_TIME: 'wait-time'
54
+ });
55
+
32
56
  export default class PRChecker {
33
57
  /**
34
58
  * @param {{}} cli
@@ -51,6 +75,22 @@ export default class PRChecker {
51
75
  this.reviews = reviews;
52
76
  this.commits = commits;
53
77
  this.argv = argv;
78
+ this.reasons = [];
79
+ }
80
+
81
+ addReason(code, message, details = {}) {
82
+ this.reasons.push({
83
+ code,
84
+ message,
85
+ ...details
86
+ });
87
+ }
88
+
89
+ getResult(status) {
90
+ return {
91
+ ready: status,
92
+ reasons: this.reasons
93
+ };
54
94
  }
55
95
 
56
96
  get waitTimeSingleApproval() {
@@ -68,6 +108,8 @@ export default class PRChecker {
68
108
  }
69
109
 
70
110
  async checkAll(checkComments = false, checkCI = true) {
111
+ this.reasons = [];
112
+
71
113
  const status = [
72
114
  this.checkCommitsAfterReview(),
73
115
  this.checkReviewsAndWait(new Date(), checkComments),
@@ -107,14 +149,20 @@ export default class PRChecker {
107
149
  displayReviews(checkComments) {
108
150
  const { cli, reviewers: { requestedChanges, approved } } = this;
109
151
  if (requestedChanges.length > 0) {
110
- cli.error(`Requested Changes: ${requestedChanges.length}`);
152
+ const message = `Requested Changes: ${requestedChanges.length}`;
153
+ cli.error(message);
154
+ this.addReason(PR_CHECK_REASON_CODES.REQUESTED_CHANGES, message, {
155
+ count: requestedChanges.length
156
+ });
111
157
  for (const { reviewer, review } of requestedChanges) {
112
158
  cli.error(this.formatReview(reviewer, review));
113
159
  }
114
160
  }
115
161
 
116
162
  if (approved.length === 0) {
117
- cli.error('Approvals: 0');
163
+ const message = 'Approvals: 0';
164
+ cli.error(message);
165
+ this.addReason(PR_CHECK_REASON_CODES.MISSING_APPROVAL, message);
118
166
  return;
119
167
  }
120
168
 
@@ -166,7 +214,12 @@ export default class PRChecker {
166
214
  .filter((p) => p.reviewer.isTSC())
167
215
  .map((p) => p.reviewer.login);
168
216
  if (tscApproved.length < 2) {
169
- cli.error('semver-major requires at least 2 TSC approvals');
217
+ const message = 'semver-major requires at least 2 TSC approvals';
218
+ cli.error(message);
219
+ this.addReason(PR_CHECK_REASON_CODES.MISSING_TSC_APPROVAL, message, {
220
+ approvals: tscApproved.length,
221
+ required: 2
222
+ });
170
223
  return false; // 7 day rule doesn't matter here
171
224
  }
172
225
  }
@@ -176,7 +229,12 @@ export default class PRChecker {
176
229
  const comment = [...this.comments].reverse().find((c) =>
177
230
  FAST_TRACK_RE.test(c.bodyText));
178
231
  if (!comment) {
179
- cli.error('Unable to find the fast-track request comment.');
232
+ const message = 'Unable to find the fast-track request comment.';
233
+ cli.error(message);
234
+ this.addReason(
235
+ PR_CHECK_REASON_CODES.MISSING_FAST_TRACK_COMMENT,
236
+ message
237
+ );
180
238
  return false;
181
239
  }
182
240
  const [, requester] = comment.bodyText.match(FAST_TRACK_RE);
@@ -221,9 +279,10 @@ export default class PRChecker {
221
279
  if (timeLeftMulti < 0) {
222
280
  return true;
223
281
  }
224
- cli.error(
225
- `This PR needs to wait ${timeToText(timeLeftMulti, 'more')} to land${fastTrackAppendix}`
226
- );
282
+ const message =
283
+ `This PR needs to wait ${timeToText(timeLeftMulti, 'more')} to land${fastTrackAppendix}`;
284
+ cli.error(message);
285
+ this.addReason(PR_CHECK_REASON_CODES.WAIT_TIME, message);
227
286
  return false;
228
287
  }
229
288
 
@@ -231,9 +290,12 @@ export default class PRChecker {
231
290
  if (timeLeftSingle < 0) {
232
291
  return true;
233
292
  }
234
- cli.error(`This PR needs to wait ${timeToText(timeLeftSingle, 'more')} to land (or ${
293
+ const message =
294
+ `This PR needs to wait ${timeToText(timeLeftSingle, 'more')} to land (or ${
235
295
  timeToText(timeLeftMulti < 0 || isFastTracked ? 0 : timeLeftMulti)
236
- } if there is one more approval)${fastTrackAppendix}`);
296
+ } if there is one more approval)${fastTrackAppendix}`;
297
+ cli.error(message);
298
+ this.addReason(PR_CHECK_REASON_CODES.WAIT_TIME, message);
237
299
  return false;
238
300
  }
239
301
  }
@@ -248,8 +310,12 @@ export default class PRChecker {
248
310
  const providers = Object.values(CI_PROVIDERS);
249
311
 
250
312
  if (!providers.includes(ciType)) {
251
- this.cli.error(
252
- `Invalid ciType ${ciType} - must be one of ${providers.join(', ')}`);
313
+ const message =
314
+ `Invalid ciType ${ciType} - must be one of ${providers.join(', ')}`;
315
+ this.cli.error(message);
316
+ this.addReason(PR_CHECK_REASON_CODES.INVALID_CI_TYPE, message, {
317
+ ciType
318
+ });
253
319
  return false;
254
320
  }
255
321
 
@@ -273,12 +339,20 @@ export default class PRChecker {
273
339
 
274
340
  let status = true;
275
341
  if (!ciMap.size) {
276
- cli.error('No Jenkins CI runs detected');
342
+ const message = 'No Jenkins CI runs detected';
343
+ cli.error(message);
344
+ this.addReason(PR_CHECK_REASON_CODES.MISSING_JENKINS_CI, message, {
345
+ provider: 'jenkins'
346
+ });
277
347
  this.CIStatus = false;
278
348
  return false;
279
349
  } else if (!this.hasFullCI(ciMap)) {
280
350
  status = false;
281
- cli.error('No full Jenkins CI runs detected');
351
+ const message = 'No full Jenkins CI runs detected';
352
+ cli.error(message);
353
+ this.addReason(PR_CHECK_REASON_CODES.MISSING_FULL_JENKINS_CI, message, {
354
+ provider: 'jenkins'
355
+ });
282
356
  }
283
357
 
284
358
  let lastCI;
@@ -310,6 +384,10 @@ export default class PRChecker {
310
384
  `${lastCI.typeName} CI run:`;
311
385
 
312
386
  cli.warn(warnMsg);
387
+ this.addReason(PR_CHECK_REASON_CODES.STALE_CI, warnMsg, {
388
+ commits: totalCommits,
389
+ provider: 'jenkins'
390
+ });
313
391
  const sliceLength = maxCommits === 0 ? totalCommits : -maxCommits;
314
392
  afterCommits.slice(sliceLength)
315
393
  .forEach(commit => {
@@ -329,13 +407,21 @@ export default class PRChecker {
329
407
  const { result, failures } = await build.getResults();
330
408
 
331
409
  if (result === 'FAILURE') {
332
- cli.error(
333
- `${failures.length} failure(s) on the last Jenkins CI run`);
410
+ const message =
411
+ `${failures.length} failure(s) on the last Jenkins CI run`;
412
+ cli.error(message);
413
+ this.addReason(PR_CHECK_REASON_CODES.FAILED_JENKINS_CI, message, {
414
+ failures: failures.length,
415
+ provider: 'jenkins'
416
+ });
334
417
  status = false;
335
418
  // NOTE(mmarchini): not sure why PEDING returns null
336
419
  } else if (result === null) {
337
- cli.error(
338
- 'Last Jenkins CI still running');
420
+ const message = 'Last Jenkins CI still running';
421
+ cli.error(message);
422
+ this.addReason(PR_CHECK_REASON_CODES.PENDING_JENKINS_CI, message, {
423
+ provider: 'jenkins'
424
+ });
339
425
  status = false;
340
426
  } else {
341
427
  cli.ok('Last Jenkins CI successful');
@@ -350,7 +436,9 @@ export default class PRChecker {
350
436
  const { cli, commits } = this;
351
437
 
352
438
  if (!commits || commits.length === 0) {
353
- cli.error('No commits detected');
439
+ const message = 'No commits detected';
440
+ cli.error(message);
441
+ this.addReason(PR_CHECK_REASON_CODES.NO_COMMITS, message);
354
442
  return false;
355
443
  }
356
444
 
@@ -361,7 +449,11 @@ export default class PRChecker {
361
449
  this.CIStatus = false;
362
450
  const checkSuites = commit.checkSuites || { nodes: [] };
363
451
  if (!commit.status && checkSuites.nodes.length === 0) {
364
- cli.error('No GitHub CI runs detected');
452
+ const message = 'No GitHub CI runs detected';
453
+ cli.error(message);
454
+ this.addReason(PR_CHECK_REASON_CODES.MISSING_GITHUB_CI, message, {
455
+ provider: 'github'
456
+ });
365
457
  return false;
366
458
  }
367
459
 
@@ -428,13 +520,23 @@ export default class PRChecker {
428
520
 
429
521
  // Report pending jobs
430
522
  if (pendingJobs.length > 0) {
431
- cli.error('GitHub CI is still running');
523
+ const message = 'GitHub CI is still running';
524
+ cli.error(message);
525
+ this.addReason(PR_CHECK_REASON_CODES.PENDING_GITHUB_CI, message, {
526
+ jobs: pendingJobs.length,
527
+ provider: 'github'
528
+ });
432
529
  return false;
433
530
  }
434
531
 
435
532
  // Report failed jobs
436
533
  if (failedJobs.length > 0) {
437
- cli.error(`${failedJobs.length} GitHub CI job(s) failed:`);
534
+ const message = `${failedJobs.length} GitHub CI job(s) failed:`;
535
+ cli.error(message);
536
+ this.addReason(PR_CHECK_REASON_CODES.FAILED_GITHUB_CI, message, {
537
+ jobs: failedJobs.length,
538
+ provider: 'github'
539
+ });
438
540
  for (const job of failedJobs) {
439
541
  const urlInfo = job.url ? ` (${job.url})` : '';
440
542
  cli.error(` - ${job.name}: ${job.conclusion}${urlInfo}`);
@@ -443,7 +545,12 @@ export default class PRChecker {
443
545
 
444
546
  // Report cancelled jobs
445
547
  if (cancelledJobs.length > 0) {
446
- cli.error(`${cancelledJobs.length} GitHub CI job(s) cancelled:`);
548
+ const message = `${cancelledJobs.length} GitHub CI job(s) cancelled:`;
549
+ cli.error(message);
550
+ this.addReason(PR_CHECK_REASON_CODES.CANCELLED_GITHUB_CI, message, {
551
+ jobs: cancelledJobs.length,
552
+ provider: 'github'
553
+ });
447
554
  for (const job of cancelledJobs) {
448
555
  const urlInfo = job.url ? ` (${job.url})` : '';
449
556
  cli.error(` - ${job.name}: ${job.conclusion}${urlInfo}`);
@@ -458,12 +565,21 @@ export default class PRChecker {
458
565
  if (commit.status) {
459
566
  const { state } = commit.status;
460
567
  if (state === 'PENDING') {
461
- cli.error('GitHub CI is still running');
568
+ const message = 'GitHub CI is still running';
569
+ cli.error(message);
570
+ this.addReason(PR_CHECK_REASON_CODES.PENDING_GITHUB_CI, message, {
571
+ provider: 'github'
572
+ });
462
573
  return false;
463
574
  }
464
575
 
465
576
  if (!['SUCCESS', 'EXPECTED'].includes(state)) {
466
- cli.error(`GitHub CI failed with status: ${state}`);
577
+ const message = `GitHub CI failed with status: ${state}`;
578
+ cli.error(message);
579
+ this.addReason(PR_CHECK_REASON_CODES.FAILED_GITHUB_CI, message, {
580
+ provider: 'github',
581
+ state
582
+ });
467
583
  return false;
468
584
  }
469
585
  }
@@ -534,7 +650,9 @@ export default class PRChecker {
534
650
  }
535
651
 
536
652
  const prAuthor = `${pr.author.login}(${pr.author.email})`;
537
- cli.warn(`PR author is a new contributor: @${prAuthor}`);
653
+ const message = `PR author is a new contributor: @${prAuthor}`;
654
+ cli.warn(message);
655
+ this.addReason(PR_CHECK_REASON_CODES.NEW_CONTRIBUTOR, message);
538
656
  for (const c of oddCommits) {
539
657
  const { oid, author } = c.commit;
540
658
  const hash = shortSha(oid);
@@ -594,7 +712,9 @@ export default class PRChecker {
594
712
  const { maxCommits } = argv;
595
713
 
596
714
  if (commits.length === 0) {
597
- cli.warn('No commits detected');
715
+ const message = 'No commits detected';
716
+ cli.warn(message);
717
+ this.addReason(PR_CHECK_REASON_CODES.NO_COMMITS, message);
598
718
  return false;
599
719
  }
600
720
 
@@ -603,7 +723,9 @@ export default class PRChecker {
603
723
  );
604
724
 
605
725
  if (reviewIndex === -1) {
606
- cli.warn('No approving reviews found');
726
+ const message = 'No approving reviews found';
727
+ cli.warn(message);
728
+ this.addReason(PR_CHECK_REASON_CODES.MISSING_APPROVAL, message);
607
729
  return false;
608
730
  }
609
731
 
@@ -611,7 +733,11 @@ export default class PRChecker {
611
733
  .findLastIndex(({ commit }) => commit.oid === reviews[reviewIndex].commit.oid);
612
734
 
613
735
  if (reviewedCommitIndex !== commits.length - 1) {
614
- cli.warn('Commits were pushed since the last approving review:');
736
+ const message = 'Commits were pushed since the last approving review:';
737
+ cli.warn(message);
738
+ this.addReason(PR_CHECK_REASON_CODES.STALE_REVIEW, message, {
739
+ commits: commits.length - reviewedCommitIndex - 1
740
+ });
615
741
  commits.slice(Math.max(reviewedCommitIndex + 1, commits.length - maxCommits))
616
742
  .forEach(({ commit }) => {
617
743
  cli.warn(`- ${commit.messageHeadline}`);
@@ -641,7 +767,9 @@ export default class PRChecker {
641
767
  } = this;
642
768
 
643
769
  if (pr.mergeable && pr.mergeable === CONFLICTING) {
644
- cli.warn('This PR has conflicts that must be resolved');
770
+ const message = 'This PR has conflicts that must be resolved';
771
+ cli.warn(message);
772
+ this.addReason(PR_CHECK_REASON_CODES.CONFLICT, message);
645
773
  return false;
646
774
  }
647
775
 
@@ -656,13 +784,17 @@ export default class PRChecker {
656
784
 
657
785
  if (merged) {
658
786
  const dateStr = new Date(mergedAt).toUTCString();
659
- cli.warn(`This PR was merged on ${dateStr}`);
787
+ const message = `This PR was merged on ${dateStr}`;
788
+ cli.warn(message);
789
+ this.addReason(PR_CHECK_REASON_CODES.MERGED, message);
660
790
  return false;
661
791
  }
662
792
 
663
793
  if (closed) {
664
794
  const dateStr = new Date(closedAt).toUTCString();
665
- cli.warn(`This PR was closed on ${dateStr}`);
795
+ const message = `This PR was closed on ${dateStr}`;
796
+ cli.warn(message);
797
+ this.addReason(PR_CHECK_REASON_CODES.CLOSED, message);
666
798
  return false;
667
799
  }
668
800
 
package/lib/pr_data.js CHANGED
@@ -112,4 +112,4 @@ export default class PRData {
112
112
  const assoc = this.pr.authorAssociation;
113
113
  return assoc === FIRST_TIME_CONTRIBUTOR || assoc === FIRST_TIMER;
114
114
  }
115
- };
115
+ }
package/lib/pr_summary.js CHANGED
@@ -58,5 +58,5 @@ export default class PRSummary {
58
58
  for (const { name, email } of committers.values()) {
59
59
  cli.log(` - ${name} <${email}>`);
60
60
  }
61
- };
61
+ }
62
62
  }