lifecycle: setup_applied seeds via fetch + tags deployed/0
new shape: applied is git-init'd at first spawn, fetches proposed's initial commit into its main, tags deployed/0 there. the wrapper flake.nix is regenerated on every spawn/rebuild but no longer tracked — apply churn vanishes, manager-authored files in the proposal flow now survive untouched. setup_applied gains an Option<&Path> for proposed (None on rebuild paths that just refresh the flake). pre-overhaul applied dirs are detected via the missing deployed/0 tag and bail loudly with the destroy --purge migration hint. apply_commit is stubbed with a clear error until the tag-driven approve flow lands.
This commit is contained in:
parent
63ef69674b
commit
8cb8fcedad
1 changed files with 77 additions and 58 deletions
|
|
@ -152,7 +152,14 @@ pub async fn spawn(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setup_proposed(proposed_dir, name).await?;
|
setup_proposed(proposed_dir, name).await?;
|
||||||
setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?;
|
setup_applied(
|
||||||
|
applied_dir,
|
||||||
|
Some(proposed_dir),
|
||||||
|
name,
|
||||||
|
hyperhive_flake,
|
||||||
|
dashboard_port,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
ensure_claude_dir(claude_dir)?;
|
ensure_claude_dir(claude_dir)?;
|
||||||
ensure_state_dir(notes_dir)?;
|
ensure_state_dir(notes_dir)?;
|
||||||
let container = container_name(name);
|
let container = container_name(name);
|
||||||
|
|
@ -230,7 +237,7 @@ pub async fn rebuild(
|
||||||
agent_web_port(name)
|
agent_web_port(name)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setup_applied(applied_dir, name, hyperhive_flake, dashboard_port).await?;
|
setup_applied(applied_dir, None, name, hyperhive_flake, dashboard_port).await?;
|
||||||
ensure_claude_dir(claude_dir)?;
|
ensure_claude_dir(claude_dir)?;
|
||||||
ensure_state_dir(notes_dir)?;
|
ensure_state_dir(notes_dir)?;
|
||||||
let container = container_name(name);
|
let container = container_name(name);
|
||||||
|
|
@ -286,11 +293,40 @@ pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Maintain the authoritative applied repo. Rewrites `flake.nix` every call
|
/// Placeholder for the old file-copy apply path; the real
|
||||||
/// (so a new hyperhive flake URL propagates on rebuild); seeds `agent.nix`
|
/// tag-driven flow lives in `actions::approve` and gets wired up
|
||||||
/// only on first call. `apply_commit` overwrites `agent.nix` later.
|
/// in a follow-up commit. Leaving this function as a hard error
|
||||||
|
/// keeps `actions.rs` compiling while the rewrite lands; an
|
||||||
|
/// ApplyCommit approval that races the deploy will surface a
|
||||||
|
/// clear failure note instead of silently no-op'ing.
|
||||||
|
#[allow(unused_variables)]
|
||||||
|
pub async fn apply_commit(
|
||||||
|
_applied_dir: &Path,
|
||||||
|
_proposed_dir: &Path,
|
||||||
|
_commit_ref: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
bail!(
|
||||||
|
"apply_commit not yet wired up to the tag-driven flow; \
|
||||||
|
approve again after the next deploy lands"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set up the applied repo. Two responsibilities:
|
||||||
|
/// - First-spawn only: init the repo, pull proposed's initial commit
|
||||||
|
/// in via `git fetch`, tag it `deployed/0`. This is the *only* time
|
||||||
|
/// hive-c0re reads from `proposed` for an agent — subsequent
|
||||||
|
/// proposals are fetched at `request_apply_commit` time and tagged
|
||||||
|
/// `proposal/<id>` (see `actions::approve` for the tag state
|
||||||
|
/// machine).
|
||||||
|
/// - Every call: regenerate the untracked `flake.nix` so flake-url /
|
||||||
|
/// dashboard-port changes pick up on rebuild without churning the
|
||||||
|
/// git log.
|
||||||
|
///
|
||||||
|
/// `proposed_dir` is `None` on rebuild paths that just want the flake
|
||||||
|
/// refreshed.
|
||||||
pub async fn setup_applied(
|
pub async fn setup_applied(
|
||||||
applied_dir: &Path,
|
applied_dir: &Path,
|
||||||
|
proposed_dir: Option<&Path>,
|
||||||
name: &str,
|
name: &str,
|
||||||
hyperhive_flake: &str,
|
hyperhive_flake: &str,
|
||||||
dashboard_port: u16,
|
dashboard_port: u16,
|
||||||
|
|
@ -298,6 +334,42 @@ pub async fn setup_applied(
|
||||||
std::fs::create_dir_all(applied_dir)
|
std::fs::create_dir_all(applied_dir)
|
||||||
.with_context(|| format!("create {}", applied_dir.display()))?;
|
.with_context(|| format!("create {}", applied_dir.display()))?;
|
||||||
|
|
||||||
|
// 1. First-spawn git init from proposed (or pre-overhaul detection).
|
||||||
|
if !applied_dir.join(".git").exists() {
|
||||||
|
let Some(proposed) = proposed_dir else {
|
||||||
|
bail!(
|
||||||
|
"applied repo at {} is missing its .git directory; \
|
||||||
|
cannot rebuild without a proposed source to seed from. \
|
||||||
|
destroy --purge and re-spawn this agent.",
|
||||||
|
applied_dir.display()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
git(applied_dir, &["init", "--initial-branch=main"]).await?;
|
||||||
|
let proposed_str = proposed.display().to_string();
|
||||||
|
git(
|
||||||
|
applied_dir,
|
||||||
|
&["fetch", "--no-tags", &proposed_str, "main:refs/heads/main"],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
git_read_tree_reset(applied_dir, "refs/heads/main").await?;
|
||||||
|
git_tag(applied_dir, "deployed/0", "refs/heads/main").await?;
|
||||||
|
} else if git_rev_parse(applied_dir, "refs/tags/deployed/0")
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
// Pre-overhaul applied repo — agent.nix is tracked directly,
|
||||||
|
// commits authored by hive-c0re, no deployed/* tag scheme.
|
||||||
|
// No in-place migration; fail loudly so the operator purges.
|
||||||
|
bail!(
|
||||||
|
"applied repo at {} predates the tag-driven config flow. \
|
||||||
|
Run `hive-c0re destroy --purge {name}` and re-spawn.",
|
||||||
|
applied_dir.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. (Re)write the untracked wrapper flake. Tracked files in the
|
||||||
|
// working tree (agent.nix and anything the manager committed) are
|
||||||
|
// untouched.
|
||||||
let port = agent_web_port(name);
|
let port = agent_web_port(name);
|
||||||
let base = flake_base(name);
|
let base = flake_base(name);
|
||||||
let service = if is_manager(name) {
|
let service = if is_manager(name) {
|
||||||
|
|
@ -339,48 +411,6 @@ pub async fn setup_applied(
|
||||||
);
|
);
|
||||||
std::fs::write(applied_dir.join("flake.nix"), flake_body)
|
std::fs::write(applied_dir.join("flake.nix"), flake_body)
|
||||||
.with_context(|| format!("write {}/flake.nix", applied_dir.display()))?;
|
.with_context(|| format!("write {}/flake.nix", applied_dir.display()))?;
|
||||||
|
|
||||||
let agent_path = applied_dir.join("agent.nix");
|
|
||||||
if !agent_path.exists() {
|
|
||||||
std::fs::write(&agent_path, initial_agent_nix(name))
|
|
||||||
.with_context(|| format!("write {}", agent_path.display()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !applied_dir.join(".git").exists() {
|
|
||||||
git(applied_dir, &["init", "--initial-branch=main"]).await?;
|
|
||||||
}
|
|
||||||
git(applied_dir, &["add", "-A"]).await?;
|
|
||||||
let clean = git_status(applied_dir, &["diff", "--cached", "--quiet"]).await?;
|
|
||||||
if !clean {
|
|
||||||
git_commit(applied_dir, "hive-c0re sync").await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Apply a manager-proposed commit: read `agent.nix` at `commit_ref` from the
|
|
||||||
/// proposed repo, write it into the applied repo, commit. Hive-c0re alone
|
|
||||||
/// advances `applied`'s `main`; the manager only sees `proposed/`.
|
|
||||||
pub async fn apply_commit(applied_dir: &Path, proposed_dir: &Path, commit_ref: &str) -> Result<()> {
|
|
||||||
let out = git_command()
|
|
||||||
.current_dir(proposed_dir)
|
|
||||||
.args(["show", &format!("{commit_ref}:agent.nix")])
|
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("git show in {}", proposed_dir.display()))?;
|
|
||||||
if !out.status.success() {
|
|
||||||
bail!(
|
|
||||||
"agent.nix at commit {commit_ref} not found in {}: {}",
|
|
||||||
proposed_dir.display(),
|
|
||||||
String::from_utf8_lossy(&out.stderr).trim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
std::fs::write(applied_dir.join("agent.nix"), &out.stdout)
|
|
||||||
.with_context(|| format!("write {}/agent.nix", applied_dir.display()))?;
|
|
||||||
git(applied_dir, &["add", "agent.nix"]).await?;
|
|
||||||
let clean = git_status(applied_dir, &["diff", "--cached", "--quiet"]).await?;
|
|
||||||
if !clean {
|
|
||||||
git_commit(applied_dir, &format!("apply {commit_ref}")).await?;
|
|
||||||
}
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -550,17 +580,6 @@ pub async fn git_update_ref(dir: &Path, refname: &str, target: &str) -> Result<(
|
||||||
git(dir, &["update-ref", refname, target]).await
|
git(dir, &["update-ref", refname, target]).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if the command exits 0.
|
|
||||||
async fn git_status(dir: &Path, args: &[&str]) -> Result<bool> {
|
|
||||||
let st = git_command()
|
|
||||||
.current_dir(dir)
|
|
||||||
.args(args)
|
|
||||||
.status()
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("git {} in {}", args.join(" "), dir.display()))?;
|
|
||||||
Ok(st.success())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Write a systemd drop-in for `container@<container>.service` that applies
|
/// Write a systemd drop-in for `container@<container>.service` that applies
|
||||||
/// our default resource caps. Goes under `/run/systemd/system/...` so it's
|
/// our default resource caps. Goes under `/run/systemd/system/...` so it's
|
||||||
/// ephemeral (regenerated on every spawn / rebuild).
|
/// ephemeral (regenerated on every spawn / rebuild).
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue