Rule 149: an artifact's ordering key must be time-derived, never a commit count. packaging.sh's cmd_patch was a count. Why that matters here rather than in the abstract. A count is per-branch: dev and main count different histories of the SAME code. Today only main signs, so nothing has ordered the two against each other and the fault is invisible. The moment dev also publishes an extension, the two versions order by which branch accumulated more commits rather than by which is newer — and a squash-merge makes it permanent, because main gains one commit where dev gained five. dev then climbs away from main and a dev install can never cross back. That is Roundtable's 2026-08-24 incident (Scribe #2993) in a different repo: their versionCode was the branch's commit count, and it produced a channel you could enter and not leave. Measured on this repo today the old formula gives main=23, dev=24 — one apart, which is exactly how the inversion stays invisible until it strands somebody. New formula: minutes since 2020-01-01 of the LATEST commit touching a packaged extension file. Same anchor and unit Roundtable settled on. Commit time, not build time, and the difference is load-bearing: - stable while the extension is unchanged, so the ext-<version> signature cache still hits and AMO is called once per extension CHANGE rather than once per push. Build-time minutes would re-sign on every push and never let two channels share a signature. - after a merge, main sees the same commit and derives the same number, so :latest reuses the signature :dev already produced for byte-identical code. Same code, same version, one signing. - monotonic: max() over a set that only gains members. Verified across all 24 extension-touching commits, zero non-monotonic steps. - reproducible from any checkout. Derives 1.0.3499884 on dev, 1.0.3465860 on main — both far above the last hand-set 1.0.11, so milestone 271's backfill guard is satisfied by construction rather than by an offset. Still shadow-only: nothing reads the derived value yet. Both shadow steps log it, and ci.yml's runs on dev too, so both channels' numbers are visible — that is the pair that has to stay ordered. Prior shadow observations describe the OLD formula and prove nothing about this one, so the window restarts; ci.yml says so at the step. New requirement recorded in ci-requirements.md: a depth-1 clone derives a wrong, too-low value rather than failing, so fetch-depth: 0 is load-bearing wherever packaging.sh version is called. Refs #3092, milestone 271
3.9 KiB
3.9 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— inbackend-lint-and-testandintegrationjobsnpm install --no-audit --no-fund— infrontend-buildjobnpm install --no-audit --no-fund— inextension.yml'slintjob (web-ext + vitest)unzip— inextension.yml's "Verify XPI contents" step, installed via apt only when absent (node:24-bookworm-slimmay or may not carry it). Debian package, ~2s. Not worth baking into a shared image for a single consumer, perdocs/process.md's ">1 project" rule.
Notes
- Integration wall time ~3 min, dominated by pgvector container start + the
pip installstep (~30-45s on cold cache) + alembic + 300+ integration tests. - The
pip installin two jobs is intentional and perdocs/process.md's "add deps to image when used by >1 project" rule: FC alone is one Python project, so the deps live inrequirements.txtand install per-job. Reconsider when a second Fabled-family Python backend lands. - Integration uses Fabled-Git Actions
services:+ socket-discovered bridge IPs becauseact_runner(swarm-runner v0.6+) puts services on the default bridge with no embedded DNS. The pattern is documented in the rulebook'sfabled-git.md"CI philosophy" section and FC'sci.ymlis the canonical example. - No
package-lock.jsonis tracked yet (FC'sfeedback_no_local_runsmemory bansnpm installlocally). Usingnpm installrather thannpm ciuntil a lockfile lands. - No
imagemagick/pandocper-job installs needed. extension/'s vitest specs loadlib/*.jsby evaluating the real file as a classic script (test/helpers/loadLib.js) rather than addingmodule.exportsshims to production code — the libs ship asbackground.scripts, not ES modules, so the specs exercise exactly the bytes packaged into the XPI.extension/scripts/packaging.shis the single definition of what ships inside the XPI. Three consumers read from it rather than keeping their own copy: web-ext's--ignore-files(extension/package.json), the:(exclude)pathspec inci.yml'sextension-versionguard, and thegit logpathspec that derives the extension version. Three hand-kept copies of that one fact is what allowed issue #2397.- Jobs that derive the extension version check out with
fetch-depth: 0. The version 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). 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 whereverpackaging.sh versionis called. - Callers MUST
set -fbefore substituting the script's output. Without it the shell expandstest/**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.jsasserts every--ignore-filesconsumer sets it, and that no consumer has quietly reinstated a hardcoded list.