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.
182 lines
8.8 KiB
Bash
Executable File
182 lines
8.8 KiB
Bash
Executable File
#!/bin/sh
|
|
# Drift gate for the generated bun2nix lockfile expressions (web/bun.nix, sdk/bun.nix).
|
|
#
|
|
# `bun.nix` is a DERIVED file: bun2nix is a pure function of `bun.lock` (it reads the lockfile text
|
|
# and emits one `fetchurl` per package, keyed by the lockfile's own integrity hashes — see
|
|
# packaging/nix/README.md). Nothing but the lockfile goes in, so any disagreement between the two
|
|
# committed files is drift, and it is always mechanically fixable.
|
|
#
|
|
# Why this exists: moving the bun packages to bun2nix (1db8f763) removed the *aggregate deps hash*
|
|
# that used to go stale, but not the second, quieter way a derived file rots. `bun.nix` regenerates
|
|
# only from a local `bun install` that runs lifecycle scripts (web's `postinstall`, the SDK's
|
|
# `prepare`). It does NOT regenerate on:
|
|
#
|
|
# * `bun install --ignore-scripts` — which is what EVERY bun install in CI uses (ci.yml,
|
|
# web-screenshots.yml, windows-host.yml, sdk-publish.yml), because web's `postinstall` shells
|
|
# out to a `bun` on PATH that CI's portable bun isn't;
|
|
# * a merge or rebase — git merges `bun.lock` and `bun.nix` as two unrelated files, so a branch
|
|
# that generated `bun.nix` before picking up someone else's lockfile change silently commits
|
|
# the pair out of step;
|
|
# * a lockfile edited or re-resolved by hand.
|
|
#
|
|
# That second case is not hypothetical: it is how `web/bun.nix` shipped on main carrying
|
|
# brace-expansion@5.0.7 (plus two nested entries the override had already collapsed) while
|
|
# `web/bun.lock` said 5.0.8 — the `^5.0.8` override from ec9aa415 landed in the lockfile, 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 offline `bun install` inside the
|
|
# derivation is then asked for a tarball the store cache does not contain and `punktfunk-web` fails
|
|
# to build — with a "package not found" that names npm, not the lockfile that actually drifted.
|
|
#
|
|
# The gate also enforces the version pin the flake and README only *state*: `bun.nix` has no schema
|
|
# stability across bun2nix releases, so the flake input ref and BOTH npm devDependencies must name
|
|
# the same exact version. Nothing checked that before; a half-moved pin regenerates the file with a
|
|
# generator the flake does not use.
|
|
#
|
|
# The list of packages to check is read out of packaging/nix/packages.nix (its `bunNix = src + …`
|
|
# lines) rather than hardcoded here, so a third bun package is covered the day it is added — and an
|
|
# empty list is a hard error, because a gate that checks nothing passes exactly like a clean tree.
|
|
#
|
|
# Usage:
|
|
# scripts/ci/check-bun-nix.sh # verify; non-zero on drift (CI)
|
|
# scripts/ci/check-bun-nix.sh --fix # regenerate the committed files in place
|
|
set -eu
|
|
|
|
FIX=0
|
|
if [ $# -gt 0 ]; then
|
|
case "$1" in
|
|
--fix) FIX=1 ;;
|
|
*) echo "usage: $0 [--fix]" >&2; exit 2 ;;
|
|
esac
|
|
fi
|
|
|
|
ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/../.." && pwd)
|
|
PACKAGES_NIX="$ROOT/packaging/nix/packages.nix"
|
|
FLAKE="$ROOT/flake.nix"
|
|
|
|
command -v bun >/dev/null 2>&1 || {
|
|
echo "check-bun-nix: bun is not on PATH (needed to run bun2nix and to read package.json)" >&2
|
|
exit 1
|
|
}
|
|
[ -f "$PACKAGES_NIX" ] || { echo "check-bun-nix: no $PACKAGES_NIX" >&2; exit 1; }
|
|
[ -f "$FLAKE" ] || { echo "check-bun-nix: no $FLAKE" >&2; exit 1; }
|
|
|
|
TMP=$(mktemp -d)
|
|
trap 'rm -rf "$TMP"' EXIT
|
|
|
|
# --- the pinned bun2nix version -------------------------------------------------------------------
|
|
# flake.nix: url = "github:nix-community/bun2nix?ref=2.1.2";
|
|
PINNED=$(sed -n 's/.*github:nix-community\/bun2nix?ref=\([^"]*\)".*/\1/p' "$FLAKE" | head -1)
|
|
[ -n "$PINNED" ] || {
|
|
echo "check-bun-nix: could not read the bun2nix input ref out of $FLAKE." >&2
|
|
echo "Expected a line like: url = \"github:nix-community/bun2nix?ref=<version>\";" >&2
|
|
exit 1
|
|
}
|
|
|
|
# --- which packages carry a generated bun.nix -----------------------------------------------------
|
|
# packages.nix: bunDeps = bun2nix.fetchBunDeps { bunNix = src + "/web/bun.nix"; };
|
|
sed -n 's/.*bunNix *= *src *+ *"\/\(.*\)\/bun\.nix".*/\1/p' "$PACKAGES_NIX" | sort -u > "$TMP/roots"
|
|
if [ ! -s "$TMP/roots" ]; then
|
|
echo "check-bun-nix: found no \`bunNix = src + \"/<dir>/bun.nix\"\` in $PACKAGES_NIX." >&2
|
|
echo "Either the bun packages were removed (delete this gate) or the expression changed shape" >&2
|
|
echo "and the gate silently stopped checking anything. Not passing vacuously." >&2
|
|
exit 1
|
|
fi
|
|
|
|
fail=0
|
|
checked=0
|
|
|
|
# --- version pin agreement ------------------------------------------------------------------------
|
|
# `bun.nix` has no schema stability across bun2nix versions, so the generator the flake builds with
|
|
# and the generator `bun install` runs must be the SAME exact version (packaging/nix/README.md).
|
|
while read -r dir; do
|
|
pkgjson="$ROOT/$dir/package.json"
|
|
[ -f "$pkgjson" ] || { echo "check-bun-nix: no $pkgjson" >&2; fail=1; continue; }
|
|
dev=$(bun -e "const d=require(process.argv[1]).devDependencies||{};console.log(d.bun2nix??'')" \
|
|
"$pkgjson")
|
|
if [ "$dev" != "$PINNED" ]; then
|
|
echo "check-bun-nix: bun2nix version pin disagrees." >&2
|
|
echo " flake.nix input ref : $PINNED" >&2
|
|
echo " $dir/package.json devDependency : ${dev:-<absent>}" >&2
|
|
echo "These must be the same exact version — bun.nix has no schema stability across" >&2
|
|
echo "bun2nix releases. Move both together, then rerun this script with --fix." >&2
|
|
fail=1
|
|
fi
|
|
done < "$TMP/roots"
|
|
|
|
# --- the generator ---------------------------------------------------------------------------------
|
|
# Prefer an already-installed bun2nix at the pinned version (fast, offline — the dev case); otherwise
|
|
# fetch exactly the pinned one, once, into $TMP. Never a floating `bunx bun2nix`: that would generate
|
|
# with whatever is newest, and `bun.nix` has no schema stability across releases.
|
|
BUN2NIX=""
|
|
while read -r dir; do
|
|
cand="$ROOT/$dir/node_modules/bun2nix/index.ts"
|
|
[ -f "$cand" ] || continue
|
|
have=$(bun -e "console.log(require(process.argv[1]).version??'')" \
|
|
"$ROOT/$dir/node_modules/bun2nix/package.json" 2>/dev/null || echo '')
|
|
if [ "$have" = "$PINNED" ]; then BUN2NIX="$cand"; break; fi
|
|
done < "$TMP/roots"
|
|
|
|
if [ -z "$BUN2NIX" ]; then
|
|
# Installed in its own scratch dir, so this never touches the repo's lockfiles or .npmrc.
|
|
mkdir -p "$TMP/gen"
|
|
if ! ( cd "$TMP/gen" && bun add --exact "bun2nix@$PINNED" ) > "$TMP/geninstall.log" 2>&1; then
|
|
echo "check-bun-nix: could not install bun2nix@$PINNED" >&2
|
|
cat "$TMP/geninstall.log" >&2
|
|
exit 1
|
|
fi
|
|
BUN2NIX="$TMP/gen/node_modules/bun2nix/index.ts"
|
|
[ -f "$BUN2NIX" ] || { echo "check-bun-nix: bun2nix@$PINNED installed but $BUN2NIX is absent" >&2; exit 1; }
|
|
fi
|
|
|
|
run_bun2nix() { # <lockfile> <outfile>
|
|
bun "$BUN2NIX" --lock-file "$1" --output-file "$2"
|
|
}
|
|
|
|
# --- regenerate + compare ---------------------------------------------------------------------------
|
|
while read -r dir; do
|
|
lock="$ROOT/$dir/bun.lock"
|
|
nix="$ROOT/$dir/bun.nix"
|
|
[ -f "$lock" ] || { echo "check-bun-nix: no $lock (packages.nix expects $dir/bun.nix)" >&2; fail=1; continue; }
|
|
|
|
out="$TMP/$(echo "$dir" | tr '/' '_').bun.nix"
|
|
run_bun2nix "$lock" "$out" >/dev/null
|
|
|
|
if [ "$FIX" -eq 1 ]; then
|
|
if [ ! -f "$nix" ] || ! cmp -s "$nix" "$out"; then
|
|
cp "$out" "$nix"
|
|
echo "check-bun-nix: regenerated $dir/bun.nix from $dir/bun.lock"
|
|
else
|
|
echo "check-bun-nix: $dir/bun.nix already in sync"
|
|
fi
|
|
checked=$((checked + 1))
|
|
continue
|
|
fi
|
|
|
|
if [ ! -f "$nix" ]; then
|
|
echo "check-bun-nix: $dir/bun.nix is MISSING — packages.nix fetches node_modules from it." >&2
|
|
fail=1
|
|
continue
|
|
fi
|
|
# Plain files, not `diff <(…) <(…)`: Gitea's runner executes a step's `run:` under `sh`, and
|
|
# dash has no process substitution — it would reject the script at parse time and the gate
|
|
# would never compare anything (exactly how the shader SPIR-V gate in ci.yml was lost).
|
|
if cmp -s "$nix" "$out"; then
|
|
echo "check-bun-nix: $dir/bun.nix matches $dir/bun.lock"
|
|
else
|
|
echo "check-bun-nix: $dir/bun.nix is STALE — it does not match $dir/bun.lock." >&2
|
|
echo "The Nix build fetches node_modules only from bun.nix, so punktfunk's bun packages" >&2
|
|
echo "would build against the wrong dependency set (or fail to fetch it at all)." >&2
|
|
echo "Regenerate and commit it: scripts/ci/check-bun-nix.sh --fix" >&2
|
|
echo "--- diff (committed -> regenerated from bun.lock) ---" >&2
|
|
diff -u "$nix" "$out" >&2 || true
|
|
fail=1
|
|
fi
|
|
checked=$((checked + 1))
|
|
done < "$TMP/roots"
|
|
|
|
[ "$checked" -gt 0 ] || { echo "check-bun-nix: checked nothing — refusing to report success" >&2; exit 1; }
|
|
if [ "$fail" -eq 0 ] && [ "$FIX" -eq 0 ]; then
|
|
echo "check-bun-nix: $checked bun package(s) in sync, bun2nix pinned at $PINNED everywhere"
|
|
fi
|
|
exit "$fail"
|