oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1#!/usr/bin/env bash
2# The only CI this repo has. Everything a branch must pass before it can
3# squash-merge, in one script, so what the runner executes and what you
4# run before pushing are one definition rather than two that drift.
5set -euo pipefail
6
7# Run from the repo root whatever the caller's working directory is:
8# .amadan/ci execs this file, an amadan runner with step support runs it
9# directly, and you will run it by hand from wherever you happen to be.
10cd "$(dirname "$0")/../.."
11
12# rustup installs to ~/.cargo/bin, which is on neither a systemd user
13# unit's PATH nor a login shell's when rustup ran with --no-modify-path.
14# Source it here rather than trusting whoever set the runner up to have
15# got it right — a gate that only works on one machine is not a gate.
16[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
17
18for tool in cargo rustc rustfmt go node npm; do
19 command -v "$tool" >/dev/null || { echo "gate: $tool not on PATH" >&2; exit 1; }
20done
21rustup target list --installed 2>/dev/null | grep -qx wasm32-unknown-unknown \
22 || { echo "gate: wasm32-unknown-unknown target missing — rustup target add wasm32-unknown-unknown" >&2; exit 1; }
23
24# The stack is cgo-free by design, here as in scripts/run-dev.sh and the
25# Makefile. A build that quietly used cgo would not be the binary that
26# ships to an arm64 instance.
27export CGO_ENABLED=0
28export GOPRIVATE="${GOPRIVATE:+$GOPRIVATE,}amadan.net/oficina/estilo.git"
29
30# Two of the steps below regenerate a committed artifact and then require
31# the tree to be unchanged. Both have the same shape, so they share one
32# definition:
33#
34# git diff alone is not enough. It sees a tracked file that changed or
35# was deleted, and never the NEW file a changed content hash writes
36# beside the old one — so a rebuild whose bytes moved leaves both a
37# deletion and an untracked file, and checking only one of them lets
38# half of that through.
39#
40# On failure the TRACKED files are put back and the untracked ones
41# are left where they are, listed. The check mutated a committed
42# directory as a side effect of reading it, and leaving a modified
43# tracked file behind would make a second run of this same script
44# pass with nobody having committed anything — the worst possible
45# outcome for a freshness check. But an untracked file here is
46# exactly the one the developer has to commit next (a vector for a
47# new data file, the bundle under its new hash), and deleting it —
48# as this once did — threw away a freshly generated artifact before
49# anybody could `git add` it. So it stays, and the message names it.
50require_committed() {
51 local path=$1 fix=$2 ok=1
52 # --name-status, not a content diff: both directories hold minified
53 # or binary artifacts, and the useful answer is which file moved, not
54 # 371 KB of bundle in the runner's log.
55 git diff --exit-code --name-status -- "$path" || ok=0
56 if [ -n "$(git status --porcelain -- "$path")" ]; then
57 git status --porcelain -- "$path" >&2
58 ok=0
59 fi
60 if [ "$ok" -eq 0 ]; then
61 echo "gate: $fix" >&2
62 echo "gate: stale or uncommitted under $path (M = restored to the committed version, ?? = left for you to commit):" >&2
63 git status --porcelain -- "$path" >&2
64 git checkout -- "$path"
65 exit 1
66 fi
67}
68
69echo "=== rust: fmt"
70# --manifest-path, because there is no workspace at the repo root: the
71# crate lives in core/ and a bare `cargo fmt` here finds no Cargo.toml.
72cargo fmt --manifest-path core/Cargo.toml --check
73
74echo "=== rust: clippy"
75# clippy.toml's disallowed-methods is the real enforcement of the
76# Doc-construction rule that core/src/lib.rs documents: Doc::new() and
77# Doc::with_client_id() both draw entropy this build has no safe source
78# for, and a doc comment alone has been got around before.
79#
80# scripts/build-wasm.sh below runs this same invocation, flag for flag,
81# before it builds. That is deliberate: it fails here in a few seconds
82# rather than forty seconds into the wasm build, and because the two
83# spellings are identical there is no looser check to sail through —
84# the drift Docs' gate hit when its two clippy runs disagreed.
85cargo clippy --manifest-path core/Cargo.toml --all-targets -- -D warnings -D clippy::disallowed_methods
86
87echo "=== wasm: the artifact the host embeds"
88# Before cargo test, not after. core/tests/no_host_imports.rs asserts the
89# module imports nothing from the host by reading the wasm32 artifact off
90# disk, and that file only exists once this line has run. Run the tests
91# first and the assertion's own presence becomes a race it reports as a
92# skip.
93./scripts/build-wasm.sh
94
95echo "=== the embedded wasm must match a fresh build"
96if ! cmp -s core/target/wasm32-unknown-unknown/release/slides_core.wasm internal/slides/core/testdata/slides_core.wasm; then
97 echo "gate: internal/slides/core/testdata/slides_core.wasm does not match a fresh build — rebuild and commit the wasm:" >&2
98 echo " ./scripts/build-wasm.sh && cp core/target/wasm32-unknown-unknown/release/slides_core.wasm internal/slides/core/testdata/slides_core.wasm" >&2
99 exit 1
100fi
101
102echo "=== rust: test"
103# SLIDES_REQUIRE_WASM=1 turns no_host_imports.rs's "artifact missing"
104# branch from a skip into a failure. Safe to set unconditionally here
105# because the build above just produced it; left unset, a bare local
106# `cargo test` on a fresh clone still skips rather than forcing a wasm
107# build first.
108SLIDES_REQUIRE_WASM=1 cargo test --manifest-path core/Cargo.toml
109
110# The PDF module (core/pdf, hosted by internal/slides/pdfcore) is a
111# second crate with the same shape and the same four steps, in the same
112# order and for the same reasons as the CRDT core's above: fmt, clippy
113# (the exact invocation scripts/build-pdf-wasm.sh runs, so nothing
114# looser sails through), build the artifact BEFORE its own
115# no_host_imports test reads it, and the byte comparison against the
116# committed copy go:embed actually ships. A stale slides_pdf.wasm is
117# the same trap as a stale slides_core.wasm: the Go tests pass against
118# whatever is committed, and only this cmp knows the difference.
119echo "=== rust: fmt (pdf)"
120cargo fmt --manifest-path core/pdf/Cargo.toml --check
121
122echo "=== rust: clippy (pdf)"
123cargo clippy --manifest-path core/pdf/Cargo.toml --all-targets -- -D warnings
124
125echo "=== wasm: the pdf artifact the host embeds"
126./scripts/build-pdf-wasm.sh
127
128echo "=== the embedded pdf wasm must match a fresh build"
129if ! cmp -s core/pdf/target/wasm32-unknown-unknown/release/slides_pdf.wasm internal/slides/pdfcore/testdata/slides_pdf.wasm; then
130 echo "gate: internal/slides/pdfcore/testdata/slides_pdf.wasm does not match a fresh build — rebuild and commit the wasm:" >&2
131 echo " ./scripts/build-pdf-wasm.sh && cp core/pdf/target/wasm32-unknown-unknown/release/slides_pdf.wasm internal/slides/pdfcore/testdata/slides_pdf.wasm" >&2
132 exit 1
133fi
134
135echo "=== rust: test (pdf)"
136SLIDES_REQUIRE_WASM=1 cargo test --manifest-path core/pdf/Cargo.toml
137
138echo "=== fixtures: node dependencies"
139# The versions are printed rather than pinned. What Node is only matters
140# as a floor — 20+, for `node --test` and the ESM loading this tree
141# assumes — because yjs and the rest come from the lockfile and esbuild's
142# actual work happens inside a pinned native binary.
143node --version
144npm --version
145
146# The runner clones a fresh worktree per branch, so fixtures/node_modules
147# (gitignored, like any node_modules) never carries over from a previous
148# run. npm ci reads fixtures/package-lock.json rather than resolving from
149# package.json, so the installed yjs is exactly the pinned 13.6.27 and not
150# whatever a bare `npm install` would pick today.
151#
152# NPM_CACHE exists because the cache directory is the one part of this
153# that is not the same everywhere. The runner's HOME is writable and
154# $HOME/.npm is right there; a sandboxed local run may have neither, and
155# npm then fails with an EROFS on its cache rather than with anything to
156# do with the install. Point NPM_CACHE somewhere writable in that case.
157echo "=== fixtures: npm ci (needs the registry — see .amadan/README.md)"
158(cd fixtures && npm ci --cache "${NPM_CACHE:-$HOME/.npm}")
159
160echo "=== fixtures: the JS side's own tests"
161# From fixtures/ with an explicit glob, never `node --test fixtures/`:
162# Node 24 takes the directory argument as a test file, runs nothing and
163# exits 0, which is a green-looking way to run no tests at all.
164(cd fixtures && node --test *.test.mjs)
165
166echo "=== the vectors must match a fresh generation"
167# fixtures/vectors is the contract between the two implementations of the
168# op vocabulary (spec §2.8): generate.mjs builds each deck through
169# ops.mjs and writes the Y update plus the JS side's view of it, and
170# internal/slides/core/vectors_test.go — run by `make ci` below — loads
171# the same bytes through the Rust core and requires the same view.
172#
173# A vector nobody regenerated is the failure this catches, and it is a
174# silent one: the Go test still passes, because it is comparing the core
175# against a JS implementation that no longer exists. Both sides change or
176# neither does.
177(cd fixtures && node generate.mjs)
178require_committed fixtures/vectors \
179 "vectors are stale: run node fixtures/generate.mjs and commit"
180
181echo "=== the editor bundle must match a fresh build"
182# build-bundles.mjs is byte-reproducible: no sourcemap, no banner, no
183# timestamp, no absolute path in the output, and esbuild's work happens
184# in the pinned 0.28.2 native binary rather than in interpreted JS, so
185# the Node running it does not enter into the result either.
186#
187# This is the same check as the wasm cmp above and it is here for the
188# same reason: nothing builds the bundle at run time, the artifact is
189# committed, and a server handed a stale one serves it perfectly happily.
190# assets_embed.go's exactly-one rule makes a leftover hashed file a panic
191# at boot rather than a wrong editor, which is why the untracked half of
192# require_committed matters here too.
193node fixtures/build-bundles.mjs
194require_committed internal/slides/static \
195 "the editor bundle is stale: run node fixtures/build-bundles.mjs and commit internal/slides/static"
196
197echo "=== go: vet, gofmt, test, migration check"
198# The Makefile's own ci target, so the Go half of this gate and the
199# `make ci` you run between edits cannot come apart.
200make ci
201
202echo "gate: ok"
203