No description
  • Rust 60.7%
  • Nix 31%
  • Shell 8.3%
Find a file
Jet 8a79139f6b Add scripts/ci to bootstrap Nix on a minimal CI runner
Odyssey's Firecracker image is Alpine on a read-only root with no xz,
no useradd, a 64 MiB /tmp, the only real disk at /workspace, and no
devpts mounted. Each of those breaks a different part of the obvious
'install nix and build' path, and the runner caps how long a single
command line can be, so the workarounds cannot live in the command box.

The script mounts devpts (nix needs a pty to capture builder output),
points TMPDIR at the big volume, then picks the best available store:
nix on PATH, else a native install if /nix can be a mountpoint, else
nix-portable with bwrap in preference to proot.

POSIX sh, verified against dash and busybox ash and clean under
shellcheck -s sh.
2026-08-16 23:29:00 -07:00
docs feat: add Cloudflare tunnel hosting 2026-05-28 14:50:07 -07:00
hosts/noisebell-do fix: keep Loki local and prune DO storage 2026-05-28 15:59:29 -07:00
pi Make CI a single hermetic Nix build 2026-08-14 17:29:38 -07:00
remote feat: add Cloudflare tunnel hosting 2026-05-28 14:50:07 -07:00
scripts Add scripts/ci to bootstrap Nix on a minimal CI runner 2026-08-16 23:29:00 -07:00
secrets feat: add Cloudflare tunnel hosting 2026-05-28 14:50:07 -07:00
.envrc feat!: make declarative version 2026-03-22 17:40:48 -07:00
.gitignore feat!: make declarative version 2026-03-22 17:40:48 -07:00
Cargo.lock feat: add noisebell observability 2026-05-27 20:09:44 -07:00
Cargo.toml feat: add home assistant capability with pi-relay 2026-03-24 00:24:12 -07:00
flake.lock feat!: make declarative version 2026-03-22 17:40:48 -07:00
flake.nix Drop rust-docs from the CI toolchain 2026-08-14 17:41:41 -07:00
README.md Correct CI runner sizing figures in README 2026-08-14 17:42:05 -07:00
rustfmt.toml feat: add basic rss feat support 2026-03-23 13:56:00 -07:00

Noisebell

Space status

Monitors the door at Noisebridge and tells you whether it's open or closed.

A Raspberry Pi reads a magnetic sensor on the door and pushes state changes to a cache server. The cache keeps the latest state and fans updates out to chat integrations such as Discord and Zulip.

Pi (door sensor) ──webhook──> Cache ──webhook──> Discord / Zulip
                                |
                     polls Pi <-+
                                |
                                +──webhook──> Pi relay ──webhook──> Home Assistant

Layout

Directory What it is
pi/ Raspberry Pi OS base with laptop-built Noisebell deploy
remote/ Server-side services (cache, RSS, Discord, Zulip)
hosts/noisebell-do/ Standalone DigitalOcean NixOS host for the remote services
secrets/ Shared agenix-encrypted secrets and recipient rules

Each directory has its own README with setup and configuration details.

For hosted deployment, this repo exports nixosConfigurations.noisebell-do, a small DigitalOcean NixOS host that imports noisebell.nixosModules.default. The host provides deployment-specific values like domains and the Pi address, while the Noisebell module itself points agenix at the encrypted files in secrets/ and consumes the decrypted runtime files on the target machine.

Useful commands:

  • ./scripts/deploy-do [jet@noisebell-do] redeploys the DigitalOcean remote host
  • ./scripts/nhs redeploys the old Hetzner host using the local checkout as the flake input
  • scripts/deploy-pios-pi.sh pi@100.66.45.36 redeploys the Raspberry Pi OS machine
  • scripts/share-grafana-public-dashboard jet@noisebell-do repairs or prints the deterministic public-safe Grafana dashboard link

The full Home Assistant relay workflow is documented in pi/README.md. Public hosting, Cloudflare Tunnel, firewall, and Grafana sharing details are documented in docs/hosting.md.

CI

Everything CI gates on is a Nix derivation, so a runner needs Nix and nothing else — no Rust install, no cross toolchain setup, no cargo invocations of its own.

