ci / rust-arm64 (pull_request) Successful in 1m24s
apple / swift (pull_request) Successful in 1m32s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Failing after 2m19s
ci / docs-site (pull_request) Successful in 1m17s
android / android (pull_request) Successful in 3m10s
ci / bun-nix (pull_request) Successful in 1m6s
nix / flake (pull_request) Failing after 1m6s
ci / rust (pull_request) Failing after 6m2s
Moving the bun packages to bun2nix (1db8f763) removed the aggregate deps hash that used to go stale, but not the second, quieter way a generated file rots. bun2nix is a pure function of bun.lock, and `bun.nix` is committed alongside it — but it regenerates ONLY from a local `bun install` that runs lifecycle scripts. It does not regenerate under `bun install --ignore-scripts`, which is what every bun install in CI uses, and it cannot regenerate on a merge or rebase, where git carries someone else's lockfile change past a `bun.nix` generated before it and reports no conflict. That is not theoretical. web/bun.nix went onto main holding brace-expansion@5.0.7 (plus two nested entries the `^5.0.8` override had already collapsed) while web/bun.lock said 5.0.8: the override landed inec9aa415, the bun2nix branch had generated bun.nix off the pre-override lock, and the merge kept both. The Nix build fetches node_modules strictly from bun.nix, so the derivation's offline `bun install` was asked for a tarball its store cache did not contain and `nix build .#punktfunk-web` failed. It stayed broken for 553 commits (2026-07-27 -> 2026-08-05) and was closed by accident, when an unrelated advisory bump inb79d90b4happened to rerun a real `bun install`. Nothing caught it because nothing could: the string "nix" appeared in exactly one workflow file, and only in a comment about bun2nix breaking a Windows step. * scripts/ci/check-bun-nix.sh regenerates each bun.nix from its committed bun.lock and diffs, with `--fix` to rewrite them. It reads which packages to check out of packages.nix's own `bunNix = src + ...` lines rather than a hardcoded list, so a third bun package is covered the day it is added, and an empty list is a hard error — a gate that checks nothing passes exactly like a clean tree. It also enforces the bun2nix version pin that flake.nix and the README only stated: bun.nix has no schema stability across releases, so the flake input ref and both package.json devDependencies must agree, and generation always uses the pinned version rather than a floating `bunx`. * ci.yml gains a `bun-nix` job running it. Deliberately unfiltered and not in nix.yml: it needs no Nix, takes under a minute, and the whole point is that this drift arrives through commits that look unrelated to Nix. * nix.yml is new and is the first CI that evaluates the flake at all — `nix flake check --no-build` over every output, then a real build of punktfunk-web + punktfunk-scripting, which are the two derivations whose inputs churn and the only ones that do not compile Rust. That leg is the end-to-end proof the drift gate cannot give: it catches a tarball the registry no longer serves, or the codegen going quietly message-less. It is path-filtered, has a workflow_dispatch opt-in for the hour-long Rust packages, and keeps `pull_request` — flatpak.yml shipped push-only and let manifest breakage reach main invisibly for weeks. The script is POSIX sh (shellcheck-clean, exercised under dash, which is the CI container's /bin/sh) and avoids process substitution, the parse-time failure that silently disabled ci.yml's shader SPIR-V gate for 35 commits.