Watch
0
0
Fork
You've already forked hyperhive
0

remove the legacy meta repo's .git and the meta remotes at startup

This commit is contained in:
atlas 2026-10-25 19:22:21 +01:00 • committed by mara
commit 0f4566c8d3
4 changed files with 110 additions and 5 deletions

View file

@ -338,6 +338,10 @@ once already applied:
repo, and refreshes the files that follow the agent set repo, and refreshes the files that follow the agent set
(`agent_set::refresh`) from the current agent list. Set (`agent_set::refresh`) from the current agent list. Set
`HIVE_SKIP_META_MIGRATION=1` on the service to defer this phase. `HIVE_SKIP_META_MIGRATION=1` on the service to defer this phase.
- **Legacy meta repo**: deletes the leftover `.git` in
`/var/lib/hyperhive/meta/` and the `meta` remote in each proposed repo.
The files beside the `.git` stay. The `core/meta` repo on the forge is
not touched.
No state loss in either migration: claude creds, `/state/` notes, the No state loss in either migration: claude creds, `/state/` notes, the
events DB, and both proposed + applied history all survive. The root events DB, and both proposed + applied history all survive. The root

View file

@ -388,8 +388,9 @@ here from merged config PRs) + any other files those PRs commit.
denied tag history. denied tag history.
Under `/var/lib/hyperhive/meta/` — system-level config files, plain state Under `/var/lib/hyperhive/meta/` — system-level config files, plain state
with no git history that anything reads. The flake agents build from is the with no git history. The flake agents build from is the
swarm's `internal/meta`, not here. Contents: swarm's `internal/meta`, not here. A hive that predates this finds a
leftover `.git` here; `migrate::run` deletes it at startup. Contents:
- `topology.json` — the agent roster (`["alice", "bob", "ruth"]`). - `topology.json` — the agent roster (`["alice", "bob", "ruth"]`).
Written by `topology::reconcile` from `agent_set::refresh`; read by Written by `topology::reconcile` from `agent_set::refresh`; read by

View file

@ -320,8 +320,8 @@ The hive keeps no flake of its own. `meta_rev.rs` picks the `internal/meta`
commit an agent builds from and records it; `agent_set.rs` keeps the commit an agent builds from and records it; `agent_set.rs` keeps the
host-side files that follow the agent set (`topology.json`, the gateway's host-side files that follow the agent set (`topology.json`, the gateway's
agent-socket map and nginx include) in line with the live agents, and agent-socket map and nginx include) in line with the live agents, and
`meta.rs` keeps the one remaining commit to the legacy `meta/` git repo, the `meta.rs` removes the permission seed files once the swarm has them, and
removal of the permission seed files. `migrate.rs` deletes the legacy `meta/` repo's leftover `.git`.
--- ---

View file

