From 3e64c255b84b283b3a65216b19b9838734258c96 Mon Sep 17 00:00:00 2001 From: Mason Daugherty Date: Tue, 24 Mar 2026 03:03:52 -0400 Subject: [PATCH] chore: use repo permissions instead of org membership for maintainer override (#36191) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/require_issue_link.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/require_issue_link.yml b/.github/workflows/require_issue_link.yml index 98f8dfb440a..5f43033aca4 100644 --- a/.github/workflows/require_issue_link.yml +++ b/.github/workflows/require_issue_link.yml @@ -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}`, ); } }