feat(#2502): re-lock deploy + rebuild from local applied, not forge
The persistent agent-<n> input url in the meta flake is now the forge config repo, but the deploy + manual-rebuild re-lock must not follow it: prepare_deploy and lock_update_for_rebuild override the input back to the local applied mirror (git+file://<applied_dir>, main head) via --override-input, which writes the applied rev into flake.lock. This keeps the forge as the declared, reviewable source while: - surviving forge unreachability (rebuilds fire on crash-restart and meta bumps too, not just config PRs -- coupling every rebuild to forge would be a regression), - deploying exactly the reviewed head applied/<n>/main was fast-forwarded to (no TOCTOU on a newer forge head merging mid-deploy), - reusing verify_commit's local-override pattern so verify and deploy eval the same source. New applied_override_url helper + unit test.
This commit is contained in:
parent
b806aa253e
commit
27a099bb5b
1 changed files with 64 additions and 2 deletions
|
|
@ -238,7 +238,29 @@ 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_logged(&dir, &["flake", "update", &input], name, "prepare-deploy").await?;
|
||||
// Re-lock the agent input against the LOCAL applied mirror, not the
|
||||
// persistent forge URL declared in the meta flake (see the `## Meta flake`
|
||||
// note in docs/approvals.md): the deploy must build the exact reviewed
|
||||
// config that `verify_commit` gated and `applied/<n>/main` was
|
||||
// fast-forwarded to, and it must keep working when the forge is
|
||||
// unreachable (rebuilds fire on crash-restart / meta bumps too, not just
|
||||
// config PRs). `--override-input` writes the applied rev into the lock;
|
||||
// the forge URL stays the declared, reviewable source of truth.
|
||||
let applied = applied_override_url(&crate::paths::applied_dir(name));
|
||||
nix_logged(
|
||||
&dir,
|
||||
&[
|
||||
"flake",
|
||||
"update",
|
||||
&input,
|
||||
"--override-input",
|
||||
&input,
|
||||
&applied,
|
||||
],
|
||||
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.
|
||||
|
|
@ -281,7 +303,22 @@ pub async fn lock_update_for_rebuild(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?;
|
||||
// Re-lock from the local applied mirror, not the persistent forge URL —
|
||||
// same rationale as `prepare_deploy`: build exactly `applied/<n>/main` and
|
||||
// stay reproducible when the forge is unreachable.
|
||||
let applied = applied_override_url(&crate::paths::applied_dir(name));
|
||||
nix(
|
||||
&dir,
|
||||
&[
|
||||
"flake",
|
||||
"update",
|
||||
&input,
|
||||
"--override-input",
|
||||
&input,
|
||||
&applied,
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
if !paths_dirty(&dir, &["flake.lock"]).await? {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -301,6 +338,17 @@ fn agent_input_override(applied_dir: &Path, sha: &str) -> String {
|
|||
format!("git+file://{}?rev={sha}", applied_dir.display())
|
||||
}
|
||||
|
||||
/// `--override-input` URL re-locking an agent's config input against its
|
||||
/// LOCAL applied mirror (`git+file://<applied_dir>`, current `main` head)
|
||||
/// instead of the persistent forge URL declared in the meta flake. The
|
||||
/// deploy + rebuild paths re-lock from here: the applied tree was already
|
||||
/// fast-forwarded to the reviewed head, so this builds exactly that config
|
||||
/// and stays reproducible when the forge is unreachable. No `?rev` — `main`
|
||||
/// head is the reviewed head at deploy time. Pure so it's unit-testable.
|
||||
fn applied_override_url(applied_dir: &Path) -> String {
|
||||
format!("git+file://{}", 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
|
||||
|
|
@ -1463,6 +1511,20 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn applied_override_url_targets_local_mirror_main_head() {
|
||||
// Deploy + rebuild re-lock against the local applied mirror's `main`
|
||||
// head (no `?rev`), never the persistent forge URL — so a rebuild
|
||||
// survives forge unreachability and builds the fast-forwarded config.
|
||||
let p = Path::new("/var/lib/hyperhive/agents/iris/applied");
|
||||
let url = applied_override_url(p);
|
||||
assert_eq!(url, "git+file:///var/lib/hyperhive/agents/iris/applied");
|
||||
assert!(
|
||||
!url.contains("?rev="),
|
||||
"must lock main head, not a pinned rev: {url}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_flake_uses_explicit_nixpkgs_url_when_provided() {
|
||||
let out = render_flake(
|
||||
|
|
|
|||
Loading…
Reference in a new issue