Skip to content

Commit 8656877

Browse files
committed
Honor standing internal approvals across later review events
1 parent a0d2342 commit 8656877

2 files changed

Lines changed: 86 additions & 26 deletions

File tree

.github/actions/javascript/reviewerChecklist/index.js

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11590,6 +11590,8 @@ const issue = github.context.payload.issue?.number ?? github.context.payload.pul
1159011590
const combinedComments = [];
1159111591
// Org members and owners are internal Expensify engineers; external contributors (including C+) are not.
1159211592
const INTERNAL_EXPENSIFY_ASSOCIATIONS = new Set(['MEMBER', 'OWNER']);
11593+
// A reviewer's standing is their latest review in one of these states; plain "commented" reviews don't change it.
11594+
const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
1159311595
function getNumberOfItemsFromReviewerChecklist() {
1159411596
console.log('Getting the number of items in the reviewer checklist...');
1159511597
return new Promise((resolve, reject) => {
@@ -11660,21 +11662,47 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) {
1166011662
});
1166111663
}
1166211664
// An approval from an internal Expensify engineer means we've decided this PR doesn't need a C+ checklist, so let the check pass.
11663-
function isApprovedByInternalEngineer() {
11664-
const review = github.context.payload.review;
11665-
return review?.state === 'approved' && INTERNAL_EXPENSIFY_ASSOCIATIONS.has(review?.author_association ?? '');
11666-
}
11667-
if (isApprovedByInternalEngineer()) {
11668-
console.log('PR was approved by an internal Expensify engineer, so the reviewer checklist is not required 🎉');
11669-
}
11670-
else {
11671-
getNumberOfItemsFromReviewerChecklist()
11672-
.then(checkIssueForCompletedChecklist)
11673-
.catch((err) => {
11674-
console.error(err);
11675-
core.setFailed(err);
11665+
// This workflow re-runs on every pull_request_review event, so we scan the whole review history: once an internal approval
11666+
// stands, a later "commented" or "changes requested" review from anyone must not re-require the checklist.
11667+
async function hasStandingInternalApproval() {
11668+
const { owner, repo } = github.context.repo;
11669+
const reviews = await GithubUtils_1.default.paginate(GithubUtils_1.default.octokit.pulls.listReviews, {
11670+
owner,
11671+
repo,
11672+
// eslint-disable-next-line @typescript-eslint/naming-convention
11673+
pull_number: issue,
11674+
// eslint-disable-next-line @typescript-eslint/naming-convention
11675+
per_page: 100,
1167611676
});
11677+
// GitHub treats a reviewer's latest decisive review as their standing, so keep only that per internal engineer.
11678+
const latestStateByInternalReviewer = new Map();
11679+
for (const review of reviews) {
11680+
const login = review.user?.login;
11681+
const state = review.state ?? '';
11682+
if (!login || !INTERNAL_EXPENSIFY_ASSOCIATIONS.has(review.author_association ?? '') || !DECISIVE_REVIEW_STATES.has(state)) {
11683+
continue;
11684+
}
11685+
latestStateByInternalReviewer.set(login, state);
11686+
}
11687+
for (const state of latestStateByInternalReviewer.values()) {
11688+
if (state === 'APPROVED') {
11689+
return true;
11690+
}
11691+
}
11692+
return false;
1167711693
}
11694+
hasStandingInternalApproval()
11695+
.then((isApproved) => {
11696+
if (isApproved) {
11697+
console.log('PR has a standing approval from an internal Expensify engineer, so the reviewer checklist is not required 🎉');
11698+
return;
11699+
}
11700+
return getNumberOfItemsFromReviewerChecklist().then(checkIssueForCompletedChecklist);
11701+
})
11702+
.catch((err) => {
11703+
console.error(err);
11704+
core.setFailed(err);
11705+
});
1167811706

1167911707

1168011708
/***/ }),

.github/actions/javascript/reviewerChecklist/reviewerChecklist.ts

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ const combinedComments: string[] = [];
1111
// Org members and owners are internal Expensify engineers; external contributors (including C+) are not.
1212
const INTERNAL_EXPENSIFY_ASSOCIATIONS = new Set(['MEMBER', 'OWNER']);
1313

14+
// A reviewer's standing is their latest review in one of these states; plain "commented" reviews don't change it.
15+
const DECISIVE_REVIEW_STATES = new Set(['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED']);
16+
1417
function getNumberOfItemsFromReviewerChecklist() {
1518
console.log('Getting the number of items in the reviewer checklist...');
1619
return new Promise<number>((resolve, reject) => {
@@ -91,18 +94,47 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems: number) {
9194
}
9295

9396
// An approval from an internal Expensify engineer means we've decided this PR doesn't need a C+ checklist, so let the check pass.
94-
function isApprovedByInternalEngineer(): boolean {
95-
const review = github.context.payload.review as Record<string, string> | undefined;
96-
return review?.state === 'approved' && INTERNAL_EXPENSIFY_ASSOCIATIONS.has(review?.author_association ?? '');
97-
}
97+
// This workflow re-runs on every pull_request_review event, so we scan the whole review history: once an internal approval
98+
// stands, a later "commented" or "changes requested" review from anyone must not re-require the checklist.
99+
async function hasStandingInternalApproval(): Promise<boolean> {
100+
const {owner, repo} = github.context.repo;
101+
const reviews = await GitHubUtils.paginate(GitHubUtils.octokit.pulls.listReviews, {
102+
owner,
103+
repo,
104+
// eslint-disable-next-line @typescript-eslint/naming-convention
105+
pull_number: issue,
106+
// eslint-disable-next-line @typescript-eslint/naming-convention
107+
per_page: 100,
108+
});
98109

99-
if (isApprovedByInternalEngineer()) {
100-
console.log('PR was approved by an internal Expensify engineer, so the reviewer checklist is not required 🎉');
101-
} else {
102-
getNumberOfItemsFromReviewerChecklist()
103-
.then(checkIssueForCompletedChecklist)
104-
.catch((err: string | Error) => {
105-
console.error(err);
106-
core.setFailed(err);
107-
});
110+
// GitHub treats a reviewer's latest decisive review as their standing, so keep only that per internal engineer.
111+
const latestStateByInternalReviewer = new Map<string, string>();
112+
for (const review of reviews) {
113+
const login = review.user?.login;
114+
const state = review.state ?? '';
115+
if (!login || !INTERNAL_EXPENSIFY_ASSOCIATIONS.has(review.author_association ?? '') || !DECISIVE_REVIEW_STATES.has(state)) {
116+
continue;
117+
}
118+
latestStateByInternalReviewer.set(login, state);
119+
}
120+
121+
for (const state of latestStateByInternalReviewer.values()) {
122+
if (state === 'APPROVED') {
123+
return true;
124+
}
125+
}
126+
return false;
108127
}
128+
129+
hasStandingInternalApproval()
130+
.then((isApproved) => {
131+
if (isApproved) {
132+
console.log('PR has a standing approval from an internal Expensify engineer, so the reviewer checklist is not required 🎉');
133+
return;
134+
}
135+
return getNumberOfItemsFromReviewerChecklist().then(checkIssueForCompletedChecklist);
136+
})
137+
.catch((err: string | Error) => {
138+
console.error(err);
139+
core.setFailed(err);
140+
});

0 commit comments

Comments
 (0)