@@ -11588,6 +11588,10 @@ const pathToReviewerChecklist = 'https://raw.githubusercontent.com/Expensify/App
1158811588const reviewerChecklistContains = '# Reviewer Checklist';
1158911589const issue = github.context.payload.issue?.number ?? github.context.payload.pull_request?.number ?? -1;
1159011590const combinedComments = [];
11591+ // Org members and owners are internal Expensify engineers; external contributors (including C+) are not.
11592+ 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']);
1159111595function getNumberOfItemsFromReviewerChecklist() {
1159211596 console.log('Getting the number of items in the reviewer checklist...');
1159311597 return new Promise((resolve, reject) => {
@@ -11657,8 +11661,44 @@ function checkIssueForCompletedChecklist(numberOfChecklistItems) {
1165711661 core.setFailed("PR Reviewer Checklist is not completely filled out. Please check every box to verify you've thought about the item.");
1165811662 });
1165911663}
11660- getNumberOfItemsFromReviewerChecklist()
11661- .then(checkIssueForCompletedChecklist)
11664+ // An approval from an internal Expensify engineer means we've decided this PR doesn't need a C+ checklist, so let the check pass.
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,
11676+ });
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;
11693+ }
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+ })
1166211702 .catch((err) => {
1166311703 console.error(err);
1166411704 core.setFailed(err);
0 commit comments