Files
FabledCurator/ci-requirements.md
T
bvandeusen a7e626a67a
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 5s
CI / frontend-build (push) Successful in 20s
CI / backend-lint-and-test (push) Successful in 32s
extension / lint (push) Successful in 28s
CI / integration (push) Successful in 3m52s
Build images / sign-extension (push) Successful in 4s
Build images / build-ml (push) Failing after 5s
Build images / build-agent (push) Successful in 13s
Build images / build-web (push) Successful in 2m4s
feat(extension): report the channel beside the version (step 7)
Closes the half of the ask the signing work didn't: a way to tell a dev
build from a main one. FC_CHANNEL is baked into the web image at build
time and /api/extension/manifest reports it as its own key, next to
version — the popup banner, the toolbar tooltip and the Settings card all
name it.

Beside the version, never inside it. A `1.0.3499884-dev` suffix is the
obvious shortcut and it is the exact failure this design comes from:
versionIsNewer parses each dotted segment with parseInt, so a suffixed
segment reads as 0, every dev build compares equal to every other, and
"no update available" stops being distinguishable from "I cannot read this
version". The comparator already degrades rather than discarding (rule
150), which is a reason not to NEED the suffix, not a licence to add one.
Two tests hold the line — one backend, asserting version and channel are
separate keys; one frontend, asserting the rendered version text stays the
bare derived number.

Optional on the read side, and absent rather than defaulted. An image
built before this field says nothing by not having the key; an image built
without a channel now says nothing the same way, so there is one absence
to handle instead of a second spelling of "unknown". Every reader drops
the label entirely when it is missing and reads exactly as it did before.
Reported verbatim rather than validated against {dev, main}: if an image
declares something else, showing what it claims helps whoever is debugging
more than dropping it would.

FC_CHANNEL is declared LAST in the Dockerfile. An ARG invalidates every
layer below it, and this is the one value that differs between the dev and
main builds of identical source — earlier, and the two channels could
never share a cached pip install. A tag push counts as main: a vYY.MM.DD
tag is cut from main, so that image is a main-channel artifact wearing an
immutable name.

No channel switcher, deliberately. background.js:34 already records that
Firefox's static update_url cannot apply, because every FC instance is a
different host — so the extension asks its configured backend, and the
channel IS the instance it points at. Switching is repointing apiUrl and
reinstalling from that host. A separate setting would contradict each
server build shipping its own extension.

This commit touches packaged extension files, so it moves the derived
version and will sign a new one via AMO — the first push to exercise the
extension-changed path from dev end to end.
2026-08-27 11:47:30 -04:00

5.2 KiB

CI Requirements — FabledCurator

Spec: https://git.fabledsword.com/bvandeusen/CI-runner/src/branch/main/docs/process.md

Runtime image

git.fabledsword.com/bvandeusen/ci-python:3.14

Image deps used

  • python 3.14
  • ruff (analyzer for backend/, tests/, alembic/)
  • node (frontend job: npm install + vitest + vite build)
  • docker CLI + buildx (.forgejo/workflows/build.yml: build-web, build-ml — Fabled-Git registry push)

Secondary runtime image

node:24-bookworm-slim — .forgejo/workflows/extension.yml only.

The extension lane is the one job that does NOT run on ci-python:3.14: it needs a current Node for web-ext and vitest and nothing Python at all. Kept on the upstream slim image rather than adding a Node toolchain to ci-python, per docs/process.md's "add deps to the image when used by >1 project".

Per-job tool installs

  • pip install -r requirements.txt pytest pytest-asyncio — in backend-lint-and-test and integration jobs
  • npm install --no-audit --no-fund — in frontend-build job
  • npm install --no-audit --no-fund — in extension.yml's lint job (web-ext + vitest)
  • unzip — in extension.yml's "Verify XPI contents" step, installed via apt only when absent (node:24-bookworm-slim may or may not carry it). Debian package, ~2s. Not worth baking into a shared image for a single consumer, per docs/process.md's ">1 project" rule.

Notes

  • Integration wall time ~3 min, dominated by pgvector container start + the pip install step (~30-45s on cold cache) + alembic + 300+ integration tests.
  • The pip install in two jobs is intentional and per docs/process.md's "add deps to image when used by >1 project" rule: FC alone is one Python project, so the deps live in requirements.txt and install per-job. Reconsider when a second Fabled-family Python backend lands.
  • Integration uses Fabled-Git Actions services: + socket-discovered bridge IPs because act_runner (swarm-runner v0.6+) puts services on the default bridge with no embedded DNS. The pattern is documented in the rulebook's fabled-git.md "CI philosophy" section and FC's ci.yml is the canonical example.
  • No package-lock.json is tracked yet (FC's feedback_no_local_runs memory bans npm install locally). Using npm install rather than npm ci until a lockfile lands.
  • No imagemagick / pandoc per-job installs needed.
  • extension/'s vitest specs load lib/*.js by evaluating the real file as a classic script (test/helpers/loadLib.js) rather than adding module.exports shims to production code — the libs ship as background.scripts, not ES modules, so the specs exercise exactly the bytes packaged into the XPI.
  • extension/scripts/packaging.sh is the single definition of what ships inside the XPI. Two consumers read from it rather than keeping their own copy: web-ext's --ignore-files (extension/package.json), and the git log pathspec inside the script's own version derivation. It was three until 2026-08-27 — ci.yml's extension-version guard held the third and went when the manual bump it guarded did (milestone 271 step 5). Hand-kept copies of that one fact is what allowed issue #2397, so extension/test/version.spec.js asserts no workflow has reintroduced a literal :(exclude)extension/….
  • The shipped extension version is derived, not committed. It is the commit TIME of the newest packaged-extension change (minutes since 2020-01-01, per family rule 149 — never a commit count, which orders by branch rather than by recency). build.yml's sign-extension computes it and stamps it into extension/manifest.json + package.json in the working tree before signing; the stamp is never committed. Treat the version in the repo as a base: only its MAJOR.MINOR is read, and its patch component is inert.
  • Every job that calls packaging.sh version checks out with fetch-depth: 0build.yml's sign-extension and build-web, and ci.yml's extension-version. A depth-1 clone sees one commit and derives a wrong, too-low value rather than failing, so the full-history checkout is load-bearing rather than incidental.
  • FC_CHANNEL is a build arg, not a runtime setting. build.yml passes dev / main to the web image only (the ml and agent images have nothing to report it to), and /api/extension/manifest reports it beside the version so an install can be traced to a channel. It is declared LAST in the Dockerfile on purpose: an ARG invalidates every layer below it, and this is the one value that differs between the dev and main builds of identical source, so placing it earlier would stop the two channels ever sharing a cached pip install. Empty by default — a local build then reports no channel at all rather than claiming one.
  • Callers MUST set -f before substituting the script's output. Without it the shell expands test/** against the working tree and silently narrows the pattern to whatever files exist at that moment — a failure that looks like nothing until dev files start appearing in the XPI. test/version.spec.js asserts every --ignore-files consumer sets it, and that no consumer has quietly reinstated a hardcoded list.