diff --git a/.github/workflows/release-pr-approvals.yml b/.github/workflows/release-pr-approvals.yml new file mode 100644 index 000000000..a53388727 --- /dev/null +++ b/.github/workflows/release-pr-approvals.yml @@ -0,0 +1,76 @@ +name: release-pr-approvals + +# Requires N approvals from users with write access before a release-please PR +# can be merged. Posts a commit status (context "release-pr-approvals") so it can +# be added as a required status check on the protected branch; the job itself +# stays green. Normal PRs pass immediately. +# +# A PR counts as a release PR based on the branch name and author. +on: + pull_request_target: + types: [opened, reopened, synchronize] + pull_request_review: + types: [submitted, dismissed, edited] + +permissions: + contents: read + pull-requests: read + statuses: write + +concurrency: + group: release-pr-approvals-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + approvals: + runs-on: ubuntu-latest + env: + REQUIRED_APPROVALS: "2" + RELEASE_BRANCH_PREFIX: "release-please--" + RELEASE_AUTHOR: "a2a-bot" + steps: + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const CONTEXT = 'release-pr-approvals'; + const required = Number(process.env.REQUIRED_APPROVALS); + const pr = context.payload.pull_request; + const { owner, repo } = context.repo; + + const setStatus = (state, description) => + github.rest.repos.createCommitStatus({ + owner, repo, sha: pr.head.sha, context: CONTEXT, state, description, + }); + + const isRelease = + pr.head.ref.startsWith(process.env.RELEASE_BRANCH_PREFIX) && + pr.user?.login === process.env.RELEASE_AUTHOR; + + if (!isRelease) { + await setStatus('success', 'Not a release PR'); + return; + } + + const reviews = await github.paginate(github.rest.pulls.listReviews, { + owner, repo, pull_number: pr.number, per_page: 100, + }); + + // Latest decisive review per user; COMMENTED does not change approval state. + const latest = new Map(); + for (const r of reviews) { + if (!r.user || r.state === 'COMMENTED') continue; + latest.set(r.user.login, r.state); + } + + let approvals = 0; + for (const [login, state] of latest) { + if (state !== 'APPROVED') continue; + const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, repo, username: login, + }); + if (perm.permission === 'admin' || perm.permission === 'write') approvals++; + } + + const description = `${approvals}/${required} approvals`; + await setStatus(approvals >= required ? 'success' : 'failure', description); + core.info(description);