Repository navigation
Finish the CI image-build refactor: SBOM emission and pr-* tag retention #2937
Description
Status as of 2026-10-01
Checked against main at bc400dff1. One of the three "Still live" items is done (the bypassing Makefiles, #4485), multi-arch builds landed in another form than the matrix axis this epic deferred, and the runner notes changed. SBOM and OCIR pr-* retention are untouched. More below under each heading.
Status
This epic has been narrowed. The premise it was filed on has expired, and two of its three headline deliverables have shipped. What remains is listed under "Still live" below. Everything under "Landed" is closed out with proof and needs no further work.
The original background blamed a dedicated self-hosted runner (cozy-runner-1) whose single embedded buildkit contended under concurrent builds, plus a cold registry cache after #2711 repointed --cache-from at a tag that stopped being published. Both causes are gone. Every job now runs on the ephemeral Oracle VM pool, each build job creates its own docker-container buildkit, and a mode=max registry cache is warmed on every push to main. gh api repos/cozystack/cozystack/actions/runners returns total_count: 0. One workflow on main targets self-hosted on purpose, e2e-proxmox.yaml ([self-hosted, proxmox], gated behind the PROXMOX_NIGHTLY variable which is unset, not triggered by pull requests).
Landed
Phase 0, verifications:
- OCIR accepts buildkit
mode=maxcache manifests. Thecache-argsmacro inhack/common-envs.mkshipsoci-mediatypes=true,image-manifest=trueand is in production use. - A smaller shared-runner label exists and is used:
oracle-vm-4cpu-16gb-x86-64forchecks,build,build-talosandfinalizein.github/workflows/pull-requests.yaml. - buildx works on the shared pool:
docker/setup-buildx-actionwithdriver: docker-containerin.github/workflows/pull-requests.yaml.
Phase 1, registry cache and warmer (#2938's design, landed via #2983):
--cache-to type=inlineis gone.hack/common-envs.mkcache-argsemits--cache-from type=registry,ref=$(CACHE_REGISTRY)/<img>:buildcachealways, and--cache-to ...,mode=maxonly whenWRITE_CACHE=1.WRITE_CACHEdefaults to0, so PR builds are read-only against the cache and cannot race on the cache manifest..github/workflows/build-main.yamlwarms the cache on every push tomainwithWRITE_CACHE: '1'..github/workflows/build-release.yamldeliberately reads only (WRITE_CACHE: '0'), keepingbuild-mainthe single serialized writer. ci: warm the build cache on release-1.5 pushes #3469 warmedrelease-1.5separately, on that branch only, via a branch-scopedCACHE_REGISTRYnamespace.
Phase 2, matrix and digest merge (#2983):
planemits the matrix fromhack/build-matrix.sh, which parses the rootMakefilebuild:target so CI cannot drift from whatmake buildbuilds.buildis one job per package,fail-fast: false, each with its owndocker-containerbuildkit. The shared-buildkit contention class is severed by construction.- Each job uploads a
pr-patch-fragment-<pkg>artifact holding only its own package's digest diff, andfinalizereassembles them withgit apply --3wayinto the unifiedpr.patchthat e2e consumes unchanged, then builds the installer last soflux push artifact --path=packagesbundles an already-patched tree. - Talos is its own always-on leg (
build-talos), andhack/build-matrix.shexcludespackages/core/talosandpackages/core/installerfrom the matrix with the reasoning inline. - The parity gate against a legacy
make buildwas run once at cutover and is not a standing test.
Phase 3, change scoping and runner cutover:
- Change scoping ships, though by parsing the Makefile rather than with
paths-filter. Shared-dependency fan-out isfull_rebuild_patterninhack/build-matrix.sh, plus a targeted seaweedfs to objectstorage-controller rule. runs-onis the shared ephemeral label everywhere onmain. Thedebuglabel no longer flipsruns-on, it only opens the breakpoint on the runner the job already holds.- The dedicated runner is decommissioned (ci: move remaining workflows off the persistent self-hosted runner #3268). No repo-registered runner remains. The physical host and the breakpoint rendezvous server are not checked from the repo,
BREAKPOINT_ENDPOINTis still read bypull-requests.yamlande2e-tag.yaml.
Still live
-
Probe OCIR referrers/attestation support, then turn SBOM on. Still open on 2026-10-01,
SBOM ?= 0and no workflow sets it.hack/common-envs.mkalready carries the seam:SBOM ?= 0, and--sbom=trueis appended toBUILDX_ARGSwhenSBOM=1. It is off because an unsupported attestation push fails the build outright, and nobody has probed OCIR. This is the one blocker for every remaining SBOM item. Done means: the probe result recorded here, and eitherSBOM=1set in the build workflows or a decision to deliver SBOMs as workflow artifacts instead. -
Bring the three bypassing package Makefiles underDone in fix(build): pass the shared buildx flags from every image target #4485 (merged 2026-09-28).BUILDX_ARGS.packages/system/dashboard/Makefile,packages/system/flux-plunger/Makefileandpackages/system/kubeovn-plunger/Makefileappend$(BUILDX_ARGS)now, so a flag added there reaches those images too. -
Retention or GC for
pr-*tags on OCIR..github/workflows/retention.yamlprunes GHCR nightly container-package versions only (dated0.0.0-nightly.*tags,KEEP: 14, via the GitHub Packages API). Nothing prunes OCIR, and every PR build pushespr-<N>-<sha>per built image. Registry accumulation persists even now that runners are ephemeral. Separate from this, the GHCR job itself is broken: its package listing returns HTTP 400 and it has been red on every scheduled run since 2026-08-18 (tracked as A19 in [Tracking] Remaining CI / build / release / E2E automation work #3278).
Reviewed and deliberately left alone
origin/release-1.5 still carries 10 unconditional runs-on: [self-hosted] sites and origin/release-1.4 carries 9, all naming the decommissioned runner. This was reviewed and is deliberately not being cleaned up. release-1.4 is closed and takes no further patches, so its 9 sites are dead text outright. On release-1.5 the lanes that carry the label do not fire, and the ones that do already target shared runners, so a pointwise fix when it is actually needed beats a sweep now. The evidence backs that. Nothing is queued or waiting repo-wide. auto-release.yaml cannot fire, because schedule only triggers for the workflow file on the default branch and the file is absent from main. update-releasenotes.yaml is gated on push: branches: [main], which a release-branch push cannot match. backport.yaml and pull-requests-release.yaml do get triggered on release-line PRs, but every recent run is completed/skipped, and a skipped job never requests a runner. The only workflow that actually runs on release-1.5 is build-main.yaml, which #3469 already moved to oracle-vm-24cpu-96gb-x86-64.
The one residual worth knowing about was tags.yaml and release-e2e.yaml on the release lines, which fire on push: tags: v*. tags.yaml on release-1.5 runs on oracle-vm-24cpu-96gb-x86-64 now and the v1.5.4 tag build succeeded. release-e2e.yaml on release-1.5 is still [self-hosted] and the v1.5.4 run of it was cancelled, which fits a label nothing serves. I haven't looked at release-1.4, it is closed.
The stale comment in pull-requests.yaml ("only the debug label flips runs-on") is gone from main.
Deferred, not dropped
- Parallelising the release build. Partly done:
tags.yamlbuilds per architecture now (build-amd64andbuild-arm64jobs), but the amd64make buildis still one serial run.build-main.yamlandbuild-release.yamlstay serial on purpose, a single serialized writer is what keeps the shared:buildcachemanifest race-free. docker buildx bakeconsolidation of the per-package buildx invocations. Not started.- Multi-arch. Landed in another form than a
platformmatrix axis:DEFAULT_PLATFORMSislinux/amd64,linux/arm64inhack/common-envs.mk,hack/stitch-multiarch.shstitches the per-architecture images into one index, and there is abuild-arm64-nightly.yamlworkflow. PRs feat(build): build package images for amd64 and arm64 #4498, ci(release): build pre-release images for amd64 and arm64 and gate releases on it #4505, feat(vpn): build shadowbox for amd64 and arm64 #4521 and feat(build): build Talos, Harbor, the Velero KubeVirt plugin and the unlisted images for arm64 #4552.
Obsolete
The original "Background" section and its three numbered causes are historical. cozy-runner-1 is decommissioned, the buildkit-contention failure class is retired by one buildkit per job, and the inline-cache multistage problem is fixed by mode=max. The three "Open questions" are also resolved: #2855 was merged as interim relief (2026-06-09), a smaller shared-runner label does exist and is in use, and SBOM delivery is still open pending the referrers probe (item 1 above).
References
- Landed here: fix(build): read --cache-from from ghcr :latest so PR builds warm-start #2855, ci: consolidated platform & e2e stabilization batch #2948, refactor(build): parallel matrix image builds on ephemeral runners #2983, feat(release): immutable tags & rc→stable promotion (#2677) #3017, fix(ci): restore e2e for fork PRs and close the merge-gate bypass #3262, ci: move remaining workflows off the persistent self-hosted runner #3268, ci: warm the build cache on release-1.5 pushes #3469, fix(build): pass the shared buildx flags from every image target #4485, feat(build): build package images for amd64 and arm64 #4498, ci(release): build pre-release images for amd64 and arm64 and gate releases on it #4505, feat(vpn): build shadowbox for amd64 and arm64 #4521, feat(build): build Talos, Harbor, the Velero KubeVirt plugin and the unlisted images for arm64 #4552
- Closed without merging, superseded by refactor(build): parallel matrix image builds on ephemeral runners #2983: refactor(build): mode=max registry cache with a main-only warmer #2938, ci(build): isolate each PR build on its own ephemeral runner VM #2939
- Closed without merging, unrelated to this epic: test(metallb): assert digest-pinned image form, not exact version literal #2873, fix(ci, kubernetes): build container disks concurrently to unblock Build #3457
- Cache regression this epic started from: refactor(build): standardize image tagging to fix concurrent PR push conflicts #2711
- Touch points:
.github/workflows/pull-requests.yaml,.github/workflows/tags.yaml,.github/workflows/build-main.yaml,.github/workflows/build-release.yaml,.github/workflows/retention.yaml,hack/common-envs.mk,hack/build-matrix.sh,packages/**/Makefile
Activity
myasnikovdaniil commentedon Jun 17, 2026
Plan amendment — live root-cause finding re-sequences the work
I caught the build-hang in the act and captured a daemon goroutine dump (
SIGUSR1) while a build was wedged. The mechanism is now pinned. The end-state (Decisions 1–7) is unchanged, but the diagnosis gets sharper and the sequencing should move isolation ahead of the cache work.Proven root cause
While a build was hung: ~290 BuildKit goroutines parked in
solver/exporter.go+bboltcachestorage/storage.go+sync.Mutex.Lock, buildx onfutex_wait_queue, zero registry connections, zero build CPU, disk at 4%. So cause #2 is specifically BuildKit's single-writer bbolt cache-metadata lock + the exporter mutex.make buildis strictly serial within a job (no-j), so the pile-up is purely cross-job across the agents sharing one embedded buildkit. This confirms only per-job buildkit isolation (Decision 3) resolves it — cache mode, context size, prune, and daemon restart cannot. Notably, the captured wedge was a read-only PR build (WRITE_CACHE=0, nomode=maxwrite), somode=maxis not implicated in the hang.Proposed amendments
1. Background cause #2 — append:
2. Decisions — framing note (does not change Decisions 1–2):
3. Decision 3 — name the driver + ephemerality gate:
4. Decision 4 — cross-package coupling caveat:
5. New checklist section — insert before Phase 1, and reframe Phase 1:
6. Open questions — add:
Net change
End-state unchanged (Decisions 1–7 stand); Decisions 3 and 4 get sharper. The only structural change is sequencing: interim isolation/cap before Phase 1, and Phase 1 reframed as caching rather than the hang fix.
ci(build): warm the main cache on an isolated docker-container builder
ci(build): route flux-plunger and kubeovn-plunger through $(BUILDER)
ci(build): install helm-unittest plugin on ephemeral build runners
ci(build): install flux CLI on ephemeral build runners
refactor(build): mode=max registry cache with a main-only warmer
ci(build): warm the main cache on an isolated docker-container builder
ci(build): route flux-plunger and kubeovn-plunger through $(BUILDER)
ci(build): isolate each PR build on its own ephemeral runner VM
ci(build): install helm-unittest plugin on ephemeral build runners
ci(build): install flux CLI on ephemeral build runners
myasnikovdaniil commentedon Jun 22, 2026
Status update (2026-06-22)
Phase 1 and part of Phase 3 landed on
mainvia #2948 (which carried the commits from #2938d3958c5band #2939eba31194). Closing #2938/#2939 as superseded and ticking the boxes above.Done
mode=maxregistry:buildcache+ main-only warmer (build-main.yaml); PR builds read-only; inline cache removed.oracle-vm-4cpu-16gb-x86-64runner, withdebugstill routing to self-hosted.:latestrepoint, now superseded by the:buildcachecache.oracle-vm-4cpu-16gb-x86-64).Remaining
plan → matrix(1 image/job) → collect, per-package patch artifacts, parity gate vsmake build, Talos nocloud as its own leg, SBOM.paths-filtermatrix (currently a coarse!docs/**filter) + shared-dependency fan-out; decommission the dedicated runner.ci(build): gate finalize on the unit/controller checks job
9 remaining items
feat(release): immutable tags & rc→stable promotion (#2677) (#3017)
myasnikovdaniil commentedon Jul 8, 2026
Status update (2026-07-08)
Phase 2 landed on
mainvia #2983 (parallel per-package matrix) and #3148 (overlay current-main images for packages a PR didn't rebuild). Verified againstmain, not the checkboxes.Done
plan → build (matrix, 1 image/job) → build-talos → finalize.hack/build-matrix.shcomputes the matrix from the PR diff;finalizereassembles per-packagevalues.yamlpatch fragments intopr.patch; e2e consumes it unchanged. First CI run validated end-to-end (plan → 25 parallel builds → talos → finalize → install). The one-timebuild-parity.yamlcutover gate served its purpose and was removed.build-matrix.sh; build runs on the ephemeraloracle-vm-4cpu-16gb-x86-64pool (self-hostedonly on thedebugbreakpoint).Remaining
SBOM ?= 0,--provenance=falseinhack/common-envs.mk). Needs the OCIR referrers/attestation probe, then--sbom=true+ a delivery decision (attestation vs artifact vs release attachment).debugpath remains.build(flux-shard-operator): read the shared registry build cache
ci: move remaining workflows off the persistent self-hosted runner
ci: move remaining workflows off the persistent self-hosted runner (#…
ci: warm the build cache on release-1.5 pushes (#3469)
docs(ci): correct the stale shared-runner claims in the fork e2e lane
[-]Refactor CI image builds: parallel matrix on ephemeral runners + mode=max registry cache + SBOM[/-][+]Finish the CI image-build refactor: SBOM emission and pr-* tag retention[/+]myasnikovdaniil commentedon Aug 14, 2026
Narrowed this epic against
origin/mainat 09dc7f4. Two of the three headline deliverables shipped, so keeping the old body made the whole thing read as untouched work.Landed: the per-image parallel matrix on ephemeral runners (#2983,
strategy.matrix.pkgat.github/workflows/pull-requests.yaml:233-236, driven byhack/build-matrix.sh), themode=maxregistry cache with a main-only warmer (hack/common-envs.mkcache-argsplus.github/workflows/build-main.yaml), change-scoped builds with shared-dependency fan-out, and the runner cutover (#3268,gh api repos/cozystack/cozystack/actions/runnersreturnstotal_count: 0and no workflow onmaintargetsself-hosted).Still live, and why the issue stays open. SBOM never shipped: the seam exists as
SBOM ?= 0inhack/common-envs.mk, and it is off because the Phase 0 OCIR referrers/attestation probe was never run and an unsupported attestation push fails the build. Three package Makefiles (dashboard,flux-plunger,kubeovn-plunger) hand-roll their buildx flags and never append$(BUILDX_ARGS), so four images would skip--sbom=trueeven once it is on. And nothing prunespr-*tags on OCIR, sinceretention.yamlonly prunes GHCR nightlies.One thing checked and deliberately not tracked:
release-1.4andrelease-1.5still carry 9 and 10 unconditionalruns-on: [self-hosted]sites naming the decommissioned runner, but those lanes do not fire.auto-release.yamlcannot trigger from a non-default branch,update-releasenotes.yamlis gated on pushes tomain, andbackport.yamlandpull-requests-release.yamlskip before requesting a runner on every recent run. The body records it, with the one untested path called out:tags.yamlandrelease-e2e.yamlfire on tag pushes and last ran on 2026-07-07, two days before #3268 removed the runner.Also recorded as deferred rather than dropped:
.github/workflows/tags.yamlstill runs a single serialmake build, the one build path the matrix work never reached.docs(gtr): finalize editorial markers and pin submission version (#4221)
myasnikovdaniil commentedon Oct 2, 2026
Checked against
mainatbc400dff1, body updated.The bypassing Makefiles (item 2) are fixed in #4485. Multi-arch landed, but as per-architecture builds stitched into one index (#4498, #4505, #4521, #4552) and not as the
platformmatrix axis this epic deferred, andtags.yamlbuilds per architecture now. SBOM (item 1) and OCIRpr-*retention (item 3) are untouched,SBOM ?= 0and nothing prunes OCIR. The GHCR retention job is broken separately, that is A19 in #3278.Runner notes changed.
e2e-proxmox.yamluses[self-hosted, proxmox]on purpose, it is gated behind a variable that is unset.tags.yamlonrelease-1.5runs on the Oracle pool now and the v1.5.4 tag build passed,release-e2e.yamlthere is still[self-hosted].