collin/anvil · 1404924c
Pin the Rust version in one place: rust-toolchain.toml
Collin Richards · 2026-08-25 04:03 UTC · 1404924cbb6e5c679ff4e2921c2f340e72a179d6 · parent f89a7ca7 · browse files
modified.githooks/pre-commit+16 −2
| 1 | 1 | #!/bin/sh | |
| 2 | - | # Pre-commit: rustfmt (nightly, for the unstable options in rustfmt.toml), | |
| 3 | - | # cargo-sort-derives, and clippy. | |
| 2 | + | # Pre-commit: the toolchain-pin check, rustfmt (nightly, for the unstable | |
| 3 | + | # options in rustfmt.toml), cargo-sort-derives, and clippy. | |
| 4 | 4 | # Wired up via `git config core.hooksPath .githooks`. | |
| 5 | 5 | set -e | |
| 6 | 6 | ||
| 7 | + | # rust-toolchain.toml is the one place the Rust version is set, but Cargo's | |
| 8 | + | # `rust-version` floor is a separate key it cannot derive. Rather than let the | |
| 9 | + | # two drift silently, check that the pin satisfies the floor. | |
| 10 | + | PIN=$(sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' rust-toolchain.toml) | |
| 11 | + | MSRV=$(sed -n 's/^[[:space:]]*rust-version[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' Cargo.toml) | |
| 12 | + | case "$PIN" in | |
| 13 | + | "$MSRV" | "$MSRV".*) ;; | |
| 14 | + | *) | |
| 15 | + | echo "pre-commit: rust-toolchain.toml pins $PIN but Cargo.toml rust-version is $MSRV." >&2 | |
| 16 | + | echo " Update the floor to match the pin." >&2 | |
| 17 | + | exit 1 | |
| 18 | + | ;; | |
| 19 | + | esac | |
| 20 | + | ||
| 7 | 21 | if ! cargo +nightly fmt --all -- --check >/dev/null 2>&1; then | |
| 8 | 22 | cargo +nightly fmt --all | |
| 9 | 23 | echo "pre-commit: rustfmt reformatted files — review, re-stage, and commit again." >&2 | |
| ⋯ 12 unchanged lines | |||
modifiedCLAUDE.md+11 −3
| ⋯ 11 unchanged lines | |||
| 12 | 12 | side). Tests may use the git CLI, but only as a fixture/interop check, never | |
| 13 | 13 | on the code path under test. | |
| 14 | 14 | ||
| 15 | - | Dev setup: `git config core.hooksPath .githooks` — the pre-commit hook runs | |
| 16 | - | `cargo +nightly fmt` (nightly, for the unstable options in rustfmt.toml), | |
| 17 | - | `cargo sort-derives` (`cargo install cargo-sort-derives`), and | |
| 15 | + | Dev setup: `git config core.hooksPath .githooks` — the pre-commit hook checks | |
| 16 | + | that `rust-toolchain.toml`'s pin satisfies Cargo.toml's `rust-version` floor, | |
| 17 | + | then runs `cargo +nightly fmt` (nightly, for the unstable options in | |
| 18 | + | rustfmt.toml), `cargo sort-derives` (`cargo install cargo-sort-derives`), and | |
| 18 | 19 | `cargo clippy --workspace --all-targets -- -D warnings`. | |
| 19 | 20 | ||
| 21 | + | **The Rust version is set in exactly one place: `rust-toolchain.toml`.** It | |
| 22 | + | pins the toolchain and the musl cross target for every `cargo` invocation here, | |
| 23 | + | and `deploy/runner/build.sh` parses `[toolchain] channel` out of it to bake the | |
| 24 | + | same version into `anvil-runner:rust` — so a CI job and a checkout compile with | |
| 25 | + | the same rustc. Bumping Rust means editing that file and Cargo.toml's floor | |
| 26 | + | (the hook catches you if you forget the second), then rebuilding the image. | |
| 27 | + | ||
| 20 | 28 | ## Viewing attachments referenced in tasks | |
| 21 | 29 | ||
| 22 | 30 | TODO items and tickets may embed an uploaded image as | |
| ⋯ 106 unchanged lines | |||
modifiedDEPLOY.md+9 −47
| ⋯ 2 unchanged lines | |||
| 3 | 3 | anvil runs as a single Docker container at `anvil.richardscollin.com`, fronted by | |
| 4 | 4 | hagrid's Caddy reverse proxy. | |
| 5 | 5 | ||
| 6 | - | - **Web** — anvil listens on `:3000` *inside* the container. Caddy (on the | |
| 7 | - | `hagrid` Docker network) reverse-proxies to it and provides HTTPS via Let's | |
| 8 | - | Encrypt. The web port is **not** published to the host. | |
| 9 | - | - **SSH** — Caddy only fronts HTTP(S), so anvil's SSH server is **published | |
| 10 | - | directly to the host**: `:2222` in the container, mapped to `:22` on the | |
| 11 | - | droplet's default public IPv4 (§4). Git-over-SSH connects to | |
| 12 | - | `git@anvil.richardscollin.com` on the default port. | |
| 13 | - | - **Runtime** — fully self-contained: SQLite and the SSH crypto are compiled in, | |
| 14 | - | and gix is pure-Rust. No git, OpenSSH, or system sqlite in the image. | |
| 15 | - | ||
| 16 | - | ## 1. DNS | |
| 17 | - | ||
| 18 | - | Add an `A`/`AAAA` (or `CNAME` to hagrid) record: | |
| 19 | - | ||
| 20 | - | ``` | |
| 21 | - | anvil.richardscollin.com -> <hagrid's public IP> | |
| 22 | - | ``` | |
| 23 | - | ||
| 24 | - | This one record covers both the web (443, via Caddy) and SSH (22, direct to | |
| 25 | - | the host). The `A` record must be the same address `compose.yaml` binds the SSH | |
| 26 | - | port to — today `165.232.162.167` (§4). | |
| 27 | - | ||
| 28 | - | ## 2. Caddy + index (in the hagrid repo) | |
| 29 | - | ||
| 30 | - | In `~/Code/hagrid/Caddyfile`, add: | |
| 31 | - | ||
| 32 | - | ``` | |
| 33 | - | anvil.richardscollin.com { | |
| 34 | - | reverse_proxy anvil:3000 | |
| 35 | - | } | |
| 36 | - | ``` | |
| 37 | - | ||
| 38 | - | In `~/Code/hagrid/sites.yaml`, add an entry: | |
| 39 | - | ||
| 40 | - | ```yaml | |
| 41 | - | - name: anvil | |
| 42 | - | host: anvil.richardscollin.com | |
| 43 | - | ``` | |
| 44 | - | ||
| 45 | - | Then reload Caddy (`./hagrid.sh reload`, or `./hagrid.sh deploy` to push to the | |
| 46 | - | host). Caddy resolves `anvil:3000` by container name over the `hagrid` network, | |
| 47 | - | so the anvil container must join that network (`compose.yaml` does this). | |
| 48 | - | ||
| 49 | - | ## 3. Deploying: `compose.yaml` + `hag` | |
| 6 | + | ## Deploying: `compose.yaml` + `hag` | |
| 50 | 7 | ||
| 51 | 8 | `compose.yaml` at the repo root describes the deployment — the image, the | |
| 52 | 9 | network, the volume, the published SSH port. Running it on hagrid is | |
| 53 | 10 | [`hag`](https://anvil.richardscollin.com/collin/hagrid), the shared deploy tool | |
| 54 | 11 | for every project on that host: it builds the image, pushes it to | |
| 55 | 12 | `registry.vibe.richardscollin.com`, copies `compose.yaml` up, then pulls and | |
| 56 | - | recreates the container there. Install it once with `~/Code/hagrid/hag install`. | |
| 13 | + | recreates the container there. | |
| 57 | 14 | ||
| 58 | 15 | `compose.override.yaml` sits next to it for local development — loopback ports, | |
| 59 | 16 | the dev config, the Docker socket agent sessions need. Compose merges it | |
| ⋯ 26 unchanged lines | |||
| 86 | 43 | ```sh | |
| 87 | 44 | dnf install zig # or: brew install zig | |
| 88 | 45 | cargo install cargo-zigbuild | |
| 89 | - | rustup target add x86_64-unknown-linux-musl | |
| 90 | 46 | docker login registry.vibe.richardscollin.com # on this machine and hagrid | |
| 91 | 47 | ``` | |
| 92 | 48 | ||
| 49 | + | The Rust toolchain itself needs no setup step: `rust-toolchain.toml` pins the | |
| 50 | + | version *and* the `x86_64-unknown-linux-musl` target, and rustup installs both | |
| 51 | + | on the first `cargo` invocation in the repo. That file is the one place the | |
| 52 | + | Rust version is set — `deploy/runner/build.sh` reads it to bake the same | |
| 53 | + | toolchain into `anvil-runner:rust`. | |
| 54 | + | ||
| 93 | 55 | Every build is tagged twice: with the short git sha of the checkout (plus a | |
| 94 | 56 | `-dirty` suffix when the working tree has uncommitted changes) and with | |
| 95 | 57 | `latest`. The deploy pins the host to the exact sha it just pushed, so `hag | |
| ⋯ 150 unchanged lines | |||
| 246 | 208 | plus steps: | |
| 247 | 209 | ||
| 248 | 210 | ```yaml | |
| 249 | - | image: rust:1.95-bookworm | |
| 211 | + | image: anvil-runner:rust | |
| 250 | 212 | steps: | |
| 251 | 213 | - name: test | |
| 252 | 214 | run: cargo test --workspace | |
| ⋯ 64 unchanged lines | |||
modifieddeploy/build.sh+2 −1
| ⋯ 17 unchanged lines | |||
| 18 | 18 | # Prereqs (one-time): | |
| 19 | 19 | # zig (dnf install zig, or brew install zig) | |
| 20 | 20 | # cargo install cargo-zigbuild | |
| 21 | - | # rustup target add x86_64-unknown-linux-musl | |
| 21 | + | # The toolchain and the musl target come from rust-toolchain.toml — rustup | |
| 22 | + | # installs both on the first cargo invocation in the repo. | |
| 22 | 23 | set -euo pipefail | |
| 23 | 24 | ||
| 24 | 25 | TARGET="x86_64-unknown-linux-musl" | |
| ⋯ 33 unchanged lines | |||
modifieddeploy/runner/Dockerfile+5 −2
| ⋯ 7 unchanged lines | |||
| 8 | 8 | # Parameterized so the same recipe produces a small general runner and a | |
| 9 | 9 | # toolchain-carrying one: | |
| 10 | 10 | # | |
| 11 | - | # anvil-runner:latest RUST_VERSION= (the default) | |
| 12 | - | # anvil-runner:rust RUST_VERSION=1.98.0 (builds anvil itself) | |
| 11 | + | # anvil-runner:latest RUST_VERSION= (the default) | |
| 12 | + | # anvil-runner:rust RUST_VERSION=<channel> (builds anvil itself) | |
| 13 | + | # | |
| 14 | + | # build.sh reads `<channel>` out of the repo's rust-toolchain.toml, so the | |
| 15 | + | # image's toolchain and the checkout's are the same by construction. | |
| 13 | 16 | # | |
| 14 | 17 | # Both sit on ubuntu:26.04. The official Rust image is Debian-based and has no | |
| 15 | 18 | # Ubuntu variant, so rather than let one tag drift onto a different distro the | |
| ⋯ 139 unchanged lines | |||
modifieddeploy/runner/build.sh+11 −3
| ⋯ 22 unchanged lines | |||
| 23 | 23 | NAME="${ANVIL_RUNNER_IMAGE:-anvil-runner}" | |
| 24 | 24 | CHANNEL="${CLAUDE_CHANNEL:-stable}" | |
| 25 | 25 | ||
| 26 | - | # Toolchain baked into the `rust` tag. Keep in step with the workspace | |
| 27 | - | # `rust-version` in Cargo.toml. | |
| 28 | - | RUST_VERSION="${RUST_VERSION:-1.98.0}" | |
| 26 | + | # Toolchain baked into the `rust` tag, read from the repo's rust-toolchain.toml | |
| 27 | + | # so the image compiles with the same rustc this checkout does. Not duplicated | |
| 28 | + | # here on purpose: that file is the one place the version is set. | |
| 29 | + | RUST_VERSION="${RUST_VERSION:-$( | |
| 30 | + | sed -n 's/^[[:space:]]*channel[[:space:]]*=[[:space:]]*"\(.*\)".*/\1/p' \ | |
| 31 | + | ../../rust-toolchain.toml | |
| 32 | + | )}" | |
| 33 | + | if [ -z "$RUST_VERSION" ]; then | |
| 34 | + | echo "could not read [toolchain] channel from rust-toolchain.toml" >&2 | |
| 35 | + | exit 1 | |
| 36 | + | fi | |
| 29 | 37 | ||
| 30 | 38 | build() { | |
| 31 | 39 | local tag="$1" rust="$2" | |
| ⋯ 30 unchanged lines | |||
modifieddocs/agent-sessions.md+7 −1
| ⋯ 53 unchanged lines | |||
| 54 | 54 | | Tag | Base | For | | |
| 55 | 55 | | -------------------- | ------------------------ | ------------------------------- | | |
| 56 | 56 | | `anvil-runner:latest`| `ubuntu:26.04` | the default, general work | | |
| 57 | - | | `anvil-runner:rust` | `ubuntu:26.04` + rustup 1.98.0 | pipelines needing the toolchain | | |
| 57 | + | | `anvil-runner:rust` | `ubuntu:26.04` + rustup | pipelines needing the toolchain | | |
| 58 | + | ||
| 59 | + | The version rustup installs is not written here or in the Dockerfile: the | |
| 60 | + | build script reads `[toolchain] channel` from the repo's `rust-toolchain.toml`, | |
| 61 | + | so a job compiles with the same rustc a checkout does. Bumping Rust is that one | |
| 62 | + | file (plus the `rust-version` floor in `Cargo.toml`, which the pre-commit hook | |
| 63 | + | checks agrees) and a rebuild of this tag. | |
| 58 | 64 | ||
| 59 | 65 | **There is no registry behind this image.** It is built straight into the | |
| 60 | 66 | Docker daemon's local store, so it must be built on whichever host owns the | |
| ⋯ 90 unchanged lines | |||
addedrust-toolchain.toml+18 −0
| 1 | + | # The one place the Rust version is set. rustup reads this for every cargo | |
| 2 | + | # invocation in the repo (installing the toolchain on first use), and | |
| 3 | + | # deploy/runner/build.sh parses `channel` out of it to bake the same version | |
| 4 | + | # into the anvil-runner:rust image — so the toolchain a CI job compiles with is | |
| 5 | + | # the one this checkout compiles with. | |
| 6 | + | # | |
| 7 | + | # The `rust-version` floor in Cargo.toml has to agree; .githooks/pre-commit | |
| 8 | + | # fails the commit if the two drift. | |
| 9 | + | # | |
| 10 | + | # `cargo +nightly fmt` (rustfmt.toml uses unstable options, see the hook) is | |
| 11 | + | # unaffected: an explicit +toolchain overrides this file. | |
| 12 | + | [toolchain] | |
| 13 | + | channel = "1.98.0" | |
| 14 | + | # deploy/build.sh cross-compiles the static binary the images COPY in. Listing | |
| 15 | + | # the target here means rustup installs it, so there is no `rustup target add` | |
| 16 | + | # step to remember. | |
| 17 | + | targets = ["x86_64-unknown-linux-musl"] | |
| 18 | + | components = ["clippy", "rustfmt"] |