chore: use repo permissions instead of org membership for maintainer override (#36191)

The `require_issue_link` workflow's maintainer override (reopen PR or
remove `missing-issue-link` to bypass enforcement) has never worked. It
calls `orgs.getMembershipForUser` to verify the sender is an org member,
but `GITHUB_TOKEN` is a GitHub App installation token — not an org
member — so the endpoint always returns 403. The catch block only
handled 404, so the unhandled 403 crashed the entire job, blocking even
the normal issue-link validation from running.

## Changes
- Replace `orgs.getMembershipForUser` with
`repos.getCollaboratorPermissionLevel` in `senderIsOrgMember()` — checks
if the event sender (the user who reopened the PR or removed the label)
has write/maintain/admin access on the repo, which works with
`GITHUB_TOKEN` and is a better proxy for "maintainer" than org
membership
This commit is contained in:
Mason Daugherty
2026-03-24 03:03:52 -04:00
committed by GitHub
parent 1778b082ec
commit 3e64c255b8

View File

@@ -74,30 +74,36 @@ jobs:
});
}
// ── Helper: check if sender is an active org member ─────────────
// ── Helper: check if the user who triggered this event (reopened
// the PR / removed the label) has write+ access on the repo ───
// Uses the repo collaborator permission endpoint instead of the
// org membership endpoint. The org endpoint requires the caller
// to be an org member, which GITHUB_TOKEN (an app installation
// token) never is — so it always returns 403.
async function senderIsOrgMember() {
const sender = context.payload.sender?.login;
if (!sender) {
throw new Error('Event has no sender — cannot check org membership');
throw new Error('Event has no sender — cannot check permissions');
}
try {
const { data: membership } = await github.rest.orgs.getMembershipForUser({
org: owner,
username: sender,
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
owner, repo, username: sender,
});
if (membership.state === 'active') {
const perm = data.permission;
if (['admin', 'maintain', 'write'].includes(perm)) {
console.log(`${sender} has ${perm} permission — treating as maintainer`);
return { isMember: true, login: sender };
}
console.log(`${sender} is an org member but state is "${membership.state}"`);
console.log(`${sender} has ${perm} permission — not a maintainer`);
return { isMember: false, login: sender };
} catch (e) {
if (e.status === 404) {
console.log(`${sender} is not an org member`);
console.log(`Cannot check permissions for ${sender} — treating as non-maintainer`);
return { isMember: false, login: sender };
}
const status = e.status ?? 'unknown';
throw new Error(
`Membership check failed for ${sender} (HTTP ${status}): ${e.message}`,
`Permission check failed for ${sender} (HTTP ${status}): ${e.message}`,
);
}
}