| 1 | # CI |
| 2 | |
| 3 | `ci.d/10-gate` is the only CI this repo has. It runs on every push to the |
| 4 | amadan remote, and it is the same script you can run locally — one |
| 5 | definition rather than two that drift. `ci` execs it, so a runner with no |
| 6 | step support runs exactly the same thing rather than a thinner Go-only |
| 7 | version of it, which is what the four scaffold steps here used to be. |
| 8 | |
| 9 | It needs a Rust toolchain with the `wasm32-unknown-unknown` target and the |
| 10 | `rustfmt` and `clippy` components (`rust-toolchain.toml` pins 1.98.0 and |
| 11 | asks for all three), Go, and — since slice 2 — **Node 20 or newer with |
| 12 | `npm`**, because the editor's bundle and the fixture vectors are both |
| 13 | built by JS this repo owns. `CGO_ENABLED=0` is exported for every step. |
| 14 | The gate sources `~/.cargo/env` itself and checks every tool up front, so |
| 15 | a missing dependency fails on the first line with its own name rather |
| 16 | than fifty lines in as `cargo: command not found`. |
| 17 | |
| 18 | `make migration-check` runs the rastrillo CLI through `go run` off |
| 19 | `go.mod` rather than needing a binary on `PATH` — a bare `rastrillo |
| 20 | migration check` is what failed a sibling repo's very first CI run, with |
| 21 | nothing in the scaffold saying what to install. |
| 22 | |
| 23 | **Known trade-off: `npm ci` needs the npm registry reachable at run |
| 24 | time.** The Rust and Go steps are offline once their caches are warm; the |
| 25 | Node step is not. It is that way for a reason — the runner clones a fresh |
| 26 | worktree per branch, so `fixtures/node_modules` never carries over, and |
| 27 | `npm ci` against the committed `fixtures/package-lock.json` is what makes |
| 28 | the installed `yjs` exactly the pinned 13.6.27 rather than whatever a |
| 29 | bare `npm install` resolves this week. Docs made the same call and |
| 30 | documents the same trade-off. If the registry is unreachable from the |
| 31 | runner, that is the failure this line produces; it is not a bug in the |
| 32 | gate. The cache directory is the one part of it that differs by machine, |
| 33 | so the step honours `NPM_CACHE` and falls back to `$HOME/.npm`: the |
| 34 | runner's HOME is writable and needs nothing, a sandboxed local run may |
| 35 | need `NPM_CACHE` pointed somewhere it can write, or npm fails on its |
| 36 | cache rather than on anything to do with the install. |
| 37 | |
| 38 | ## The JS half |
| 39 | |
| 40 | Four steps, in this order, between the Rust tests and `make ci`. |
| 41 | |
| 42 | - **`(cd fixtures && npm ci --cache "${NPM_CACHE:-$HOME/.npm}")`** — the |
| 43 | trade-off above, and the reason for it. |
| 44 | - **`(cd fixtures && node --test *.test.mjs)`** — the JS side's own |
| 45 | tests: the op vocabulary, the ProseMirror schema, the save queue, the |
| 46 | live-update cursor, the resync controller, the inspector's ops, the |
| 47 | canvas maths, the switcher, uploads. Most of what the editor gets wrong |
| 48 | it gets wrong in a browser with nothing in a log, which is the argument |
| 49 | for running them here rather than by hand. |
| 50 | |
| 51 | Run from `fixtures/` with a glob, never as `node --test fixtures/`. |
| 52 | Node 24 takes a directory argument as if it were a test file, runs |
| 53 | nothing, and exits 0 — a green-looking way to run no tests at all. |
| 54 | - **`(cd fixtures && node generate.mjs)` then a clean-tree check on |
| 55 | `fixtures/vectors`** — the vectors are the contract between the JS ops |
| 56 | and the Rust core (spec §2.8). `internal/slides/core/vectors_test.go`, |
| 57 | under `make ci` below, loads each committed vector through the core and |
| 58 | requires the same view the JS side produced. A vector nobody |
| 59 | regenerated makes that test compare the core against a JS |
| 60 | implementation that no longer exists, so **it still passes** while the |
| 61 | two sides have already drifted. This step is what turns that into a |
| 62 | failure: `vectors are stale: run node fixtures/generate.mjs and commit`. |
| 63 | - **`node fixtures/build-bundles.mjs` then a clean-tree check on |
| 64 | `internal/slides/static`** — the editor bundle is committed, hashed, |
| 65 | and built by nothing at run time, so a stale one is served perfectly |
| 66 | happily by a server that cannot tell. Same check as the wasm `cmp` |
| 67 | below, and safe for the same reason: the build is byte-reproducible (no |
| 68 | sourcemap, no banner, no timestamp, no absolute paths, and esbuild's |
| 69 | work happens in the pinned 0.28.2 native binary, so the Node running it |
| 70 | does not enter into the result). |
| 71 | |
| 72 | The last two check `git diff` **and** `git status --porcelain`, because |
| 73 | each catches half of it. `git diff` sees a tracked file that changed or |
| 74 | was deleted; it never sees the new file a changed content hash writes |
| 75 | beside the old one. A stale hashed bundle left behind is not a cosmetic |
| 76 | problem — `assets_embed.go` requires exactly one `editor.<hash>.js` and |
| 77 | panics at boot when it finds two. |
| 78 | |
| 79 | Both restore the tracked files on failure (`git checkout`) and print |
| 80 | what they found: they mutated a committed directory as a side effect of |
| 81 | reading it, and a modified tracked file left behind would let a second |
| 82 | run of the same script pass with nobody having committed anything. The |
| 83 | untracked half is deliberately left alone — a new vector or a bundle |
| 84 | under a new hash is exactly the file you have to commit next, and an |
| 85 | earlier version of this gate `git clean`ed it away before you could. |
| 86 | |
| 87 | ## Two things that fail silently |
| 88 | |
| 89 | - **The gate fails if `internal/slides/core/testdata/slides_core.wasm` |
| 90 | differs from a fresh build.** `go:embed` will otherwise embed a stale |
| 91 | module and every Go test will pass against it. When it fails, rebuild |
| 92 | and commit the artifact: |
| 93 | |
| 94 | ```sh |
| 95 | ./scripts/build-wasm.sh |
| 96 | cp core/target/wasm32-unknown-unknown/release/slides_core.wasm \ |
| 97 | internal/slides/core/testdata/slides_core.wasm |
| 98 | ``` |
| 99 | |
| 100 | That comparison only works because `scripts/build-wasm.sh` remaps the |
| 101 | registry and workspace path prefixes rustc would otherwise bake into the |
| 102 | binary. Without it the artifact reproduces byte-for-byte only on the |
| 103 | machine, and under the `$HOME`, that built it, and the `cmp` reports a |
| 104 | stale artifact everywhere else. |
| 105 | |
| 106 | - **`cargo fmt --check` covers `core/`.** It was not in the gate before |
| 107 | 2026-09-07, and by then three files had drifted; they were reformatted in |
| 108 | the commit that added the check. |
| 109 | |
| 110 | ## The runner |
| 111 | |
| 112 | Jobs are claimed by an `amadan runner` on the machine that has one. Two |
| 113 | things about it are wrong by default, and both fail in ways that read like |
| 114 | something else: |
| 115 | |
| 116 | - **The runner must be told to serve this repo.** `amadan runner -repo …` |
| 117 | is an allowlist, and a repo nobody listed gets `amadan ci status` exit 3 |
| 118 | forever — "no status recorded", which reads like the gate has not run |
| 119 | yet rather than like nothing will ever run it. |
| 120 | `~/.config/systemd/user/amadan-runner.service` was edited to list |
| 121 | `-repo oficina/slides` on 2026-09-07. |
| 122 | - **The runner's `PATH` is the systemd unit's, not your shell's**, and |
| 123 | `~/.cargo/bin` is not on it. The gate sources `~/.cargo/env` rather than |
| 124 | depending on that being fixed. |
| 125 | |
| 126 | A restart abandons any job already running, so check the runner is idle |
| 127 | before restarting it. |
| 128 | |
| 129 | ## The Go half |
| 130 | |
| 131 | `make ci` is the last step, so the Go half of the gate and the `make ci` |
| 132 | you run between edits are the same definition: `go vet ./...`, a gofmt |
| 133 | check, `go test ./...`, and `rastrillo migration check`, which fails the |
| 134 | moment `models.go` and `migrations/` disagree instead of at boot on |
| 135 | whatever machine notices next. |
| 136 | |
| 137 | The browser drive (`go test -tags browser ./...`) is deliberately outside |
| 138 | all of this. It needs a Chromium, and this project's sandbox will not start |
| 139 | one, so a gate that ran it would be reporting "could not see" as green. |
| 140 | |