Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/nix/checks.nix
atlas be7eaa8181 refresh-consumer: compare mtimes locale-independently so an unchanged secret doesn't restart its unit
`refresh_unit` read the credential file's mtime with
`stat -c %.6Y | tr -d .`. coreutils prints that fraction with the
locale's decimal separator, so on a host whose locale uses a comma
(muede-lpt2: `1791232888,193159`) `tr` stripped nothing, the `-le`
against the unit's start errored, the "unchanged" early return was
skipped, and every run try-restarted the consumer. Every unit on a
store-refresh timer was restarted every ~10 minutes; for
`swarm-bao-ci-runner` that is `gitea-runner-swarm`, which killed every
CI job running at the time.

The mtime is now read with `date -r … +%s%6N`, the same digit-only
format the start time already used (`date -d … +%s%6N` on systemd's
fixed English timestamp), so neither side has a separator for a locale
to change.

A value that still does not read as digits — an unparsable start time,
a missing file — now restarts nothing and logs at `err`. Skipping on
doubt rather than restarting on doubt: the file's mtime is kept, so a
genuinely changed credential still restarts its consumer on the first
run that can read both times, whereas restarting on doubt restarts it
on every run, which is this bug. Both reads also fall back to empty
instead of aborting the caller's `set -e` script.

`script-test-refresh-consumer` runs the rendered helper under
`de_DE.UTF-8` against a stub `systemctl`, and checks first that `stat`
really prints a comma there. The helper on main fails 14 of its
assertions.

Fixes #5230
2026-10-13 17:00:56 +02:00

481 lines
21 KiB
Nix

