fix(ci): fetch author_association via REST API instead of webhook payload (#444)

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.

Co-authored-by: John Myers <johntmyers@users.noreply.github.com>
This commit is contained in:
John T. Myers
2026-03-18 09:12:56 -07:00
committed by GitHub
co-authored by John Myers
parent 240d0ee3ed
commit 85a3d83d75
+13 -35
View File
@@ -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.