From 66a19aa3949fc5cfcd24caa575477326a40a16f7 Mon Sep 17 00:00:00 2001 From: pi Date: Tue, 4 Aug 2026 18:06:34 +0200 Subject: [PATCH] docs(agents): how to find the release run (tag push fires two workflows) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The release-day checklist said "Watch CI" without saying which run, and the Gitea API example used limit=5. Both are traps, because a tag push produces TWO runs here: lint.yml has a bare `push:` trigger so it fires on the tag ref as well, and docker-publish.yml fires on v*. The runs listing is newest-first and the lint run sorts ABOVE the publish run, so "first run matching refs/tags/" picks lint reliably. Verified against the real API for v1.6.4: id=531 #104 lint.yml@refs/tags/v1.6.4 <- picked by the naive rule id=530 #103 docker-publish.yml@refs/tags/v1.6.4 <- the actual release build id=529 #102 lint.yml@refs/heads/main <- same sha, already linted Lint goes green in minutes while the image is still building, so watching it makes a release look finished before anything is published. limit=5 compounds it: the publish run is already at position 4 of 5 in the current listing. Documents: head_sha-filtered discovery with limit=20; the jobs endpoint takes the internal id, never the run_number (silently returns another run's jobs); and the correct ci-release-watcher config for this repo — EXPECT_WORKFLOW, the studio tag pair, base-latest as existence-only, and CRITICAL_JOBS with build-variant-studio spelled out (job names are matched exactly, and the skill's default omits it) while excluding promote-base-latest, which legitimately skips on a base cache hit. Smoke-gate detail in step 5 is retained. --- AGENTS.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index ab1b8e2..24f18f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -76,7 +76,10 @@ re-brand of opencode-devbox's `pi-only` variant. 4. Push tag: `git tag vX.Y.Z && git push origin vX.Y.Z`. 5. Watch CI: smoke job builds amd64 only and asserts size + extensions + pi version + new-base-tooling presence. Variant build is multi-arch - (amd64 + arm64) only after smoke passes. + (amd64 + arm64) only after smoke passes. **A tag push produces two runs, not + one** — `lint.yml` fires on every push (including tag refs) and + `docker-publish.yml` fires on `v*` tags. Watch the **publish** run; see + *Gitea API access* below for how to find it without picking lint by mistake. 6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus base-latest if the base was rebuilt this run). 7. **Revoke any short-lived Gitea PAT** used during the release at @@ -92,13 +95,61 @@ host `.env` via `docker-compose.yml` (`${GITEA_ACCESS_TOKEN:-}` / **not** baked into the image. When configured, they are also available for **any** direct 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/pi-devbox/actions/runs?limit=5"`. +`curl -H "Authorization: token $GITEA_ACCESS_TOKEN" "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20"`. 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 too, 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). +**Gotcha — a tag push fires EVERY workflow whose triggers match the tag ref.** +`lint.yml` uses a bare `push:` trigger, so a release tag yields *both* a lint run +and the publish run. The listing is newest-first and lint sorts **above** the +publish run, so "take the first run whose `path` contains `refs/tags/`" +picks the wrong one **reliably, not occasionally**. Real listing for v1.6.4: + +``` +id=531 #104 lint.yml@refs/tags/v1.6.4 <- wrong; sorts first +id=530 #103 docker-publish.yml@refs/tags/v1.6.4 <- the release build +id=529 #102 lint.yml@refs/heads/main <- same commit, linted on push +``` + +Lint goes green in minutes while the image is still building, so watching it +makes a release look finished when nothing has been published yet. + +**Gotcha — the jobs endpoint takes the internal `id`, NOT the `run_number` the +UI shows as `#104`.** The two diverge widely, and `GET +.../actions/runs//jobs` does **not** error — it silently returns a +*different* run's jobs. Always read `id` from the run listing: + +```bash +# Which runs did this tag/commit trigger? Filter on head_sha; never trust +# ordering or run numbering. limit=20, not 5 — with two runs per push the +# publish run falls off a 5-item window fast. +curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \ + "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs?limit=20" \ + | jq --arg sha "$(git rev-list -n1 vX.Y.Z)" \ + '.workflow_runs[] | select(.head_sha==$sha) | {id, run_number, path, status, conclusion}' +# pick the id whose .path starts with docker-publish.yml, then: +curl -sS -H "Authorization: token $GITEA_ACCESS_TOKEN" \ + "$GITEA_HOST/api/v1/repos/joakimp/pi-devbox/actions/runs//jobs" \ + | jq '.jobs[] | {name, status, conclusion}' +``` + +**Watcher config for this repo** (`ci-release-watcher` skill, hub-only shape — +pi-devbox has no downstream host to deploy to): + +- `EXPECT_WORKFLOW=docker-publish.yml` — the skill's `preflight_run()` aborts at + startup if the run id belongs to lint instead. +- `EXPECTED_FRESH_TAGS='vX.Y.Z latest vX.Y.Z-studio latest-studio'` +- `EXPECTED_EXISTS_TAGS='base-latest'` — existence only: it is content-addressed + and legitimately keeps its old timestamp when the base is a cache hit. +- `CRITICAL_JOBS='build-variant build-variant-studio'` — job names are matched + **exactly** (`critical.issubset(succeeded)`), so the studio variant must be + listed explicitly; the skill's default omits it. Leave `promote-base-latest` + out: it legitimately skips on a base cache hit, which would misclassify a good + run. `update-description` is the cosmetic post-publish job. + ## Cache-hit footgun (must-know) `PI_VERSION` defaults to `latest` in `Dockerfile.variant` but **CI must