From 34cbb60f35cbb07f6953df41e485aa20ec159623 Mon Sep 17 00:00:00 2001 From: autonomic-bot Date: Wed, 27 May 2026 02:37:17 +0100 Subject: [PATCH] =?UTF-8?q?plan=20=C2=A74.1/=C2=A71.5:=20polling=20primary?= =?UTF-8?q?=20+=20read-only=20CI;=20webhook=20is=20optional=20manual-admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- cc-ci-plan/plan.md | 58 +++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/cc-ci-plan/plan.md b/cc-ci-plan/plan.md index 1cf2a05..10ca841 100644 --- a/cc-ci-plan/plan.md +++ b/cc-ci-plan/plan.md @@ -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//` 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//` 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/`, mirrored from the **official upstream `git.coopcloud.tech`**. To bring a recipe under CI: `abra recipe fetch ` (pulls