diff --git a/.github/workflows/pr_approval_check.yml b/.github/workflows/pr_approval_check.yml index f219d2d1a4e..cdbbbea9ff2 100644 --- a/.github/workflows/pr_approval_check.yml +++ b/.github/workflows/pr_approval_check.yml @@ -1,45 +1,106 @@ -name: PR Approval Check +name: Review Checks on: pull_request: - types: [opened, synchronize, reopened, ready_for_review] + types: [opened, synchronize, reopened] pull_request_review: - types: [submitted] + types: [submitted, dismissed] + merge_group: permissions: contents: read + pull-requests: read + statuses: write + +concurrency: + group: pr-approval-check-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true jobs: - check-approvals: + publish-approval-status: + name: Set approval status runs-on: ubuntu-latest - # Skip this check if PR is in draft state - if: github.event.pull_request.draft == false steps: - - name: Checkout code - uses: actions/checkout@v3 + - name: Evaluate and publish approval status + uses: actions/github-script@v7 with: - fetch-depth: 0 - - - name: Check PR approvals - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Check if PR author is clockwork-labs-bot - if [[ "${{ github.event.pull_request.user.login }}" != "clockwork-labs-bot" ]]; then - echo "PR opened by ${{ github.event.pull_request.user.login }}, not clockwork-labs-bot. Skipping check." - exit 0 - fi - - PR_NUMBER="${{ github.event.pull_request.number }}" - # Get approval count - APPROVALS=$(gh pr view $PR_NUMBER --json reviews -q '.reviews | map(select(.state == "APPROVED")) | length') - - echo "PR has $APPROVALS approvals" - - if [[ $APPROVALS -lt 2 ]]; then - echo "Error: PRs from clockwork-labs-bot require at least 2 approvals" - exit 1 - else - echo "PR has the required number of approvals" - fi + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const contextName = "PR approval check"; + + let targetSha; + let state; + let description; + + if (context.eventName === "merge_group") { + targetSha = process.env.GITHUB_SHA; + state = "success"; + description = "Merge group entry; approvals already satisfied"; + } else { + const pr = context.payload.pull_request; + targetSha = pr.head.sha; + + if (pr.user.login !== "clockwork-labs-bot") { + state = "success"; + description = "PR author is not clockwork-labs-bot"; + } else { + const result = await github.graphql( + ` + query($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + latestOpinionatedReviews(first: 100, writersOnly: true) { + nodes { + state + author { + login + } + } + } + } + } + } + `, + { + owner: context.repo.owner, + repo: context.repo.repo, + number: pr.number, + } + ); + + const effectiveApprovers = + result.repository.pullRequest.latestOpinionatedReviews.nodes + .filter((review) => review.state === "APPROVED") + .map((review) => review.author?.login) + .filter(Boolean); + + core.info( + `Latest effective approvers (${effectiveApprovers.length}): ${effectiveApprovers.join(", ")}` + ); + + if (effectiveApprovers.length < 2) { + state = "failure"; + description = "PRs from clockwork-labs-bot require at least 2 approvals"; + } else { + state = "success"; + description = "PR has the required number of approvals"; + } + } + } + + core.info(`Publishing status ${state} for ${targetSha}: ${description}`); + + // We need to set a separate commit status for this, because it runs on both + // pull_request and pull_request_review events. If we don't set an explicit context, + // what happens is that there are sometimes two separate statuses on the same commit - + // one from each event type. This leads to weird cases where one copy of the check is failed, + // and the other is successful, and the failed one blocks the PR from merging. + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: targetSha, + state, + context: contextName, + description, + });