Symptom
The API-fallback added in #253 (PR #254) does not work in practice. On glitchwerks/mom-bot#21 (run 25603534852), the new auth code ran but blocked the PR with:
Not authorized: cbeaulieu-gt (association=CONTRIBUTOR, not in authorized_users, not a repo collaborator)
…even though cbeaulieu-gt is repo admin on mom-bot.
Root cause
gh api repos/{owner}/{repo}/collaborators/{username} requires the calling identity to have push access to enumerate collaborators. This is intentional GitHub security behavior (prevents outside contributors from listing collaborators).
In pull_request_target runs, github.token resolves to github-actions[bot] with whatever scopes are declared in the workflow's permissions: block. Even with contents: read and pull-requests: write, the bot identity does not have the push relationship to the repo that the collaborators endpoint requires — it returns 403 (which gh api surfaces as a non-zero exit). The code falls through to "not a collaborator" and blocks.
Reproducible from a local PAT: gh api repos/glitchwerks/mom-bot/collaborators/cbeaulieu-gt exits 0 (because the PAT has admin). The same call inside the workflow exits non-zero.
Why it shipped
The implementation was tested only against the CSV-input path (step 1 of three). The API fallback (step 3) was not exercised end-to-end before merge — and the code reviewer's review was scoped to logic correctness, not GitHub auth-model correctness. Today's session also documented (PR #486 in claude-configs) the rule that "CI green is not dogfood evidence" — and indeed this PR's own CI was green only because the dogfood ran the OLD @v2 code at the time.
Fix options
- Use the App token for the collaborator check. The App's installation has admin rights on its installed repos, so the API call succeeds. Requires reordering: resolve App token → run auth check using App token → continue. Chicken-and-egg concern is resolved by the fact that App token resolution is itself bounded by the repo's installation scope (the App is either installed or it's not).
- Use
actions/github-script with a custom octokit setup using the App-token credentials — same idea as (1) but more idiomatic in JS.
- Drop the API fallback entirely. Document the
authorized_users allowlist as the canonical solution for private-org-membership cases, and revert step 3. This is the cheapest path; users with private membership add a one-line input to their caller workflow.
- Hybrid: keep step 1 + step 2; replace step 3 with a different observable that github.token CAN read (e.g. the
commits/{sha}/check-suites endpoint or users/{username} to fetch type: User). Likely cannot fully distinguish collaborator from non-collaborator without push access.
Recommended: (1) — reorder to use App token. The App token is now mandatory after #250 anyway; using it for the auth check is the same security model as everything downstream.
Stopgap: (3) — document authorized_users. Easier to ship today; users add authorized_users: cbeaulieu-gt to their caller workflow. The API-fallback step can stay dormant (it never fires for users in the allowlist or with public membership).
Acceptance
Related
🤖 Generated by Claude Code on behalf of @cbeaulieu-gt
Symptom
The API-fallback added in #253 (PR #254) does not work in practice. On
glitchwerks/mom-bot#21(run25603534852), the new auth code ran but blocked the PR with:…even though
cbeaulieu-gtis repo admin on mom-bot.Root cause
gh api repos/{owner}/{repo}/collaborators/{username}requires the calling identity to have push access to enumerate collaborators. This is intentional GitHub security behavior (prevents outside contributors from listing collaborators).In
pull_request_targetruns,github.tokenresolves togithub-actions[bot]with whatever scopes are declared in the workflow'spermissions:block. Even withcontents: readandpull-requests: write, the bot identity does not have the push relationship to the repo that the collaborators endpoint requires — it returns 403 (whichgh apisurfaces as a non-zero exit). The code falls through to "not a collaborator" and blocks.Reproducible from a local PAT:
gh api repos/glitchwerks/mom-bot/collaborators/cbeaulieu-gtexits 0 (because the PAT has admin). The same call inside the workflow exits non-zero.Why it shipped
The implementation was tested only against the CSV-input path (step 1 of three). The API fallback (step 3) was not exercised end-to-end before merge — and the code reviewer's review was scoped to logic correctness, not GitHub auth-model correctness. Today's session also documented (PR #486 in claude-configs) the rule that "CI green is not dogfood evidence" — and indeed this PR's own CI was green only because the dogfood ran the OLD
@v2code at the time.Fix options
actions/github-scriptwith a custom octokit setup using the App-token credentials — same idea as (1) but more idiomatic in JS.authorized_usersallowlist as the canonical solution for private-org-membership cases, and revert step 3. This is the cheapest path; users with private membership add a one-line input to their caller workflow.commits/{sha}/check-suitesendpoint orusers/{username}to fetchtype: User). Likely cannot fully distinguish collaborator from non-collaborator without push access.Recommended: (1) — reorder to use App token. The App token is now mandatory after #250 anyway; using it for the auth check is the same security model as everything downstream.
Stopgap: (3) — document
authorized_users. Easier to ship today; users addauthorized_users: cbeaulieu-gtto their caller workflow. The API-fallback step can stay dormant (it never fires for users in the allowlist or with public membership).Acceptance
authorized_usersconfiguration. (Strong — option 1.)authorized_usersas the required pattern for private membership; the API fallback is removed or noted as ineffective. (Weak — option 3.)glitchwerks/mom-bot) — the verification rule fromclaude-configs#486applies; do not trust dogfood-on-self.Related
🤖 Generated by Claude Code on behalf of @cbeaulieu-gt