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 5s
Build images / build-web (push) Successful in 6s
CI / frontend-build (push) Successful in 23s
extension / lint (push) Successful in 28s
CI / backend-lint-and-test (push) Successful in 46s
CI / integration (push) Successful in 3m56s
buildx on this runner pushes the first tag to the registry and then re-pushes the remaining ones through the DOCKER driver, reading them out of a local image store that a registry-direct build never populated: #27 pushing …/fabledcurator:latest DONE 15.8s #28 pushing …/fabledcurator:c-0e15c44 with docker #28 ERROR: tag does not exist: …:c-0e15c44 It is intermittent — build-ml made the identical two-tag push seconds later in the same run and succeeded — and the consequence is worse than the red job suggests. `:latest` had already published, so production was correct while the immutable rollback tag rule 145 requires of every main push simply did not exist. Nothing else would ever have noticed: a missing :c-<sha> has no consumer that fails, so it surfaces at the moment somebody needs to roll back, which is the worst time to learn a rollback target was never written. So the build now pushes exactly one ref — the channel's — and the existing repoint step, which already excluded the source tag and already ran on every reuse, now runs on the build path too and owns every other tag. `imagetools create` is a registry-side manifest copy: no local daemon, nothing that can be absent. This adds no new code path; it puts the build case onto the one that was already proven. Chosen over the alternative of asserting each tag resolves after the build, which would have made the failure loud without making it rarer. The cost, accepted: `imagetools create` wraps its source in an index, so :c-<sha> is an index rather than a plain image and fc.revision does not resolve through it. Nothing reads that label off :c-<sha> — the reuse check only ever inspects the CHANNEL tag — and the index names the same manifest, so a pull is byte-identical. The reuse path already produced :c-<sha> this way; this only makes it uniform. `build_tags` goes with it — the tag list now has exactly one consumer.
147 lines
9.3 KiB
Markdown
147 lines
9.3 KiB
Markdown
# 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/`, `agent/`, `scripts/`)
|
|
- node (frontend job: `npm install` + vitest + vite build)
|
|
- 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`,
|
|
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.** 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
|
|
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 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).
|
|
- **The build pushes exactly ONE tag — the channel's — and every other tag is
|
|
written registry-side afterwards** (#3190). buildx on this runner pushes the
|
|
first tag to the registry and then re-pushes the rest through the docker
|
|
driver, out of a local image store that a registry-direct build never fills;
|
|
it fails intermittently with `tag does not exist`. On `dev` that only reddens
|
|
a job, but on `main` it silently skips `:c-<sha>` while `:latest` publishes
|
|
fine — a missing rollback tag has no consumer that fails, so nothing but the
|
|
red job would notice until somebody needs to roll back. `imagetools create`
|
|
has no local store to be absent from, and it is the code the reuse path
|
|
already ran, so both paths now share one proven route. The cost: `:c-<sha>`
|
|
is an index rather than a plain image, so `fc.revision` does not resolve
|
|
through it — nothing reads it there, and the index names the same manifest.
|
|
- **`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
|
|
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.
|