Skip to content

Commit 4636220

Browse files
matthew29tangcopybara-github
authored andcommitted
chore: Add multiple approval check for major releases
PiperOrigin-RevId: 944159301
1 parent eb73db2 commit 4636220

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# This workflow blocks release-please PRs for major versions unless they have at least 2 approvals.
2+
name: Enforce Multiple Approvals for Major Releases
3+
4+
on:
5+
pull_request_review:
6+
types: [submitted, dismissed]
7+
pull_request:
8+
types: [opened, synchronize, reopened]
9+
10+
jobs:
11+
check-major-approval:
12+
runs-on: ubuntu-latest
13+
if: github.event.pull_request.user.login == 'release-please[bot]'
14+
permissions:
15+
pull-requests: read
16+
steps:
17+
- name: Enforce Multiple Approvals for Major Releases
18+
uses: actions/github-script@v8
19+
with:
20+
script: |
21+
const prTitle = context.payload.pull_request.title;
22+
console.log(`PR Title: ${prTitle}`);
23+
24+
// 1. Extract proposed version securely from the PR title
25+
const versionMatch = prTitle.match(/\b\d+\.\d+\.\d+\b/);
26+
if (!versionMatch) {
27+
console.log("No version pattern found in the PR title. Skipping check.");
28+
return;
29+
}
30+
31+
const version = versionMatch[0];
32+
console.log(`Extracted Version: ${version}`);
33+
34+
// 2. Identify if this is a major release (ends with .0.0)
35+
if (!version.endsWith('.0.0')) {
36+
console.log("This is a minor or patch version release. Skipping check.");
37+
return;
38+
}
39+
40+
console.log(`MAJOR version release detected (${version})! Verifying approvals...`);
41+
42+
const prNumber = context.payload.pull_request.number;
43+
const owner = context.repo.owner;
44+
const repo = context.repo.repo;
45+
46+
// 3. Fetch reviews for the PR (with pagination safety)
47+
const { data: reviews } = await github.rest.pulls.listReviews({
48+
owner,
49+
repo,
50+
pull_number: prNumber,
51+
per_page: 100,
52+
});
53+
54+
// 4. Handle potential API delay by including the triggering review if missing
55+
if (context.eventName === 'pull_request_review' && context.payload.action === 'submitted') {
56+
const currentReview = context.payload.review;
57+
if (currentReview && !reviews.some(r => r.id === currentReview.id)) {
58+
reviews.push(currentReview);
59+
}
60+
}
61+
62+
// 5. Gather unique approvals
63+
const approvals = {};
64+
for (const review of reviews) {
65+
if (review.state === 'APPROVED') {
66+
approvals[review.user.login] = true;
67+
} else if (review.state === 'CHANGES_REQUESTED' || review.state === 'DISMISSED') {
68+
delete approvals[review.user.login];
69+
}
70+
}
71+
72+
// 6. Verify repository write/admin permissions for each unique approver
73+
const approvers = Object.keys(approvals);
74+
const verifiedGooglers = [];
75+
for (const username of approvers) {
76+
try {
77+
const response = await github.rest.repos.getCollaboratorPermissionLevel({
78+
owner,
79+
repo,
80+
username: username,
81+
});
82+
const permission = response.data.permission; // 'admin', 'write', 'read', or 'none'
83+
if (['admin', 'write'].includes(permission)) {
84+
verifiedGooglers.push(username);
85+
} else {
86+
console.log(`User ${username} has permission level '${permission}', which is insufficient (requires 'write' or 'admin').`);
87+
}
88+
} catch (e) {
89+
console.log(`Could not verify repository permissions for ${username}:`, e.message);
90+
}
91+
}
92+
93+
const approvalCount = verifiedGooglers.length;
94+
console.log(`Current approved reviews by authorized Googlers: ${verifiedGooglers.join(', ')} (${approvalCount})`);
95+
96+
if (approvalCount < 2) {
97+
core.setFailed(`Major version releases (${version}) require at least 2 approvals from authorized maintainers. Current approval count: ${approvalCount}.`);
98+
} else {
99+
console.log("Approval requirements successfully met.");
100+
}

0 commit comments

Comments
 (0)