Skip to content

Commit ae95540

Browse files
authored
chore(prlint): use pr/needs-review label (#25414)
It is currently very difficult to use the issue search functionality to figure out which PRs are ready for review from a core team member. Usually it is ~ 1/3 of open PRs that are in a reviewable state. Some of the information we need is not available as a search filter in the issue search page (or it is difficult to create a correct filter). A PR is ready for review when: 1. Not a draft 2. Does not have any merge conflicts 3. PR linter is not failing OR the user has requested an exemption 4. A maintainer has not requested changes ...others may be added in the future This PR adds functionality to add a label `pr/needs-review` to any PR that meets our criteria. This will allow us to easily use this to filter PRs. **Note** I tested this locally using `act`, but there is no way to test this for real until it is merged. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 8189fbe commit ae95540

File tree

8 files changed

+429
-69
lines changed

8 files changed

+429
-69
lines changed

.github/workflows/pr-linter.yml

+3
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ on:
1212
- opened
1313
- synchronize
1414
- reopened
15+
status:
1516

1617
jobs:
1718
validate-pr:
1819
permissions:
1920
contents: read
2021
pull-requests: write
22+
statuses: read
23+
issues: read
2124
runs-on: ubuntu-latest
2225
steps:
2326

CONTRIBUTING.md

+17
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,23 @@ CDK integration tests.
542542
* Make sure to update the PR title/description if things change. The PR title/description are going to be used as the
543543
commit title/message and will appear in the CHANGELOG, so maintain them all the way throughout the process.
544544

545+
#### Getting a review from a maintainer
546+
547+
We get A LOT of pull requests, which is a great thing! To help us prioritize
548+
which pull requests to review we first make sure that the pull request is in a
549+
mergeable state. This means that the pull request:
550+
551+
1. Is ready for review (not a draft)
552+
2. Does not have any merge conflicts
553+
3. Has a passing build
554+
4. Does not have any requested changes by a maintainer
555+
5. Has a passing `PR Linter` workflow **OR** the contributor has requested
556+
an exemption/clarification.
557+
558+
To make this easier we have a `pr/needs-review` label that we can add to each
559+
PR. If you do not see this label on your PR then it means that something needs
560+
to be fixed before it can be reviewed.
561+
545562
#### Adding construct runtime dependencies
546563

547564
Any tool that is not part of the CDK, and needs to be used by a construct during

tools/@aws-cdk/prlint/index.ts

+23-8
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,35 @@ import * as core from '@actions/core';
22
import * as github from '@actions/github';
33
import { Octokit } from '@octokit/rest';
44
import * as linter from './lint';
5+
import { StatusEvent } from '@octokit/webhooks-definitions/schema';
56

67
async function run() {
78
const token: string = process.env.GITHUB_TOKEN!;
89
const client = new Octokit({ auth: token });
910

10-
const prLinter = new linter.PullRequestLinter({
11-
client,
12-
owner: github.context.repo.owner,
13-
repo: github.context.repo.repo,
14-
number: github.context.issue.number,
15-
});
16-
1711
try {
18-
await prLinter.validate()
12+
switch (github.context.eventName) {
13+
case 'status':
14+
const pr = await linter.PullRequestLinter.getPRFromCommit(client, 'aws', 'aws-cdk', github.context.payload.sha);
15+
if (pr) {
16+
const prLinter = new linter.PullRequestLinter({
17+
client,
18+
owner: 'aws',
19+
repo: 'aws-cdk',
20+
number: pr.number,
21+
});
22+
await prLinter.validateStatusEvent(pr, github.context.payload as StatusEvent);
23+
}
24+
break;
25+
default:
26+
const prLinter = new linter.PullRequestLinter({
27+
client,
28+
owner: github.context.repo.owner,
29+
repo: github.context.repo.repo,
30+
number: github.context.issue.number,
31+
});
32+
await prLinter.validatePullRequestTarget(github.context.payload.sha);
33+
}
1934
} catch (error: any) {
2035
core.setFailed(error.message);
2136
}

tools/@aws-cdk/prlint/lint.ts

+127-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import * as path from 'path';
22
import { Octokit } from '@octokit/rest';
3+
import { StatusEvent } from '@octokit/webhooks-definitions/schema';
34
import { breakingModules } from './parser';
45
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)';
513

614
/**
715
* Types of exemption labels in aws-cdk project.
@@ -12,16 +20,14 @@ enum Exemption {
1220
INTEG_TEST = 'pr-linter/exempt-integ-test',
1321
BREAKING_CHANGE = 'pr-linter/exempt-breaking-change',
1422
CLI_INTEG_TESTED = 'pr-linter/cli-integ-tested',
23+
REQUEST_CLARIFICATION = 'pr/reviewer-clarification-requested',
24+
REQUEST_EXEMPTION = 'pr-linter/exemption-requested',
1525
}
1626

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;
2531
}
2632

2733
export interface GitHubLabel {
@@ -172,6 +178,32 @@ export interface PullRequestLinterProps {
172178
* in the body of the review, and dismiss any previous reviews upon changes to the pull request.
173179
*/
174180
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+
175207
private readonly client: Octokit;
176208
private readonly prParams: { owner: string, repo: string, pull_number: number };
177209
private readonly issueParams: { owner: string, repo: string, issue_number: number };
@@ -272,11 +304,91 @@ export class PullRequestLinter {
272304
}
273305
}
274306

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+
275387
/**
276388
* Performs validations and communicates results via pull request comments, upon failure.
277389
* This also dismisses previous reviews so they do not remain in REQUEST_CHANGES upon fix of failures.
278390
*/
279-
public async validate(): Promise<void> {
391+
public async validatePullRequestTarget(sha: string): Promise<void> {
280392
const number = this.props.number;
281393

282394
console.log(`⌛ Fetching PR number ${number}`);
@@ -331,6 +443,12 @@ export class PullRequestLinter {
331443

332444
await this.deletePRLinterComment();
333445
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+
}
334452
}
335453

336454
private formatErrors(errors: string[]) {

tools/@aws-cdk/prlint/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
"dependencies": {
1414
"@actions/core": "^1.10.0",
1515
"@actions/github": "^5.1.1",
16+
"@octokit/webhooks-definitions": "^3.67.3",
1617
"conventional-commits-parser": "^3.2.4",
1718
"fs-extra": "^9.1.0",
1819
"glob": "^7.2.3"

0 commit comments

Comments
 (0)