@node-core/utils 4.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 (98) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +158 -0
  3. package/bin/get-metadata.js +11 -0
  4. package/bin/git-node.js +30 -0
  5. package/bin/ncu-ci.js +600 -0
  6. package/bin/ncu-config.js +101 -0
  7. package/bin/ncu-team.js +76 -0
  8. package/components/git/backport.js +70 -0
  9. package/components/git/epilogue.js +18 -0
  10. package/components/git/land.js +223 -0
  11. package/components/git/metadata.js +94 -0
  12. package/components/git/release.js +99 -0
  13. package/components/git/security.js +35 -0
  14. package/components/git/status.js +32 -0
  15. package/components/git/sync.js +24 -0
  16. package/components/git/v8.js +121 -0
  17. package/components/git/vote.js +84 -0
  18. package/components/git/wpt.js +87 -0
  19. package/components/metadata.js +49 -0
  20. package/lib/auth.js +133 -0
  21. package/lib/backport_session.js +302 -0
  22. package/lib/cache.js +107 -0
  23. package/lib/cherry_pick.js +304 -0
  24. package/lib/ci/build-types/benchmark_run.js +72 -0
  25. package/lib/ci/build-types/citgm_build.js +194 -0
  26. package/lib/ci/build-types/citgm_comparison_build.js +174 -0
  27. package/lib/ci/build-types/commit_build.js +112 -0
  28. package/lib/ci/build-types/daily_build.js +24 -0
  29. package/lib/ci/build-types/fanned_build.js +87 -0
  30. package/lib/ci/build-types/health_build.js +63 -0
  31. package/lib/ci/build-types/job.js +114 -0
  32. package/lib/ci/build-types/linter_build.js +35 -0
  33. package/lib/ci/build-types/normal_build.js +89 -0
  34. package/lib/ci/build-types/pr_build.js +101 -0
  35. package/lib/ci/build-types/test_build.js +186 -0
  36. package/lib/ci/build-types/test_run.js +41 -0
  37. package/lib/ci/ci_failure_parser.js +325 -0
  38. package/lib/ci/ci_type_parser.js +203 -0
  39. package/lib/ci/ci_utils.js +106 -0
  40. package/lib/ci/failure_aggregator.js +152 -0
  41. package/lib/ci/jenkins_constants.js +28 -0
  42. package/lib/ci/run_ci.js +120 -0
  43. package/lib/cli.js +192 -0
  44. package/lib/collaborators.js +140 -0
  45. package/lib/config.js +72 -0
  46. package/lib/figures.js +7 -0
  47. package/lib/file.js +43 -0
  48. package/lib/github/templates/next-security-release.md +97 -0
  49. package/lib/github/tree.js +162 -0
  50. package/lib/landing_session.js +506 -0
  51. package/lib/links.js +123 -0
  52. package/lib/mergeable_state.js +3 -0
  53. package/lib/metadata_gen.js +61 -0
  54. package/lib/pr_checker.js +605 -0
  55. package/lib/pr_data.js +115 -0
  56. package/lib/pr_summary.js +62 -0
  57. package/lib/prepare_release.js +772 -0
  58. package/lib/prepare_security.js +117 -0
  59. package/lib/proxy.js +21 -0
  60. package/lib/queries/DefaultBranchRef.gql +8 -0
  61. package/lib/queries/LastCommit.gql +16 -0
  62. package/lib/queries/PR.gql +37 -0
  63. package/lib/queries/PRComments.gql +27 -0
  64. package/lib/queries/PRCommits.gql +45 -0
  65. package/lib/queries/PRs.gql +25 -0
  66. package/lib/queries/Reviews.gql +23 -0
  67. package/lib/queries/SearchIssue.gql +51 -0
  68. package/lib/queries/Team.gql +22 -0
  69. package/lib/queries/TreeEntries.gql +12 -0
  70. package/lib/queries/VotePRInfo.gql +28 -0
  71. package/lib/release/utils.js +53 -0
  72. package/lib/request.js +185 -0
  73. package/lib/review_state.js +5 -0
  74. package/lib/reviews.js +178 -0
  75. package/lib/run.js +106 -0
  76. package/lib/session.js +415 -0
  77. package/lib/sync_session.js +15 -0
  78. package/lib/team_info.js +95 -0
  79. package/lib/update-v8/applyNodeChanges.js +49 -0
  80. package/lib/update-v8/backport.js +258 -0
  81. package/lib/update-v8/commitUpdate.js +26 -0
  82. package/lib/update-v8/common.js +35 -0
  83. package/lib/update-v8/constants.js +86 -0
  84. package/lib/update-v8/index.js +56 -0
  85. package/lib/update-v8/majorUpdate.js +171 -0
  86. package/lib/update-v8/minorUpdate.js +105 -0
  87. package/lib/update-v8/updateMaintainingDependencies.js +34 -0
  88. package/lib/update-v8/updateV8Clone.js +53 -0
  89. package/lib/update-v8/updateVersionNumbers.js +122 -0
  90. package/lib/update-v8/util.js +62 -0
  91. package/lib/user.js +4 -0
  92. package/lib/user_status.js +5 -0
  93. package/lib/utils.js +66 -0
  94. package/lib/verbosity.js +26 -0
  95. package/lib/voting_session.js +136 -0
  96. package/lib/wpt/index.js +243 -0
  97. package/lib/wpt/templates/README.md +16 -0
  98. package/package.json +69 -0
