CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 3s
extension / lint (push) Successful in 21s
CI and images / frontend-build (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 31s
CI and images / integration (push) Successful in 2m10s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 6s
CI and images / build-web (push) Successful in 1m51s
CI and images / smoke-web (push) Successful in 57s
CI and images / promote (push) Skipped
Operator, 2026-09-23: *"tighten the gate so :dev can't publish on red tests"*, then *"I don't want failing builds to publish anywhere going forward."* Run 7348 is the worked example. The backend unit lane went red on `2f8f0bc` and `build-web` pushed `:dev` in the same minute, because the lanes and the build were SEPARATE WORKFLOWS on the same push trigger. Neither could see the other's verdict. `:dev` was a "it built" signal, never a "it passed" one, and nothing about that was visible from either run. Two workflows cannot express the gate. A `needs:` edge only exists inside one graph. So `ci.yml`'s five lanes move into `build.yml` and `ci.yml` is deleted; `sign-extension`, `build-web` and `build-agent` now need all five. Nothing here is a new mechanism — it is the same edge that has gated `promote` since milestone 362 step 4, and it keeps that step's hardest-won property: **not running is not the same as passing.** `needs` treats a SKIPPED dependency as unsatisfied, so a lane that silently skips itself blocks the publish exactly as a failing one does. Run 5290 is why that is worth stating. Scope, said plainly rather than implied: - Gated: every image tag (`:dev`, `:latest`, `:c-<sha>`), the weekly base refresh, and the `ext-<version>` signed-XPI release asset — `sign-extension` publishes too, so it is gated with the rest. - Not gated, deliberately: `extension.yml` publishes nothing, and `release.yml` runs on a `v*` tag, generates notes rather than an artifact, and its commit already went through main's gated build. - `pull_request` (Renovate bumps into `dev`) comes across with the lanes. Its runs are the lanes and nothing else, via an `if:` on each publishing job rather than an inference from the `needs` chain. The cost, accepted knowingly: this workflow queues per branch and never cancels, so on two pushes in quick succession the second's lint feedback waits out the first's build. A slower red beats a fast red that ships. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
113 lines
5.4 KiB
Markdown
113 lines
5.4 KiB
Markdown
# FabledCurator Firefox Extension
|
|
|
|
Self-hosted Firefox extension that pushes session cookies from supported
|
|
platforms (Patreon, SubscribeStar, Hentai-Foundry, Discord)
|
|
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
|
|
|
|
```sh
|
|
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"
|
|
- [ ] Add as source: visit patreon.com/<creator>, click floating button → toast
|
|
- [ ] Subscriptions list: popup → "Sources" tab → list renders
|
|
- [ ] Check now: click play icon on source row → no error toast
|
|
|
|
## Versioning — the committed number decides nothing
|
|
|
|
The shipped version is **derived**, not committed. `scripts/packaging.sh
|
|
version` returns `YYYY.M.D.HHMM` in UTC: the commit *time* of the newest change
|
|
to a packaged extension file. `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 version does nothing.** All of it is overwritten before web-ext
|
|
ever reads it. There is no bump to make, and none to forget. There is no
|
|
hand-set part left either: MAJOR.MINOR went away with milestone 318 step 8.
|
|
- `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 the extension is the one artifact that does not zero-pad.** Every other
|
|
FC artifact emits rule 148's `YYYY.MM.DD.HHMM`. AMO will not take it: Mozilla's
|
|
grammar for addons.mozilla.org is
|
|
|
|
```
|
|
^(0|[1-9][0-9]{0,8})([.](0|[1-9][0-9]{0,8})){0,3}$
|
|
```
|
|
|
|
— each segment is the single digit `0` or starts 1-9, so `08` and `0201` are
|
|
rejected, and at most four segments are allowed. The extension therefore emits
|
|
**the same numbers unpadded**: `2026.8.29.201` where the rest of the family
|
|
says `2026.08.29.0201`. Rule 148 already defines comparison as numeric per
|
|
segment, under which the two are equal, so nothing is reordered by the choice
|
|
and left-padding each segment recovers the family string exactly. `build.yml`'s
|
|
`extension-version` lane checks the derived string against that regex on every
|
|
push — the cheap place to find out, because AMO 409s on re-signing and a
|
|
rejected version is burned for good.
|
|
|
|
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.
|