ci(images): publish :dev and :stg ingestor channels - #422
Conversation
The ingestor image was a prod-release-only artifact: release-image.yml builds only from a vX.Y.Z tag and gates on the version being on PyPI, and PyPI publishes only from master. Testing a proposed change on a dev or staging edge therefore required a full production release -- on 2026-07-30 that cost a prod PyPI publish, a skip-fr-gate override and a manual dispatch to validate one change. Adds develop -> :dev and staging -> :stg, matching client-runtime and tracebloc-engine, which already have per-environment channels. release-image.yml is untouched: prod stays tag-triggered, PyPI-gated and signed, so the cosign identity customers verify against keeps its exact meaning. This workflow structurally cannot mint a prod tag -- it refuses any ref other than develop/staging and any channel other than dev/stg. No PyPI gate here: that lockstep is release policy, not a build dependency (the Dockerfile builds from source). Internal channels are unsigned on purpose, keeping exactly one signed trust root. Native per-arch runners instead of QEMU: client-runtime measured the emulated arm64 leg as the bulk of its ~13 min build, and today's QEMU-based v0.8.0 release-image run took ~25 min. Two native runners in parallel plus a manifest merge makes multi-arch cost roughly amd64-alone wall-clock, so arm64 edges still get a pullable image without an emulation tax on every develop push. The merge refuses to publish a partial (single-arch) index -- an amd64-only tag ImagePullBackOffs on arm64 kubelets (client#186).
|
bugbot run |
|
👋 Heads-up — Code review queue is at 37 / 30 Above the WIP limit. The team convention is to review existing PRs before opening new work. Open PRs currently in Code review (oldest first):
Pull from review before opening new work. (This is a nudge from the kanban WIP check, not a block.) |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit cfc7ca7. Configure here.
…ef alone Bugbot on #422: - HIGH: no schema smoke probe. Every other publisher runs _load_schema() against what it ships, guarding the v0.3.0-rc1 regression (schema/ missing from site-packages because the dir had no __init__.py). Added per-arch, natively, against the just-pushed digest -- which is unreferenced until the merge creates the channel tag, so a packaging break leaves :dev/:stg on the last good image instead of a broken one. Also invokes the console script and asserts it names INGEST_CONFIG, matching release-image.yml. - MEDIUM: the channel input skipped the develop/staging allowlist when set, so a dispatch from master could publish master code to :dev; and the choice preselected dev, so a staging dispatch silently defaulted to the wrong channel. Removed the input entirely -- the ref is now the only source of the channel, so code and tag cannot disagree. Dispatch still works by picking the ref. - MEDIUM: concurrency split by trigger (publish-images-develop for a push vs publish-images-dev for a dispatch), so two runs could race the same moving tag without cancelling each other. Keyed on the ref alone, which the input removal makes exact.
|
All three findings were valid — fixed in 936d864.
Added it per-arch, natively, against the just-pushed digest — and placed deliberately: that digest is unreferenced until the merge job creates the channel tag, so a packaging failure leaves
Neatly, one change fixed two: dropping the input eliminated both the allowlist bypass and the group split. Verified: All three threads resolved. bugbot run |

Part 1 of tracebloc/backend#1360. @saadqbal flagging you as the release-pipeline owner.
What this fixes
The ingestor image is the only component without per-environment channels — client-runtime (
publish-images.yml) and tracebloc-engine (docker-build.yml) both dodevelop -> :dev,staging -> :stg. The ingestor image only ever existed as a byproduct of a prod release, so validating a change on a staging edge required a production release. Today that meant a prod PyPI publish + askip-fr-gateoverride + a manual dispatch, to test one change (#421 and the failedv0.8.0tag-build are the receipts).Note
publish-dev.ymldoes not already cover this: it publishes the Python package to GitHub Packages, and edges never pip-install the ingestor — jobs-manager spawns the image.Design notes
release-image.ymlis untouched. Prod remainsvX.Y.Z-triggered, PyPI-gated, cosign-signed with the Release digest. A separate file (rather than adding branch triggers there) keeps the customer-facing cosign identityrelease-image.yml@refs/tags/v.*meaning exactly what it says, and makes it structurally impossible for a branch build to reach the semver tags, the GitHub Release, or the PyPI gate.No PyPI gate here.
verify-publishedis a release-policy lockstep ("never ship an image for a version that did not also publish to PyPI"), not a build dependency — the Dockerfile builds from source (pip wheel ., commented "ships the exact code being released, not whatever is on PyPI").:dev/:stgaren't releases.Unsigned on purpose. One signed trust root (the prod tags) is clearer than two with different identities. Documented in the file header; don't pin
:dev/:stgas a chart default.Native runners, not QEMU — this is the part that removes the cost objection in #1360. client-runtime's own comment measures the emulated arm64 leg as the bulk of its ~13 min build; today's QEMU-based
release-imagerun took ~25 min. Building each arch onubuntu-24.04/ubuntu-24.04-armin parallel and merging withimagetoolsmakes multi-arch cost about amd64-alone wall-clock — so arm64 edges (Apple Silicon laptops, arm64 nodes) get a pullable image without an emulation tax on every push. Path filters keep docs/workflow-only pushes from spending two runners.Safety properties
develop/staging, and any channel that isn'tdev/stg— verified againstmaster,main,v0.8.0, and forced inputsprod/latest, all correctly refused.concurrencyper channel withcancel-in-progress— a channel tag is a moving pointer, so superseding an in-flight build is correct.env:, never interpolated into shell (R8).Verification
actionlint 1.7.12 -shellcheck shellcheck 0.11.0(the versions CI pins): 0 findings. Channel-mapping and partial-index guard logic exercised locally across all branches shown above.Still needed to make this usable (Part 2, not in this PR)
Publishing the tags is necessary but not sufficient:
client/values.yamlhas a singleimages.ingestor.tag(default"0.7") shared by dev and staging, so the two can't sit on different channels.prodDigestis env-aware buttagisn't. #1360 covers making the effective tag env-aware (dev -> dev,stg -> stg, prod unchanged). Until then a test edge can adopt a channel with an explicitimages.ingestor.tag: dev|stgoverride.The first
developpush after this merges will publish:dev— worth watching that run to confirm both arches land.Note
Low Risk
CI-only change publishing unsigned internal images to GHCR; prod release signing and PyPI-gated flow stay in
release-image.yml.Overview
Adds
.github/workflows/publish-images.ymlso the ingestor Docker image can ship on internal:dev/:stgchannels fromdevelopandstaging, without touching prodrelease-image.yml.The workflow maps branch → tag only (no dispatch channel input), path-filters ingestor-related changes, and uses native amd64/arm64 runners with digest push + manifest merge to
ghcr.io/tracebloc/ingestor. It deliberately skips PyPI verification, cosign, and provenance for these channels. Per-arch smoke tests run against the digest before the channel tag moves; merge refuses a single-arch index and verifies both linux/amd64 and linux/arm64 on the published tag.Reviewed by Cursor Bugbot for commit 936d864. Bugbot is set up for automated code reviews on this repo. Configure here.