# CI

`ci.d/10-gate` is the only CI this repo has. It runs on every push to the
amadan remote, and it is the same script you can run locally — one
definition rather than two that drift. `ci` execs it, so a runner with no
step support runs exactly the same thing rather than a thinner Go-only
version of it, which is what the four scaffold steps here used to be.

It needs a Rust toolchain with the `wasm32-unknown-unknown` target and the
`rustfmt` and `clippy` components (`rust-toolchain.toml` pins 1.98.0 and
asks for all three), Go, and — since slice 2 — **Node 20 or newer with
`npm`**, because the editor's bundle and the fixture vectors are both
built by JS this repo owns. `CGO_ENABLED=0` is exported for every step.
The gate sources `~/.cargo/env` itself and checks every tool up front, so
a missing dependency fails on the first line with its own name rather
than fifty lines in as `cargo: command not found`.

`make migration-check` runs the rastrillo CLI through `go run` off
`go.mod` rather than needing a binary on `PATH` — a bare `rastrillo
migration check` is what failed a sibling repo's very first CI run, with
nothing in the scaffold saying what to install.

**Known trade-off: `npm ci` needs the npm registry reachable at run
time.** The Rust and Go steps are offline once their caches are warm; the
Node step is not. It is that way for a reason — the runner clones a fresh
worktree per branch, so `fixtures/node_modules` never carries over, and
`npm ci` against the committed `fixtures/package-lock.json` is what makes
the installed `yjs` exactly the pinned 13.6.27 rather than whatever a
bare `npm install` resolves this week. Docs made the same call and
documents the same trade-off. If the registry is unreachable from the
runner, that is the failure this line produces; it is not a bug in the
gate. The cache directory is the one part of it that differs by machine,
so the step honours `NPM_CACHE` and falls back to `$HOME/.npm`: the
runner's HOME is writable and needs nothing, a sandboxed local run may
need `NPM_CACHE` pointed somewhere it can write, or npm fails on its
cache rather than on anything to do with the install.

## The JS half

Four steps, in this order, between the Rust tests and `make ci`.

- **`(cd fixtures && npm ci --cache "${NPM_CACHE:-$HOME/.npm}")`** — the
  trade-off above, and the reason for it.
- **`(cd fixtures && node --test *.test.mjs)`** — the JS side's own
  tests: the op vocabulary, the ProseMirror schema, the save queue, the
  live-update cursor, the resync controller, the inspector's ops, the
  canvas maths, the switcher, uploads. Most of what the editor gets wrong
  it gets wrong in a browser with nothing in a log, which is the argument
  for running them here rather than by hand.

  Run from `fixtures/` with a glob, never as `node --test fixtures/`.
  Node 24 takes a directory argument as if it were a test file, runs
  nothing, and exits 0 — a green-looking way to run no tests at all.
- **`(cd fixtures && node generate.mjs)` then a clean-tree check on
  `fixtures/vectors`** — the vectors are the contract between the JS ops
  and the Rust core (spec §2.8). `internal/slides/core/vectors_test.go`,
  under `make ci` below, loads each committed vector through the core and
  requires the same view the JS side produced. A vector nobody
  regenerated makes that test compare the core against a JS
  implementation that no longer exists, so **it still passes** while the
  two sides have already drifted. This step is what turns that into a
  failure: `vectors are stale: run node fixtures/generate.mjs and commit`.
- **`node fixtures/build-bundles.mjs` then a clean-tree check on
  `internal/slides/static`** — the editor bundle is committed, hashed,
  and built by nothing at run time, so a stale one is served perfectly
  happily by a server that cannot tell. Same check as the wasm `cmp`
  below, and safe for the same reason: the build is byte-reproducible (no
  sourcemap, no banner, no timestamp, no absolute paths, and esbuild's
  work happens in the pinned 0.28.2 native binary, so the Node running it
  does not enter into the result).

The last two check `git diff` **and** `git status --porcelain`, because
each catches half of it. `git diff` sees a tracked file that changed or
was deleted; it never sees the new file a changed content hash writes
beside the old one. A stale hashed bundle left behind is not a cosmetic
problem — `assets_embed.go` requires exactly one `editor.<hash>.js` and
panics at boot when it finds two.

Both restore the tracked files on failure (`git checkout`) and print
what they found: they mutated a committed directory as a side effect of
reading it, and a modified tracked file left behind would let a second
run of the same script pass with nobody having committed anything. The
untracked half is deliberately left alone — a new vector or a bundle
under a new hash is exactly the file you have to commit next, and an
earlier version of this gate `git clean`ed it away before you could.

## Two things that fail silently

- **The gate fails if `internal/slides/core/testdata/slides_core.wasm`
  differs from a fresh build.** `go:embed` will otherwise embed a stale
  module and every Go test will pass against it. When it fails, rebuild
  and commit the artifact:

  ```sh
  ./scripts/build-wasm.sh
  cp core/target/wasm32-unknown-unknown/release/slides_core.wasm \
     internal/slides/core/testdata/slides_core.wasm
  ```

  That comparison only works because `scripts/build-wasm.sh` remaps the
  registry and workspace path prefixes rustc would otherwise bake into the
  binary. Without it the artifact reproduces byte-for-byte only on the
  machine, and under the `$HOME`, that built it, and the `cmp` reports a
  stale artifact everywhere else.

- **`cargo fmt --check` covers `core/`.** It was not in the gate before
  2026-09-07, and by then three files had drifted; they were reformatted in
  the commit that added the check.

## The runner

Jobs are claimed by an `amadan runner` on the machine that has one. Two
things about it are wrong by default, and both fail in ways that read like
something else:

- **The runner must be told to serve this repo.** `amadan runner -repo …`
  is an allowlist, and a repo nobody listed gets `amadan ci status` exit 3
  forever — "no status recorded", which reads like the gate has not run
  yet rather than like nothing will ever run it.
  `~/.config/systemd/user/amadan-runner.service` was edited to list
  `-repo oficina/slides` on 2026-09-07.
- **The runner's `PATH` is the systemd unit's, not your shell's**, and
  `~/.cargo/bin` is not on it. The gate sources `~/.cargo/env` rather than
  depending on that being fixed.

A restart abandons any job already running, so check the runner is idle
before restarting it.

## The Go half

`make ci` is the last step, so the Go half of the gate and the `make ci`
you run between edits are the same definition: `go vet ./...`, a gofmt
check, `go test ./...`, and `rastrillo migration check`, which fails the
moment `models.go` and `migrations/` disagree instead of at boot on
whatever machine notices next.

The browser drive (`go test -tags browser ./...`) is deliberately outside
all of this. It needs a Chromium, and this project's sandbox will not start
one, so a gate that ran it would be reporting "could not see" as green.
