From 71f0cd962c08ad7e4668875fd0a34e0b7353558c Mon Sep 17 00:00:00 2001 From: John Myers Date: Wed, 18 Mar 2026 09:18:38 -0700 Subject: [PATCH] fix(ci): use ORG_READ_TOKEN for org membership check in vouch gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GITHUB_TOKEN cannot determine org membership — it lacks read:org scope and this is not configurable via the permissions block. Both author_association and orgs.checkMembershipForUser return NONE/404 for org members (even public ones) when called with the repo-scoped token. Use an ORG_READ_TOKEN secret (fine-grained PAT with read:org) when available, falling back to GITHUB_TOKEN. Also restores the checkCollaborator fallback since the PAT can resolve that too. Setup required: create a fine-grained PAT with Organization > Members > Read permission, then add it as a repo secret named ORG_READ_TOKEN. --- .github/workflows/vouch-check.yml | 42 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/.github/workflows/vouch-check.yml b/.github/workflows/vouch-check.yml index b03a26d7..8ac0f133 100644 --- a/.github/workflows/vouch-check.yml +++ b/.github/workflows/vouch-check.yml @@ -16,6 +16,7 @@ jobs: - name: Check if contributor is vouched uses: actions/github-script@v7 with: + github-token: ${{ secrets.ORG_READ_TOKEN || secrets.GITHUB_TOKEN }} script: | const author = context.payload.pull_request.user.login; const authorType = context.payload.pull_request.user.type; @@ -26,27 +27,40 @@ jobs: return; } - // Fetch author_association via the REST API. The webhook payload - // field (context.payload.pull_request.author_association) is - // unreliable under pull_request_target — it can be absent or stale. - // The pulls.get endpoint only needs pull-requests permission, which - // we already have, and reliably returns MEMBER for org members even - // when their membership is private. - const trustedAssociations = ['MEMBER', 'OWNER', 'COLLABORATOR']; + // Check org membership. Requires a token with read:org scope + // (ORG_READ_TOKEN secret). The default GITHUB_TOKEN cannot see org + // membership, so author_association and orgs.checkMembershipForUser + // both return NONE/404 for private members. try { - const { data: pr } = await github.rest.pulls.get({ + const { status } = await github.rest.orgs.checkMembershipForUser({ + org: context.repo.owner, + username: author, + }); + if (status === 204 || status === 302) { + console.log(`${author} is an org member. Skipping vouch check.`); + return; + } + } catch (e) { + if (e.status !== 404) { + console.log(`Org membership check error (status=${e.status}): ${e.message}`); + } + } + + // Check collaborator status — direct collaborators bypass. + try { + const { status } = await github.rest.repos.checkCollaborator({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: context.payload.pull_request.number, + username: author, }); - const association = pr.author_association; - console.log(`${author}: author_association=${association}`); - if (trustedAssociations.includes(association)) { - console.log(`${author} has author_association=${association}. Skipping vouch check.`); + if (status === 204) { + console.log(`${author} is a repo collaborator. Skipping vouch check.`); return; } } catch (e) { - console.log(`Failed to fetch PR author_association: ${e.message}`); + if (e.status !== 404) { + console.log(`Collaborator check error (status=${e.status}): ${e.message}`); + } } // Check the VOUCHED.td file on the dedicated "vouched" branch.