From 783709da200d4849ca9420e0daa2de1a5666b5cf Mon Sep 17 00:00:00 2001 From: Tom Alexander Date: Mon, 29 Jun 2026 16:49:45 -0400 Subject: [PATCH] fix: don't flag private org members as external contributors author_association only reports MEMBER for *public* org members, so a teammate with private (concealed) membership was classified as external, alerted to Slack, and labeled. Resolve actual repo access via the collaborator-permission endpoint as a fallback. Threshold on granted access (triage and up) rather than permission != 'none', since public repos grant everyone implicit read. Uses the built-in GITHUB_TOKEN; no new secret or permission required. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workflows/external-contributor-alerts.yml | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/.github/workflows/external-contributor-alerts.yml b/.github/workflows/external-contributor-alerts.yml index bdd5db5..c0feaf6 100644 --- a/.github/workflows/external-contributor-alerts.yml +++ b/.github/workflows/external-contributor-alerts.yml @@ -29,14 +29,30 @@ jobs: if (item.draft) { core.info(`Skipping draft PR #${item.number}`); return; } if (item.user?.type === 'Bot') { core.info(`Skipping bot ${item.user?.login}`); return; } - const INTERNAL = ['OWNER', 'MEMBER', 'COLLABORATOR']; - if (INTERNAL.includes(item.author_association)) { - core.info(`#${item.number} by ${item.user?.login} is internal (${item.author_association}); skipping.`); + const { owner, repo } = context.repo; + // fast path (free): public members & direct collaborators + let internal = ['OWNER', 'MEMBER', 'COLLABORATOR'].includes(item.author_association); + // slow path: catches PRIVATE org members via their granted repo access. + // author_association only reports MEMBER for *public* members, so private + // members would otherwise be misflagged as external. Public repos grant + // everyone implicit 'read', so require triage+ (not permission != 'none'). + if (!internal) { + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, repo, username: item.user.login, + }); + const p = data.user?.permissions ?? {}; + internal = !!(p.admin || p.maintain || p.push || p.triage); + } catch (e) { + if (e.status !== 404) throw e; // 404 = not a collaborator => external + } + } + if (internal) { + core.info(`#${item.number} by ${item.user?.login} is internal; skipping.`); return; } if (item.labels?.some(l => l.name === LABEL)) { core.info(`#${item.number} already handled.`); return; } - const { owner, repo } = context.repo; const webhook = process.env.SLACK_WEBHOOK_OSS_PRS; if (!webhook) { core.warning('SLACK_WEBHOOK_OSS_PRS not set — no alert sent and not labeled (will retry on next event).');