Skip to content

Commit 666044d

Browse files
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 666044d

File tree

1 file changed

+24
-16
lines changed

1 file changed

+24
-16
lines changed

validate-pr/scripts/validate-pr.js

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,34 @@ 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+
return ['admin', 'maintain', 'write'].includes(role);
60+
}
61+
62+
async function isMaintainer(owner, repoName, username) {
63+
const role = await getRole(owner, repoName, username);
64+
return ['admin', 'maintain'].includes(role);
5765
}
5866

5967
// --- Step 1: Skip if a maintainer reopened the PR ---
@@ -67,14 +75,14 @@ module.exports = async ({ github, context, core }) => {
6775
}
6876
}
6977

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.`);
78+
// --- Step 2: Check if PR author has write access (admin, maintain, or write role) ---
79+
const authorHasWriteAccess = await hasWriteAccess(repo.owner, repo.repo, prAuthor);
80+
if (authorHasWriteAccess) {
81+
core.info(`PR author ${prAuthor} has write+ access. Skipping.`);
7482
core.setOutput('skipped', 'true');
7583
return;
7684
}
77-
core.info(`PR author ${prAuthor} is not a maintainer.`);
85+
core.info(`PR author ${prAuthor} does not have write access.`);
7886

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

0 commit comments

Comments
 (0)