From b59c4114de61a54daf6d5bf7ac13f12b7ac9c4fc Mon Sep 17 00:00:00 2001 From: John Myers Date: Wed, 18 Mar 2026 09:09:43 -0700 Subject: [PATCH] fix(ci): fetch author_association via REST API instead of webhook payload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The webhook payload field context.payload.pull_request.author_association is unreliable under pull_request_target events — it was absent or not populated, causing the previous fix (#442) to still fail. Switch to fetching author_association via pulls.get REST API, which only needs pull-requests permission (already granted) and reliably returns MEMBER for org members regardless of membership visibility. Also removes the redundant orgs.checkMembershipForUser and repos.checkCollaborator fallbacks, which suffered from the same GITHUB_TOKEN permission limitation that started this bug. --- .github/workflows/vouch-check.yml | 48 +++++++++---------------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/.github/workflows/vouch-check.yml b/.github/workflows/vouch-check.yml index 8cd3e0ad..b03a26d7 100644 --- a/.github/workflows/vouch-check.yml +++ b/.github/workflows/vouch-check.yml @@ -19,7 +19,6 @@ jobs: script: | const author = context.payload.pull_request.user.login; const authorType = context.payload.pull_request.user.type; - const authorAssociation = context.payload.pull_request.author_association; // Skip bots (dependabot, renovate, github-actions, etc.). if (authorType === 'Bot') { @@ -27,48 +26,27 @@ jobs: return; } - // Check author_association from the webhook payload. This is set by - // GitHub itself and doesn't require extra token permissions, so it - // works reliably for org members even when their membership is private. + // 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']; - if (trustedAssociations.includes(authorAssociation)) { - console.log(`${author} has author_association=${authorAssociation}. Skipping vouch check.`); - return; - } - - // Fallback: explicit API checks in case author_association is unexpected. - - // Check org membership — members bypass the vouch gate. try { - 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 (API). Skipping vouch check.`); - return; - } - } catch (e) { - if (e.status !== 404) { - console.log(`Org membership check error: ${e.message}`); - } - } - - // Check collaborator status — direct collaborators bypass. - try { - const { status } = await github.rest.repos.checkCollaborator({ + const { data: pr } = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, - username: author, + pull_number: context.payload.pull_request.number, }); - if (status === 204) { - console.log(`${author} is a collaborator (API). Skipping vouch check.`); + 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.`); return; } } catch (e) { - if (e.status !== 404) { - console.log(`Collaborator check error: ${e.message}`); - } + console.log(`Failed to fetch PR author_association: ${e.message}`); } // Check the VOUCHED.td file on the dedicated "vouched" branch.