From 207b66e35d16833bc4e20db95d1fcf543a3d24e0 Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 15 Jul 2026 17:43:24 +0200 Subject: [PATCH] feat(#2470): capture config-approval eval/deploy nix runs in build logs --- hive-c0re/src/meta.rs | 82 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 71 insertions(+), 11 deletions(-) diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index b2ae343a..66f61023 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -238,7 +238,7 @@ pub async fn prepare_deploy(name: &str) -> Result<()> { let _guard = META_LOCK.lock().await; let dir = crate::paths::meta_root(); let input = format!("agent-{name}"); - nix(&dir, &["flake", "update", &input]).await?; + nix_logged(&dir, &["flake", "update", &input], name, "prepare-deploy").await?; // Stage the new lock — git+file://'s dirty-tree fetcher reads // index entries, so the upcoming nixos-container update sees the // bumped rev without a commit yet. @@ -324,7 +324,7 @@ pub async fn verify_commit(name: &str, applied_dir: &Path, sha: &str) -> Result< let input = format!("agent-{name}"); let over = agent_input_override(applied_dir, sha); let attr = format!(".#nixosConfigurations.{name}.config.system.build.toplevel.drvPath"); - nix( + nix_logged( &dir, &[ "eval", @@ -334,6 +334,8 @@ pub async fn verify_commit(name: &str, applied_dir: &Path, sha: &str) -> Result< &over, "--no-write-lock-file", ], + name, + "verify", ) .await } @@ -1308,19 +1310,31 @@ async fn paths_dirty(dir: &Path, paths: &[&str]) -> Result { Ok(!out.status.success()) } -async fn nix(dir: &Path, args: &[&str]) -> Result<()> { - // `--extra-experimental-features` belt-and-suspenders for hosts - // that haven't set this in nix.conf. The hyperhive module's - // deploy guide assumes flakes are already enabled, but the cost - // of being defensive is one extra argv each call. +/// Full argv for a `nix` invocation, prefixed with the flakes-enabling +/// experimental-features flag. `--extra-experimental-features` is +/// belt-and-suspenders for hosts that haven't set this in nix.conf: the +/// hyperhive module's deploy guide assumes flakes are already enabled, +/// but the cost of being defensive is one extra argv each call. +fn nix_argv<'a>(args: &[&'a str]) -> Vec<&'a str> { let mut all = vec!["--extra-experimental-features", "nix-command flakes"]; - all.extend(args); - let out = Command::new("nix") + all.extend_from_slice(args); + all +} + +/// Run `nix ` in `dir` (flakes enabled), capturing combined output. +/// Shared core of [`nix`] and [`nix_logged`]. +async fn nix_output(dir: &Path, args: &[&str]) -> Result { + Command::new("nix") .current_dir(dir) - .args(&all) + .args(nix_argv(args)) .output() .await - .with_context(|| format!("nix {} in {}", args.join(" "), dir.display()))?; + .with_context(|| format!("nix {} in {}", args.join(" "), dir.display())) +} + +/// Turn a finished nix [`Output`](std::process::Output) into `Result<()>`, +/// bailing with the trimmed stderr tail on non-zero exit. +fn nix_check(args: &[&str], out: &std::process::Output) -> Result<()> { if !out.status.success() { bail!( "nix {} failed ({}): {}", @@ -1332,6 +1346,52 @@ async fn nix(dir: &Path, args: &[&str]) -> Result<()> { Ok(()) } +async fn nix(dir: &Path, args: &[&str]) -> Result<()> { + let out = nix_output(dir, args).await?; + nix_check(args, &out) +} + +/// Like [`nix`] but records the invocation (cmdline + stdout + stderr + +/// terminal status) into a `build_logs.sqlite` row so a config-approval +/// eval/deploy step shows up on the dashboard. This is what makes a +/// failing eval-verify visible: it runs before any container build, so +/// without a row a rejected config approval leaves the operator with +/// zero build logs to look at. Best-effort logging: a missing global +/// handle or a failed `start()` just skips the row — the command still +/// runs and its exit status is still enforced. +async fn nix_logged(dir: &Path, args: &[&str], agent: &str, kind: &str) -> Result<()> { + let cmdline = format!("nix {}", nix_argv(args).join(" ")); + let logs = crate::build_logs::global(); + let log_id = logs.as_ref().and_then(|h| { + h.start(agent, kind, &cmdline) + .map_err(|e| { + tracing::warn!(error = ?e, %kind, "build_logs: start failed (meta log dropped)"); + }) + .ok() + }); + + let out = nix_output(dir, args).await?; + + if let (Some(h), Some(id)) = (&logs, log_id) { + for line in String::from_utf8_lossy(&out.stdout).lines() { + h.append_stdout(id, line); + } + for line in String::from_utf8_lossy(&out.stderr).lines() { + h.append_stderr(id, line); + } + h.finish( + id, + if out.status.success() { + crate::build_logs::BuildStatus::Ok + } else { + crate::build_logs::BuildStatus::Fail + }, + ); + } + + nix_check(args, &out) +} + #[cfg(test)] mod tests { use super::*;