1
1
import * as path from 'path' ;
2
2
import { Octokit } from '@octokit/rest' ;
3
+ import { StatusEvent } from '@octokit/webhooks-definitions/schema' ;
3
4
import { breakingModules } from './parser' ;
4
5
import { findModulePath , moduleStability } from './module' ;
6
+ import { Endpoints } from "@octokit/types" ;
7
+
8
+ export type GitHubPr =
9
+ Endpoints [ "GET /repos/{owner}/{repo}/pulls/{pull_number}" ] [ "response" ] [ "data" ] ;
10
+
11
+
12
+ export const CODE_BUILD_CONTEXT = 'AWS CodeBuild us-east-1 (AutoBuildv2Project1C6BFA3F-wQm2hXv2jqQv)' ;
5
13
6
14
/**
7
15
* Types of exemption labels in aws-cdk project.
@@ -12,16 +20,14 @@ enum Exemption {
12
20
INTEG_TEST = 'pr-linter/exempt-integ-test' ,
13
21
BREAKING_CHANGE = 'pr-linter/exempt-breaking-change' ,
14
22
CLI_INTEG_TESTED = 'pr-linter/cli-integ-tested' ,
23
+ REQUEST_CLARIFICATION = 'pr/reviewer-clarification-requested' ,
24
+ REQUEST_EXEMPTION = 'pr-linter/exemption-requested' ,
15
25
}
16
26
17
- export interface GitHubPr {
18
- readonly number : number ;
19
- readonly title : string ;
20
- readonly body : string | null ;
21
- readonly labels : GitHubLabel [ ] ;
22
- readonly user ?: {
23
- login : string ;
24
- }
27
+ export interface GithubStatusEvent {
28
+ readonly sha : string ;
29
+ readonly state ?: StatusEvent [ 'state' ] ;
30
+ readonly context ?: string ;
25
31
}
26
32
27
33
export interface GitHubLabel {
@@ -172,6 +178,32 @@ export interface PullRequestLinterProps {
172
178
* in the body of the review, and dismiss any previous reviews upon changes to the pull request.
173
179
*/
174
180
export class PullRequestLinter {
181
+ /**
182
+ * Find an open PR for the given commit.
183
+ * @param sha the commit sha to find the PR of
184
+ */
185
+ public static async getPRFromCommit ( client : Octokit , owner : string , repo : string , sha : string ) : Promise < GitHubPr | undefined > {
186
+ const prs = await client . search . issuesAndPullRequests ( {
187
+ q : sha ,
188
+ } ) ;
189
+ const foundPr = prs . data . items . find ( pr => pr . state === 'open' ) ;
190
+ if ( foundPr ) {
191
+ // need to do this because the list PR response does not have
192
+ // all the necessary information
193
+ const pr = ( await client . pulls . get ( {
194
+ owner,
195
+ repo,
196
+ pull_number : foundPr . number ,
197
+ } ) ) . data ;
198
+ const latestCommit = pr . statuses_url . split ( '/' ) . pop ( ) ;
199
+ // only process latest commit
200
+ if ( latestCommit === sha ) {
201
+ return pr ;
202
+ }
203
+ }
204
+ return ;
205
+ }
206
+
175
207
private readonly client : Octokit ;
176
208
private readonly prParams : { owner : string , repo : string , pull_number : number } ;
177
209
private readonly issueParams : { owner : string , repo : string , issue_number : number } ;
@@ -272,11 +304,91 @@ export class PullRequestLinter {
272
304
}
273
305
}
274
306
307
+ /**
308
+ * Whether or not the codebuild job for the given commit is successful
309
+ *
310
+ * @param sha the commit sha to evaluate
311
+ */
312
+ private async codeBuildJobSucceeded ( sha : string ) : Promise < boolean > {
313
+ const statuses = await this . client . rest . repos . listCommitStatusesForRef ( {
314
+ owner : this . prParams . owner ,
315
+ repo : this . prParams . repo ,
316
+ ref : sha ,
317
+ } ) ;
318
+ return statuses . data . some ( status => status . context === CODE_BUILD_CONTEXT && status . state === 'success' ) ;
319
+ }
320
+
321
+ public async validateStatusEvent ( pr : GitHubPr , status : StatusEvent ) : Promise < void > {
322
+ if ( status . context === CODE_BUILD_CONTEXT && status . state === 'success' ) {
323
+ await this . assessNeedsReview ( pr ) ;
324
+ }
325
+ }
326
+
327
+ /**
328
+ * Assess whether or not a PR is ready for review from a core team member.
329
+ * This is needed because some things that we need to evaluate are not filterable on
330
+ * the builtin issue search. A PR is ready for review when:
331
+ *
332
+ * 1. Not a draft
333
+ * 2. Does not have any merge conflicts
334
+ * 3. PR linter is not failing OR the user has requested an exemption
335
+ * 4. A maintainer has not requested changes
336
+ */
337
+ private async assessNeedsReview (
338
+ pr : Pick < GitHubPr , "mergeable_state" | "draft" | "labels" | "number" > ,
339
+ ) : Promise < void > {
340
+ const reviews = await this . client . pulls . listReviews ( this . prParams ) ;
341
+ // NOTE: MEMBER = a member of the organization that owns the repository
342
+ // COLLABORATOR = has been invited to collaborate on the repository
343
+ const maintainerRequestedChanges = reviews . data . some ( review => review . author_association === 'MEMBER' && review . state === 'CHANGES_REQUESTED' ) ;
344
+ const prLinterFailed = reviews . data . find ( ( review ) => review . user ?. login === 'aws-cdk-automation' && review . state !== 'DISMISSED' ) as Review ;
345
+ const userRequestsExemption = pr . labels . some ( label => ( label . name === Exemption . REQUEST_EXEMPTION || label . name === Exemption . REQUEST_CLARIFICATION ) ) ;
346
+ console . log ( 'evaluation: ' , JSON . stringify ( {
347
+ draft : pr . draft ,
348
+ mergeable_state : pr . mergeable_state ,
349
+ prLinterFailed,
350
+ maintainerRequestedChanges,
351
+ userRequestsExemption,
352
+ } , undefined , 2 ) ) ;
353
+
354
+ if (
355
+ // we don't need to review drafts
356
+ pr . draft
357
+ // or PRs with conflicts
358
+ || pr . mergeable_state === 'dirty'
359
+ // or PRs that already have changes requested by a maintainer
360
+ || maintainerRequestedChanges
361
+ // or the PR linter failed and the user didn't request an exemption
362
+ || ( prLinterFailed && ! userRequestsExemption )
363
+ ) {
364
+ console . log ( `removing labels from pr ${ pr . number } ` ) ;
365
+ this . client . issues . removeLabel ( {
366
+ owner : this . prParams . owner ,
367
+ repo : this . prParams . repo ,
368
+ issue_number : pr . number ,
369
+ name : 'pr/needs-review' ,
370
+ } ) ;
371
+ return ;
372
+ } else {
373
+ console . log ( `adding labels to pr ${ pr . number } ` ) ;
374
+ // add needs-review label
375
+ this . client . issues . addLabels ( {
376
+ issue_number : pr . number ,
377
+ owner : this . prParams . owner ,
378
+ repo : this . prParams . repo ,
379
+ labels : [
380
+ 'pr/needs-review' ,
381
+ ] ,
382
+ } ) ;
383
+ return ;
384
+ }
385
+ }
386
+
275
387
/**
276
388
* Performs validations and communicates results via pull request comments, upon failure.
277
389
* This also dismisses previous reviews so they do not remain in REQUEST_CHANGES upon fix of failures.
278
390
*/
279
- public async validate ( ) : Promise < void > {
391
+ public async validatePullRequestTarget ( sha : string ) : Promise < void > {
280
392
const number = this . props . number ;
281
393
282
394
console . log ( `⌛ Fetching PR number ${ number } ` ) ;
@@ -331,6 +443,12 @@ export class PullRequestLinter {
331
443
332
444
await this . deletePRLinterComment ( ) ;
333
445
await this . communicateResult ( validationCollector ) ;
446
+
447
+ // also assess whether the PR needs review or not
448
+ const state = await this . codeBuildJobSucceeded ( sha ) ;
449
+ if ( state ) {
450
+ await this . assessNeedsReview ( pr ) ;
451
+ }
334
452
}
335
453
336
454
private formatErrors ( errors : string [ ] ) {
0 commit comments