bvandeusen b6b9fd8287
CI / extension-version (push) Successful in 3s
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
Build images / build-ml (push) Successful in 4s
Build images / build-agent (push) Successful in 5s
Build images / build-web (push) Successful in 4s
CI / frontend-build (push) Successful in 18s
extension / lint (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 30s
CI / integration (push) Successful in 3m52s
ci: a release publishes a changelog, not an image (318 step 7)
Step 2 took the build consequence away from a `v*` tag — `main` has already
built and published the commit by the time anyone tags it, and rebuilding
would re-push `:c-<sha>`, which rule 145 forbids even when the source
matches. That left the tag with nothing to do at all.

This is the job it has instead. Step 6 put the derived version in the
Settings footer, so an operator can say WHICH build they are running; this
says what is in it that was not in the one they ran last month. Both halves
of one question (note #3127 §5).

The previous release is found by walking ANCESTRY, not by sorting a list.
That is load-bearing here specifically: rule 148 moved the tag shape from
`v26.05.22.0` to `v2026.08.28.2208`, and lexicographically `v2026...` sorts
BEFORE `v26...` — the third character is `0` against `6`. A sorted
implementation would reach back past every new-shape tag to the newest
old-shape one and publish months of commits as "changes since", looking
entirely correct while doing it. `git describe --exclude` is immune to the
shape change, and reachability is the more honest question anyway.

The publisher GETs and PATCHes rather than POSTing and recovering the id
from a 409 — note #3127 §6.7, which is ThoughtSync #2182's bug. A `v*` tag
is created once so the conflict path is rare, but "rare" is how that one
survived to be found somewhere else.

Cross-checks are reported on the release, not enforced. The tag is already
pushed by the time this runs, so failing would leave the operator with a tag,
no release, and a red lane to explain it — while the release is still the
useful object. It says so at the top when the tag names a version the web
image does not report, or when the commit is not on `main` and the `:c-`
rollback refs it lists were never published.

Nothing runs on a schedule and nothing auto-tags on merge. Release tags are
bookmarks (note #3127 §0); FC went twelve weeks without one and nothing was
wrong.

Also here:
- `scripts/` joins the ruff lane. release_notes.py runs only on a tag push,
  so a syntax error there would otherwise surface at the one moment nobody
  wants to be debugging a workflow.
- version.spec.js reads the workflow directory instead of listing three
  files by hand. Its own comment says the assertion should survive consumers
  coming and going; the hardcoded list was the part that could not, and
  release.yml would have joined the directory without joining the check.

Tests build a synthetic history spanning the tag-shape change rather than
leaning on this repo's tags, so the span assertion holds whether or not a
checkout brought the tags along — a span test that quietly skips is worse
than one that fails.
2026-08-28 20:57:25 -04:00

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 — for docker push to git.fabledsword.com
    • write:release — for the ext-<version> releases that cache the signed XPI
    • write:issue — for issue-management automation

    Generate at https://git.fabledsword.com/user/settings/applications. The injected GITHUB_TOKEN cannot be used because it lacks write: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.

S
Description
Self-hosted media curation — gallery, ML tagging, and subscription-driven downloads. Part of the FabledSword family. (Merge of ImageRepo + GallerySubscriber.)
Readme AGPL-3.0
8.6 MiB
2026-08-29 13:46:09 -04:00
Languages
Python 73.1%
Vue 17.8%
JavaScript 8%
Shell 0.5%
CSS 0.3%
Other 0.2%