meta: non-mutating verify_commit + pre-flight eval before apply

This commit is contained in:
damocles 2026-06-22 19:28:45 +02:00 committed by mara
commit 4beaeea7b3
2 changed files with 84 additions and 0 deletions

View file

@ -493,6 +493,37 @@ async fn run_apply_commit(
}
};
// Pre-flight eval-verify the proposal commit against the meta flake
// WITHOUT mutating applied/main or the meta lock, so an evaluation
// error (bad nix, missing module option, unresolvable lock) fails
// fast here instead of after we've fast-forwarded main and have to
// roll it back. Skipped on first spawn: the agent has no
// `agent-<name>` meta input to override yet (sync_agents adds it
// below). This is the reusable verify primitive the PR-based config
// flow gates its irreversible ff-push on.
if !is_first_spawn {
let proposal_sha = match lifecycle::git_rev_parse(applied_dir, &proposal_ref).await {
Ok(s) => s,
Err(e) => {
return (
Err(anyhow::anyhow!("rev-parse {proposal_ref}: {e:#}")),
None,
is_first_spawn,
);
}
};
coord.set_queue_step(queue_entry_id, "verify proposal (eval)");
if let Err(e) =
crate::meta::verify_commit(&approval.agent, applied_dir, &proposal_sha).await
{
return (
Err(anyhow::anyhow!("verify proposal {proposal_ref}: {e:#}")),
None,
is_first_spawn,
);
}
}
coord.set_queue_step(queue_entry_id, "plant tags");
if let Err(e) = lifecycle::git_tag(applied_dir, &format!("approved/{id}"), &proposal_ref).await
{

View file

@ -287,6 +287,50 @@ pub async fn lock_update_for_rebuild(name: &str) -> Result<()> {
git_commit(&dir, &format!("rebuild {name}: lock update")).await
}
/// Build the `--override-input` value pinning an agent's config repo to
/// an exact revision: `git+file://<applied_dir>?rev=<sha>`. Pure so the
/// URL shape is unit-testable without a nix shellout.
fn agent_input_override(applied_dir: &Path, sha: &str) -> String {
format!("git+file://{}?rev={sha}", applied_dir.display())
}
/// Non-mutating "would this commit apply?" verify for the PR-based config
/// flow. Evaluates the agent's nixos configuration with its meta
/// input overridden to the exact `sha`, WITHOUT moving `applied/main` or
/// writing the real meta `flake.lock`: `--override-input` pins the input
/// at eval time only and `--no-write-lock-file` guarantees the on-disk
/// lock is never touched (a genuinely-needed lock change surfaces as an
/// error rather than a silent mutation). Confirms the flake at `sha`
/// evaluates and the meta lock resolves around it. `Ok(())` = would
/// apply; `Err` carries the eval/lock failure so the operator's approve
/// is rejected before any live-state change.
///
/// Precondition: `sha` must be reachable in `applied_dir`'s object store
/// — the caller fetches the PR head into `applied_dir` first. Eval-only
/// (no build): catches nix / eval / module-option errors + lock
/// resolution at the same cost profile as the legacy pre-merge check;
/// the real container build still runs (and can still roll back) on the
/// actual apply, so this is the right "would this apply" gate.
pub async fn verify_commit(name: &str, applied_dir: &Path, sha: &str) -> Result<()> {
let _guard = META_LOCK.lock().await;
let dir = meta_dir();
let input = format!("agent-{name}");
let over = agent_input_override(applied_dir, sha);
let attr = format!(".#nixosConfigurations.{name}.config.system.build.toplevel.drvPath");
nix(
&dir,
&[
"eval",
&attr,
"--override-input",
&input,
&over,
"--no-write-lock-file",
],
)
.await
}
/// Update one or more named inputs in the meta flake and commit
/// the resulting lock change with a single combined message.
/// Used by the dashboard's "update meta inputs" form so the
@ -1131,6 +1175,15 @@ mod tests {
}
}
#[test]
fn agent_input_override_pins_exact_rev() {
let p = Path::new("/var/lib/hyperhive/agents/iris/applied");
assert_eq!(
agent_input_override(p, "abc123def"),
"git+file:///var/lib/hyperhive/agents/iris/applied?rev=abc123def"
);
}
#[test]
fn render_flake_uses_explicit_nixpkgs_url_when_provided() {
let out = render_flake(