in tools/@aws-cdk/prlint/lint.ts [58:174]
private async assessNeedsReview(): Promise<LinterActions> {
const pr = await this.pr();
const reviewsData = await this.client.paginate(this.client.pulls.listReviews, this.prParams);
console.log(JSON.stringify(reviewsData.map(r => ({
user: r.user?.login,
state: r.state,
author_association: r.author_association,
submitted_at: r.submitted_at,
})), undefined, 2));
// NOTE: MEMBER = a member of the organization that owns the repository
// COLLABORATOR = has been invited to collaborate on the repository
const maintainerRequestedChanges = reviewsData.some(
review => review.author_association === 'MEMBER'
&& review.user?.login !== 'aws-cdk-automation'
&& review.state === 'CHANGES_REQUESTED',
);
const maintainerApproved = reviewsData.some(
review => review.author_association === 'MEMBER'
&& review.state === 'APPROVED',
);
// NOTE: community reviewers may approve, comment, or request changes; however, it
// is possible for the same member to perform any combination of those actions on
// a single PR. We solve this by:
// 1. Filtering reviews to those by trusted community members
// 2. Filtering out reviews that only leave comments (without approving or requesting changes).
// This allows a reviewer to participate in a conversation about their review without
// effectively dismissing their review. While GitHub does not allow community reviewers
// to dismiss their reviews (which requires privileges on the repo), they can leave a
// new review with the opposite approve/request state to update their review.
// 3. Mapping reviewers to only their newest review
// 4. Checking if any reviewers' most recent review is an approval
// -> If so, the PR is considered community approved; the approval can always
// be dismissed by a maintainer to respect another reviewer's requested changes.
// 5. Checking if any reviewers' most recent review requested changes
// -> If so, the PR is considered to still need changes to meet community review.
const trustedCommunityMembers = await this.getTrustedCommunityMembers();
const reviewsByTrustedCommunityMembers = reviewsData
.filter(review => trustedCommunityMembers.includes(review.user?.login ?? ''))
.filter(review => review.state !== 'PENDING' && review.state !== 'COMMENTED')
.reduce((grouping, review) => {
// submitted_at is not present for PENDING comments but is present for other states.
// Because of that, it is optional on the type but sure to be present here. Likewise,
// review.user is sure to be defined because we're operating on reviews by trusted
// community members
let newest = grouping[review.user!.login] ?? review;
if (review.submitted_at! > newest.submitted_at!) {
newest = review;
}
return {
...grouping,
[review.user!.login]: newest,
};
}, {} as Record<string, typeof reviewsData[0]>);
console.log('raw data: ', JSON.stringify(reviewsByTrustedCommunityMembers));
const communityApproved = Object.values(reviewsByTrustedCommunityMembers).some(({state}) => state === 'APPROVED');
const communityRequestedChanges = !communityApproved && Object.values(reviewsByTrustedCommunityMembers).some(({state}) => state === 'CHANGES_REQUESTED')
const prLinterFailed = reviewsData.find((review) => review.user?.login === 'aws-cdk-automation' && review.state !== 'DISMISSED') as Review;
const userRequestsExemption = pr.labels.some(label => (label.name === Exemption.REQUEST_EXEMPTION || label.name === Exemption.REQUEST_CLARIFICATION));
console.log('evaluation: ', JSON.stringify({
draft: pr.draft,
mergeable_state: pr.mergeable_state,
prLinterFailed,
maintainerRequestedChanges,
maintainerApproved,
communityRequestedChanges,
communityApproved,
userRequestsExemption,
}, undefined, 2));
const fixesP1 = pr.labels.some(label => label.name === 'p1');
let readyForReview = true;
if (
// we don't need to review drafts
pr.draft
// or PRs with conflicts
|| pr.mergeable_state === 'dirty'
// or PRs that already have changes requested by a maintainer
|| maintainerRequestedChanges
// or the PR linter failed and the user didn't request an exemption
|| (prLinterFailed && !userRequestsExemption)
// or a maintainer has already approved the PR
|| maintainerApproved
// or a trusted community member has requested changes on a p2 PR
|| (!fixesP1 && communityRequestedChanges)
) {
readyForReview = false;
}
// needs-maintainer-review means one of the following
// 1) fixes a p1 bug
// 2) is already community approved
// 3) is authored by a core team member
if (readyForReview && (fixesP1 || communityApproved || pr.labels.some(label => label.name === 'contribution/core'))) {
return {
addLabels: ['pr/needs-maintainer-review'],
removeLabels: ['pr/needs-community-review'],
};
} else if (readyForReview && !fixesP1) {
return {
addLabels: ['pr/needs-community-review'],
removeLabels: ['pr/needs-maintainer-review'],
};
} else {
return {
removeLabels: [
'pr/needs-community-review',
'pr/needs-maintainer-review',
],
};
}
}