From 49d56078588ed8770218b9f6bef854d54952ca41 Mon Sep 17 00:00:00 2001 From: David Young Date: Thu, 13 Aug 2026 22:58:42 +1200 Subject: [PATCH 1/2] fix(ubuntu): one owner for :rolling, and pin consumers to noble 24.04 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ubuntu` ships three *stable* channels (noble, jammy, focal) and stable channels share a single package, so all three were pushing the same floating `ubuntu:rolling` tag — last build wins. On 2026-08-11 `rolling` was noble's build (b6f7450, 24.04); on 2026-08-12 the focal build overwrote it (dd5112a, 20.04). Renovate saw only "a digest changed", auto-merged, and kavita/openbooks/plextraktsync/resilio-sync/wireguard were rebuilt on Ubuntu 20.04 without anyone choosing that. ombi failed to build on it. Same bug class as 4a399fc5 (which fixed the *version lookup* for multi-stable-channel apps) but in the *tagging*. Two parts: - A stable channel now owns `:rolling` only if it opts in with "rolling": true, or if it is the app's only stable channel. When an app has >1 stable channel and doesn't have exactly one owner, the build fails loudly rather than silently racing. Declared in metadata.rules.cue (#Channels is a closed definition, so the field has to be schema'd) and set on ubuntu's noble channel. - The six consumers that pinned the floating tag now pin `ubuntu:noble-20260730.1` explicitly, restoring the 24.04 base they were on before 08-12. jellyfin stays on focal-20250404 — that pin is deliberate and self-consistent. Behaviour is unchanged for every other image: ubuntu is the only app in the estate (292 scanned, public + private overlay) with more than one stable channel, and single-stable apps keep publishing `:rolling` implicitly. Non-stable channels each get their own `${app}-${channel}` package and never collided. The GHCR security scan is unaffected — it prefers the per-channel versioned tag and only falls back to `:rolling`. Known limitation, commented at the call site: a metadata-only ownership change doesn't trigger a build (image-rebuild ignores metadata.json, because the build writes publishedVersion back into it and would loop), so `:rolling` stays on the old owner until the new one rebuilds. That applies to this commit too — `ubuntu:rolling` is still the focal image and needs a one-off "Release: Manual" run for ubuntu/noble with push=true. Consumers are unaffected either way now that they pin an explicit tag + digest. Validated locally: cue vet passes on all 274 public + 2 private metadata files; the workflow YAML parses; the tag-ownership logic and its failure guard were exercised against noble/jammy/focal, a single-stable app, and a non-stable channel. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/action-image-build.yaml | 33 +++++++++++++++++++++-- apps/kavita/Dockerfile | 2 +- apps/ombi/Dockerfile | 2 +- apps/openbooks/Dockerfile | 2 +- apps/plextraktsync/Dockerfile | 2 +- apps/resilio-sync/Dockerfile | 2 +- apps/ubuntu/metadata.json | 1 + apps/wireguard/Dockerfile | 2 +- metadata.rules.cue | 4 +++ 9 files changed, 42 insertions(+), 8 deletions(-) diff --git a/.github/workflows/action-image-build.yaml b/.github/workflows/action-image-build.yaml index b26755a50e..5be8ba8de9 100644 --- a/.github/workflows/action-image-build.yaml +++ b/.github/workflows/action-image-build.yaml @@ -110,9 +110,36 @@ jobs: fi chan_tag_version="${chan_upstream_version//+/-}" + + # An app's stable channels all share one package — ubuntu publishes + # noble, jammy and focal as `ubuntu:` — so when every one of them + # writes the floating `:rolling` tag it is last-build-wins, and + # `ubuntu:rolling` flips between OS releases behind consumers' backs + # (it silently went 24.04 -> 20.04 on 2026-08-12). A stable channel + # now owns `:rolling` only when it claims it with "rolling": true, or + # when it is the app's only stable channel. Non-stable channels each + # get their own `${app}-${channel}` package, so they cannot collide. + # + # Moving ownership between channels needs a manual rebuild of the new + # owner ("Release: Manual", app + channel, push=true). image-rebuild + # deliberately ignores apps/**/metadata.json — the build writes + # publishedVersion back into it, so triggering on it would loop — and + # the scheduled fetch only builds on a version change, so a + # metadata-only ownership commit leaves `:rolling` on the old owner's + # image until the new owner next builds. + chan_stable_total=$(jq '[.channels[] | select(.stable == true)] | length' ./apps/${{ matrix.image.app }}/metadata.json) + chan_rolling_total=$(jq '[.channels[] | select(.stable == true and .rolling == true)] | length' ./apps/${{ matrix.image.app }}/metadata.json) + chan_rolling=$(jq --raw-output '.rolling // false' <<< "${chan_config}") + if [[ "${chan_stable}" == true ]]; then + if (( chan_stable_total > 1 )) && (( chan_rolling_total != 1 )); then + echo "::error::${{ matrix.image.app }} has ${chan_stable_total} stable channels but ${chan_rolling_total} marked \"rolling\": true — exactly one must own the floating :rolling tag" + exit 1 + fi echo "::set-output name=chan_tag_testing::${{ matrix.image.app }}:testingz" - echo "::set-output name=chan_tag_rolling::${{ matrix.image.app }}:rolling" + if (( chan_stable_total == 1 )) || [[ "${chan_rolling}" == true ]]; then + echo "::set-output name=chan_tag_rolling::${{ matrix.image.app }}:rolling" + fi echo "::set-output name=chan_tag_version::${{ matrix.image.app }}:${chan_tag_version}" else echo "::set-output name=chan_tag_testing::${{ matrix.image.app }}-${{ matrix.image.channel }}:testingz" @@ -517,9 +544,11 @@ jobs: platforms: ${{ steps.vars.outputs.chan_platforms }} file: ${{ steps.vars.outputs.chan_dockerfile }} push: ${{ inputs.pushImages == 'true' }} + # chan_tag_rolling is unset for a stable channel that doesn't own the + # floating tag; build-push-action skips the resulting empty line. tags: |- - ghcr.io/${{ github.repository_owner }}/${{ steps.vars.outputs.chan_tag_rolling }} ghcr.io/${{ github.repository_owner }}/${{ steps.vars.outputs.chan_tag_version }} + ${{ steps.vars.outputs.chan_tag_rolling && format('ghcr.io/{0}/{1}', github.repository_owner, steps.vars.outputs.chan_tag_rolling) || '' }} cache-from: type=gha cache-to: type=gha,mode=max diff --git a/apps/kavita/Dockerfile b/apps/kavita/Dockerfile index c2f42af75f..dc854fa4cd 100644 --- a/apps/kavita/Dockerfile +++ b/apps/kavita/Dockerfile @@ -1,7 +1,7 @@ ARG VERSION FROM jvmilazz0/kavita:nightly-${VERSION} as upstream -FROM ghcr.io/elfhosted/ubuntu:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 # for initial install USER root diff --git a/apps/ombi/Dockerfile b/apps/ombi/Dockerfile index c9b451370e..1d63412c3f 100644 --- a/apps/ombi/Dockerfile +++ b/apps/ombi/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 ARG TARGETPLATFORM ARG BUILDPLATFORM diff --git a/apps/openbooks/Dockerfile b/apps/openbooks/Dockerfile index b1fd4232a9..202ee5f788 100644 --- a/apps/openbooks/Dockerfile +++ b/apps/openbooks/Dockerfile @@ -1,6 +1,6 @@ ARG VERSION FROM ghcr.io/evan-buss/openbooks:${VERSION} as upstream -FROM ghcr.io/elfhosted/ubuntu:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 # default to ephemeral storage ENV DATA_DIR=/config diff --git a/apps/plextraktsync/Dockerfile b/apps/plextraktsync/Dockerfile index 5a65ac2f57..fde243405c 100644 --- a/apps/plextraktsync/Dockerfile +++ b/apps/plextraktsync/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 RUN apt-get -y update && apt-get -y install curl tmux pipx python3.8-venv diff --git a/apps/resilio-sync/Dockerfile b/apps/resilio-sync/Dockerfile index a9cb3eddfb..5fa75bec57 100644 --- a/apps/resilio-sync/Dockerfile +++ b/apps/resilio-sync/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 # set version label ARG BUILD_DATE diff --git a/apps/ubuntu/metadata.json b/apps/ubuntu/metadata.json index 6056eb8bf8..5ea75067be 100644 --- a/apps/ubuntu/metadata.json +++ b/apps/ubuntu/metadata.json @@ -9,6 +9,7 @@ "linux/arm64" ], "stable": true, + "rolling": true, "tests": { "enabled": false, "type": "cli" diff --git a/apps/wireguard/Dockerfile b/apps/wireguard/Dockerfile index 15f1d000bb..f5ca82af3b 100644 --- a/apps/wireguard/Dockerfile +++ b/apps/wireguard/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 ARG VERSION USER root diff --git a/metadata.rules.cue b/metadata.rules.cue index b144c359aa..574686d64c 100644 --- a/metadata.rules.cue +++ b/metadata.rules.cue @@ -8,6 +8,10 @@ name: #NonEmptyString platforms: [...#AcceptedPlatforms] stable: bool + // Stable channels of one app share a package, so only one of them may own + // the floating `:rolling` tag. Required when an app has more than one + // stable channel; implicit (and unnecessary) when it has exactly one. + rolling?: bool tests: { enabled: bool type?: =~"^(cli|web)$" From 9036cd4c0eddad9ab1c16b5fe1165b0b3357f2fc Mon Sep 17 00:00:00 2001 From: David Young Date: Thu, 13 Aug 2026 23:40:37 +1200 Subject: [PATCH 2/2] chore(ubuntu): re-pin consumers to the fresh noble build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manual Release run (actions/runs/31696019320) rebuilt ubuntu/noble and handed :rolling back to it, so noble-20260730.1 now resolves to 7d06c5e6 rather than b6f7450. Pin the current build so the merged state matches the registry — b6f7450 was still noble 24.04 and would have worked, renovate would just have bumped it immediately. Verified: ubuntu:rolling == ubuntu:noble-20260730.1 == 7d06c5e6, labelled ubuntu (noble) / 24.04; focal-20250404 stays separate on dd5112a. Co-Authored-By: Claude Opus 5 (1M context) --- apps/kavita/Dockerfile | 2 +- apps/ombi/Dockerfile | 2 +- apps/openbooks/Dockerfile | 2 +- apps/plextraktsync/Dockerfile | 2 +- apps/resilio-sync/Dockerfile | 2 +- apps/wireguard/Dockerfile | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/kavita/Dockerfile b/apps/kavita/Dockerfile index dc854fa4cd..6df9911801 100644 --- a/apps/kavita/Dockerfile +++ b/apps/kavita/Dockerfile @@ -1,7 +1,7 @@ ARG VERSION FROM jvmilazz0/kavita:nightly-${VERSION} as upstream -FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a # for initial install USER root diff --git a/apps/ombi/Dockerfile b/apps/ombi/Dockerfile index 1d63412c3f..7f52878afc 100644 --- a/apps/ombi/Dockerfile +++ b/apps/ombi/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a ARG TARGETPLATFORM ARG BUILDPLATFORM diff --git a/apps/openbooks/Dockerfile b/apps/openbooks/Dockerfile index 202ee5f788..9e035a215b 100644 --- a/apps/openbooks/Dockerfile +++ b/apps/openbooks/Dockerfile @@ -1,6 +1,6 @@ ARG VERSION FROM ghcr.io/evan-buss/openbooks:${VERSION} as upstream -FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a # default to ephemeral storage ENV DATA_DIR=/config diff --git a/apps/plextraktsync/Dockerfile b/apps/plextraktsync/Dockerfile index fde243405c..7e8174e965 100644 --- a/apps/plextraktsync/Dockerfile +++ b/apps/plextraktsync/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a RUN apt-get -y update && apt-get -y install curl tmux pipx python3.8-venv diff --git a/apps/resilio-sync/Dockerfile b/apps/resilio-sync/Dockerfile index 5fa75bec57..6963501a40 100644 --- a/apps/resilio-sync/Dockerfile +++ b/apps/resilio-sync/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a # set version label ARG BUILD_DATE diff --git a/apps/wireguard/Dockerfile b/apps/wireguard/Dockerfile index f5ca82af3b..c64df8f38f 100644 --- a/apps/wireguard/Dockerfile +++ b/apps/wireguard/Dockerfile @@ -1,4 +1,4 @@ -FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:b6f74509fc1f31dfb5808f261f6187a9597e17a0359c962090366d9649aa62e0 +FROM ghcr.io/elfhosted/ubuntu:noble-20260730.1@sha256:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a ARG VERSION USER root