From e4753778fe569c2918006ca6a79162d884eba6e3 Mon Sep 17 00:00:00 2001 From: Keith Williams Date: Tue, 30 Dec 2025 08:04:09 -0300 Subject: [PATCH] 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 * 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 * Apply suggestion from @keithwillcode --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/pr.yml | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 6cf9c12afa..825d7cd286 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -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: