plan §4.1/§1.5: polling primary + read-only CI; webhook is optional manual-admin

Finalize trigger model per operator: polling is the primary trigger (outbound, read-only,
no admin); the server never self-registers webhooks (that needs admin) — webhook is an
optional push optimization an admin registers manually, documented in enroll-recipe.md.
Commenter auth via org-membership endpoint (read-level), not the admin-only permission
endpoint. Bot's required privilege is read + comment + org-membership, never repo-admin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-27 02:37:17 +01:00
parent e157a943bb
commit 34cbb60f35

View File

@ -82,7 +82,7 @@ secret value into the repo, a commit, a log, or the dashboard** (§9) — refere
|---|---|---|
| **Tailscale auth key** (joins cc-ci's tailnet `taila4a0bf.ts.net`) | `/srv/cc-ci/.testenv``TS_AUTH_KEY` (Tailscale SaaS key, keyID ends `CNTRL`) | Used to bring up the userspace tailscaled (below). It's reusable; re-run `tailscale up` with it if the node drops. |
| **cc-ci SSH (root)** | private key `~/.ssh/cc-ci-root-ed25519`; config `Host cc-ci` in `~/.ssh/config` | Just run `ssh cc-ci` (logs in as **root**). The pubkey is already in cc-ci's `/root/.ssh/authorized_keys`. |
| **Gitea bot account** | `/srv/cc-ci/.testenv``GITEA_USERNAME` (`autonomic-bot`), `GITEA_PASSWORD`, `GITEA_URL` (`git.autonomic.zone`) | Basic-auth to the Gitea API, or mint a scoped token: `POST https://$GITEA_URL/api/v1/users/$GITEA_USERNAME/tokens`. Used to create/push the `cc-ci` repo, read recipe repos, comment on PRs, and register `!testme` webhooks. |
| **Gitea bot account** | `/srv/cc-ci/.testenv``GITEA_USERNAME` (`autonomic-bot`), `GITEA_PASSWORD`, `GITEA_URL` (`git.autonomic.zone`) | Basic-auth to the Gitea API, or mint a scoped token: `POST https://$GITEA_URL/api/v1/users/$GITEA_USERNAME/tokens`. Used to push the `cc-ci` project repo, read recipe repos, comment on PRs, and poll for `!testme` (read-level; the bot does not register webhooks). |
Load them in a shell with: `set -a; . /srv/cc-ci/.testenv; set +a` (don't echo the values).
@ -134,9 +134,12 @@ without the auth key.
- **Registry pull credentials** (e.g. Docker Hub) — *recommended* to avoid anonymous pull-rate
limits breaking deploys under load. Treat a rate-limit failure traced to this as a finding, then
request creds. Store sops-encrypted in `secrets/`.
- **Gitea bot permissions** (a grant, not a secret) — confirm `autonomic-bot` can: create/push
`recipe-maintainers/cc-ci`, read the recipe repos to be enrolled, comment on their PRs, and add
webhooks to them. If any is missing, that's a `## Blocked` item for the operator to fix.
- **Gitea bot permissions** (a grant, not a secret) — **least privilege: read, not admin.** The bot
needs: write on its own `recipe-maintainers/cc-ci` project repo; **read** + **comment** on the
recipe repos under test; and **org membership** in `recipe-maintainers` (read-level — used both to
authorize commenters via the members endpoint and to read members). It does **not** need repo-admin
and does **not** register webhooks (that's an optional manual admin task, §4.1). If a needed grant
is missing, that's a `## Blocked` item for the operator.
---
@ -329,26 +332,33 @@ Bridge posts/updates a Gitea PR comment with the run URL and (on completion) pas
- The bridge is a tiny service (Go or Python+FastAPI). Keep it dependency-light; it's a NixOS
systemd service behind Traefik at e.g. `ci.commoninternet.net/hook` (§4.0).
- **Trigger mode: webhook OR poll, mutually exclusive, flag-selected (SETTLED).** Two
implementations exist, but **only one runs at a time**, chosen by env (e.g. `BRIDGE_TRIGGER_MODE=
webhook|poll`): (1) the Gitea `issue_comment` **webhook** — the default/primary, low-latency push
path (confirmed working); (2) **polling** the Gitea API for new `!testme` comments — kept in the
codebase but **disabled by default**, the fallback you flip on when webhook delivery isn't arriving
(e.g. a gateway/network hiccup, as bit M3 early on). Polling reverses direction (cc-ci →
git.autonomic.zone, outbound — the reliably-working path) at ≤60s to satisfy D1. Because the modes
are exclusive, no cross-path dedupe is needed; just don't re-fire already-seen comments when poll
mode is switched on. Either mode alone satisfies D1.
- **Commenter auth uses effective permission, not the collaborators list.** The repo's explicit
collaborator list is empty — the bot *and* the real maintainers (`trav`/`notplants`) all reach
`recipe-maintainers/cc-ci` as **org owners**, so `GET /collaborators/{user}` 404s for everyone and
a naive is-collaborator check rejects all legitimate `!testme`. Authorize instead via
`GET /repos/{repo}/collaborators/{user}/permission` and require `owner`/`admin`/`write` (rejects
`read`/`none`/404 → still satisfies §6's non-collaborator-rejection check; fail-closed on any API
error). The bot token needs repo-admin to read another user's permission — fine, it's org owner.
- Enrollment = registering the Gitea webhook on a recipe repo (script in `runner/` or documented
in `enroll-recipe.md`) + ensuring a `tests/<recipe>/` dir exists. The `autonomic-bot` account is
**admin on the `recipe-maintainers` org**, so it can create repos there and add webhooks to any
recipe repo — no extra grant needed.
- **Trigger: POLLING is primary; webhook is an optional, admin-registered push optimization
(SETTLED).** Hard constraint: **the CI server/bot must run on READ-level access — never repo-admin.**
- **Polling (primary, default):** the bridge polls the Gitea API for new `!testme` comments on
enrolled repos at ≤60s (satisfies D1). This is **outbound** (cc-ci → git.autonomic.zone, the
reliably-working direction) and needs only **read**. It is the source of truth for triggering.
- **Webhook (optional):** the bridge keeps its `/hook` endpoint so a Gitea `issue_comment` webhook,
**if present**, gives lower latency. But the **server does NOT self-register webhooks** (that
needs repo-admin, which we refuse to require). Registration is a **manual admin task, documented**
in `docs/enroll-recipe.md` (URL `https://ci.commoninternet.net/hook`, event `issue_comment`,
content-type `json`, the shared HMAC secret, and the note that the Gitea instance must allow the
host). The two paths are mutually exclusive in effect; don't double-fire a comment seen by both.
- (Webhook delivery on this instance was flaky early on — `last_status: None` — so polling being
primary is also the robust choice, not just the low-privilege one.)
- **Commenter auth via org membership (read-level — no admin).** The repo's explicit collaborator
list is empty: the bot *and* the maintainers (`trav`/`notplants`) all reach the repo as
`recipe-maintainers` **org members/owners**, so `GET /collaborators/{user}` 404s for everyone, and
`GET /collaborators/{user}/permission` would authorize correctly but **requires repo-admin** — which
we refuse. Instead authorize with **`GET /orgs/recipe-maintainers/members/{user}`** (204 = member =
authorized; 404 = rejected) — readable by any **org member** (read-level), verified to admit
`trav`/`notplants`/the bot and reject non-members. Note `public_members` is hidden here, so use the
authenticated `members` endpoint (bot must be an org member, still read-level). Fail-closed on
error. Zero-privilege fallback: a configured allowlist of usernames. (Still satisfies §6's
non-collaborator-rejection check.)
- Enrollment = adding the recipe to the bridge's **poll list** + ensuring a `tests/<recipe>/` dir
exists. The bot needs only **read** on the recipe repo (+ comment-back to post status). Registering
a webhook is **optional and operator/admin-side** (documented in `enroll-recipe.md`), never required
for CI to work.
- **Recipe mirror+PR flow (how a recipe gets a testable PR).** Recipe repos under test live on the
**private mirror** `git.autonomic.zone/recipe-maintainers/<recipe>`, mirrored from the **official
upstream `git.coopcloud.tech`**. To bring a recipe under CI: `abra recipe fetch <recipe>` (pulls