@ -1,5 +1,6 @@
//! Startup convergence. Three phases, all idempotent and unguarded: //! Startup convergence. Three phases, all idempotent and unguarded:
//! harness files, applied + proposed repos, the agent-set files. They re-run every //! harness files, applied + proposed repos, the agent-set files, plus the
//! removal of the legacy meta repo's leftovers. They re-run every
//! boot on purpose — each one is a no-op once its state is already //! boot on purpose — each one is a no-op once its state is already
//! correct. //! correct.
//! //!
@ -68,6 +69,12 @@ pub async fn run() -> Result<()> {
migrate_harness_files(name); migrate_harness_files(name);
} }
match remove_legacy_meta_git(&crate::paths::meta_root()) {
Ok(true) => tracing::info!("migration: removed the legacy meta repo's .git"),
Ok(false) => {}
Err(e) => tracing::warn!(error = ?e, "migration: removing the legacy meta .git failed"),
}
// Phase 1 + 2: per-agent applied + proposed. // Phase 1 + 2: per-agent applied + proposed.
tracing::debug!("migration: phase 1+2 (applied + proposed repos)"); tracing::debug!("migration: phase 1+2 (applied + proposed repos)");
// An archetype agent has neither repo, and seeding one here would hand it // An archetype agent has neither repo, and seeding one here would hand it
@ -85,6 +92,13 @@ pub async fn run() -> Result<()> {
tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed"); tracing::warn!(%name, error = ?e, "migration: applied repo rewrite failed");
} }
let proposed_dir = Coordinator::agent_proposed_dir(name); let proposed_dir = Coordinator::agent_proposed_dir(name);
match remove_meta_remote(&proposed_dir).await {
Ok(true) => tracing::info!(%name, "migration: removed the legacy meta remote"),
Ok(false) => {}
Err(e) => {
tracing::warn!(%name, error = ?e, "migration: removing the meta remote failed");
}
}
let proposed = lifecycle::setup_proposed(&proposed_dir, name.as_str()); let proposed = lifecycle::setup_proposed(&proposed_dir, name.as_str());
match tokio::time::timeout(GIT_TIMEOUT, proposed).await { match tokio::time::timeout(GIT_TIMEOUT, proposed).await {
Ok(Err(e)) => tracing::warn!(%name, error = ?e, "migration: setup_proposed failed"), Ok(Err(e)) => tracing::warn!(%name, error = ?e, "migration: setup_proposed failed"),
@ -148,6 +162,31 @@ fn migrate_harness_files(name: &hive_types::Ident) {
} }
} }
/// Remove the `.git` of the legacy per-hive meta repo from `meta`, leaving
/// the plain state beside it (`topology.json`, the swarm limits, any
/// permission seed file not yet imported). `true` when it was there.
fn remove_legacy_meta_git(meta: &Path) -> Result<bool> {
let git_dir = meta.join(".git");
match std::fs::remove_dir_all(&git_dir) {
Ok(()) => Ok(true),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(false),
Err(e) => Err(e).with_context(|| format!("removing {}", git_dir.display())),
}
}
/// Remove the `meta` remote from the proposed repo at `dir`. `true` when it
/// was there; a missing remote, or a `dir` that is no repo, is `false`.
async fn remove_meta_remote(dir: &Path) -> Result<bool> {
let mut cmd = lifecycle::git_command();
cmd.current_dir(dir).args(["remote", "get-url", "meta"]);
let probe = output_with_timeout(cmd, GIT_TIMEOUT, "git remote get-url meta").await?;
if !probe.status.success() {
return Ok(false);
}
raw_git(dir, &["remote", "remove", "meta"]).await?;
Ok(true)
}
async fn enumerate_agents() -> Result<Vec<hive_types::Ident>> { async fn enumerate_agents() -> Result<Vec<hive_types::Ident>> {
let containers = lifecycle::list().await?; let containers = lifecycle::list().await?;
Ok(containers Ok(containers
@ -248,6 +287,67 @@ async fn raw_git(dir: &Path, args: &[&str]) -> Result<()> {
mod tests { mod tests {
use super::*; use super::*;
const STATE_FILES: [&str; 3] = [
"topology.json",
"swarm-resource-limits.json",
"capabilities.json",
];
#[test]
fn legacy_meta_git_goes_and_the_state_beside_it_stays() {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::create_dir_all(dir.path().join(".git/objects")).expect("mkdir .git");
for name in STATE_FILES {
std::fs::write(dir.path().join(name), "{}").expect("write state");
}
assert!(remove_legacy_meta_git(dir.path()).expect("first"));
assert!(!dir.path().join(".git").exists());
for name in STATE_FILES {
assert!(dir.path().join(name).exists(), "{name} stays");
}
assert!(
!remove_legacy_meta_git(dir.path()).expect("second"),
"no-op"
);
assert!(dir.path().join("topology.json").exists());
}
#[tokio::test]
async fn meta_remote_goes_and_the_others_stay() {
let dir = tempfile::tempdir().expect("tempdir");
raw_git(dir.path(), &["init", "-q", "--initial-branch=main"])
.await
.expect("init");
for (name, url) in [
("meta", "http://forge/core/meta.git"),
("applied", "/applied/x/.git"),
] {
raw_git(dir.path(), &["remote", "add", name, url])
.await
.expect("add remote");
}
assert!(remove_meta_remote(dir.path()).await.expect("first"));
let remotes = lifecycle::git_command()
.current_dir(dir.path())
.arg("remote")
.output()
.await
.expect("list");
assert_eq!(String::from_utf8_lossy(&remotes.stdout).trim(), "applied");
assert!(
!remove_meta_remote(dir.path()).await.expect("second"),
"no-op"
);
}
#[tokio::test]
async fn a_dir_that_is_no_repo_has_no_meta_remote() {
let dir = tempfile::tempdir().expect("tempdir");
assert!(!remove_meta_remote(dir.path()).await.expect("probe"));
}
#[tokio::test] #[tokio::test]
async fn unreadable_flake_is_left_untouched() { async fn unreadable_flake_is_left_untouched() {
let dir = tempfile::tempdir().expect("tempdir"); let dir = tempfile::tempdir().expect("tempdir");