fix(ci): use getCollaboratorPermissionLevel for trust check (#26306)

* fix(ci): use GitHub App token for team membership check

The default GITHUB_TOKEN doesn't have org-level permissions to query
team membership via the teams.getMembershipForUserInOrg API. This causes
the API to return 404 even for valid team members, making the trust check
incorrectly fail for core team members.

This change generates a short-lived GitHub App token with org-level access
to properly verify team membership.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* fix(ci): use getCollaboratorPermissionLevel for trust check

Replace the team membership check (teams.getMembershipForUserInOrg) with
getCollaboratorPermissionLevel which works with the default GITHUB_TOKEN.

This approach:
- Checks if users have write/maintain/admin access to the repo
- Works without requiring a GitHub App token with org-level permissions
- Is semantically correct: if someone can push to the repo, they're trusted

The previous approach using teams.getMembershipForUserInOrg required
org-level permissions that the default GITHUB_TOKEN doesn't have,
causing 404 errors even for valid team members.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Apply suggestion from @keithwillcode

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Keith WilliamsandDevin AI authored and GitHub committed 2025-12-30 08:04:09 -03:00
1 parent ae14e10b24
commit e4753778fe
1 file changed
+19 -18
+19 -18
View File
@@ -57,18 +57,19 @@ jobs:
const prNumber = pr.number;
const headSha = pr.head.sha;
async function isCoreTeamMember(username) {
// Check if user has write access or higher (admin, maintain, write)
async function hasWriteAccess(username) {
try {
await github.rest.teams.getMembershipForUserInOrg({
org: owner,
team_slug: 'core',
const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({
owner,
repo,
username,
});
return true;
const hasAccess = ['admin', 'maintain', 'write'].includes(permission.permission);
console.log(`User ${username}: hasWriteAccess=${hasAccess}`);
return hasAccess;
} catch (e) {
if (e.status !== 404) {
console.log(`Could not verify core team membership for ${username}: ${e.message}`);
}
console.log(`Could not check permission for ${username}: ${e.message}`);
return false;
}
}
@@ -85,16 +86,16 @@ jobs:
return;
}
// Check 2: Verify core team membership via API (author_association can be unreliable)
if (await isCoreTeamMember(pr.user.login)) {
console.log(`Author ${pr.user.login} verified as core team member`);
// Check 2: Verify write access via API (author_association can be unreliable)
if (await hasWriteAccess(pr.user.login)) {
console.log(`Author ${pr.user.login} verified as having write access`);
core.setOutput('is-trusted', true);
return;
}
console.log(`Author ${pr.user.login} is not a core team member, checking for approval...`);
console.log(`Author ${pr.user.login} does not have write access, checking for approval...`);
// Check 3: Has a core team member approved the current commit?
// Check 3: Has someone with write access approved the current commit?
const reviews = await github.paginate(github.rest.pulls.listReviews, {
owner,
repo,
@@ -128,21 +129,21 @@ jobs:
}
}
// Check if any approver is a core team member
// Check if any approver has write access
for (const [reviewer, reviewData] of reviewerStates) {
if (reviewData.state !== 'APPROVED') {
continue;
}
if (await isCoreTeamMember(reviewer)) {
console.log(`PR approved by core team member ${reviewer} for commit ${headSha}`);
if (await hasWriteAccess(reviewer)) {
console.log(`PR approved by ${reviewer} (has write access) for commit ${headSha}`);
core.setOutput('is-trusted', true);
return;
}
console.log(`Reviewer ${reviewer} is not a core team member`);
console.log(`Reviewer ${reviewer} does not have write access`);
}
console.log('PR requires approval from a core team member before CI can run');
console.log('PR requires approval from someone with write access before CI can run');
core.setOutput('is-trusted', false);
prepare: