@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.
- package/LICENSE +7 -0
- package/README.md +158 -0
- package/bin/get-metadata.js +11 -0
- package/bin/git-node.js +30 -0
- package/bin/ncu-ci.js +600 -0
- package/bin/ncu-config.js +101 -0
- package/bin/ncu-team.js +76 -0
- package/components/git/backport.js +70 -0
- package/components/git/epilogue.js +18 -0
- package/components/git/land.js +223 -0
- package/components/git/metadata.js +94 -0
- package/components/git/release.js +99 -0
- package/components/git/security.js +35 -0
- package/components/git/status.js +32 -0
- package/components/git/sync.js +24 -0
- package/components/git/v8.js +121 -0
- package/components/git/vote.js +84 -0
- package/components/git/wpt.js +87 -0
- package/components/metadata.js +49 -0
- package/lib/auth.js +133 -0
- package/lib/backport_session.js +302 -0
- package/lib/cache.js +107 -0
- package/lib/cherry_pick.js +304 -0
- package/lib/ci/build-types/benchmark_run.js +72 -0
- package/lib/ci/build-types/citgm_build.js +194 -0
- package/lib/ci/build-types/citgm_comparison_build.js +174 -0
- package/lib/ci/build-types/commit_build.js +112 -0
- package/lib/ci/build-types/daily_build.js +24 -0
- package/lib/ci/build-types/fanned_build.js +87 -0
- package/lib/ci/build-types/health_build.js +63 -0
- package/lib/ci/build-types/job.js +114 -0
- package/lib/ci/build-types/linter_build.js +35 -0
- package/lib/ci/build-types/normal_build.js +89 -0
- package/lib/ci/build-types/pr_build.js +101 -0
- package/lib/ci/build-types/test_build.js +186 -0
- package/lib/ci/build-types/test_run.js +41 -0
- package/lib/ci/ci_failure_parser.js +325 -0
- package/lib/ci/ci_type_parser.js +203 -0
- package/lib/ci/ci_utils.js +106 -0
- package/lib/ci/failure_aggregator.js +152 -0
- package/lib/ci/jenkins_constants.js +28 -0
- package/lib/ci/run_ci.js +120 -0
- package/lib/cli.js +192 -0
- package/lib/collaborators.js +140 -0
- package/lib/config.js +72 -0
- package/lib/figures.js +7 -0
- package/lib/file.js +43 -0
- package/lib/github/templates/next-security-release.md +97 -0
- package/lib/github/tree.js +162 -0
- package/lib/landing_session.js +506 -0
- package/lib/links.js +123 -0
- package/lib/mergeable_state.js +3 -0
- package/lib/metadata_gen.js +61 -0
- package/lib/pr_checker.js +605 -0
- package/lib/pr_data.js +115 -0
- package/lib/pr_summary.js +62 -0
- package/lib/prepare_release.js +772 -0
- package/lib/prepare_security.js +117 -0
- package/lib/proxy.js +21 -0
- package/lib/queries/DefaultBranchRef.gql +8 -0
- package/lib/queries/LastCommit.gql +16 -0
- package/lib/queries/PR.gql +37 -0
- package/lib/queries/PRComments.gql +27 -0
- package/lib/queries/PRCommits.gql +45 -0
- package/lib/queries/PRs.gql +25 -0
- package/lib/queries/Reviews.gql +23 -0
- package/lib/queries/SearchIssue.gql +51 -0
- package/lib/queries/Team.gql +22 -0
- package/lib/queries/TreeEntries.gql +12 -0
- package/lib/queries/VotePRInfo.gql +28 -0
- package/lib/release/utils.js +53 -0
- package/lib/request.js +185 -0
- package/lib/review_state.js +5 -0
- package/lib/reviews.js +178 -0
- package/lib/run.js +106 -0
- package/lib/session.js +415 -0
- package/lib/sync_session.js +15 -0
- package/lib/team_info.js +95 -0
- package/lib/update-v8/applyNodeChanges.js +49 -0
- package/lib/update-v8/backport.js +258 -0
- package/lib/update-v8/commitUpdate.js +26 -0
- package/lib/update-v8/common.js +35 -0
- package/lib/update-v8/constants.js +86 -0
- package/lib/update-v8/index.js +56 -0
- package/lib/update-v8/majorUpdate.js +171 -0
- package/lib/update-v8/minorUpdate.js +105 -0
- package/lib/update-v8/updateMaintainingDependencies.js +34 -0
- package/lib/update-v8/updateV8Clone.js +53 -0
- package/lib/update-v8/updateVersionNumbers.js +122 -0
- package/lib/update-v8/util.js +62 -0
- package/lib/user.js +4 -0
- package/lib/user_status.js +5 -0
- package/lib/utils.js +66 -0
- package/lib/verbosity.js +26 -0
- package/lib/voting_session.js +136 -0
- package/lib/wpt/index.js +243 -0
- package/lib/wpt/templates/README.md +16 -0
- 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
|
+
};
|