Files
FabledCurator/extension
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 Firefox Extension

Self-hosted Firefox extension that pushes session cookies from supported platforms (Patreon, SubscribeStar, Hentai-Foundry, Discord, Pixiv) into FabledCurator, and lets you add a creator as a Source from their page in one click.

Install (operator)

The signed XPI is bundled into the FC Docker image — :dev and :latest each carry their own channel's build. Open FC → Settings → Maintenance → Browser extension → click "Install Firefox extension". Firefox shows its native install prompt. After installing, open the extension's options page (about:addons → FabledCurator → Preferences) and paste in the FC URL + extension API key shown on the same card.

Develop

cd extension/
npm install --no-save        # web-ext only
npm run lint                 # web-ext lint
npm run test:unit            # vitest — lib/ logic + packaging/version checks
npm run start                # launches Firefox with extension loaded
npm run build                # unsigned XPI in web-ext-artifacts/

Smoke checklist (after every release that touches extension/**)

  • npm run lint passes
  • npm run start loads the extension in a clean Firefox profile
  • Options page accepts FC URL + key, indicator turns green
  • Cookie export: log into patreon.com, click Patreon card → "X cookies exported"
  • Discord token: open discord.com, click Discord card → "Token captured"
  • Pixiv OAuth: click Pixiv card → login redirects, token stored
  • Add as source: visit patreon.com/, click floating button → toast
  • Subscriptions list: popup → "Sources" tab → list renders
  • Check now: click play icon on source row → no error toast

Versioning — don't hand-edit the patch number

The shipped version is derived, not committed. scripts/packaging.sh version returns MAJOR.MINOR from manifest.json plus a patch component that is the commit time of the newest change to a packaged extension file, in minutes since 2020-01-01. build.yml computes it and stamps it into both manifest.json and package.json at build time. The stamp is never committed — the commit carrying it would itself be a change to the extension, which would move the version again.

So:

  • Editing the patch number does nothing. It is overwritten before web-ext ever reads it. There is no bump to make, and none to forget.
  • MAJOR.MINOR is still yours. It carries the deliberate meaning, it is read from manifest.json alone, and CI fails the extension-version lane if the two files disagree on it.
  • npm run build locally produces an XPI labelled with the committed version, since nothing stamped it. Fine for loading into a test profile; not what ships.

Why commit time and not a commit count: a count is per-branch, so dev and main count different histories of the same code and their versions end up ordered by which branch accumulated more commits rather than by which is newer. Commit time gives both branches the same number for the same source — which is exactly what lets one AMO signature serve both channels (family rule 149, FC issue #3092).

Channels

dev and main each build and sign their own extension, and an install is tied to whichever FC instance it points at — Firefox's static update_url cannot apply here, since every FC install is a different host, so the extension asks its configured backend. The channel therefore IS the instance. Switching channel means repointing the FC URL in options and reinstalling from that host; there is no separate channel setting, and adding one would contradict each server build shipping its own extension.

The channel is reported beside the version, never inside it: /api/extension/manifest answers {"version": "...", "channel": "dev"}. It is optional — an instance that declares none simply omits the key, and the popup, the toolbar tooltip and the Settings card all read exactly as they did before the field existed. Do not be tempted to make it a -dev version suffix: the comparator parses each dotted segment with parseInt, so a suffixed segment reads as 0 and every dev build compares equal to every other, collapsing "no update available" and "I cannot read this version" into one answer.

Release

Nothing to do by hand. Push to dev: build.yml signs the extension if this change moved the version, caches the signed XPI as a Forgejo ext-<version> release, and bundles it into fabledcurator:dev. Merging to main derives the same version, hits that cache, and bundles the byte-identical XPI into :latest with no second AMO call.

AMO refuses to re-sign a version it has already issued, so signing is one-shot per version — which is why the cache exists and why the version must never move backwards.