refactor(build): standardize image tagging to fix concurrent PR push conflicts - #2711
Conversation
…conflicts Every package Makefile tagged images via `settag` which, on a non-release commit, collapsed to `:latest`. Concurrent PR builds therefore raced on every image's `:latest` manifest in the OCIR registry. Two outliers (metallb, redis-operator) used hardcoded version tags and collided across PRs even when `settag` would not have. Replace `settag` and the `TAG=latest` fallback with an `image-tags` macro driven by three env vars: - IMAGE_TAG — build-unique tag, always pushed - PUBLISH_VERSIONED — also push :<component-version> - PUBLISH_FLOATING — also push :latest PR CI sets IMAGE_TAG=pr-<N>-<sha> with both PUBLISH_* off, so every push is unique across runs. Release CI sets IMAGE_TAG=<git-tag> with both PUBLISH_* on, preserving the previous release output and adding a uniform :<git-tag> tag across every image. Local builds default to IMAGE_TAG=dev with PUSH=0 already off. Image consumers reference digests pinned into values.yaml by each Makefile, so the tag is decorative for cluster-side pulls; the build-unique tag is sufficient for E2E. Drops the redundant `:<component-version>-<git-tag>` form (cilium, linstor, matchbox, ubuntu-container-disk, kubevirt-*, cluster-autoscaler) — nothing in the repo or downstream values.yaml consumed it. The same information is preserved via the new uniform `:<git-tag>` plus the existing `:<component-version>` on release. ubuntu-container-disk builds a separate image per Kubernetes version in a loop. Switched its primary tag to `:<k8s-ver>-<IMAGE_TAG>` so loop iterations within a single build do not push to the same tag. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request refactors the image building and tagging process to resolve registry push conflicts caused by concurrent PR builds. By introducing a centralized Highlights
New Features🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Ignored Files
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on Gemini (@gemini-code-assist) comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughIntroduce a build-unique IMAGE_TAG, add PUBLISH_VERSIONED/PUBLISH_FLOATING flags, define an image-tags make macro, set these envs in PR/release workflows, and update many package Makefiles to use image-tags and IMAGE_TAG-based digest pinning. ChangesDocker Image Tagging Infrastructure Refactor
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request refactors the container image tagging system across the project. It introduces a new IMAGE_TAG variable for build-unique tags and optional PUBLISH_VERSIONED and PUBLISH_FLOATING flags for release management. The legacy settag macro is replaced by a more robust image-tags macro in hack/common-envs.mk, and all package Makefiles are updated accordingly. Feedback was provided regarding the Talos Makefile, where skopeo copy commands should be conditionally executed based on the PUSH variable to ensure local builds do not fail without registry access.
| SRC=docker-archive:../../../_out/assets/installer-amd64.tar; \ | ||
| skopeo copy $$SRC docker://$(REGISTRY)/talos:$(IMAGE_TAG); \ | ||
| if [ "$(PUBLISH_VERSIONED)" = "1" ]; then skopeo copy $$SRC docker://$(REGISTRY)/talos:$(TALOS_VERSION); fi; \ | ||
| if [ "$(PUBLISH_FLOATING)" = "1" ]; then skopeo copy $$SRC docker://$(REGISTRY)/talos:latest; fi |
There was a problem hiding this comment.
The skopeo copy commands are executed regardless of the PUSH variable's value. This will cause local builds to fail if the user does not have push access to the registry or is working offline. For consistency with other targets and to support local development, these commands should be wrapped in a check for PUSH=1.
SRC=docker-archive:../../../_out/assets/installer-amd64.tar; \
if [ "$(PUSH)" = "1" ]; then \
skopeo copy $$SRC docker://$(REGISTRY)/talos:$(IMAGE_TAG); \
if [ "$(PUBLISH_VERSIONED)" = "1" ]; then skopeo copy $$SRC docker://$(REGISTRY)/talos:$(TALOS_VERSION); fi; \
if [ "$(PUBLISH_FLOATING)" = "1" ]; then skopeo copy $$SRC docker://$(REGISTRY)/talos:latest; fi; \
fi
There was a problem hiding this comment.
Good observation, but this is pre-existing behavior — the previous recipe also called skopeo copy ... unconditionally, so it isn't a regression introduced by the tagging refactor. The scope of this PR is just standardizing image tags. Happy to add a PUSH=1 gate (consistent with the buildx recipes that pass --push=$(PUSH)) in a follow-up if you'd like — keeping it out of this PR to stay focused.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/core/talos/Makefile`:
- Around line 15-18: The Makefile recipe for the image push uses SRC and three
skopeo copy invocations separated by semicolons, which masks failures; update
the recipe for the image-talos target so skopeo copy failures cause an immediate
exit — e.g., start the shell with "set -e" / "set -o errexit" or change the
separators so each skopeo copy is chained with "&&" (or append "|| exit 1" to
each skopeo copy) for the commands referencing SRC,
$(REGISTRY)/talos:$(IMAGE_TAG), the conditional pushes using
$(PUBLISH_VERSIONED)/$(TALOS_VERSION) and $(PUBLISH_FLOATING)/latest to ensure
any failed skopeo copy aborts the recipe immediately.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 26220233-d3cb-4ac4-a362-abb20c26a21c
📒 Files selected for processing (34)
.github/workflows/pull-requests.yaml.github/workflows/tags.yamlhack/common-envs.mkpackages/apps/clickhouse/Makefilepackages/apps/http-cache/Makefilepackages/apps/kubernetes/Makefilepackages/apps/mariadb/Makefilepackages/core/installer/Makefilepackages/core/platform/Makefilepackages/core/talos/Makefilepackages/core/testing/Makefilepackages/extra/monitoring/Makefilepackages/system/backup-controller/Makefilepackages/system/backupstrategy-controller/Makefilepackages/system/bucket/Makefilepackages/system/cilium/Makefilepackages/system/cozystack-api/Makefilepackages/system/cozystack-controller/Makefilepackages/system/dashboard/Makefilepackages/system/flux-plunger/Makefilepackages/system/grafana-operator/Makefilepackages/system/kamaji/Makefilepackages/system/keycloak-operator/Makefilepackages/system/kilo/Makefilepackages/system/kubeovn-plunger/Makefilepackages/system/kubeovn-webhook/Makefilepackages/system/lineage-controller-webhook/Makefilepackages/system/linstor-gui/Makefilepackages/system/linstor/Makefilepackages/system/metallb/Makefilepackages/system/monitoring/Makefilepackages/system/multus/Makefilepackages/system/objectstorage-controller/Makefilepackages/system/redis-operator/Makefile
The image-talos recipe chains skopeo copy with `;` in a single shell. Without `set -e`, the recipe's exit status is the last command's, so a failed copy in the middle of the chain can be masked by a later successful command — or worse, by a false-branch `if`/`fi` (which exits 0 and would silently hide a push failure on PR builds where both PUBLISH_* flags are 0). Prepend `set -e;` to abort the recipe on the first failure. Also quote "$SRC" defensively. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
LGTM — clean refactor that solves the concurrent PR push race; no functional regressions found in the build/E2E flow. The fail-fast issue raised on image-talos was addressed in 6c8a67d.
Business context: PR builds were racing in the registry because settag(versioned) collapsed to :latest on non-release commits, so every concurrent PR build pushed to the same tag and got 409s on manifest PUT. This change replaces the legacy macro with an explicit IMAGE_TAG + PUBLISH_VERSIONED + PUBLISH_FLOATING triplet so PR builds push under a unique pr-<N>-<sha> handle and release builds keep their versioned + floating tags intact.
Non-blocking follow-ups
-
packages/system/flux-plunger/Makefile:4carries a pre-existing broken include path (../../../scripts/common-envs.mk—scripts/does not exist; onlyhack/common-envs.mkis present). Introduced in2d1c8aae0long before this PR, latent becauseflux-plungeris not in the rootmake buildtarget. Worth fixing while this Makefile is being touched — withoutcommon-envs.mkthe new$(call image-tags,...)macro is undefined, so the recipe in this file is effectively dead code regardless of the tag refactor. -
Tag duplication on release for packages that pass
v$(COZYSTACK_VERSION)as the secondimage-tagsargument (kilo, kamaji, multus, cozystack-api/controller, dashboard, kubeovn-*, backup-controller, lineage-controller-webhook, objectstorage-controller, grafana-operator, flux-plunger, keycloak-operator, platform-migrations, e2e-sandbox, cozystack-operator). On a releaseIMAGE_TAG = github.ref_name = vX.Y.Zand the versioned arg expands tovX.Y.Ztoo, so the macro emits--tag repo:vX.Y.Z --tag repo:vX.Y.Z --tag repo:latest.docker buildx builddeduplicates internally — no functional impact, just cosmetic noise in build logs. The macro could short-circuit when$(strip $(2)) == $(IMAGE_TAG). -
pr-<N>-<sha>tag retention. Every PR push creates ~30 new tags in the registry with no cleanup policy in this PR. The previous design overwrote:latestand accumulated nothing, so this is a real (if benign) growth shift in registry storage over many PRs. A registry-side retention rule forpr-*tags older than N days is the natural follow-up. -
image.tagsemantic shift invalues.yamlon releases for packages that pin bothimage.tagandimage.digest(cilium, linstor, metallb, kilo, kamaji, …). Previously the tag string corresponded to the upstream image version; now it is<cozystack-ref-name>. Image pulls go by digest, so the runtime is unaffected, but tooling that surfacesimage.tagto humans now shows the cozystack tag rather than the upstream version. -
installer/Makefile:23— the operator image'sVERSIONbuild-arg changed from$(call settag,$(TAG))(which collapsed to literallateston non-release commits) tov$(COZYSTACK_VERSION)(realgit describeoutput). This is a positive side effect not called out in the PR description — the operator will now report a meaningful--versionon non-release builds.
The include pointed at ../../../scripts/common-envs.mk, but that directory does not exist — only hack/common-envs.mk does. flux-plunger is not part of the root `make build`, so the broken path went unnoticed until the new image-tags macro made the recipe depend on it. Without the include, `make -C packages/system/flux-plunger image` fails immediately with "No such file or directory". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
On release builds IMAGE_TAG = github.ref_name = vX.Y.Z, and Makefiles that pass v$(COZYSTACK_VERSION) as the versioned arg evaluate to the same vX.Y.Z on the tag commit. The macro then emitted `--tag :vX.Y.Z --tag :vX.Y.Z --tag :latest`. docker buildx dedupes internally so there is no functional impact, but the build logs carried the redundant flag for every affected package. Replace the empty-string guard with filter-out so the versioned tag is also skipped when it would duplicate IMAGE_TAG. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
Aleksei Sviridkin (lexfrei)
left a comment
There was a problem hiding this comment.
Re-approving after the two follow-up commits.
1bcaa3de fix(flux-plunger): correct common-envs.mk include path — pure path fix; the previous scripts/common-envs.mk reference would have made flux-plunger's image target fail with No such file or directory because the include now lives under hack/. Confirmed: hack/common-envs.mk exists, scripts/common-envs.mk does not.
e42a5828 refactor(build): skip duplicate versioned tag when it matches IMAGE_TAG — uses $(filter-out $(IMAGE_TAG),$(strip $(2))) so the --tag for the versioned name is omitted when it would already equal the build-unique tag. Avoids a duplicate --tag flag in docker buildx build and a redundant skopeo copy on subsequent pushes; behavior in the PR build (where IMAGE_TAG == pr-N-sha and the versioned arg is empty) and in the release build (where IMAGE_TAG != versioned) is unchanged.
No regressions. The original LGTM from the previous SHA still stands.
…rt (#2855) ## What this PR does [#2711](#2711) moved PR builds to unique `pr-<N>-<sha>` tags (`PUBLISH_FLOATING=0`) to end concurrent-push 409 conflicts, but left `--cache-from` pointing at `$(REGISTRY)/<img>:latest`. PR builds set `REGISTRY` to the per-CI registry, which never publishes `:latest`, so every PR build's registry cache lookup `404`s and rebuilds cold — slowing builds and, on a loaded runner, pushing them into the 30-minute job timeout. This adds a `CACHE_REGISTRY` knob (default `ghcr.io/cozystack/cozystack`, where release builds *do* publish `:latest`) and points every `--cache-from` at it. Push targets (`$(REGISTRY)`) and the unique per-PR tags are unchanged, so #2711's anti-conflict behaviour is preserved while PR builds regain a warm cache from the last release. - `hack/common-envs.mk`: new `CACHE_REGISTRY ?= ghcr.io/cozystack/cozystack` with rationale. - 31 package Makefiles: 37 `--cache-from type=registry,ref=$(REGISTRY)/…` → `$(CACHE_REGISTRY)/…`. Verified with `make -n image REGISTRY=iad.ocir.io/…`: push tag stays on OCIR (`…/cozystack-controller:pr-<N>-<sha>`), cache-from now reads `ghcr.io/cozystack/cozystack/cozystack-controller:latest`. Makefile-only change — no generated artifacts affected. > Note: cache freshness is bounded by the last release's `:latest`. PR-to-PR freshness (a `main`-built `:buildcache` tag) is a possible follow-up, intentionally out of scope here. ### Release note ```release-note fix(build): PR CI image builds now read layer cache from ghcr.io `:latest` (published by releases) instead of the per-CI registry where `:latest` was never pushed after #2711, restoring warm-start caching for PR builds. ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated Docker build configuration across multiple components to use a dedicated cache registry for improved build layer reuse. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
PR cozystack#2711 switched PR builds to push only :pr-<N>-<sha> tags (PUBLISH_FLOATING=0) to stop concurrent-push 409 conflicts, but left --cache-from pointing at $(REGISTRY)/<img>:latest. PR builds set REGISTRY to the per-CI registry (iad.ocir.io) and never publish :latest there, so every PR build's registry cache lookup 404s and rebuilds cold. Add a CACHE_REGISTRY knob (default ghcr.io/cozystack/cozystack, where release builds do publish :latest) and point every --cache-from at it. Push targets ($(REGISTRY)) and the unique per-PR tags are unchanged, so the cozystack#2711 anti-conflict behaviour is preserved while PR builds regain a warm cache from the last release. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
`--cache-to type=inline` only exports the final stage. 25 of 38 Dockerfiles are multistage, so the expensive `builder` stages (Go/Node compiles) were never cached even with a warm tag. Switch every image to a shared mode=max registry cache. - New `cache-args` macro (hack/common-envs.mk) emits `--cache-from` always and `--cache-to … mode=max` only when WRITE_CACHE=1, so PR builds stay read-only and concurrent PRs never race on the cache ref (the 409 class #2711 fixed for tags). oci-mediatypes/image-manifest keep the cache manifest portable across registries. - Cache ref is CACHE_REGISTRY/<img>:buildcache, co-located with the build registry (CACHE_REGISTRY now defaults to $(REGISTRY)). - New build-main.yaml warms :buildcache on every push to main (WRITE_CACHE=1), serialized via concurrency. All 31 package image recipes converted from the inline cache-from/cache-to pair to `$(call cache-args,…)`; ubuntu-container-disk keeps a per-k8s-version cache tag. Verified with `make -n image` across all packages in both PR (read-only) and main (mode=max write) contexts. Part of #2937. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
…3268) ## What this PR does Completes the last step of #2937 — no workflow depends on the persistent self-hosted runner anymore, so it can be decommissioned. PR builds, the main cache warmer, nightly, promote-rc, and pull-requests-release already run on the ephemeral pool; this PR moves the stragglers: - **`tags.yaml` / `prepare-release`** moves to the large ephemeral shape (same as the main cache warmer). Release builds now warm-start from the shared `mode=max` build cache in OCIR (written by `build-main.yaml` on every push to main) while still pushing images to GHCR. Cache manifests are registry-portable, and a missing cache ref degrades harmlessly into a cold build. Without this wiring, every tag build on an ephemeral runner would start 100% cold — the persistent runner used to warm-start from local buildkit state. - **`tags.yaml` / `generate-changelog` and `update-website-docs`**, **`backport.yaml`**, **`update-releasenotes.yaml`** move to GitHub-hosted runners: these jobs only need git and the GitHub API, no docker or repo toolchain. - **`pull-requests.yaml`**: the `debug` label no longer reroutes jobs to the self-hosted runner — that path would queue forever once the runner is gone. The label still gates the SSH breakpoint on e2e failure, which works from ephemeral runners (the breakpoint connects out to the rendezvous server). - **`flux-shard-operator`** was the last image built with the stale inline/`:latest` cache pattern (always cold since #2711); it now uses the shared `cache-args` registry cache like every other image. Operational notes for the actual decommission: - The breakpoint rendezvous server (`BREAKPOINT_ENDPOINT`) is separate infrastructure — if it happens to live on the same host as the runner, it needs a new home before the host is retired. - `tags.yaml` now uses the `OCIR_USER`/`OCIR_TOKEN` secrets on tag events (already used there by `nightly.yaml` and `release-e2e` before it). ### Screenshots Not applicable — CI-only change. ### Release note ```release-note ci: all CI jobs now run on ephemeral or GitHub-hosted runners; release tag builds warm-start from the shared registry build cache ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Chores** * Updated multiple GitHub Actions workflows to use more consistent hosted runner environments for checks, PR validation, tagging, backports, and release note updates. * Improved tag/release build setup for ephemeral runners, including optional build toolchain setup and container registry authentication. * Enhanced image build caching for the shard operator to improve packaging efficiency. * **Bug Fixes** * Removed label-driven runner switching in pull request checks to make build and test execution more predictable. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
… artifacts Supersedes the interim guard in this branch`s first commit, which skipped the overlay entirely for release-line PRs. Skipping fixed the wrong images but left those PRs testing their line`s last release: for any package the PR did not rebuild, the committed ref is the released digest, so a component changed by an earlier backport was exercised as its pre-backport binary until the next rc. The overlay now reads `cozystack-packages:<base branch>` instead of always `:main`, and build-release.yaml publishes that artifact for every maintained `release-<major>.<minor>` branch the way build-main.yaml does for main: images tagged with the branch, and the whole packages tree pushed with each reference digest-pinned to what the run just built. Each base branch therefore has its own generation to overlay from, which is what the original bug was really about — #3437 failed install because main`s cozystack-controller served an aggregated OpenAPI release-1.6`s charts could not validate against. Three deliberate choices: * The trigger matches line branches only (`release-[0-9]+.[0-9]+`). The per-release and rc staging branches promote-rc.yaml and tags.yaml create (release-1.6.1, release-1.6.0-rc.4) must not trigger a full rebuild — their images come from the tag build, and rebuilding them would be waste. * WRITE_CACHE stays 0. CACHE_REGISTRY/<img>:buildcache is a single ref per image and build-main.yaml is deliberately its only, serialized writer so concurrent builds cannot race on the cache manifest (the 409 class #2711 fixed for image tags). A line build can overlap a main build, so writing here would reintroduce that race. Line builds read the cache. * A missing artifact still degrades to committed refs, but on a release line it now emits a ::warning:: naming the branch. Silent degradation is indistinguishable from a working overlay, which is how a mis-specified branch filter would hide for a whole release cycle. Cost: one `make build` per push to a maintained line, i.e. per merged backport. hack/overlay-main-images_test.bats pins the artifact tag to the base branch, rejects a hardcoded :main in either overlay step, and pins build-release.yaml`s branch filter, image tag and WRITE_CACHE=0. Mutation-checked: restoring :main, setting WRITE_CACHE=1, and broadening the filter to release-* each fail a test. 13/13 green; actionlint and zizmor clean. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
… artifacts (#3471) ## What this PR does The PR finalize job overlays image refs for every package a PR did not rebuild, so e2e and the installer do not test last-release images for everything outside the PR's build matrix. It could only ever read `cozystack-packages:main`, which meant a **release-line PR was handed main's binaries to run against its own line's charts**. #3437 is the demonstration: a one-line change on `release-1.6` deactivating an app failed install deterministically, twice, with ``` helmrelease/backupstrategy-controller: Helm install failed … error validating data: SchemaError(github.com/cozystack/cozystack/pkg/apis/core/v1alpha1.Option.spec): unknown model in reference: "github.com~1cozystack~1cozystack~1pkg~1apis~1core~1v1alpha1.OptionSpec" ``` main's `cozystack-controller` served an aggregated OpenAPI that `release-1.6`'s charts could not validate against. Nothing in that PR was broken; the lane was. Both branches carry `option_types.go` and key `OptionSpec` identically in the committed generated OpenAPI, so this is a generation mismatch at runtime, not a codegen drift. This PR fixes it by giving every base branch its own artifact to overlay from, rather than by turning the overlay off. ### The change 1. **`pull-requests.yaml`** reads `cozystack-packages:${BASE_REF}` (`github.base_ref`) instead of a hardcoded `:main`. 2. **`build-release.yaml`** (new) publishes that artifact for maintained `release-<major>.<minor>` branches exactly as `build-main.yaml` does for main: every image tagged with the branch, and the whole packages tree pushed with each reference digest-pinned to what the run just built. An earlier revision of this branch simply skipped the overlay for non-main bases. That fixed the wrong-images problem but left release-line PRs testing their line's *last release*: for any package the PR did not rebuild, the committed ref is the released digest, so a component changed by an earlier backport was still exercised as its pre-backport binary until the next rc. Per-line artifacts remove that gap too, which is why the guard was replaced rather than kept. ### Three deliberate choices **The trigger matches line branches only** (`release-[0-9]+.[0-9]+`). The per-release and rc staging branches `promote-rc.yaml` and `tags.yaml` create — `release-1.6.1`, `release-1.6.0-rc.4` — must not trigger a full rebuild; their images come from the tag build and rebuilding them is waste. **`WRITE_CACHE` stays `0`.** `CACHE_REGISTRY/<img>:buildcache` is a single ref per image and `build-main.yaml` is deliberately its only, serialized writer so concurrent builds cannot race on the cache manifest — the 409 class #2711 fixed for image tags. A line build can overlap a main build, so writing here would reintroduce that race. Line builds read the cache. **A missing artifact still degrades to committed refs, but says so.** On a release line it emits a `::warning::` naming the branch. Silent degradation is indistinguishable from a working overlay, which is how a mis-specified branch filter could hide for a whole release cycle. ### Cost One `make build` per push to a maintained line — in practice per merged backport. That is the price of release-line PRs testing their line's tip instead of its last release. ### Verification `hack/overlay-main-images_test.bats` pins the artifact tag to the base branch, rejects a hardcoded `:main` in either overlay step, and pins `build-release.yaml`'s branch filter, image tag and `WRITE_CACHE: '0'`. Mutation-checked: restoring `:main`, setting `WRITE_CACHE: '1'`, and broadening the filter to `release-*` each fail a test. 13/13 green; `actionlint` and `zizmor` clean. Worth an explicit ack in review: the branch-filter pattern is the one thing no local test can prove, since only GitHub evaluates it. If it does not match, `build-release` never runs and the new `::warning::` is what surfaces it on the next release-line PR. ### Backport `release-1.6` needs this too — for `pull_request` events GitHub builds the workflow from the merge ref, so a release-line PR only stops receiving main's images once the change is on its base branch. #3472 carried the interim guard and is closed in favour of backporting this instead. ### Release note ```release-note NONE ``` <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a “Build release line” workflow that builds and publishes images and the packages artifact for maintained `release-<major>.<minor>` branches. * **Bug Fixes** * Updated PR workflow finalization to use base-branch–specific package overlays, avoiding incorrect main-branch image references when targeting release branches. * Improved fallback behavior when the base-branch packages artifact is unavailable. * **Tests** * Added workflow wiring tests to verify base-branch artifact usage and that each maintained release line publishes its own packages artifact. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
The workflow_dispatch trigger added for the freeze takes any ref, and the "Run workflow" ref selector preselects the default branch — which is exactly where this workflow's own recovery advice, and cut-prerelease's warning, send an operator. IMAGE_TAG is github.ref_name, so a dispatch from main spends a two-hour `make build` republishing cozystack-packages:main and every main image tag while build-main.yaml may be writing the same ones: the 409 tag-collision class #2711 fixed. Nothing is ever mislabeled, since the content really is that ref, so the cost is a wasted runner rather than a wrong artifact. Guard it anyway: it is five lines, and the mis-click is easy. Anchored at both ends so the per-release staging branches (release-1.6.1, release-1.6.0-rc.4) do not qualify — those are built by tags.yaml — and gated on ref_type so a tag named release-1.6 cannot pass the pattern on its own. Runs before the checkout and the OCIR login, so a refused dispatch never touches the credentials. Fails rather than skips: a skipped job reads in the run list like one that produced the artifact. The push trigger was already filtered to release-X.Y; this closes the dispatch path to the same set. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
What this PR does
Concurrent PR builds were racing in the OCIR registry. Every package Makefile tagged its images via
settag, which on any non-release commit collapsed to:latest— so every image across every PR was pushed to the same tag, and parallel builds got 409s on the manifest PUT. Two outliers (metallb,redis-operator) used hardcoded version tags and collided across PRs unconditionally.This PR replaces
settagand theTAG=latestfallback inhack/common-envs.mkwith a singleimage-tagsmacro driven by three env vars:IMAGE_TAGPUBLISH_VERSIONED:<component-version>(when provided)PUBLISH_FLOATING:latestWired into the two workflows:
pull-requests.yaml—IMAGE_TAG=pr-<N>-<sha>, bothPUBLISH_*off. Every PR build pushes a unique tag set; no collisions, no:latestmove.tags.yaml—IMAGE_TAG=<ref_name>, bothPUBLISH_*on. Release output preserved (component versions +:latest) plus a uniform:<git-tag>handle on every image for traceability.IMAGE_TAG=dev, no flags,PUSH=0.All 30 package Makefiles converted to
$(call image-tags,<repo>,<versioned-tag>). Image consumers reference digests pinned intovalues.yamlby each Makefile, so the tag is decorative for cluster-side pulls — the build-unique tag is sufficient for E2E.Also drops the redundant
:<component-version>-<git-tag>form (cilium, linstor, matchbox, ubuntu-container-disk, kubevirt-*, cluster-autoscaler). Nothing in the repo or downstream values.yaml consumed it; the same information is preserved via the new:<git-tag>plus the existing:<component-version>tags on release.ubuntu-container-diskbuilds a separate image per Kubernetes version in a loop, so its primary tag became:<k8s-ver>-<IMAGE_TAG>to prevent loop iterations within a single build from pushing to the same tag.Verified by dry-running
make -n imageacross representative packages (simple, dual-tag, skopeo, flux-push, loop) under all four contexts (PR / release / main / dev) — tag sets are unique and behave as expected.Release note
Summary by CodeRabbit