Repository navigation
bug(ci): release-fast-path verifies and deletes against ghcr.io/kpa-clawbot, not our registry #164
Description
Parent: #158. Found while running the #159 diagnostic.
.github/workflows/release-fast-path.yml tags our registry and then verifies and deletes against upstream's. A release through the fast path cannot succeed, and one of its steps attempts a destructive operation on a registry we do not own.
Evidence [repo: .github/workflows/release-fast-path.yml @ master]
Every IMAGE= assignment in the file:
| line | step | registry |
|---|---|---|
| 95 | Inspect :edge revision label |
ghcr.io/oki-mesh/corescope ✅ |
| 137 | Re-tag :edge → :vX.Y.Z / :vX.Y / :vX / :latest |
ghcr.io/oki-mesh/corescope ✅ |
| 205 | Verify the published tags carry every :edge platform |
ghcr.io/kpa-clawbot/corescope ❌ |
| 240 | Remove the per-platform scratch tags | ghcr.io/kpa-clawbot/corescope ❌ |
What each wrong step does
Line 205, the verify gate. Reads :edge and each release tag from upstream's registry and compares their platform lists, with exit 1 on mismatch:
WANT="$(platforms_of "${IMAGE}:edge")"
... if [[ "$GOT" != "$WANT" ]]; then echo "::error::..."; exit 1
Two possible outcomes, both wrong. It fails the release on a registry we never wrote to, or it passes by comparing upstream's images against each other — in which case our freshly published tags are never verified at all. The step comment says why this gate exists: "A release tag that resolves to one platform is worse than a slow release: docker pull on the other architecture fails outright." That protection is currently pointed at the wrong registry.
Line 240, the scratch-tag cleanup. Runs crane delete against upstream's registry:
for T in $(crane ls "$IMAGE" | grep "^tmp-${TAG}-" || true); do
crane delete "${IMAGE}:${T}" || echo " (could not delete ${T}, leaving it)"
We almost certainly lack write access to ghcr.io/kpa-clawbot, and the step is best-effort, so it fails silently. The consequence is that our tmp-vX.Y.Z-linux-amd64 / -arm64 scratch tags are never cleaned up and accumulate in our registry, while the job reports success.
Why nobody noticed
The registry moved to ghcr.io/oki-mesh in 0a43f9f (2026-07-26, #60), which repointed the main pipeline but left these two steps. The fast path has not run since: prod has been held on v0.1.1 and the last tag cut, v0.2.0 → 9a66159, predates it mattering. So this has been latent for ~10 weeks with no signal.
Fix
Change both to ghcr.io/oki-mesh/corescope. Four lines of mechanical change in one file; no logic change.
Better: hoist the registry to one place — a env: key on the job or a workflow-level env — so a future move cannot leave half the steps behind. That is the actual defect; two of four steps being wrong is a symptom of the value being written out four times.
Relation to this Epic
This is in scope for #158 rather than beside it: it is the pre-handover repository reachable from live CI. Detaching from the fork network does not fix it — the string is hardcoded, so it would keep pointing at kpa-clawbot afterwards and keep failing. Worth landing with the detach so the break with upstream is real in CI, not just in GitHub's metadata.
Correction on the record
When #147 was triaged, the remaining pre-handover references were described as historical logs that could stay. That was wrong: this file was in the same set and is live CI. The characterisation was made without reading the file.
Done when
No step in release-fast-path.yml references kpa-clawbot, the registry is defined once, and a dry run or a real tag cut shows the verify gate reading our own published tags.
Activity
diffwireauto commentedon Oct 3, 2026
Fix pushed, Gemini pre-PR review run
Branch
fix/164-release-fast-path-registry, commit7a9e1c3e. One file, +13 / −4.REGISTRY_IMAGE: ghcr.io/oki-mesh/corescopeIMAGE="${REGISTRY_IMAGE}"kpa-clawbotin the fileenvpresent, both jobs intactThe duplication is fixed, not just the two wrong values. Four hardcoded copies is what let #60 update only two of them; correcting the strings alone would leave the same trap for the next registry move.
Gemini 2.5 Pro adversarial review
Three of five claims confirmed:
Shell expansion works, and is better than
${{ env.X }}here — confirmed. Workflow-levelenvreaches the step's shell as a real environment variable. Gemini added a reason I had not considered: underset -u,IMAGE="${REGISTRY_IMAGE}"fails loudly with "unbound variable" if it is ever undefined, whereasIMAGE="${{ env.REGISTRY_IMAGE }}"would be interpolated to an empty string before the shell runs, satisfyingset -uand then failing later with a confusingcrane"invalid reference format". The chosen form fails earlier and louder.set -euo pipefailinteraction — confirmed as an improvement: a silent wrong-registry becomes an explicit failure. The one step usingset -uo pipefail(no-e) is the best-effort cleanup, which is deliberate.No other registry reference missed — confirmed.
registry: ghcr.ioindocker/login-actionis the host, not an image path, and is correct. The${{ github.repository }}uses resolve dynamically.One Gemini finding rejected, with evidence
Gemini recommended moving the
envblock from workflow level to job level, on the grounds that workflow-levelenvis "implicitly passed into thedeploy-prod.ymlreusable workflow's environment" and could collide with a variable of the same name there.That premise is wrong
[verified: docs 2026-10-03]. GitHub's reusable-workflow reference states:deploy-prodin this file is a reusable-workflow call (uses: ./.github/workflows/deploy-prod.yml), so it cannot seeREGISTRY_IMAGEat all and no collision is possible. Workflow level and job level are functionally identical here. Keeping it at workflow level; moving it to satisfy a disproven risk would be change without reason.Two real findings, deliberately out of scope
Both are genuine and neither belongs in a registry fix. Filed separately rather than widened into this PR:
if [[ "${{ steps.edge.outputs.no_edge == 'false' && ... }}" == 'true' ]]— instead of consuming thefast_pathoutput that theedgestep already computes and that thedeploy-prodjob consumes cleanly. Same logic in three places.crane ls "$IMAGE" | grep ... || truemasks acrane lsfailure, not just an empty grep. A permissions error would be swallowed and the step would report success having deleted nothing.Gates
Gemini review: done, no blocking finding. PR creation needs a grant from the owner.
test(ci): honour workflow-level env in the release-routing harness
diffwireauto commentedon Oct 4, 2026
PR #165 CI status: required check green, E2E failure is unrelated
[verified: ci 2026-10-04]✅ Go Build & Test(the only required context)🏗️ Build & Publish Docker ImageDetect changes🕵️ Commit Author Email Check🎭 Playwright E2E TestsThe E2E failure is
test-observer-iata-1188-e2e.jsat a 375px viewport, timing out with 35 rows present but none visible. It is not caused by this PR: the diff is.github/workflows/release-fast-path.ymlandtests/unit/test-issue-1956-release-routing.jsonly, nothing underpublic/,cmd/ortests/e2e/. The same test passes onmasterat the same base commitff5e338, on two separate dispatches. Full reproduction record in #166.Also worth recording, because it was a real defect in this change: the first CI run on this branch failed the required check with
tests/unit/test-issue-1956-release-routing.jsextracts the release workflows'run:blocks and executes them against stubbedcrane/gh, but built each step's environment without the workflow-levelenv:block that Actions exposes. So the harness did not model the platform, and the moment the workflow started reading a workflow-level var the extracted step died underset -uwhile the real workflow would have been fine.Fixed in
bcf6b5adby addingworkflowEnv()and aworkflowVarsparameter onrunSteps(), applied at all four call sites — includingdeploy.yml's, whose five top-level vars were equally unmodelled. The required check has passed since.Ready for review. Merge needs a grant pinned to
bcf6b5ad689935282b4009cea7b75d196e9d3577.fix(ci): point release-fast-path at our own registry and define it on…
docs(#163): how we take upstream changes now the fork link is gone
docs(#163): close the gaps an adversarial review found in UPSTREAM.md
docs(#163): how we take upstream changes now the fork link is gone
docs(#163): close the gaps an adversarial review found in UPSTREAM.md