diff --git a/.github/workflows/merge-gate.yml b/.github/workflows/merge-gate.yml index 7290a70..462bcbe 100644 --- a/.github/workflows/merge-gate.yml +++ b/.github/workflows/merge-gate.yml @@ -9,12 +9,13 @@ on: jobs: merge-gate: - name: Maintainer Approval Required + name: merge-gate runs-on: ubuntu-latest permissions: pull-requests: read + statuses: write steps: - - name: Require maintainer approval + - name: Check maintainer approval uses: actions/github-script@v7 with: script: | @@ -24,6 +25,16 @@ jobs: const prAuthor = context.payload.pull_request.user.login; const currentHead = context.payload.pull_request.head.sha; + // Get the latest commit's timestamp to detect stale reviews + // GitHub updates review.commit_id to current HEAD, making it + // unreliable for staleness checks. Timestamps are immutable. + const { data: headCommit } = await github.rest.repos.getCommit({ + owner, + repo, + ref: currentHead, + }); + const headCommitDate = new Date(headCommit.commit.committer.date); + // Get all reviews on this PR const { data: reviews } = await github.rest.pulls.listReviews({ owner, @@ -38,15 +49,16 @@ jobs: } // Check each approver's repo permission level - // Only count approvals against the current HEAD commit + // Only count approvals submitted AFTER the latest commit const validApprovals = []; const staleApprovals = []; for (const [login, review] of latestReviews) { if (review.state !== 'APPROVED') continue; if (login === prAuthor) continue; - // Reject approvals made against an older commit - if (review.commit_id !== currentHead) { + // Reject approvals submitted before the latest commit + const reviewDate = new Date(review.submitted_at); + if (reviewDate < headCommitDate) { staleApprovals.push(login); continue; } @@ -62,13 +74,32 @@ jobs: } } + // Use the Commit Status API instead of core.setFailed() + // This ensures multiple runs for the same commit overwrite + // each other instead of creating conflicting check runs if (validApprovals.length === 0) { - let message = `❌ Requires approval from a user with Maintain or Admin permission before merging.\n` + - `Contributors (Write permission) cannot satisfy this requirement.`; + let description = 'Requires approval from a Maintain/Admin user'; if (staleApprovals.length > 0) { - message += `\n\n⚠️ Stale approvals (new commits pushed since review): ${staleApprovals.join(', ')}`; + description = `Stale approvals (pushed after review): ${staleApprovals.join(', ')}`; } - core.setFailed(message); + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: currentHead, + state: 'failure', + context: 'Maintainer Approval Required', + description, + }); + core.info(`❌ ${description}`); } else { - core.info(`✅ Approved by maintainer(s): ${validApprovals.join(', ')}`); + const approvedBy = validApprovals.join(', '); + await github.rest.repos.createCommitStatus({ + owner, + repo, + sha: currentHead, + state: 'success', + context: 'Maintainer Approval Required', + description: `Approved by: ${approvedBy}`, + }); + core.info(`✅ Approved by maintainer(s): ${approvedBy}`); }