@@ -11,6 +11,9 @@ const combinedComments: string[] = [];
1111// Org members and owners are internal Expensify engineers; external contributors (including C+) are not.
1212const 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+
1417function 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