Skip to content

feat(client): add RetryTransport for automatic retry with exponential backoff #119

feat(client): add RetryTransport for automatic retry with exponential backoff

feat(client): add RetryTransport for automatic retry with exponential backoff #119

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;
// pull_request_review runs on a fork PR get a read-only GITHUB_TOKEN,
// so createCommitStatus would 403. Release PRs are always created in
// this repo (release-please pushes release-please--* to the base repo,
// never a fork), so their review events keep a write token. Every other
// PR already gets its status from the pull_request_target run, which
// has a write token even for forks. So skip fork review events.
const isReviewEvent = context.eventName === 'pull_request_review';
const isFork = pr.head.repo?.full_name !== `${owner}/${repo}`;
if (isReviewEvent && isFork) {
core.info('Fork review event has a read-only token; status handled by pull_request_target.');
return;
}
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);