# Flake checks: formatting, the clippy gate, the workspace test run,
# the nix-options docs eval, the hivectl CLI-reference freshness check,
# and the module-eval property table. Imported per system from flake.nix.
{
pkgs,
craneLib,
rust,
self,
system,
treefmt-eval,
nixosSystem,
}:
let
inherit (rust) cleanSrc cargoArtifacts nativeBuildInputs;
# Bound here, once, so every cluster handed it reads the same evaluation of
# each fixture; see ./module-eval/shared.nix.
moduleEvalShared = import ./module-eval/shared.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
in
{
formatting = treefmt-eval.config.build.check self;
# The only checks here that cover **nix**. Every other one is a Rust
# derivation, so a `.nix`-only diff moves no hash and the whole set is
# cache hits — green without evaluating what changed. See
# ./module-eval/lib.nix for what belongs in this suite and what needs
# something that executes rather than evaluates.
#
# Split into one derivation per subsystem cluster (./module-eval/*.nix)
# rather than one `module-eval` derivation: the combined version forced
# ~62 full `nixosSystem` fixtures live at once to compute its single
# `cases` list, measured at 10.6GB peak RSS / 5m25s to evaluate. Each
# cluster below only holds its own handful of fixtures.
module-eval-core-toggle = import ./module-eval/core-toggle.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-swarm-services-switch = import ./module-eval/swarm-services-switch.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-nats-authelia = import ./module-eval/nats-authelia.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
# Executes the queue server's config render, which `module-eval-nats-authelia`
# only sees as a store path: the callout xkey it writes is a build output.
script-test-nats-render-config = import ./script-tests/nats-render-config.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
# Runs ./host-modules/lib/refresh-consumer.nix's restart decision under a
# comma-decimal locale, against a stub `systemctl`.
script-test-refresh-consumer = import ./script-tests/refresh-consumer.nix { inherit pkgs; };
module-eval-nats-tls = import ./module-eval/nats-tls.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-hive-tls = import ./module-eval/hive-tls.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-matrix-core = import ./module-eval/matrix-core.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-grafana = import ./module-eval/grafana.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-forge-placement = import ./module-eval/forge-placement.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-bao-basics = import ./module-eval/bao-basics.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-bao-matrix-reader = import ./module-eval/bao-matrix-reader.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-bao-grants = import ./module-eval/bao-grants.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-bao-controller = import ./module-eval/bao-controller.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-secret-publisher = import ./module-eval/secret-publisher.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-bao-otel-collector = import ./module-eval/bao-otel-collector.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-swarm-otel-core = import ./module-eval/swarm-otel-core.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-swarm-otel-identity = import ./module-eval/swarm-otel-identity.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-hive-otel = import ./module-eval/hive-otel.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-name-guards = import ./module-eval/name-guards.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
module-eval-agent-otel = import ./module-eval/agent-otel.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
# Not a subsystem cluster like its siblings but a single shared file, and
# its own derivation for that reason: both journald receivers import one
# PRIORITY → severity mapping, so asserting the table inside each tier's
# suite would check one file twice and report one defect as two failures.
# Holds no fixture, so it costs nothing next to the clusters.
module-eval-journald-severity = import ./module-eval/journald-severity.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
module-eval-agent-queue-bao = import ./module-eval/agent-queue-bao.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-agent-forge-bao = import ./module-eval/agent-forge-bao.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
# Executes what the two `module-eval-agent-*-bao` suites above only
# evaluate: both fetch units' rendered scripts, against a stub `bao`.
script-test-agent-bao-fetch = import ./script-tests/agent-bao-fetch.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
# Runs ./agent-modules/hive-facts.nix's generator and login-shell snippet
# against sample facts files.
script-test-agent-hive-facts = import ./script-tests/agent-hive-facts.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
# Runs ./agent-modules/memory-cap.nix's generators and login-shell snippet
# against sample cap files.
script-test-agent-memory-cap = import ./script-tests/agent-memory-cap.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
module-eval-agent-github-bao = import ./module-eval/agent-github-bao.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-agent-memory = import ./module-eval/agent-memory.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-agent-plugins = import ./module-eval/agent-plugins.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
module-eval-agent-runtime = import ./module-eval/agent-runtime.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
module-eval-agent-icon = import ./module-eval/agent-icon.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
module-eval-agent-user = import ./module-eval/agent-user.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
};
module-eval-agent-matrix = import ./module-eval/agent-matrix.nix {
inherit pkgs self nixosSystem;
inherit (pkgs) lib;
shared = moduleEvalShared;
};
# Clippy via crane's first-class `cargoClippy` builder. Reuses the
# shared `cargoArtifacts` (deps already built) and runs
# `cargo clippy --workspace --all-targets` directly.
#
# `-D warnings` makes every rustc/clippy warning a hard CI gate.
# Pedantic is gated too, deliberately: the workspace lint table
# (Cargo.toml) sets `pedantic = deny`, so pedantic lints are errors
# both locally and here. We want that — a toolchain bump that adds a
# new pedantic lint reds the build until the code is updated, rather
# than sliding in unnoticed. (The lint table allows a few noisy
# pedantic lints explicitly, e.g. `must_use_candidate`; those keep
# their allow via higher priority.)
clippy = craneLib.cargoClippy {
src = cleanSrc;
inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace";
version = "0.1.0";
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
};
# `cargo test --workspace` lifted out of the package builds so the
# `hyperhive-assets` dep (which `hive-agent::prompt::tests`
# needs via `HIVE_ASSETS_DIR` to assert against the actual
# production prompt template) is scoped to this one check
# instead of bleeding into the binary derivations' input
# hash. Net: editing `hive-agent/prompts/system.md` still
# rebuilds this test check (correct — the tests assert
# against its wording), but `packages.default` and the
# per-container toplevels stay fully cached.
cargo-test = craneLib.cargoTest {
src = cleanSrc;
inherit cargoArtifacts;
# `swarm-controller`'s forge tests build a real `forgejo_api::Forgejo`
# (hence a real `reqwest::Client`) against a loopback stub server, never
# the real forge — but `reqwest`/`rustls` still resolves roots through
# `rustls-native-certs` at *client-build* time, unconditionally, even
# though every request that client ever sends is plain `http://` to
# `127.0.0.1`. The nix build sandbox has no system CA store, so that
# resolution finds zero certs and `ClientBuilder::build()` itself fails
# with "No CA certificates were loaded from the system" — the tests
# never get as far as making a request. Same root cause the runtime
# deployment already works around (see `nix/host-modules/swarm-
# controller.nix`'s `caTrust` comment) and the same fix: give
# `rustls-native-certs` something to find via `SSL_CERT_FILE`. The
# bundle's actual contents don't matter here — nothing in these tests
# ever presents or verifies a real certificate — only that the store is
# non-empty.
nativeBuildInputs = nativeBuildInputs ++ [ pkgs.cacert ];
SSL_CERT_FILE = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
pname = "hyperhive-workspace";
version = "0.1.0";
cargoTestExtraArgs = "--workspace";
HIVE_ASSETS_DIR = "${self.packages.${system}.assets}/share/hyperhive";
# The reserved-name blacklist lives in `reserved-names.nix`, not in the
# Rust tree, so the test that pins the message layer's
# sentinels against it can only run if nix hands it the same list the
# daemons get. Exported here and in `devshell.nix`, and the test PANICS
# rather than skipping when the variable is missing: a drift test that
# quietly does nothing is worse than no drift test, because it still
# shows up green.
HIVE_RESERVED_NAMES = pkgs.lib.concatStringsSep " " (import ./reserved-names.nix);
# swarm-controller's stream and replay tests run against a real
# `nats-server` on loopback (`swarm-controller/src/test_nats.rs`), since
# what they check is the server's own create and delivery behaviour.
# Exported here and in `devshell.nix`; the tests panic when it is unset.
HIVE_NATS_SERVER = "${pkgs.nats-server}/bin/nats-server";
};
# Rustdoc gate. Builds the workspace's docs and turns rustdoc's own
# lints into hard failures, so a `[`Foo`]` pointing at a renamed,
# moved or deleted item reds the PR instead of silently rendering as
# plain text.
#
# Why this needs to exist at all: nothing else reads doc-comments.
# Clippy doesn't check intra-doc links, `cargo test` doesn't, and no
# other check builds docs — so a dangling pointer had no discoverer
# but a human happening to read the comment. That matters more here
# than in most repos, because the convention is to put a thing's
# authoritative description in one doc-comment and link to it from
# everywhere else: the design leans on the pointers being real, and a
# dangling link is worse than no link, since it names something and
# so sends the reader looking.
#
# `--document-private-items` is load-bearing, not thoroughness for its
# own sake: most of this workspace's doc-comments live on private
# items and `//!` module headers. Without it rustdoc checks a small
# fraction of the links and the gate would sit green while the rot
# continued.
#
# ⚠️ This does NOT reuse the `cargoArtifacts` the way `clippy` and
# `cargo-test` do — it takes them, but rustdoc needs its own flavour
# of dependency metadata, which `cargo build` does not produce. So a
# `--no-deps` docs build still *compiles* dependencies it will never
# document (matrix-sdk dominates), and this check costs its own build
# rather than riding the others' cache.
docs-rustdoc = craneLib.cargoDoc {
src = cleanSrc;
inherit cargoArtifacts nativeBuildInputs;
pname = "hyperhive-workspace";
version = "0.1.0";
cargoDocExtraArgs = "--workspace --no-deps --document-private-items";
# The lints themselves are NOT set here — they live in
# `[workspace.lints.rustdoc]` in the root Cargo.toml, alongside the
# clippy table, and every crate inherits them via `[lints] workspace
# = true`. That way a plain local `cargo doc` fails exactly the way
# this check does; setting them as `RUSTDOCFLAGS` here would make CI
# the only place the gate exists.
};
# Nix options docs evaluation. Cheap: pulls in `nixosOptionsDoc` +
# the host module's stub eval, no rust or frontend deps. CI fails
# fast if a module change breaks option declarations or the doc
# rendering. Reuses the `packages.<system>.docs` derivation so the
# per-system eval of nix/docs/default.nix happens once.
inherit (self.packages.${system}) docs;
# Frontend build. Builds the `hyperhive-frontend` npm package, whose
# fixed-output npm-deps derivation pins `npmDepsHash`
# (nix/packages/frontend.nix). No other check exercises that FOD:
# `cargo-test` forces `.#assets`, but `assets` carries no frontend
# dependency, so without this check a stale `npmDepsHash` after a
# `package-lock.json` bump that forgets to recompute it sails through
# every PR and only breaks when the host toplevel is built on deploy.
# Reusing the already-defined `packages.<system>.frontend` derivation
# makes such a hash mismatch a red PR instead of a red main.
#
# `swarm-ui` alongside it for the same FOD-staleness reason (shares
# the same lockfile / `npmDepsHash`, see nix/packages/swarm-ui.nix) —
# plus, since it's not in `packages.default`'s dependency closure like
# `frontend` is, this is also the only thing that actually *builds*
# it in CI rather than just evaluating it (`nix flake check` doesn't
# build every `packages.<system>.*` output on its own).
inherit (self.packages.${system}) frontend swarm-ui;
# TypeScript type-check for `swarm-ui` (`npm run typecheck`, i.e.
# `tsc --noEmit`). esbuild only strips types without checking them
# (see `frontend/packages/swarm-ui/build.mjs`'s own comment), so
# without this a real type error would still build clean and pass
# every other check. Argus flagged the gap on this PR's review;
# wiring it in now per mara's follow-up ask on the same PR, rather
# than leaving it as a someday-issue.
#
# A separate `buildNpmPackage` derivation rather than folding into
# `swarm-ui` itself: this one's whole job is to *fail loudly* on a
# type error, and `npm ci` + `tsc` needs `typescript` in
# `node_modules`, which `swarm-ui.nix`'s actual build doesn't need
# (esbuild transpiles TS without it) — no reason to make the real
# build depend on the dev-only type-checker.
swarm-ui-typecheck = pkgs.buildNpmPackage {
pname = "hyperhive-swarm-ui-typecheck";
version = "0.0.0";
src = ../frontend;
# See `nix/packages/frontend.nix`'s comment on the same line.
npmDepsHash = builtins.readFile ../frontend/npm-deps-hash;
buildPhase = ''
runHook preBuild
npm run typecheck --workspace=packages/swarm-ui
runHook postBuild
'';
dontNpmInstall = true;
installPhase = ''
runHook preInstall
mkdir -p $out
touch $out/ok
runHook postInstall
'';
};
# Same rationale + shape as `swarm-ui-typecheck` above, for the `agent`
# package now that its ongoing Preact rewrite has grown enough real
# `.tsx`/`.ts` for a type error to matter — argus flagged the same gap
# on that PR's review.
agent-typecheck = pkgs.buildNpmPackage {
pname = "hyperhive-agent-typecheck";
version = "0.0.0";
src = ../frontend;
npmDepsHash = builtins.readFile ../frontend/npm-deps-hash;
buildPhase = ''
runHook preBuild
npm run typecheck --workspace=packages/agent
runHook postBuild
'';
dontNpmInstall = true;
installPhase = ''
runHook preInstall
mkdir -p $out
touch $out/ok
runHook postInstall
'';
};
# `hivectl` CLI reference freshness check. The committed
# markdown at `docs/tools/hivectl-cli.md` is the rendered
# output of the hidden `hivectl markdown-docs` subcommand
# (clap-markdown walks the binary's own command tree). This
# check regenerates it from the built binary and fails if the
# committed copy drifted — so a verb / flag / help-string edit
# that forgets to refresh the doc is caught in CI. Reuses the
# already-built `packages.<system>.default` (no extra compile).
# A doc guarded this way must also be listed in `.prettierignore`:
# the generator owns its layout, and a formatter that rewrites it
# makes this check unsatisfiable rather than merely stale.
# Regenerate locally with:
# nix build .#default
# ./result/bin/hivectl markdown-docs > docs/tools/hivectl-cli.md
hivectl-docs = pkgs.runCommand "hivectl-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; } ''
${self.packages.${system}.default}/bin/hivectl markdown-docs > generated.md
if ! diff -u ${../docs/tools/hivectl-cli.md} generated.md; then
echo "" >&2
echo "ERROR: docs/tools/hivectl-cli.md is out of date — regenerate it:" >&2
echo " nix build .#default && ./result/bin/hivectl markdown-docs > docs/tools/hivectl-cli.md" >&2
exit 1
fi
touch "$out"
'';
# `swarmctl` CLI reference freshness check — same shape as
# `hivectl-docs` above, `swarmctl markdown-docs` (clap-markdown over
# its own command tree) instead. Reuses `packages.<system>.swarmctl`
# (already built as its own package, out of `daemonBins` — see
# nix/packages/default.nix's comment on it).
swarmctl-docs = pkgs.runCommand "swarmctl-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; } ''
${self.packages.${system}.swarmctl}/bin/swarmctl markdown-docs > generated.md
if ! diff -u ${../docs/tools/swarmctl-cli.md} generated.md; then
echo "" >&2
echo "ERROR: docs/tools/swarmctl-cli.md is out of date — regenerate it:" >&2
echo " nix build .#swarmctl && ./result/bin/swarmctl markdown-docs > docs/tools/swarmctl-cli.md" >&2
exit 1
fi
touch "$out"
'';
# `swarm-logs` CLI reference freshness check — same shape as
# `hivectl-docs` above, `swarm-logs markdown-docs` (clap-markdown over
# its own command tree) instead. Reuses `packages.<system>.swarm-logs`
# (a per-bin package out of `daemonBins` — see nix/packages/default.nix).
swarm-logs-docs =
pkgs.runCommand "swarm-logs-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; }
''
${self.packages.${system}.swarm-logs}/bin/swarm-logs markdown-docs > generated.md
if ! diff -u ${../docs/tools/swarm-logs-cli.md} generated.md; then
echo "" >&2
echo "ERROR: docs/tools/swarm-logs-cli.md is out of date — regenerate it:" >&2
echo " nix build .#swarm-logs && ./result/bin/swarm-logs markdown-docs > docs/tools/swarm-logs-cli.md" >&2
exit 1
fi
touch "$out"
'';
# `hive-forge` CLI reference freshness check — same shape as
# `hivectl-docs` above, `hive-forge markdown-docs` (clap-markdown over
# its own command tree) instead. Reuses `packages.<system>.hive-forge`
# (already built as its own package, out of `daemonBins` — see
# nix/packages/default.nix's comment on it).
hive-forge-docs =
pkgs.runCommand "hive-forge-docs-fresh" { nativeBuildInputs = [ pkgs.diffutils ]; }
''
${self.packages.${system}.hive-forge}/bin/hive-forge markdown-docs > generated.md
if ! diff -u ${../docs/tools/forge-cli.md} generated.md; then
echo "" >&2
echo "ERROR: docs/tools/forge-cli.md is out of date — regenerate it:" >&2
echo " nix build .#hive-forge && ./result/bin/hive-forge markdown-docs > docs/tools/forge-cli.md" >&2
exit 1
fi
touch "$out"
'';
}