diff --git a/foc-mechanical-rules/README.md b/foc-mechanical-rules/README.md index 258abef..89860a1 100644 --- a/foc-mechanical-rules/README.md +++ b/foc-mechanical-rules/README.md @@ -67,6 +67,8 @@ uv run foc-mechanical-rules --dry-run --rule R-FC-013 --rule R-PR-001 # or a fe Requires a `GITHUB_TOKEN` (or `--token`) with `read:project` (board reads) and issue/PR write access (`repo` scope, or fine-grained `Issues: write` + `Pull requests: write`) on the blessed orgs. CI uses the org's `FILOZZY_CI_ADD_TO_PROJECT` secret (also used by [`add-issues-and-prs-to-fs-project-board.yml`](../.github/workflows/add-issues-and-prs-to-fs-project-board.yml)). +That org-wide scope isn't sufficient on its own: GitHub still checks the token's account's *per-repo* permission for repo-level writes like `R-PR-001`'s assignee mutation. A new repo added to the board doesn't automatically inherit this -- its `FilOzone/github-mgmt` config (or a direct collaborator grant) needs to give the bot account triage+ access, or `add_assignee` fails with a 404 that reads like the item wasn't found rather than a permissions gap. + ## Testing ```bash diff --git a/foc-mechanical-rules/foc_mechanical_rules/github_api.py b/foc-mechanical-rules/foc_mechanical_rules/github_api.py index 9c87af5..f214fc8 100644 --- a/foc-mechanical-rules/foc_mechanical_rules/github_api.py +++ b/foc-mechanical-rules/foc_mechanical_rules/github_api.py @@ -104,12 +104,28 @@ def get_issue_events( def add_assignee( session: requests.Session, *, owner: str, repo: str, number: str, login: str ) -> None: - """Add an assignee to an issue/PR.""" + """Add an assignee to an issue/PR. + + Raises requests.HTTPError. GitHub returns 404 here (never 403) both when + the calling token lacks write/triage access to `owner/repo` and when + `login` isn't a valid assignee on it -- the two cases are + indistinguishable from the response alone, but in this codebase's usage + (the target is always the PR's own author) it's almost always the + former, so the error is annotated with that hint rather than left as a + bare "Not Found". + """ resp = session.post( f"https://api.github.com/repos/{owner}/{repo}/issues/{number}/assignees", json={"assignees": [login]}, timeout=30, ) + if resp.status_code == 404: + raise requests.HTTPError( + f"{resp.status_code} {resp.reason} for url: {resp.url} -- likely a " + f"permissions issue: check that this token's account has " + f"triage+ access to {owner}/{repo} (see foc-mechanical-rules/README.md)", + response=resp, + ) resp.raise_for_status()