The audit's other "do it differently" (F76). v1's backend was an image built by CI; its frontend was `rsync -azX --delete` from a laptop, run by hand, with the API's absolute URL compiled into the WASM by an environment variable set in the same script. Two artefacts, one pipeline, and nothing keeping them in step. Here there is one image with the server binary at `/usr/local/bin` and the built frontend at `/srv/dist`, and the frontend asks for `/api/...` relative to wherever it is served -- so the artefact is the same in every environment and the two halves cannot be deployed apart. Caddy still serves the static files from a directory, because that is what the reverse proxy in front of everything already does; the updater lifts them out of the image rather than out of a build on somebody's machine. It stages them and moves the directory into place, since `index.html` names hashed files and a browser that fetches new HTML with old JavaScript gets a blank page. The image is 97 MB and holds no build tooling. Migrations are compiled into the binary and run at startup, and the database is created if it is missing, so there is no `sqlx-cli`, no entrypoint script, and nothing that can decide to carry on after a failed migration -- v1's start.sh ran migrations with `|| echo "Migration failed but continuing..."`. TLS is rustls with its roots compiled in, so there is no OpenSSL to keep patched. There is no dummy-source dance for dependency caching either; a BuildKit cache mount does what that trick was inventing, and the binary is copied out of the mount because a cache mount is not part of the layer. Deployment is a timer on the server rather than CI reaching into it. Nothing in the workflow holds a credential for the machine it deploys to, and a bad build cannot take the site down on its own; the cost is a few minutes between push and deploy, and `deploy/runway-update` for when that is too long. The rest of this is the CI that was promised at M1 and never written. "Rules that only live in a doc get forgotten, so these are lints and CI checks." The lints landed. The CI half did not exist, which meant for twenty-seven milestones the forgetting was still perfectly possible -- it was just mine rather than the repository's. `cargo fmt`, `clippy -D warnings` and the whole test suite now run on every push, alongside the three guardrails that were only ever configuration: `cargo machete` passes. `deny.toml`, written at M1, did not: three permissive licences its allow-list had not anticipated, and two `unmaintained` advisories arriving through Leptos's macros. Both are now allowed by name with a date and a reason rather than by widening a category. MPL-2.0 came out of the list, because nothing uses it and an allow-list should say what is actually there. That file had been quietly wrong since the day it was written, which is the argument for CI in one line. The bundle budget is 1.8 MB against today's 1.31 MB and v1's 2.5 MB, printed on every run. A bundle grows one convenient dependency at a time. The print rule is deliberately not in CI: it already exists as a test, it covers the CalDAV client too, and a shell grep cannot tell a call from a comment about a call -- the first draft of that step failed on the paragraph in `observability.rs` that explains the rule. Two things the image found that reading could not. `/db` is created in the image now. SQLite creates the database file if it is missing but not the directory holding it, so the container started only when something happened to be mounted there and otherwise died with "unable to open database file", which says nothing about what is wrong. And the graceful shutdown only listened for Ctrl-C. A container runtime stops a service with SIGTERM, waits ten seconds, and sends SIGKILL -- so `podman stop` took ten seconds and killed the process outright, and the handler written to stop a restart dropping a CalDAV write half-way through worked everywhere except deployment, which is the one place restarts happen. It listens for both now, and the container stops in two.
122 lines
4.3 KiB
YAML
122 lines
4.3 KiB
YAML
# The checks the audit said would be machine-enforced.
|
|
#
|
|
# They were written down at M1 as "lints and CI checks" on the grounds that
|
|
# rules living only in a document get forgotten. The lints landed; the CI half
|
|
# did not exist until now, which means for twenty-seven milestones the
|
|
# forgetting was still possible -- it was just my forgetting rather than the
|
|
# repository's.
|
|
|
|
name: Check
|
|
|
|
on:
|
|
push:
|
|
pull_request:
|
|
|
|
jobs:
|
|
check:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
# The toolchain, its components and the wasm target all come from
|
|
# rust-toolchain.toml, so there is one place that says which Rust this is.
|
|
- name: Install the pinned toolchain
|
|
run: |
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
| sh -s -- -y --default-toolchain none
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
- uses: actions/cache@v4
|
|
with:
|
|
path: |
|
|
~/.cargo/registry
|
|
~/.cargo/git
|
|
target
|
|
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
|
restore-keys: cargo-${{ runner.os }}-
|
|
|
|
- name: Formatting
|
|
run: cargo fmt --all --check
|
|
|
|
- name: Clippy
|
|
run: cargo clippy --workspace --all-targets -- -D warnings
|
|
|
|
# The CalDAV integration tests skip themselves when there is no server to
|
|
# talk to, which is what happens here. They are exercised against a real
|
|
# Baikal by `crates/runway-caldav/tests/baikal/run.sh`.
|
|
- name: Tests
|
|
run: cargo test --workspace
|
|
|
|
- name: The frontend still builds for the browser
|
|
run: cargo check -p runway-web --target wasm32-unknown-unknown
|
|
|
|
guardrails:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install the pinned toolchain
|
|
run: |
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
| sh -s -- -y --default-toolchain none
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
# v1 shipped reqwest, ical, regex and indexed_db_futures into a WASM
|
|
# bundle that used none of them.
|
|
- name: No unused dependencies
|
|
run: |
|
|
cargo install cargo-machete --locked
|
|
cargo machete
|
|
|
|
- name: Licences and advisories
|
|
run: |
|
|
cargo install cargo-deny --locked
|
|
cargo deny check
|
|
|
|
# v1 had 157 of these in its backend, one of which logged the length of a
|
|
# password. Tests may print; the server may not.
|
|
# The print rule is not here on purpose. It already exists as a test --
|
|
# `no_println_or_dbg_in_the_server_or_the_caldav_client` -- so `cargo
|
|
# test` above enforces it, and it covers the CalDAV client too. Doing it
|
|
# again here would mean two rules to keep in step, and the CI copy would
|
|
# be the worse of the two: grep from a shell cannot tell a call from a
|
|
# comment about a call, and this file's first draft failed on the
|
|
# paragraph in `observability.rs` explaining the rule.
|
|
|
|
bundle:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Install the pinned toolchain
|
|
run: |
|
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
|
|
| sh -s -- -y --default-toolchain none
|
|
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
|
|
- name: Install Trunk
|
|
run: |
|
|
curl -fsSL https://github.com/trunk-rs/trunk/releases/download/v0.21.14/trunk-x86_64-unknown-linux-gnu.tar.gz \
|
|
| tar -xzC /usr/local/bin trunk
|
|
|
|
- run: npm ci
|
|
|
|
- name: Build the frontend
|
|
working-directory: crates/runway-web
|
|
run: trunk build --release
|
|
|
|
# Printed on every run and enforced, because a bundle grows one
|
|
# convenient dependency at a time and nobody notices until it is 2.5 MB.
|
|
# That was v1's. This budget is comfortably above today's and well below
|
|
# that, so it is a ratchet rather than a rubber stamp.
|
|
- name: WASM bundle size
|
|
run: |
|
|
wasm=$(find crates/runway-web/dist -name '*.wasm' -print -quit)
|
|
size=$(stat -c %s "$wasm")
|
|
budget=1800000
|
|
echo "$wasm is $size bytes (budget $budget, v1 shipped 2500000)"
|
|
if [ "$size" -gt "$budget" ]; then
|
|
echo "over budget by $((size - budget)) bytes" >&2
|
|
exit 1
|
|
fi
|