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 in ec9aa415, 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 in b79d90b4 happened 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.