Skip to content

Commit 71588dd

Browse files
feat(validate-pr): Skip checks for users with write access (#162)
* feat(validate-pr): Skip checks for users with write access Users with write repository access (admin, maintain, or write role) now bypass PR validation. Maintainer-only checks (reopening closed PRs, counting as maintainer in issue discussions) remain restricted to admin/maintain roles. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 02fd7a2 commit 71588dd

File tree

1 file changed

+25
-16
lines changed

1 file changed

+25
-16
lines changed

validate-pr/scripts/validate-pr.js

Lines changed: 25 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,35 @@ module.exports = async ({ github, context, core }) => {
3434
return;
3535
}
3636

37-
// --- Helper: check if a user has admin or maintain permission on a repo (cached) ---
38-
const maintainerCache = new Map();
39-
async function isMaintainer(owner, repoName, username) {
37+
// --- Helpers: check user permission on a repo (cached) ---
38+
const roleCache = new Map();
39+
async function getRole(owner, repoName, username) {
4040
const key = `${owner}/${repoName}:${username}`;
41-
if (maintainerCache.has(key)) return maintainerCache.get(key);
42-
let result = false;
41+
if (roleCache.has(key)) return roleCache.get(key);
42+
let roleName = null;
4343
try {
4444
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
4545
owner,
4646
repo: repoName,
4747
username,
4848
});
49-
// permission field uses legacy values (admin/write/read/none) where
50-
// maintain maps to write. Use role_name for the actual role.
51-
result = ['admin', 'maintain'].includes(data.role_name);
49+
roleName = data.role_name;
5250
} catch {
53-
// noop — result stays false
51+
// noop — roleName stays null
5452
}
55-
maintainerCache.set(key, result);
56-
return result;
53+
roleCache.set(key, roleName);
54+
return roleName;
55+
}
56+
57+
async function hasWriteAccess(owner, repoName, username) {
58+
const role = await getRole(owner, repoName, username);
59+
// role_name values: admin, maintain, push, triage, pull (+ custom roles)
60+
return ['admin', 'maintain', 'push', 'write'].includes(role);
61+
}
62+
63+
async function isMaintainer(owner, repoName, username) {
64+
const role = await getRole(owner, repoName, username);
65+
return ['admin', 'maintain'].includes(role);
5766
}
5867

5968
// --- Step 1: Skip if a maintainer reopened the PR ---
@@ -67,14 +76,14 @@ module.exports = async ({ github, context, core }) => {
6776
}
6877
}
6978

70-
// --- Step 2: Check if PR author is a maintainer (admin or maintain role) ---
71-
const authorIsMaintainer = await isMaintainer(repo.owner, repo.repo, prAuthor);
72-
if (authorIsMaintainer) {
73-
core.info(`PR author ${prAuthor} has admin/maintain access. Skipping.`);
79+
// --- Step 2: Check if PR author has write access (admin, maintain, or write role) ---
80+
const authorHasWriteAccess = await hasWriteAccess(repo.owner, repo.repo, prAuthor);
81+
if (authorHasWriteAccess) {
82+
core.info(`PR author ${prAuthor} has write+ access. Skipping.`);
7483
core.setOutput('skipped', 'true');
7584
return;
7685
}
77-
core.info(`PR author ${prAuthor} is not a maintainer.`);
86+
core.info(`PR author ${prAuthor} does not have write access.`);
7887

7988
// --- Step 3: Parse issue references from PR body ---
8089
const body = pullRequest.body || '';

0 commit comments

Comments
 (0)