From ba1515aa5e5906adb55055a121cd6c7d7d2cbc98 Mon Sep 17 00:00:00 2001 From: atlas Date: Fri, 16 Oct 2026 13:46:05 +0200 Subject: [PATCH] migrate: an unreadable applied flake.nix fails that repo's migration instead of being overwritten Only a missing file counts as 'marker absent'; any other read error (EACCES, EIO, non-UTF-8) used to read as empty, so the agent's flake.nix was replaced and committed. --- hive-c0re/src/migrate.rs | 39 ++++++++++++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index a6f926fd..29bce958 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -168,19 +168,29 @@ async fn enumerate_agents() -> Result> { } async fn migrate_applied_repo(name: &str) -> Result<()> { - let dir = crate::paths::applied_dir(name); + migrate_applied_dir(name, &crate::paths::applied_dir(name)).await +} + +/// Rewrites the applied repo at `dir` to the module-only flake. A missing +/// `flake.nix` counts as "marker absent"; any other read error fails the +/// migration so the agent's file is never overwritten unread. +async fn migrate_applied_dir(name: &str, dir: &Path) -> Result<()> { if !dir.join(".git").exists() { return Ok(()); } let flake_path = dir.join("flake.nix"); - let cur = std::fs::read_to_string(&flake_path).unwrap_or_default(); + let cur = match std::fs::read_to_string(&flake_path) { + Ok(cur) => cur, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(), + Err(e) => return Err(e).with_context(|| format!("read {}", flake_path.display())), + }; if cur.contains(MODULE_FLAKE_MARKER) { return Ok(()); } let want = lifecycle::initial_flake_nix(); std::fs::write(&flake_path, want).with_context(|| format!("write {}", flake_path.display()))?; raw_git( - &dir, + dir, &[ "-c", "user.name=c0re", @@ -192,7 +202,7 @@ async fn migrate_applied_repo(name: &str) -> Result<()> { ) .await?; raw_git( - &dir, + dir, &[ "-c", "user.name=c0re", @@ -206,7 +216,7 @@ async fn migrate_applied_repo(name: &str) -> Result<()> { .await?; // Relocate deployed/0 to the migration commit so // setup_applied's existence check passes. - raw_git(&dir, &["tag", "-f", "deployed/0", "HEAD"]).await?; + raw_git(dir, &["tag", "-f", "deployed/0", "HEAD"]).await?; tracing::info!(%name, "migration: applied repo migrated to module-only flake"); Ok(()) } @@ -241,3 +251,22 @@ async fn raw_git(dir: &Path, args: &[&str]) -> Result<()> { } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn unreadable_flake_is_left_untouched() { + let dir = tempfile::tempdir().expect("tempdir"); + std::fs::create_dir(dir.path().join(".git")).expect("mkdir .git"); + let flake = dir.path().join("flake.nix"); + let bytes = [0xff_u8, 0xfe, b'x']; + std::fs::write(&flake, bytes).expect("write flake"); + + let res = migrate_applied_dir("t", dir.path()).await; + + assert!(res.is_err(), "non-UTF-8 flake.nix must fail the migration"); + assert_eq!(std::fs::read(&flake).expect("read back"), bytes); + } +}