@@ -0,0 +1,605 @@
1
+ import {
2
+ REVIEW_SOURCES
3
+ } from './reviews.js';
4
+ import {
5
+ CONFLICTING
6
+ } from './mergeable_state.js';
7
+ import {
8
+ shortSha
9
+ } from './utils.js';
10
+ import {
11
+ JobParser,
12
+ CI_TYPES,
13
+ CI_PROVIDERS,
14
+ isFullCI
15
+ } from './ci/ci_type_parser.js';
16
+ import { PRBuild } from './ci/build-types/pr_build.js';
17
+
18
+ const { FROM_COMMENT, FROM_REVIEW_COMMENT } = REVIEW_SOURCES;
19
+
20
+ const SECOND = 1000;
21
+ const MINUTE = SECOND * 60;
22
+ const HOUR = MINUTE * 60;
23
+
24
+ const WAIT_TIME_MULTI_APPROVAL = 24 * 2;
25
+ const WAIT_TIME_SINGLE_APPROVAL = 24 * 7;
26
+
27
+ const GITHUB_SUCCESS_CONCLUSIONS = ['SUCCESS', 'NEUTRAL', 'SKIPPED'];
28
+
29
+ const FAST_TRACK_RE = /^Fast-track has been requested by @(.+?)\. Please 👍 to approve\.$/;
30
+ const FAST_TRACK_MIN_APPROVALS = 2;
31
+ const GIT_CONFIG_GUIDE_URL = 'https://github.com/nodejs/node/blob/99b1ada/doc/guides/contributing/pull-requests.md#step-1-fork';
32
+
33
+ // eslint-disable-next-line no-extend-native
34
+ Array.prototype.findLastIndex ??= function findLastIndex(fn) {
35
+ const reversedIndex = Reflect.apply(
36
+ Array.prototype.findIndex,
37
+ this.slice().reverse(),
38
+ arguments);
39
+ return reversedIndex === -1 ? -1 : this.length - reversedIndex - 1;
40
+ };
41
+
42
+ export default class PRChecker {
43
+ /**
44
+ * @param {{}} cli
45
+ * @param {PRData} data
46
+ */
47
+ constructor(cli, data, request, argv) {
48
+ this.cli = cli;
49
+ this.request = request;
50
+ this.data = data;
51
+ const {
52
+ pr, reviewers, comments, reviews, commits, collaborators
53
+ } = data;
54
+ this.reviewers = reviewers;
55
+ this.pr = pr;
56
+ this.comments = comments;
57
+ // this.reviews and this.commits must
58
+ // be in order as received from github api
59
+ // to check if new commits were pushed after
60
+ // the last review.
61
+ this.reviews = reviews;
62
+ this.commits = commits;
63
+ this.argv = argv;
64
+ this.collaboratorEmails = new Set(
65
+ Array.from(collaborators).map((c) => c[1].email)
66
+ );
67
+ }
68
+
69
+ get waitTimeSingleApproval() {
70
+ if (this.argv.waitTimeSingleApproval === undefined) {
71
+ return WAIT_TIME_SINGLE_APPROVAL;
72
+ }
73
+ return this.argv.waitTimeSingleApproval;
74
+ }
75
+
76
+ get waitTimeMultiApproval() {
77
+ if (this.argv.waitTimeMultiApproval === undefined) {
78
+ return WAIT_TIME_MULTI_APPROVAL;
79
+ }
80
+ return this.argv.waitTimeMultiApproval;
81
+ }
82
+
83
+ async checkAll(checkComments = false, checkCI = true) {
84
+ const status = [
85
+ this.checkCommitsAfterReview(),
86
+ this.checkReviewsAndWait(new Date(), checkComments),
87
+ this.checkMergeableState(),
88
+ this.checkPRState(),
89
+ this.checkGitConfig()
90
+ ];
91
+
92
+ if (checkCI) {
93
+ status.push(await this.checkCI());
94
+ }
95
+
96
+ if (this.data.authorIsNew()) {
97
+ status.push(this.checkAuthor());
98
+ }
99
+
100
+ // TODO: check for pre-backport, Github API v4
101
+ // does not support reading files changed
102
+
103
+ return status.every((i) => i);
104
+ }
105
+
106
+ getTSC(people) {
107
+ return people
108
+ .filter((p) => p.reviewer.isTSC())
109
+ .map((p) => p.reviewer.login);
110
+ }
111
+
112
+ formatReview(reviewer, review) {
113
+ let hint = '';
114
+ if (reviewer.isTSC()) {
115
+ hint = ' (TSC)';
116
+ }
117
+ return `- ${reviewer.getName()}${hint}: ${review.ref}`;
118
+ }
119
+
120
+ displayReviews(checkComments) {
121
+ const { cli, reviewers: { requestedChanges, approved } } = this;
122
+ if (requestedChanges.length > 0) {
123
+ cli.error(`Requested Changes: ${requestedChanges.length}`);
124
+ for (const { reviewer, review } of requestedChanges) {
125
+ cli.error(this.formatReview(reviewer, review));
126
+ }
127
+ }
128
+
129
+ if (approved.length === 0) {
130
+ cli.error('Approvals: 0');
131
+ return;
132
+ }
133
+
134
+ cli.ok(`Approvals: ${approved.length}`);
135
+ for (const { reviewer, review } of approved) {
136
+ cli.ok(this.formatReview(reviewer, review));
137
+ if (checkComments &&
138
+ [FROM_COMMENT, FROM_REVIEW_COMMENT].includes(review.source)) {
139
+ cli.info(`- ${reviewer.getName()} approved in via LGTM in comments`);
140
+ }
141
+ }
142
+ }
143
+
144
+ checkReviewsAndWait(now, checkComments) {
145
+ const {
146
+ pr, cli, reviewers
147
+ } = this;
148
+ const { requestedChanges, approved } = reviewers;
149
+ const labels = pr.labels.nodes.map((l) => l.name);
150
+
151
+ let isFastTracked = labels.includes('fast-track');
152
+ const isCodeAndLearn = labels.includes('code-and-learn');
153
+ const isSemverMajor = labels.includes('semver-major');
154
+
155
+ const dateStr = new Date(pr.createdAt).toUTCString();
156
+ cli.info(`This PR was created on ${dateStr}`);
157
+ this.displayReviews(checkComments);
158
+ // NOTE: a semver-major PR with fast-track should have either one of
159
+ // these labels removed because that doesn't make sense
160
+ if (isFastTracked) {
161
+ cli.info('This PR is being fast-tracked');
162
+ } else if (isCodeAndLearn) {
163
+ cli.info('This PR is being fast-tracked because ' +
164
+ 'it is from a Code and Learn event');
165
+ }
166
+
167
+ if (approved.length === 0 || requestedChanges.length > 0) {
168
+ return false;
169
+ }
170
+
171
+ if (isSemverMajor) {
172
+ const tscApproved = approved
173
+ .filter((p) => p.reviewer.isTSC())
174
+ .map((p) => p.reviewer.login);
175
+ if (tscApproved.length < 2) {
176
+ cli.error('semver-major requires at least 2 TSC approvals');
177
+ return false; // 7 day rule doesn't matter here
178
+ }
179
+ }
180
+
181
+ let fastTrackAppendix = '';
182
+ if (isFastTracked) {
183
+ const comment = [...this.comments].reverse().find((c) =>
184
+ FAST_TRACK_RE.test(c.bodyText));
185
+ if (!comment) {
186
+ cli.error('Unable to find the fast-track request comment.');
187
+ return false;
188
+ }
189
+ const [, requester] = comment.bodyText.match(FAST_TRACK_RE);
190
+ const collaborators = Array.from(this.data.collaborators.values(),
191
+ (c) => c.login.toLowerCase());
192
+ const approvals = comment.reactions.nodes.filter((r) =>
193
+ r.user.login !== requester &&
194
+ r.user.login !== pr.author.login &&
195
+ collaborators.includes(r.user.login.toLowerCase())).length;
196
+
197
+ const missingFastTrackApprovals = FAST_TRACK_MIN_APPROVALS - approvals -
198
+ (requester === pr.author.login ? 0 : 1);
199
+ if (missingFastTrackApprovals > 0) {
200
+ isFastTracked = false;
201
+ fastTrackAppendix = ' (or 0 hours if there ' +
202
+ `${missingFastTrackApprovals === 1 ? 'is' : 'are'} ` +
203
+ `${missingFastTrackApprovals} more approval` +
204
+ `${missingFastTrackApprovals === 1 ? '' : 's'} (👍) of ` +
205
+ 'the fast-track request from collaborators).';
206
+ }
207
+ }
208
+
209
+ const createTime = new Date(this.pr.createdAt);
210
+ const msFromCreateTime = now.getTime() - createTime.getTime();
211
+ const minutesFromCreateTime = Math.ceil(msFromCreateTime / MINUTE);
212
+ const hoursFromCreateTime = Math.ceil(msFromCreateTime / HOUR);
213
+ let timeLeftMulti = this.waitTimeMultiApproval - hoursFromCreateTime;
214
+ const timeLeftSingle = this.waitTimeSingleApproval - hoursFromCreateTime;
215
+
216
+ if (approved.length >= 2) {
217
+ if (isFastTracked || isCodeAndLearn) {
218
+ return true;
219
+ }
220
+ if (timeLeftMulti < 0) {
221
+ return true;
222
+ }
223
+ if (timeLeftMulti === 0) {
224
+ const timeLeftMins =
225
+ this.waitTimeMultiApproval * 60 - minutesFromCreateTime;
226
+ cli.error(`This PR needs to wait ${timeLeftMins} ` +
227
+ `more minutes to land${fastTrackAppendix}`);
228
+ return false;
229
+ }
230
+ cli.error(`This PR needs to wait ${timeLeftMulti} more ` +
231
+ `hours to land${fastTrackAppendix}`);
232
+ return false;
233
+ }
234
+
235
+ if (approved.length === 1) {
236
+ if (timeLeftSingle < 0) {
237
+ return true;
238
+ }
239
+ timeLeftMulti = timeLeftMulti < 0 || isFastTracked ? 0 : timeLeftMulti;
240
+ cli.error(`This PR needs to wait ${timeLeftSingle} more hours to land ` +
241
+ `(or ${timeLeftMulti} hours if there is one more approval)` +
242
+ fastTrackAppendix);
243
+ return false;
244
+ }
245
+ }
246
+
247
+ hasFullCI(ciMap) {
248
+ const cis = [...ciMap.keys()];
249
+ return cis.find(isFullCI);
250
+ }
251
+
252
+ async checkCI() {
253
+ const ciType = this.argv.ciType || CI_PROVIDERS.NODEJS;
254
+ const providers = Object.values(CI_PROVIDERS);
255
+
256
+ if (!providers.includes(ciType)) {
257
+ this.cli.error(
258
+ `Invalid ciType ${ciType} - must be one of ${providers.join(', ')}`);
259
+ return false;
260
+ }
261
+
262
+ let status = false;
263
+ if (ciType === CI_PROVIDERS.NODEJS) {
264
+ status = await this.checkNodejsCI();
265
+ } else if (ciType === CI_PROVIDERS.GITHUB) {
266
+ status = this.checkGitHubCI();
267
+ }
268
+
269
+ return status;
270
+ }
271
+
272
+ // TODO: we might want to check CI status when it's less flaky...
273
+ // TODO: not all PR requires CI...labels?
274
+ async checkJenkinsCI() {
275
+ const { cli, commits, request, argv } = this;
276
+ const { maxCommits } = argv;
277
+ const thread = this.data.getThread();
278
+ const ciMap = new JobParser(thread).parse();
279
+
280
+ let status = true;
281
+ if (!ciMap.size) {
282
+ cli.error('No Jenkins CI runs detected');
283
+ this.CIStatus = false;
284
+ return false;
285
+ } else if (!this.hasFullCI(ciMap)) {
286
+ status = false;
287
+ cli.error('No full Jenkins CI runs detected');
288
+ }
289
+
290
+ let lastCI;
291
+ for (const [type, ci] of ciMap) {
292
+ const name = CI_TYPES.get(type).name;
293
+ cli.info(`Last ${name} CI on ${ci.date}: ${ci.link}`);
294
+ if (!lastCI || lastCI.date < ci.date) {
295
+ lastCI = {
296
+ typeName: name,
297
+ date: ci.date,
298
+ jobId: ci.jobid
299
+ };
300
+ }
301
+ }
302
+
303
+ if (lastCI) {
304
+ const afterCommits = [];
305
+ commits.forEach((commit) => {
306
+ commit = commit.commit;
307
+ if (commit.committedDate > lastCI.date) {
308
+ status = false;
309
+ afterCommits.push(commit);
310
+ }
311
+ });
312
+
313
+ const totalCommits = afterCommits.length;
314
+ if (totalCommits > 0) {
315
+ const warnMsg = 'Commits were pushed after the last ' +
316
+ `${lastCI.typeName} CI run:`;
317
+
318
+ cli.warn(warnMsg);
319
+ const sliceLength = maxCommits === 0 ? totalCommits : -maxCommits;
320
+ afterCommits.slice(sliceLength)
321
+ .forEach(commit => {
322
+ cli.warn(`- ${commit.messageHeadline}`);
323
+ });
324
+
325
+ if (totalCommits > maxCommits) {
326
+ const infoMsg = '...(use `' +
327
+ `--max-commits ${totalCommits}` +
328
+ '` to see the full list of commits)';
329
+ cli.warn(infoMsg);
330
+ }
331
+ }
332
+
333
+ // Check the last CI run for its results.
334
+ const build = new PRBuild(cli, request, lastCI.jobId);
335
+ const { result, failures } = await build.getResults();
336
+
337
+ if (result === 'FAILURE') {
338
+ cli.error(
339
+ `${failures.length} failure(s) on the last Jenkins CI run`);
340
+ status = false;
341
+ // NOTE(mmarchini): not sure why PEDING returns null
342
+ } else if (result === null) {
343
+ cli.error(
344
+ 'Last Jenkins CI still running');
345
+ status = false;
346
+ } else {
347
+ cli.ok('Last Jenkins CI successful');
348
+ }
349
+ }
350
+
351
+ this.CIStatus = status;
352
+ return status;
353
+ }
354
+
355
+ checkGitHubCI() {
356
+ const { cli, commits } = this;
357
+
358
+ if (!commits || commits.length === 0) {
359
+ cli.error('No commits detected');
360
+ return false;
361
+ }
362
+
363
+ // NOTE(mmarchini): we only care about the last commit. Maybe in the future
364
+ // we'll want to check all commits for a successful CI.
365
+ const { commit } = commits[commits.length - 1];
366
+
367
+ this.CIStatus = false;
368
+ const checkSuites = commit.checkSuites || { nodes: [] };
369
+ if (!commit.status && checkSuites.nodes.length === 0) {
370
+ cli.error('No GitHub CI runs detected');
371
+ return false;
372
+ }
373
+
374
+ // GitHub new Check API
375
+ for (const { status, conclusion, app } of checkSuites.nodes) {
376
+ if (app && app.slug === 'dependabot') {
377
+ // Ignore Dependabot check suites. They are expected to show up
378
+ // sometimes and never complete.
379
+ continue;
380
+ }
381
+
382
+ if (status !== 'COMPLETED') {
383
+ cli.error('GitHub CI is still running');
384
+ return false;
385
+ }
386
+
387
+ if (!GITHUB_SUCCESS_CONCLUSIONS.includes(conclusion)) {
388
+ cli.error('Last GitHub CI failed');
389
+ return false;
390
+ }
391
+ }
392
+
393
+ // GitHub old commit status API
394
+ if (commit.status) {
395
+ const { state } = commit.status;
396
+ if (state === 'PENDING') {
397
+ cli.error('GitHub CI is still running');
398
+ return false;
399
+ }
400
+
401
+ if (!['SUCCESS', 'EXPECTED'].includes(state)) {
402
+ cli.error('Last GitHub CI failed');
403
+ return false;
404
+ }
405
+ }
406
+
407
+ cli.ok('Last GitHub CI successful');
408
+ this.CIStatus = true;
409
+ return true;
410
+ }
411
+
412
+ requiresJenkinsRun() {
413
+ const { pr } = this;
414
+
415
+ // NOTE(mmarchini): if files not present, fallback
416
+ // to old behavior. This should only be the case on old tests
417
+ // TODO(mmarchini): add files to all fixtures on old tests
418
+ if (!pr.files) {
419
+ return false;
420
+ }
421
+
422
+ const files = pr.files.nodes;
423
+
424
+ // Don't require Jenkins run for doc-only change.
425
+ if (files.every(({ path }) => path.endsWith('.md'))) {
426
+ return false;
427
+ }
428
+
429
+ const ciNeededFolderRx = /^(deps|lib|src|test)\//;
430
+ const ciNeededToolFolderRx =
431
+ /^tools\/(code_cache|gyp|icu|inspector|msvs|snapshot|v8_gypfiles)/;
432
+ const ciNeededFileRx = /^tools\/\.+.py$/;
433
+ const ciNeededFileList = [
434
+ 'tools/build-addons.js',
435
+ 'configure',
436
+ 'configure.py',
437
+ 'Makefile'
438
+ ];
439
+ const ciNeededExtensionList = ['.gyp', '.gypi', '.bat'];
440
+
441
+ return files.some(
442
+ ({ path }) =>
443
+ ciNeededFolderRx.test(path) ||
444
+ ciNeededToolFolderRx.test(path) ||
445
+ ciNeededFileRx.test(path) ||
446
+ ciNeededFileList.includes(path) ||
447
+ ciNeededExtensionList.some((ext) => path.endsWith(ext))
448
+ );
449
+ }
450
+
451
+ async checkNodejsCI() {
452
+ let status = this.checkGitHubCI();
453
+ if (
454
+ this.pr.labels.nodes.some((l) => l.name === 'needs-ci') ||
455
+ this.requiresJenkinsRun()
456
+ ) {
457
+ status &= await this.checkJenkinsCI();
458
+ } else {
459
+ this.cli.info('Green GitHub CI is sufficient');
460
+ }
461
+ return status;
462
+ }
463
+
464
+ checkAuthor() {
465
+ const { cli, commits, pr } = this;
466
+
467
+ const oddCommits = this.filterOddCommits(commits);
468
+ if (!oddCommits.length) {
469
+ return true;
470
+ }
471
+
472
+ const prAuthor = `${pr.author.login}(${pr.author.email})`;
473
+ cli.warn(`PR author is a new contributor: @${prAuthor}`);
474
+ for (const c of oddCommits) {
475
+ const { oid, author } = c.commit;
476
+ const hash = shortSha(oid);
477
+ cli.warn(`- commit ${hash} is authored by ${author.email}`);
478
+ }
479
+ return false;
480
+ }
481
+
482
+ filterOddCommits(commits) {
483
+ return commits.filter((c) => this.isOddAuthor(c.commit));
484
+ }
485
+
486
+ isOddAuthor(commit) {
487
+ const { pr } = this;
488
+
489
+ // They have turned on the private email feature, can't really check
490
+ // anything, GitHub should know how to link that, see nodejs/node#15489
491
+ if (!pr.author.email) {
492
+ return false;
493
+ }
494
+
495
+ // If they have added the alternative email to their account,
496
+ // commit.authoredByCommitter should be set to true by Github
497
+ if (commit.authoredByCommitter) {
498
+ return false;
499
+ }
500
+
501
+ if (commit.author.email === pr.author.email) {
502
+ return false;
503
+ }
504
+
505
+ // At this point, the commit:
506
+ // 1. is not authored by the commiter i.e. author email is not in the
507
+ // committer's Github account
508
+ // 3. is not authored by the people opening the PR
509
+ return true;
510
+ }
511
+
512
+ checkGitConfig() {
513
+ const { cli, commits } = this;
514
+ for (const { commit } of commits) {
515
+ if (commit.author.user === null) {
516
+ cli.warn('GitHub cannot link the author of ' +
517
+ `'${commit.messageHeadline}' to their GitHub account.`);
518
+ cli.warn('Please suggest them to take a look at ' +
519
+ `${GIT_CONFIG_GUIDE_URL}`);
520
+ }
521
+ }
522
+
523
+ return true;
524
+ }
525
+
526
+ checkCommitsAfterReview() {
527
+ const {
528
+ commits, reviews, cli, argv
529
+ } = this;
530
+ const { maxCommits } = argv;
531
+
532
+ const reviewIndex = reviews.findLastIndex(
533
+ review => review.authorCanPushToRepository && review.state === 'APPROVED'
534
+ );
535
+
536
+ if (reviewIndex === -1) {
537
+ return false;
538
+ }
539
+
540
+ const reviewDate = reviews[reviewIndex].publishedAt;
541
+
542
+ const afterCommits = [];
543
+ commits.forEach((commit) => {
544
+ commit = commit.commit;
545
+ if (commit.committedDate > reviewDate) {
546
+ afterCommits.push(commit);
547
+ }
548
+ });
549
+
550
+ const totalCommits = afterCommits.length;
551
+ if (totalCommits > 0) {
552
+ cli.warn('Commits were pushed since the last approving review:');
553
+ const sliceLength = maxCommits === 0 ? totalCommits : -maxCommits;
554
+ afterCommits.slice(sliceLength)
555
+ .forEach(commit => {
556
+ cli.warn(`- ${commit.messageHeadline}`);
557
+ });
558
+
559
+ if (totalCommits > maxCommits) {
560
+ const infoMsg = '...(use `' +
561
+ `--max-commits ${totalCommits}` +
562
+ '` to see the full list of commits)';
563
+ cli.warn(infoMsg);
564
+ }
565
+
566
+ return false;
567
+ }
568
+
569
+ return true;
570
+ }
571
+
572
+ checkMergeableState() {
573
+ const {
574
+ pr, cli
575
+ } = this;
576
+
577
+ if (pr.mergeable && pr.mergeable === CONFLICTING) {
578
+ cli.warn('This PR has conflicts that must be resolved');
579
+ return false;
580
+ }
581
+
582
+ return true;
583
+ }
584
+
585
+ checkPRState() {
586
+ const {
587
+ pr: { closed, closedAt, merged, mergedAt },
588
+ cli
589
+ } = this;
590
+
591
+ if (merged) {
592
+ const dateStr = new Date(mergedAt).toUTCString();
593
+ cli.warn(`This PR was merged on ${dateStr}`);
594
+ return false;
595
+ }
596
+
597
+ if (closed) {
598
+ const dateStr = new Date(closedAt).toUTCString();
599
+ cli.warn(`This PR was closed on ${dateStr}`);
600
+ return false;
601
+ }
602
+
603
+ return true;
604
+ }
605
+ }
package/lib/pr_data.js ADDED
@@ -0,0 +1,115 @@
1
+ import { getCollaborators } from './collaborators.js';
2
+ import { ReviewAnalyzer } from './reviews.js';
3
+ import {
4
+ FIRST_TIME_CONTRIBUTOR, FIRST_TIMER
5
+ } from './user_status.js';
6
+
7
+ // lib/queries/*.gql file names
8
+ const PR_QUERY = 'PR';
9
+ const REVIEWS_QUERY = 'Reviews';
10
+ const COMMENTS_QUERY = 'PRComments';
11
+ const COMMITS_QUERY = 'PRCommits';
12
+
13
+ export default class PRData {
14
+ /**
15
+ * @param {Object} argv
16
+ * @param {Object} cli
17
+ * @param {Object} request
18
+ */
19
+ constructor(argv, cli, request) {
20
+ const { prid, owner, repo } = argv;
21
+ this.prid = prid;
22
+ this.owner = owner;
23
+ this.repo = repo;
24
+ this.cli = cli;
25
+ this.argv = argv;
26
+ this.request = request;
27
+ this.prStr = `${owner}/${repo}/pull/${prid}`;
28
+
29
+ // Data
30
+ this.collaborators = new Map();
31
+ this.pr = {};
32
+ this.reviews = [];
33
+ this.comments = [];
34
+ this.commits = [];
35
+ this.reviewers = [];
36
+ }
37
+
38
+ getThread() {
39
+ const { pr, comments, reviews } = this;
40
+ const prNode = {
41
+ publishedAt: pr.createdAt,
42
+ bodyText: pr.bodyText
43
+ };
44
+ return comments.concat([prNode]).concat(reviews);
45
+ }
46
+
47
+ async getThreadData() {
48
+ return Promise.all([
49
+ this.getPR(),
50
+ this.getReviews(),
51
+ this.getComments()
52
+ ]);
53
+ }
54
+
55
+ async getAll(argv) {
56
+ const { prStr } = this;
57
+ this.cli.startSpinner(`Loading data for ${prStr}`);
58
+ await Promise.all([
59
+ this.getCollaborators(),
60
+ this.getThreadData(),
61
+ this.getCommits()
62
+ ]).then(() => {
63
+ this.cli.stopSpinner(`Done loading data for ${prStr}`);
64
+ });
65
+ this.analyzeReviewers();
66
+ }
67
+
68
+ analyzeReviewers() {
69
+ this.reviewers = new ReviewAnalyzer(this).getReviewers();
70
+ }
71
+
72
+ async getCollaborators() {
73
+ const { cli, request, argv } = this;
74
+ this.collaborators = await getCollaborators(cli, request, argv);
75
+ }
76
+
77
+ async getPR() {
78
+ const { prid, owner, repo, cli, request, prStr } = this;
79
+ cli.updateSpinner(`Getting PR from ${prStr}`);
80
+ const prData = await request.gql(PR_QUERY, { prid, owner, repo });
81
+ this.pr = prData.repository.pullRequest;
82
+ }
83
+
84
+ async getReviews() {
85
+ const { prid, owner, repo, cli, request, prStr } = this;
86
+ const vars = { prid, owner, repo };
87
+ cli.updateSpinner(`Getting reviews from ${prStr}`);
88
+ this.reviews = await request.gql(REVIEWS_QUERY, vars, [
89
+ 'repository', 'pullRequest', 'reviews'
90
+ ]);
91
+ }
92
+
93
+ async getComments() {
94
+ const { prid, owner, repo, cli, request, prStr } = this;
95
+ const vars = { prid, owner, repo };
96
+ cli.updateSpinner(`Getting comments from ${prStr}`);
97
+ this.comments = await request.gql(COMMENTS_QUERY, vars, [
98
+ 'repository', 'pullRequest', 'comments'
99
+ ]);
100
+ }
101
+
102
+ async getCommits() {
103
+ const { prid, owner, repo, cli, request, prStr } = this;
104
+ const vars = { prid, owner, repo };
105
+ cli.updateSpinner(`Getting commits from ${prStr}`);
106
+ this.commits = await request.gql(COMMITS_QUERY, vars, [
107
+ 'repository', 'pullRequest', 'commits'
108
+ ]);
109
+ }
110
+
111
+ authorIsNew() {
112
+ const assoc = this.pr.authorAssociation;
113
+ return assoc === FIRST_TIME_CONTRIBUTOR || assoc === FIRST_TIMER;
114
+ }
115
+ };