docs: true up the tag scheme against what the pipeline publishes (318 step 9)
CI / lint (push) Successful in 3s
Build images / sign-extension (push) Successful in 4s
Build images / build-ml (push) Successful in 4s
CI / extension-version (push) Successful in 3s
Build images / build-agent (push) Successful in 4s
Build images / build-web (push) Successful in 5s
CI / frontend-build (push) Successful in 22s
extension / lint (push) Successful in 30s
CI / backend-lint-and-test (push) Successful in 48s
CI / integration (push) Successful in 3m57s
extension / lint (pull_request) Successful in 28s

A doc describing a tag scheme the pipeline stopped using is worse than no
doc — it is a confident wrong answer, and #3159 named it as the drift that
had just bitten rule 148.

build.yml (each edit ×3, once per build job):

* The derived-values block claimed three values including a `tag`
  subcommand that no longer exists, and said "on dev the date tag is
  computed nowhere else". There are two values and no date tag.
* The BASE-IMAGE FRESHNESS paragraph argued from a pin that no longer
  exists — "a date tag has to keep serving the bytes it served
  (fabledcurator:2026.7.17 still resolves to July's image)". Milestone 318
  removed the argument's premise rather than answering it: with no version
  tags there is no immutable name a refresh could contradict, and rule 145
  already allows a moving tag to be republished. Deleted the argument,
  kept a line saying why there is nothing to argue about (#3159's phrasing:
  the workflow becomes obviously correct instead of carefully correct).
* The repoint comment said ":c-<sha> and the date pin". There is no date pin.
* "its set is too narrow — the direction that serves stale bytes on a pin"
  now names the failure that actually exists: the reuse check hits and the
  channel serves a web image bundling the previous XPI (#3156).

README gains a "Versions and tags" section — the three tags, why there is
no fourth, and the fact that with no registry name carrying it, the
instance's own report is the only answer to "which build is this?". Also
fixes two stale claims: three workflows (four since release.yml) and "AMO
signing runs on main only", which has been false since milestone 271
step 6 signed on both channels.

ci-requirements.md was missing milestones 313 and 318 entirely: no
artifacts.sh, no label-keyed reuse, no FC_VERSION. Added those, plus the
#3156/#3202 membership test (can changing this file change the published
bytes — not is it copied in), and corrected packaging.sh's "two consumers"
to three now that artifacts.sh reads it.

docker-compose.yml needed nothing — it names `:dev` directly and never
offered a pin target.

Note #3127 and rule 145's relation notes are updated in Scribe.
This commit is contained in:
2026-08-29 00:09:52 -04:00
parent a3071a7549
commit d38585ed94
3 changed files with 159 additions and 86 deletions
+67 -23
View File
@@ -9,14 +9,17 @@ git.fabledsword.com/bvandeusen/ci-python:3.14
## Image deps used
- python 3.14
- ruff (analyzer for `backend/`, `tests/`, `alembic/`)
- ruff (analyzer for `backend/`, `tests/`, `alembic/`, `agent/`, `scripts/`)
- node (frontend job: `npm install` + vitest + vite build)
- docker CLI + buildx (`.forgejo/workflows/build.yml`: build-web, build-ml — Fabled-Git registry push)
- docker CLI + buildx (`.forgejo/workflows/build.yml`: build-web, build-ml, build-agent — Fabled-Git registry push, and `imagetools inspect`/`create` for the reuse path)
## Secondary runtime image
node:24-bookworm-slim — `.forgejo/workflows/extension.yml` only.
`.forgejo/workflows/release.yml` runs on `ci-python:3.14` like everything else
and installs nothing: it needs git and stdlib python, and builds no image.
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`,
@@ -54,13 +57,19 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
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/…`.
inside the XPI.** Three consumers read from it rather than keeping their own
copy: web-ext's `--ignore-files` (`extension/package.json`), the `git log`
pathspec inside the script's own version derivation, and `scripts/artifacts.sh`,
which appends the extension's set to web's because the web image bundles the
signed XPI. 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/…`.
- **Packaged and version-relevant are two different sets** (#3156). `scripts/`
is excluded from the XPI and is NOT excluded from the version derivation,
because `packaging.sh` decides the version string stamped into the packaged
`manifest.json`. The membership test is *"can changing this file change the
published bytes?"*, not *"is this file copied in?"* — which is why the script
keeps two lists rather than one.
- **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
@@ -68,20 +77,55 @@ per `docs/process.md`'s "add deps to the image when used by >1 project".
`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.
- Every job that derives anything checks out with `fetch-depth: 0` all four
`build.yml` jobs, `ci.yml`'s `extension-version` and `backend-lint-and-test`
(for `tests/test_artifact_paths.py` and `test_artifact_identity.py`), and
`release.yml`, which additionally walks the tag graph. 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.
- **`scripts/artifacts.sh` is the same shape one level up: one definition per
artifact of what it is built from, and the two values derived from it.**
`revision` (12 hex of the newest commit touching that set) and `version`
(`YYYY.MM.DD.HHMM` UTC, rule 148). Four artifacts, four independent answers,
so a push touching only `agent/` leaves web and ml alone.
`tests/test_artifact_paths.py` reads each Dockerfile and asserts every COPY
source is covered, so adding a COPY without updating the script fails CI.
- **A file that DECIDES an artifact's identity belongs in its set even though it
is copied into nothing** — `packaging.sh` for the extension and web (#3156),
and `artifacts.sh` itself for web (#3202), which decides the `FC_VERSION`
baked into that image. Only web needs the second entry: every artifact stamps
a revision, but a revision has a backstop (a changed derivation stops matching
the published label and forces a rebuild) and a version has none, because
nothing compares it to anything. `tests/test_artifact_paths.py`'s `DERIVERS`
table is the guard.
- **Builds are skipped when the content is already published.** Each image
carries its revision as an `fc.revision` LABEL, and `build.yml` reads that
label back off the moving channel tag (`imagetools inspect --format`). Equal
to the derived revision means the bytes are already published, so the job
repoints the remaining tags at the existing manifest instead of rebuilding.
Two things this depends on: an inspect that errors for ANY reason reads as a
MISS so no needed build is ever skipped, and the repoint must EXCLUDE the
source tag — `imagetools create` wraps its source in a manifest index, and
config labels do not resolve through an index, so writing the channel tag
from itself destroys the label the next run reads (#3183).
- **`FC_CHANNEL` and `FC_VERSION` are build args, not runtime settings.**
`build.yml` passes them to the web image only — the ml and agent images have
nothing to report them to. `/api/health` returns both, the foot of Settings
renders them, and `/api/extension/manifest` reports the channel beside the
extension version so an install can be traced to a channel. With no version image tags, that
self-report is the ONLY answer to "which build is this?" — which is why a
missing version renders `unknown` rather than a blank: an empty footer reads
as "no version", a different and false claim.
Both are declared LAST in the Dockerfile on purpose: an ARG invalidates every
layer below it, and these are the values that differ between the dev and main
builds of identical source, so placing them earlier would stop the two
channels ever sharing a cached `pip install`. Empty by default — a local build
then reports nothing rather than claiming a channel it is not on.
- **The channel is never folded into the version.** A `-dev` suffix makes the
extension's per-segment `parseInt` comparator read that segment as 0, so every
dev build compares equal to every other — issue #2993 exactly (rule 149).
`frontend/test/systemBuild.spec.js` pins the rendered version to the bare
number.
- 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