nix build -L --max-jobs auto --cores 0 .#ci

.#ci is a link farm over every entry in checks.x86_64-linux, which is what makes the run parallel: Nix sees the checks as independent derivations and schedules them across all available cores at once, rather than running fmt, then clippy, then tests, then builds.

Check What it covers
fmt cargo fmt --check
clippy --all-targets with --deny warnings
nextest the workspace test suite
noisebell-{cache,rss,discord,zulip} x86_64 release binaries
noisebell-pi{,-relay} aarch64 glibc, cross-compiled
noisebell-pi{,-relay}-static aarch64 musl, statically linked

Individual checks build on their own, which is the faster loop when iterating:

nix build -L --max-jobs auto --cores 0 \
  .#checks.x86_64-linux.clippy .#checks.x86_64-linux.nextest

nix flake check also works and additionally evaluates the NixOS configurations, but it skips the aarch64-linux outputs, so it is the slower and less complete of the two.

Toolchain

The flake pins a nightly toolchain via selectLatestNightlyWith, which walks back from the newest nightly until it finds one that ships every component the build asks for — a partially published nightly cannot break CI. Nightly buys two things:

  • -Z threads parallelises the rustc frontend within a single crate. Cargo already builds independent crates in parallel; this fills in the tail of the dependency graph, where a workspace this narrow otherwise compiles one crate on one core.
  • Cranelift (rustc-codegen-cranelift-preview) replaces LLVM as the codegen backend for the dev profile only. It emits object code much faster and the code it emits is slower, which is the right trade for clippy and tests. Release binaries — the ones that actually ship to the Pi and the DO host — stay on LLVM.

Cross-compilation for both Pi targets comes from the same toolchain carrying the aarch64 std; only the C toolchain and the cargo target environment differ between the glibc and musl variants. Each target does one shared dependency build for both of its binaries, and that build is scoped to -p noisebell -p noisebell-relay so host-only crates never enter the cross graph.

Runner sizing

The build closure is 1207 store paths totalling 4.1 GB, most of it the nightly toolchain, the aarch64 cross toolchains, and vendored crates. Roughly 0.4 GB of that substitutes straight from cache.nixos.org (including both cross C toolchains, so there is no gcc bootstrap); another ~0.2 GB is Rust nightly tarballs from static.rust-lang.org that are unpacked and patchelfed locally.

A disposable VM therefore wants ~16 GB of disk — closure plus cargo target directories during the build — and 16 GB of RAM if you run with --max-jobs auto --cores 0, since that can put several dependency builds in flight at once with a full complement of rustc processes in each. Halve --max-jobs if memory is tighter than that.

Every run from a fresh VM redoes all of it. If this runs often, pointing the runner at a binary cache (attic, cachix) or giving it a persistent /nix turns most of the work into a download, and an unchanged tree into a no-op.

Observability

The DigitalOcean host runs Prometheus, Loki, Grafana, Alloy, node_exporter, and blackbox_exporter via hosts/noisebell-do/observability.nix. Grafana provisions Noisebell Full Debug for authenticated operators and Noisebell Public for externally shared, Prometheus-only status.

  • Grafana: https://grafana-noisebell.extremist.software/ through Cloudflare Tunnel, login required
  • Public-safe Grafana dashboard: https://grafana-noisebell.extremist.software/public-dashboards/6e6f69736562656c6c7075626c696330
  • Prometheus: http://noisebell-do:9090/ over Tailscale
  • Loki: http://noisebell-do:3100/ over Tailscale

The Pi deploy script enables persistent journald, installs prometheus-node-exporter, and installs noisebell-loki-journal.service to ship Pi journal logs to Loki on the DO host.

Prometheus is the source of truth for regular time-based data: scrape health, host CPU/memory/disk/uptime, DO-to-Pi poll counts and last results, GPIO state, Pi hardware readings, webhook counters, and retry counters. Loki/journald is reserved for sparse event logs that should be readable in chronological order: service start/stop, door state changes, cache state changes, Pi offline/online transitions, auth or rate-limit rejections, webhook retries/failures, stale events, and GPIO read errors. Routine successful polls, unchanged poll results, metrics scrapes, and badge/image/status reads are intentionally not logged at INFO.