ci(nix): a stale bun.nix can no longer reach main unnoticed #73

Merged
enricobuehler merged 1 commits from worktree-nix-bun2nix-drift-gate into main 2026-08-06 14:46:26 +00:00
Owner

The drift bun2nix did not remove

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 (WASM; reads the lockfile text, emits one fetchurl per package), so bun.nix drift is always exactly detectable and mechanically fixable. The problem is when it regenerates: only on 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 bun.lock 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 (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.

What this adds

  • scripts/ci/check-bun-nix.sh — regenerates each bun.nix from its committed bun.lock and diffs; --fix rewrites 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 job bun-nix — runs 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.

  • .gitea/workflows/nix.yml — 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, the two derivations whose inputs churn constantly 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 (see packages.nix's inlang note). Path-filtered, with a workflow_dispatch opt-in for the hour-long Rust packages, and pull_request kept — flatpak.yml shipped push-only and let manifest breakage reach main invisibly for weeks.

  • packaging/nix/README.md — it had been recommending an unpinned bunx bun2nix -o bun.nix, which is precisely the floating-version hazard it warns about a paragraph later. Now documents the gate, the --fix command, and why the devDependency hook is a convenience rather than a guarantee.

Verification

All four arms of the script were exercised locally:

case result
clean tree passes, EXIT=0
injected drift in web/bun.nix fails EXIT=1 with an actionable diff
--fix restores byte-identically (git status clean)
bun2nix devDep bumped out of step with flake.nix fails EXIT=1
bunNix lines removed from packages.nix fails EXIT=1 (vacuous-gate guard)

shellcheck -s sh clean; runs correctly under dash, which is oven/bun:1's /bin/sh; cwd-independent. No process substitution — that parse-time trap is what silently disabled ci.yml's shader SPIR-V gate for 35 commits. Both workflow YAMLs parse.

⚠ Known gap

nix.yml has not been dry-run — no Nix runner was reachable to do it (.21 has nix + flakes but its disk is 100% full; every other Linux box was unreachable). Its first run needs watching. The risky parts are nixos/nix:latest shipping no node (installed via nix profile install nixpkgs#nodejs before actions/checkout, following flatpak.yml's pattern) and whether the fleet tolerates that image at all. The ci.yml drift gate is fully validated and does not depend on it.

Aside

deb.yml:174 is the one CI bun install without --ignore-scripts, so web's postinstall regenerates bun.nix there and throws it away. Harmless — but it means bun must stay on PATH there, the same thing that broke windows-host.yml. Left alone.

## The drift bun2nix did not remove 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` (WASM; reads the lockfile text, emits one `fetchurl` per package), so `bun.nix` drift is always exactly detectable and mechanically fixable. The problem is *when* it regenerates: only on 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 `bun.lock` 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 (`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. ## What this adds - **`scripts/ci/check-bun-nix.sh`** — regenerates each `bun.nix` from its committed `bun.lock` and diffs; `--fix` rewrites 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` job `bun-nix`** — runs 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. - **`.gitea/workflows/nix.yml`** — 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`, the two derivations whose inputs churn constantly 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 (see `packages.nix`'s inlang note). Path-filtered, with a `workflow_dispatch` opt-in for the hour-long Rust packages, and `pull_request` kept — `flatpak.yml` shipped push-only and let manifest breakage reach main invisibly for weeks. - **`packaging/nix/README.md`** — it had been recommending an **unpinned** `bunx bun2nix -o bun.nix`, which is precisely the floating-version hazard it warns about a paragraph later. Now documents the gate, the `--fix` command, and why the devDependency hook is a convenience rather than a guarantee. ## Verification All four arms of the script were exercised locally: | case | result | |---|---| | clean tree | passes, `EXIT=0` | | injected drift in `web/bun.nix` | fails `EXIT=1` with an actionable diff | | `--fix` | restores byte-identically (`git status` clean) | | `bun2nix` devDep bumped out of step with `flake.nix` | fails `EXIT=1` | | `bunNix` lines removed from `packages.nix` | fails `EXIT=1` (vacuous-gate guard) | `shellcheck -s sh` clean; runs correctly under **dash**, which is `oven/bun:1`'s `/bin/sh`; cwd-independent. No process substitution — that parse-time trap is what silently disabled `ci.yml`'s shader SPIR-V gate for 35 commits. Both workflow YAMLs parse. ## ⚠ Known gap **`nix.yml` has not been dry-run** — no Nix runner was reachable to do it (`.21` has nix + flakes but its disk is 100% full; every other Linux box was unreachable). Its first run needs watching. The risky parts are `nixos/nix:latest` shipping no node (installed via `nix profile install nixpkgs#nodejs` *before* `actions/checkout`, following `flatpak.yml`'s pattern) and whether the fleet tolerates that image at all. The `ci.yml` drift gate is fully validated and does not depend on it. ## Aside `deb.yml:174` is the one CI `bun install` *without* `--ignore-scripts`, so web's `postinstall` regenerates `bun.nix` there and throws it away. Harmless — but it means `bun` must stay on PATH there, the same thing that broke `windows-host.yml`. Left alone.
enricobuehler added 1 commit 2026-08-06 13:36:11 +00:00
ci(nix): a stale bun.nix can no longer reach main unnoticed
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
fb707b4956
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.
enricobuehler merged commit 8e02f3a8e1 into main 2026-08-06 14:46:26 +00:00
enricobuehler deleted branch worktree-nix-bun2nix-drift-gate 2026-08-06 14:46:27 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#73