Removes the `v*` image-build trigger. A release tag names a commit `main` already built and published; rebuilding it produced the same source under the same names and RE-PUSHED `:c-<sha>` — which rule 145 forbids even when the bytes match, because image configs carry timestamps and "same source" does not mean "same manifest". The tag build was publishing nothing new and violating an immutability rule to do it. Three constructs go with it, all of which existed only to manage that build: The 10-minute XPI polling loop. A release cut fired the tag build and the main build together; the tag build skipped sign-extension and raced straight to the download, losing every time (operator-flagged 2026-05-27 after v26.05.27.0). Polling was the fix for a build that should not have run. It is now a single fetch, and a 404 means the two jobs derived different versions for one commit — which should fail loudly rather than be slept through. The IS_TAG_PUSH carve-out from milestone 313 step 4, which stopped a tag build from claiming an identity tag it would have re-pointed at fresh bytes. build-web's `always() && (success || skipped)` gate, which existed to let a SKIPPED sign-extension through on a tag push while still blocking a failed one. sign-extension now always runs, so a plain `needs` gives exactly the wanted behaviour. Its own branch condition goes too: main and dev are the only triggers, so a gate naming both matched everything, and a condition that is always true reads as though some path avoids it. Releases still happen — rule 148 is untouched and tags are still cut on explicit request per rule 2. They stop building images and start carrying a changelog (step 7). Net 115 lines deleted, 73 added, most of that comments explaining races that can no longer occur. Nothing in the repo referenced the tag build, so no doc changes were needed; the broader doc pass is step 9.
FabledCurator
Self-hosted media curation — gallery, ML tagging, and subscription-driven downloading in one app. Part of the FabledSword family.
Combines what was ImageRepo (gallery, ML, importer) and GallerySubscriber (gallery-dl wrapper, subscriptions, credential capture) into a single product.
Status
In production. main is continuously deployed — every merge to main builds
and publishes :latest images, so whatever is on main is what is running.
Day-to-day work happens on dev, which publishes :dev images.
What's in here
Five deployable pieces, built by .forgejo/workflows/build.yml:
| Piece | Built from | Image | Role |
|---|---|---|---|
| Web / workers | Dockerfile |
fabledcurator |
Quart API + the built Vue SPA in one image. entrypoint.sh picks the role: web, worker, scheduler. The maintenance-long service is a second worker pinned to the long-running maintenance queue. |
| ML worker | Dockerfile.ml |
fabledcurator-ml |
Same app, plus requirements-ml.txt — tagging and embedding models that run in-container. |
| GPU agent | agent/Dockerfile |
fabledcurator-agent |
Optional desktop-GPU worker (agent/). Leases jobs over HTTP only — never touches the database or Redis. Run it for a burst, stop it to reclaim the card. See agent/README.md. |
| Firefox extension | extension/ |
signed XPI | MV3 extension: pushes platform session cookies into FC and adds a creator as a Source in one click. AMO-signed on both dev and main (one signature per extension change, shared by the two channels), bundled into that channel's web image and served from Settings → Maintenance. See extension/README.md. |
| Data | — | pgvector/pgvector:pg16, redis:7-alpine |
Postgres with pgvector for embeddings; Redis as the Celery broker. |
Quick start
For local development and testing, just:
docker compose up -d
# UI: http://localhost:8080
That uses sane dev defaults baked into docker-compose.yml and the dev
override (docker-compose.override.yml, auto-merged) — local builds, DEBUG
logging, exposed Postgres + Redis ports on the host. No .env required.
For a production-like deployment, override the dev defaults via shell env
or a .env file (see .env.example for the variable names) and use:
docker compose -f docker-compose.yml up -d
# (skips the override so containers pull registry images)
The GPU agent is deployed separately, on the machine with the card —
agent/docker-compose.yml, not this stack.
Deployment posture
FabledCurator is designed to run inside a self-hosted homelab environment over plain HTTP. If you want TLS, terminate it at your reverse proxy. The app does not generate certificates, redirect to HTTPS, or set HSTS.
CI / Forgejo setup
Three workflows: ci.yml (lint, extension-version check, backend unit tests,
frontend build, integration), extension.yml (extension lint, vitest, XPI
content verification), and build.yml (sign + publish).
The toolchain each job runs in is its container.image, not its runs-on
label. runs-on: python-ci only schedules the job onto a runner; every job
then names the image it actually wants. ci-requirements.md is the current,
authoritative list of images and per-job installs — read that rather than a
copy here, so the two can't drift.
The repo expects one secret:
-
RELEASE_TOKEN— a Forgejo PAT with:write:package+read:package— fordocker pushtogit.fabledsword.comwrite:release— for theext-<version>releases that cache the signed XPIwrite:issue— for issue-management automation
Generate at https://git.fabledsword.com/user/settings/applications. The injected
GITHUB_TOKENcannot be used because it lackswrite:package.
AMO signing additionally needs MOZILLA_AMO_JWT_KEY / MOZILLA_AMO_JWT_SECRET; it runs on
main only and is cached per version, since AMO rejects a re-signed version.
License
Personal project; use at your own discretion.