From 69fc80aca0778b9fbba1c85c2d638853c4c274ac Mon Sep 17 00:00:00 2001 From: Joakim Persson Date: Sat, 15 Aug 2026 12:21:32 +0200 Subject: [PATCH] =?UTF-8?q?docs:=20"public=20repo"=20does=20not=20mean=20"?= =?UTF-8?q?readable=20CI"=20=E2=80=94=20the=20Actions=20API=20always=20nee?= =?UTF-8?q?ds=20a=20token?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Gitea-token section claimed "Public-repo GET listings work unauthenticated, so the token matters mainly for private repos or rate-limit headroom", while the same paragraph recommends the token for "inspecting CI runs". Those two statements are incompatible, and the optimistic one is wrong. Measured 2026-08-15 on gitea.jordbo.se against joakimp/pi-devbox (private=false): GET /api/v1/repos/joakimp/pi-devbox -> 200 GET /api/v1/repos/joakimp/mempalace-toolkit/tags -> 200 GET /api/v1/repos/joakimp/mempalace-toolkit/commits -> 200 GET /api/v1/repos/joakimp/pi-devbox/actions/runs -> 401 "token is required" So the unauthenticated claim holds for repo metadata, tags, and commit listings (which is what the resolve-versions mempalace-toolkit note above relies on, and that note stays correct), but NOT for any /actions/* endpoint. Narrowed the claim rather than deleting it. Note the workflow comment in pi-devbox .gitea/workflows/docker-publish.yml ("Gitea API requires auth even for public-repo commit listing") is wrong in the opposite direction — harmless, since it passes auth anyway, but it is not evidence for the strong claim. This cost a real detour while triaging the v1.8.0 smoke failure: repo -> 200 was read as "public, so CI is readable", then actions/runs 401'd. Also recorded two things learned in the same session: - The env GITEA_ACCESS_TOKEN was REVOKED, not stale. Proved it by comparing sha256 fingerprints (not values) against the host's compose .env: identical fingerprint, and 401 from the host too. Container env is baked at START, so a host-side renewal is invisible until recreate. Added /api/v1/version as the cheap "is this token alive at all" probe. - With no usable token, Docker Hub tags localise a CI failure, because smoke gates build-variant. A fresh base- with no vX.Y.Z and latest still on the previous release == "base built, smoke failed, nothing shipped". That is how v1.8.0 was diagnosed before a token existed; the logs later confirmed it exactly, including the corollary that a failed (not skipped) smoke proves GITEA_BUILD_TOKEN was alive in CI. --- AGENTS.md | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fa40d97..0a2ef08 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,11 +148,46 @@ Gitea API interaction from inside the container — inspecting CI runs, checking published tags, listing commits — e.g. `curl -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/repos/joakimp/opencode-devbox/actions/runs?limit=5"`. Prefer this over a short-lived PAT file when the env token is present (the -`ci-release-watcher` skill auto-detects it). Public-repo GET listings work -unauthenticated (see the `resolve-versions` mempalace-toolkit note above), so -the token matters mainly for private repos or rate-limit headroom; its -lifecycle is host-managed, so there is nothing to revoke after use. Never -echo the token value (including into logs). +`ci-release-watcher` skill auto-detects it). Never echo the token value +(including into logs). + +**Gotcha — "public repos need no token" does NOT extend to the Actions API.** +Repo metadata, tags, and commit listings on a public repo are readable +unauthenticated (measured 2026-08-15 on `gitea.jordbo.se`: `GET /repos/{o}/{r}`, +`/tags`, `/commits?limit=1&sha=main` → all `200`; this is what the +`resolve-versions` mempalace-toolkit note above relies on). But **every +`/actions/*` endpoint returns `401 {"message":"token is required"}` even when +`private=false`** — including `/actions/runs`, `/actions/runs//jobs`, and +`/actions/jobs//logs`. So the token is *mandatory* for exactly the use case +this section opens with (inspecting CI runs), not merely "nice for private repos +or rate-limit headroom". Cost a wasted detour once: `repo → 200` was read as +"public, so CI is readable", which it is not. + +**Gotcha — the env token can be silently REVOKED, and renewing the host `.env` +does not reach a running container.** The container's environment is baked at +**start**, so a host-side token renewal is invisible until the container is +recreated (or the value is re-read explicitly over SSH). Worse, a revoked token +looks identical to a valid one — the failure only shows up as `401` per request. +Verify before concluding anything about a repo's visibility: +`curl -s -o /dev/null -w '%{http_code}' -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/version"` → `200` means the token +itself is alive. To tell staleness from revocation, compare fingerprints rather +than values: `printf '%s' "$GITEA_ACCESS_TOKEN" | sha256sum` against +`shasum -a 256` of the token in the host's compose `.env` — identical +fingerprints that both `401` mean the token is revoked upstream, not stale in +the container. + +**Workaround — with no usable token, Docker Hub tags localise a CI failure.** +The job graph makes registry state a progress oracle, because `smoke` *gates* +`build-variant` and `build-variant` gates `promote-base-latest`. Read +`https://hub.docker.com/v2/repositories///tags?page_size=100` (public, +no auth) and compare timestamps: a fresh `base-` with **no** `vX.Y.Z` tag +and `latest` still on the previous release pinpoints "base built, smoke failed, +nothing shipped" without reading a single log line. Diagnosed v1.8.0 this way +before a token was available; the log later confirmed it exactly. A second free +deduction from the same graph: since `smoke`'s `if:` requires +`needs.resolve-versions.result == 'success'`, a *failed* smoke proves +`GITEA_BUILD_TOKEN` was alive in CI — a dead build token would have **skipped** +smoke (via `require_sha`), not failed it. **Gotcha — the jobs endpoint takes the internal `id`, NOT the `run_number` the UI shows as `#239`.** The two diverge widely (`id=534` was `run_number=238`),