diff --git a/.github/workflows/contributor-check-external-template.yml b/.github/workflows/contributor-check-external-template.yml index 92f8fd3..62e7f79 100644 --- a/.github/workflows/contributor-check-external-template.yml +++ b/.github/workflows/contributor-check-external-template.yml @@ -48,7 +48,11 @@ jobs: uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: agentrust-io/.github - ref: main + # Pinned to a specific commit of agentrust-io/.github: this workflow + # runs on pull_request_target with a write-capable token, so the + # code it pulls in must not be a mutable ref. Bump this SHA + # deliberately (after review) when the org action changes. + ref: 8a2b77bc35057566fd675f0a24e814564cf5322f # main as of 2026-06-10 persist-credentials: false sparse-checkout: | scripts diff --git a/.github/workflows/require-maintainer-approval.yml b/.github/workflows/require-maintainer-approval.yml index f3a2600..c62b6aa 100644 --- a/.github/workflows/require-maintainer-approval.yml +++ b/.github/workflows/require-maintainer-approval.yml @@ -7,6 +7,12 @@ on: pull_request_target: types: [opened, synchronize, reopened] branches: [main] + # Re-evaluate the gate when a review is submitted or dismissed so an + # approval clears the failing status without waiting for a new push. + # Note: pull_request_review does not support a branches filter, so the + # base-branch check lives in the job-level `if` below. + pull_request_review: + types: [submitted, dismissed] permissions: contents: read @@ -19,6 +25,7 @@ jobs: contents: read pull-requests: read if: >- + github.event.pull_request.base.ref == 'main' && github.event.pull_request.author_association != 'MEMBER' && github.event.pull_request.author_association != 'OWNER' steps: @@ -28,25 +35,55 @@ jobs: script: | const MAINTAINERS = ['imran-siddique']; - const reviews = await github.rest.pulls.listReviews({ + // Both pull_request_target and pull_request_review payloads + // carry the PR at context.payload.pull_request. + const prNumber = context.payload.pull_request.number; + + // Re-fetch the PR so we compare against the head SHA at + // evaluation time, not a possibly stale SHA from the payload. + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + }); + const headSha = pr.head.sha; + + // Paginate: listReviews otherwise returns only the first 30. + const reviews = await github.paginate(github.rest.pulls.listReviews, { owner: context.repo.owner, repo: context.repo.repo, - pull_number: context.payload.pull_request.number, + pull_number: prNumber, }); - const maintainerApproval = reviews.data.find( - (r) => - r.state === 'APPROVED' && - MAINTAINERS.includes(r.user.login) && - r.user.type === 'User' + // Only a maintainer's most recent non-comment review counts, + // and it must approve the current head commit. An approval of + // an older commit does not clear the gate after a later push + // (prevents approve-then-swap). + const latestByMaintainer = new Map(); + for (const review of reviews) { + if ( + review.user && + review.user.type === 'User' && + MAINTAINERS.includes(review.user.login) && + review.state !== 'COMMENTED' + ) { + latestByMaintainer.set(review.user.login, review); + } + } + + const maintainerApproval = [...latestByMaintainer.values()].find( + (r) => r.state === 'APPROVED' && r.commit_id === headSha ); if (!maintainerApproval) { core.setFailed( - 'This PR is awaiting a named-maintainer review.\n' + + 'This PR is awaiting a named-maintainer review of the current head commit.\n' + 'This is a policy gate, not a CI failure: the PR is mergeable once a named maintainer approves.\n' + + 'Approvals of earlier commits do not count after new pushes.\n' + 'See CODEOWNERS or MAINTAINERS.md for the list of maintainers.' ); } else { - core.info(`Maintainer @${maintainerApproval.user.login} approved this PR.`); + core.info( + `Maintainer @${maintainerApproval.user.login} approved this PR at head ${headSha}.` + ); }