Skip to content

bug(ci): release-fast-path verifies and deletes against ghcr.io/kpa-clawbot, not our registry #164

Open

Description

@diffwireauto

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

  1. diffwireauto commented on Oct 3, 2026

    @diffwireauto
    ContributorAuthor

    Fix pushed, Gemini pre-PR review run

    Branch fix/164-release-fast-path-registry, commit 7a9e1c3e. One file, +13 / −4.

    REGISTRY_IMAGE: ghcr.io/oki-mesh/corescope defined once, workflow level
    lines 104, 146, 214, 249 all now IMAGE="${REGISTRY_IMAGE}"
    kpa-clawbot in the file 0 occurrences
    YAML parses; workflow env present, both jobs intact

    The 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-level env reaches the step's shell as a real environment variable. Gemini added a reason I had not considered: under set -u, IMAGE="${REGISTRY_IMAGE}" fails loudly with "unbound variable" if it is ever undefined, whereas IMAGE="${{ env.REGISTRY_IMAGE }}" would be interpolated to an empty string before the shell runs, satisfying set -u and then failing later with a confusing crane "invalid reference format". The chosen form fails earlier and louder.

    set -euo pipefail interaction — confirmed as an improvement: a silent wrong-registry becomes an explicit failure. The one step using set -uo pipefail (no -e) is the best-effort cleanup, which is deliberate.

    No other registry reference missed — confirmed. registry: ghcr.io in docker/login-action is 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 env block from workflow level to job level, on the grounds that workflow-level env is "implicitly passed into the deploy-prod.yml reusable 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:

    "Any environment variables set in an env context defined at the workflow level in the caller workflow are not propagated to the called workflow."

    deploy-prod in this file is a reusable-workflow call (uses: ./.github/workflows/deploy-prod.yml), so it cannot see REGISTRY_IMAGE at 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:

    1. The "Dispatch release artifacts" step re-implements the fast-path condition in shell — if [[ "${{ steps.edge.outputs.no_edge == 'false' && ... }}" == 'true' ]] — instead of consuming the fast_path output that the edge step already computes and that the deploy-prod job consumes cleanly. Same logic in three places.
    2. crane ls "$IMAGE" | grep ... || true masks a crane ls failure, 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.

  2. diffwireauto commented on Oct 4, 2026

    @diffwireauto
    ContributorAuthor

    PR #165 CI status: required check green, E2E failure is unrelated

    [verified: ci 2026-10-04]

    check result
    ✅ Go Build & Test (the only required context) pass, 12m43s
    🏗️ Build & Publish Docker Image pass
    Detect changes pass
    🕵️ Commit Author Email Check pass
    🎭 Playwright E2E Tests fail — not required, filed as #166

    The E2E failure is test-observer-iata-1188-e2e.js at 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.yml and tests/unit/test-issue-1956-release-routing.js only, nothing under public/, cmd/ or tests/e2e/. The same test passes on master at the same base commit ff5e338, 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

    bash: line 61: REGISTRY_IMAGE: unbound variable
    

    tests/unit/test-issue-1956-release-routing.js extracts the release workflows' run: blocks and executes them against stubbed crane/gh, but built each step's environment without the workflow-level env: 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 under set -u while the real workflow would have been fine.

    Fixed in bcf6b5ad by adding workflowEnv() and a workflowVars parameter on runSteps(), applied at all four call sites — including deploy.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.

  3. added 2 commits that reference this issue on Oct 7, 2026
  4. added 2 commits that reference this issue on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions