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..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:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +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 c9b451370e..7f52878afc 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:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a ARG TARGETPLATFORM ARG BUILDPLATFORM diff --git a/apps/openbooks/Dockerfile b/apps/openbooks/Dockerfile index b1fd4232a9..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:rolling@sha256:dd5112a70dbae5ae5c53126969b9168c7ef2141576938272f7bff8d194be4823 +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 5a65ac2f57..7e8174e965 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: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 a9cb3eddfb..6963501a40 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:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a # 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..c64df8f38f 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:7d06c5e6e4f39b2d7b88a0a439207bfd6d7c31474c75ad8969f621cabddcd96a 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)$"