# 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: 0` —
  `build.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.
