meta: non-mutating verify_commit + pre-flight eval before apply
This commit is contained in:
parent
2b4e928afe
commit
4beaeea7b3
2 changed files with 84 additions and 0 deletions
|
|
@ -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");
|
coord.set_queue_step(queue_entry_id, "plant tags");
|
||||||
if let Err(e) = lifecycle::git_tag(applied_dir, &format!("approved/{id}"), &proposal_ref).await
|
if let Err(e) = lifecycle::git_tag(applied_dir, &format!("approved/{id}"), &proposal_ref).await
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,50 @@ pub async fn lock_update_for_rebuild(name: &str) -> Result<()> {
|
||||||
git_commit(&dir, &format!("rebuild {name}: lock update")).await
|
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
|
/// Update one or more named inputs in the meta flake and commit
|
||||||
/// the resulting lock change with a single combined message.
|
/// the resulting lock change with a single combined message.
|
||||||
/// Used by the dashboard's "update meta inputs" form so the
|
/// 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]
|
#[test]
|
||||||
fn render_flake_uses_explicit_nixpkgs_url_when_provided() {
|
fn render_flake_uses_explicit_nixpkgs_url_when_provided() {
|
||||||
let out = render_flake(
|
let out = render_flake(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue