From f58139063cbe6ad4581d1bc24c228a08dbd06217 Mon Sep 17 00:00:00 2001
From: Steve Adams
Date: Tue, 30 Jun 2026 11:11:24 +1000
Subject: [PATCH] chore: retire the old Docker image
(stevezzau/plex_generate_vid_previews)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The project was renamed to stevezzau/media_preview_generator. The old image
had ~57k pulls (vs ~19k for the new one) so rather than keep building a mirror,
push a one-shot "tombstone" image over its tags that tells stragglers to switch,
then drop the recurring deprecated build from CI.
- tombstone/: a tiny nginx:1.27-alpine image serving a static "this image moved
-> switch to stevezzau/media_preview_generator" page on port 8080 (the app's
port, so existing compose port-maps hit it; serves the page for every path).
- .github/workflows/retire-deprecated-image.yml: workflow_dispatch (gated on
typing "retire") that buildx-pushes the tombstone multi-arch (amd64+arm64)
over stevezzau/plex_generate_vid_previews:latest and :dev. Run ONCE.
- ci.yml: removed the deprecated image flavour entirely — the 2 deprecated
matrix entries, docker_tags_deprecated output + echoes, the conditional image
name (now always canonical), the deprecated digest download + manifest steps,
the deprecated Docker Hub description step, and the DOCKER_IMAGE_DEPRECATED env.
- In-app banner + startup log: now say the old image is RETIRED (no longer
updated, switch now) instead of "mirrors builds until 2026-10-29"; removed the
DEPRECATED_IMAGE_SUNSET_DATE constant.
- DOCKERHUB_README.md: updated the rename note to "retired"; deleted the now-
orphaned DOCKERHUB_DEPRECATED_README.md (its publish step was removed).
After merge, run the retire-deprecated-image workflow once to push the tombstone.
Co-Authored-By: Claude Opus 4.8 (1M context)
Claude-Session: https://claude.ai/code/session_01YC1Z1JFBVJ5xKi5YAqYKfc
---
.github/workflows/ci.yml | 57 +++----------------
.github/workflows/retire-deprecated-image.yml | 54 ++++++++++++++++++
DOCKERHUB_DEPRECATED_README.md | 43 --------------
DOCKERHUB_README.md | 2 +-
media_preview_generator/web/app.py | 13 ++---
media_preview_generator/web/notifications.py | 26 ++++-----
tests/test_notifications_api.py | 5 +-
tombstone/Dockerfile | 10 ++++
tombstone/default.conf | 8 +++
tombstone/index.html | 36 ++++++++++++
10 files changed, 138 insertions(+), 116 deletions(-)
create mode 100644 .github/workflows/retire-deprecated-image.yml
delete mode 100644 DOCKERHUB_DEPRECATED_README.md
create mode 100644 tombstone/Dockerfile
create mode 100644 tombstone/default.conf
create mode 100644 tombstone/index.html
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 69534372..2a046f88 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -32,12 +32,11 @@ on:
workflow_dispatch:
env:
- # Canonical Docker Hub image name. The deprecated mirror below keeps
- # publishing alongside this one until 2026-10-29 so users on the old
- # name keep getting updates while they migrate.
+ # Canonical Docker Hub image name. The old mirror
+ # (stevezzau/plex_generate_vid_previews) was retired — a one-shot tombstone
+ # (retire-deprecated-image.yml) now sits on its tags telling users to switch.
# Change DOCKER_IMAGE for forks; see CONTRIBUTING.md.
DOCKER_IMAGE: stevezzau/media_preview_generator
- DOCKER_IMAGE_DEPRECATED: stevezzau/plex_generate_vid_previews
concurrency:
# Cancel superseded PR runs; never cancel in-flight pushes to main/dev/tags
@@ -366,7 +365,6 @@ jobs:
head_sha: ${{ steps.ctx.outputs.head_sha }}
is_tag: ${{ steps.ctx.outputs.is_tag }}
docker_tags_canonical: ${{ steps.ctx.outputs.docker_tags_canonical }}
- docker_tags_deprecated: ${{ steps.ctx.outputs.docker_tags_deprecated }}
steps:
- name: Compute build context
id: ctx
@@ -380,12 +378,10 @@ jobs:
if [[ "$REF_TYPE" == "tag" ]]; then
echo "is_tag=true" >> "$GITHUB_OUTPUT"
echo "docker_tags_canonical=-t ${{ env.DOCKER_IMAGE }}:$HEAD_BRANCH -t ${{ env.DOCKER_IMAGE }}:latest" >> "$GITHUB_OUTPUT"
- echo "docker_tags_deprecated=-t ${{ env.DOCKER_IMAGE_DEPRECATED }}:$HEAD_BRANCH -t ${{ env.DOCKER_IMAGE_DEPRECATED }}:latest" >> "$GITHUB_OUTPUT"
else
# dev branch push or manual workflow_dispatch
echo "is_tag=false" >> "$GITHUB_OUTPUT"
echo "docker_tags_canonical=-t ${{ env.DOCKER_IMAGE }}:dev" >> "$GITHUB_OUTPUT"
- echo "docker_tags_deprecated=-t ${{ env.DOCKER_IMAGE_DEPRECATED }}:dev" >> "$GITHUB_OUTPUT"
fi
build:
@@ -394,24 +390,16 @@ jobs:
fail-fast: true
matrix:
include:
- # 4 jobs total: 2 platforms × 2 image flavours. The deprecated
- # mirror and the canonical image differ only in the published
- # name + the DOCKER_IMAGE_NAME build-arg baked in (drives the
- # in-app deprecation banner). Layer cache is shared across
- # flavours via cache-scope so the second flavour's build is
- # near-instant.
+ # 2 jobs: amd64 + arm64 of the canonical image. The deprecated
+ # mirror (stevezzau/plex_generate_vid_previews) was retired — a
+ # one-shot tombstone image (see retire-deprecated-image.yml) now
+ # sits on its tags telling users to switch to the canonical name.
- platform: linux/amd64
runner: ubuntu-latest
image_flavour: canonical
- - platform: linux/amd64
- runner: ubuntu-latest
- image_flavour: deprecated
- platform: linux/arm64
runner: ubuntu-24.04-arm
image_flavour: canonical
- - platform: linux/arm64
- runner: ubuntu-24.04-arm
- image_flavour: deprecated
runs-on: ${{ matrix.runner }}
steps:
@@ -437,14 +425,9 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Resolve image name for this flavour
+ - name: Resolve image name
id: image
- run: |
- if [[ "${{ matrix.image_flavour }}" == "canonical" ]]; then
- echo "name=${{ env.DOCKER_IMAGE }}" >> "$GITHUB_OUTPUT"
- else
- echo "name=${{ env.DOCKER_IMAGE_DEPRECATED }}" >> "$GITHUB_OUTPUT"
- fi
+ run: echo "name=${{ env.DOCKER_IMAGE }}" >> "$GITHUB_OUTPUT"
- name: Generate bundled release notes
# Fetches GitHub Releases into media_preview_generator/release_notes.json
@@ -512,13 +495,6 @@ jobs:
pattern: digest-*-canonical
merge-multiple: true
- - name: Download deprecated digests
- uses: actions/download-artifact@v4
- with:
- path: /tmp/digests/deprecated
- pattern: digest-*-deprecated
- merge-multiple: true
-
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -535,13 +511,6 @@ jobs:
${{ needs.context.outputs.docker_tags_canonical }} \
$(printf '${{ env.DOCKER_IMAGE }}@sha256:%s ' *)
- - name: Create deprecated mirror manifest list and push
- working-directory: /tmp/digests/deprecated
- run: |
- docker buildx imagetools create \
- ${{ needs.context.outputs.docker_tags_deprecated }} \
- $(printf '${{ env.DOCKER_IMAGE_DEPRECATED }}@sha256:%s ' *)
-
update-dockerhub-description:
runs-on: ubuntu-latest
needs: context
@@ -558,11 +527,3 @@ jobs:
password: ${{ secrets.DOCKERHUB_TOKEN }}
repository: stevezzau/media_preview_generator
readme-filepath: ./DOCKERHUB_README.md
-
- - name: Update deprecated mirror Docker Hub description
- uses: peter-evans/dockerhub-description@v5
- with:
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKERHUB_TOKEN }}
- repository: stevezzau/plex_generate_vid_previews
- readme-filepath: ./DOCKERHUB_DEPRECATED_README.md
diff --git a/.github/workflows/retire-deprecated-image.yml b/.github/workflows/retire-deprecated-image.yml
new file mode 100644
index 00000000..45c421fe
--- /dev/null
+++ b/.github/workflows/retire-deprecated-image.yml
@@ -0,0 +1,54 @@
+name: Retire deprecated Docker image
+
+# One-shot, manually triggered. Builds the tombstone image (a static "this
+# image moved" page) and pushes it OVER the deprecated repo's :latest and :dev
+# tags, so anyone still pulling stevezzau/plex_generate_vid_previews gets a
+# clear "switch to stevezzau/media_preview_generator" notice. Run this ONCE,
+# then the deprecated build is already removed from ci.yml.
+on:
+ workflow_dispatch:
+ inputs:
+ confirm:
+ description: 'Type "retire" to confirm pushing the tombstone over the old image'
+ required: true
+ default: ''
+
+env:
+ DEPRECATED_IMAGE: stevezzau/plex_generate_vid_previews
+
+jobs:
+ tombstone:
+ runs-on: ubuntu-latest
+ if: ${{ github.event.inputs.confirm == 'retire' }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Validate Docker secrets
+ run: |
+ if [ -z "${{ secrets.DOCKER_USERNAME }}" ] || [ -z "${{ secrets.DOCKER_PASSWORD }}" ]; then
+ echo "::error::DOCKER_USERNAME and DOCKER_PASSWORD must be set in repository secrets"
+ exit 1
+ fi
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+
+ - name: Build & push tombstone (multi-arch) over :latest and :dev
+ uses: docker/build-push-action@v6
+ with:
+ context: ./tombstone
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: |
+ ${{ env.DEPRECATED_IMAGE }}:latest
+ ${{ env.DEPRECATED_IMAGE }}:dev
diff --git a/DOCKERHUB_DEPRECATED_README.md b/DOCKERHUB_DEPRECATED_README.md
deleted file mode 100644
index f76be7a4..00000000
--- a/DOCKERHUB_DEPRECATED_README.md
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-# ⚠️ This image has been renamed
-
-This Docker Hub repository — `stevezzau/plex_generate_vid_previews` — is now a **mirror** of the canonical image:
-
-> **➡ [`stevezzau/media_preview_generator`](https://hub.docker.com/r/stevezzau/media_preview_generator)**
-
-The project supports **Plex, Emby, and Jellyfin** today, so the old "Plex" name is misleading. We renamed the image (and the upstream repository) to match the broader scope.
-
-## What you need to do
-
-Update your `docker-compose.yml` (or `docker run` script):
-
-```diff
- services:
- media-preview-generator:
-- image: stevezzau/plex_generate_vid_previews:latest
-+ image: stevezzau/media_preview_generator:latest
- # ... everything else stays the same
-```
-
-Then `docker compose pull && docker compose up -d`. Existing volumes, settings, jobs, schedules, and configuration are unchanged — only the image name moves.
-
-## Timeline
-
-- **Now → 2026-10-29**: Both image names mirror the same builds. Watchtower / `:latest` users on the old name keep getting updates automatically.
-- **After 2026-10-29**: Only `stevezzau/media_preview_generator` receives updates. The old name stops being published.
-
-## Why the rename?
-
-The app started life as a Plex-only tool. Phases 0–L of the multi-server refactor (PR #225) added Emby and Jellyfin adapters with full per-server libraries, path mappings, exclude paths, webhooks, and dispatch fan-out. The "Plex Generate Previews" name no longer reflected what the project does.
-
-## Where to read more
-
-- Canonical Docker Hub:
-- GitHub: (auto-redirects from the old URL)
-- Documentation:
-
-If you have any issues migrating, open an issue on GitHub — the import path inside the container is unchanged, so any third-party scripts hitting `/api/...` keep working without modification.
diff --git a/DOCKERHUB_README.md b/DOCKERHUB_README.md
index 4bfcd428..eebe91c0 100644
--- a/DOCKERHUB_README.md
+++ b/DOCKERHUB_README.md
@@ -6,7 +6,7 @@
GPU-accelerated video preview thumbnail generation for **Plex, Emby, and Jellyfin**. **Web UI only** — no CLI.
-> Previously named **Plex Generate Previews** at `stevezzau/plex_generate_vid_previews`. That image keeps mirroring updates until **2026-10-29**; after that, only this repo (`stevezzau/media_preview_generator`) is published. Update your `compose` to the new name when convenient — settings and volumes carry over unchanged.
+> Previously named **Plex Generate Previews** at `stevezzau/plex_generate_vid_previews`. **That image has been retired and no longer receives updates** — this repo (`stevezzau/media_preview_generator`) is the only one published. If you're still on the old name, update your `compose` file's `image:` line to `stevezzau/media_preview_generator` and re-pull — settings and volumes carry over unchanged.
**The Problem:** Built-in preview generation has gaps depending on which server you run:
diff --git a/media_preview_generator/web/app.py b/media_preview_generator/web/app.py
index 02d22349..252599ee 100644
--- a/media_preview_generator/web/app.py
+++ b/media_preview_generator/web/app.py
@@ -520,22 +520,17 @@ def _log_image_deprecation_warning() -> None:
rather than open the dashboard. Silent when the env var is unset
(local dev) or set to the canonical name.
"""
- from .notifications import (
- CANONICAL_IMAGE_NAME,
- DEPRECATED_IMAGE_NAME,
- DEPRECATED_IMAGE_SUNSET_DATE,
- )
+ from .notifications import CANONICAL_IMAGE_NAME, DEPRECATED_IMAGE_NAME
image_name = (os.environ.get("DOCKER_IMAGE_NAME") or "").strip()
if image_name != DEPRECATED_IMAGE_NAME:
return
logger.warning(
- "Running deprecated Docker image {!r} — switch your compose to {!r} "
- "before {} to keep getting updates. (Both image names mirror the same "
- "builds until then; only the canonical name receives updates after.)",
+ "Running RETIRED Docker image {!r} — it no longer receives updates. "
+ "Switch your compose 'image:' line to {!r} and re-pull to keep getting "
+ "updates. Your volumes and settings are unchanged.",
DEPRECATED_IMAGE_NAME,
CANONICAL_IMAGE_NAME,
- DEPRECATED_IMAGE_SUNSET_DATE,
)
diff --git a/media_preview_generator/web/notifications.py b/media_preview_generator/web/notifications.py
index 065c00d5..ae2f2b09 100644
--- a/media_preview_generator/web/notifications.py
+++ b/media_preview_generator/web/notifications.py
@@ -30,12 +30,12 @@
DEPRECATED_IMAGE_ID = "deprecated_docker_image_name"
MEDIA_MOUNT_UNHEALTHY_ID = "media_mount_unhealthy"
-# Image names recognised by the deprecation banner. The deprecated image
-# keeps publishing alongside the canonical name until 2026-10-29 (six months
-# after the rename); after that, only the canonical name receives updates.
+# Image names recognised by the deprecation banner. The deprecated image was
+# retired — a one-shot tombstone image now sits on its tags. This banner still
+# fires for anyone running the last real build of the old image (no auto-update
+# yet) to push them to switch to the canonical name.
DEPRECATED_IMAGE_NAME = "stevezzau/plex_generate_vid_previews"
CANONICAL_IMAGE_NAME = "stevezzau/media_preview_generator"
-DEPRECATED_IMAGE_SUNSET_DATE = "2026-10-29"
_SESSION_DISMISSED: set[str] = set()
@@ -201,21 +201,21 @@ def _build_deprecated_image_notification() -> dict[str, Any] | None:
body = (
f"You're running the Docker image "
- f"{DEPRECATED_IMAGE_NAME}, which has been renamed to "
- f"{CANONICAL_IMAGE_NAME} to reflect that this app now "
- f"supports Plex, Emby, and Jellyfin.
"
- f"Both image names mirror the same builds until "
- f"{DEPRECATED_IMAGE_SUNSET_DATE}; after that, only "
- f"{CANONICAL_IMAGE_NAME} receives updates. Update your "
- f"compose file's image: line and pull "
- f"the new image to keep getting updates.
"
+ f"{DEPRECATED_IMAGE_NAME}, which has been retired "
+ f"and renamed to {CANONICAL_IMAGE_NAME} (this app now supports "
+ f"Plex, Emby, and Jellyfin).
"
+ f"The old image name no longer receives updates. "
+ f"Update your compose file's image: line to "
+ f"{CANONICAL_IMAGE_NAME} and run "
+ f"docker compose pull && docker compose up -d to keep "
+ f"getting updates.
"
f"Existing volumes, settings, and "
f"configuration are unchanged — only the image name moves.
"
)
return {
"id": DEPRECATED_IMAGE_ID,
"severity": "warning",
- "title": "Update your Docker image",
+ "title": "Your Docker image has been retired",
"body_html": body,
"dismissable": True,
"source": "image_deprecation",
diff --git a/tests/test_notifications_api.py b/tests/test_notifications_api.py
index 45a7e8dc..04a0f8f9 100644
--- a/tests/test_notifications_api.py
+++ b/tests/test_notifications_api.py
@@ -311,8 +311,9 @@ def test_fires_when_running_deprecated_image(self, monkeypatch):
# The body names both the old and the new image so users can copy-paste.
assert "stevezzau/plex_generate_vid_previews" in deprecated["body_html"]
assert "stevezzau/media_preview_generator" in deprecated["body_html"]
- # And carries the sunset date.
- assert "2026-10-29" in deprecated["body_html"]
+ # And makes clear the old image is retired (no longer updated).
+ assert "retired" in deprecated["body_html"].lower()
+ assert deprecated["title"] == "Your Docker image has been retired"
class TestSettingsManagerDismissedNotifications:
diff --git a/tombstone/Dockerfile b/tombstone/Dockerfile
new file mode 100644
index 00000000..d467df10
--- /dev/null
+++ b/tombstone/Dockerfile
@@ -0,0 +1,10 @@
+# Minimal "this image was retired" tombstone for the old Docker Hub repo
+# (stevezzau/plex_generate_vid_previews). Built + pushed ONCE by
+# .github/workflows/retire-deprecated-image.yml, then the deprecated build is
+# removed from ci.yml. Anyone who pulls the old image after that gets a page
+# telling them to switch to stevezzau/media_preview_generator.
+FROM nginx:1.27-alpine
+COPY index.html /usr/share/nginx/html/index.html
+COPY default.conf /etc/nginx/conf.d/default.conf
+EXPOSE 8080
+HEALTHCHECK CMD wget -qO- http://127.0.0.1:8080/ >/dev/null 2>&1 || exit 1
diff --git a/tombstone/default.conf b/tombstone/default.conf
new file mode 100644
index 00000000..1b8a21be
--- /dev/null
+++ b/tombstone/default.conf
@@ -0,0 +1,8 @@
+# Tombstone server: every request returns the "this image moved" page on the
+# app's normal port (8080) so existing compose port-maps still hit it.
+server {
+ listen 8080 default_server;
+ server_name _;
+ root /usr/share/nginx/html;
+ location / { try_files /index.html =404; }
+}
diff --git a/tombstone/index.html b/tombstone/index.html
new file mode 100644
index 00000000..514b3433
--- /dev/null
+++ b/tombstone/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+ This image moved — Media Preview Generator
+
+
+
+
+
Image retired
+
This Docker image has moved 🚚
+
stevezzau/plex_generate_vid_previews is no longer published.
+ The project was renamed (it now supports Plex, Emby & Jellyfin).
+
Switch your image: line to the new name — your config, volumes and settings carry over unchanged:
+
+
image: stevezzau/plex_generate_vid_previews:latest
+
image: stevezzau/media_preview_generator:latest
+
+
Then docker compose pull && docker compose up -d. Same app, same data — just the new image name.
+ Docs: github.com/stevezau/media_preview_generator
+
+
+