//! Repo plumbing on the local Forgejo: reseeding an agent's `applied` and //! `proposed` repos from `agent-configs/`. The typed API-client //! constructor + org-name constants live in the module root (`super`). use std::path::Path; use super::{CONFIG_ORG, forge_git_url, reader_token}; /// Reseed a revived agent's `applied` repo — missing its `.git` entirely, /// e.g. after `destroy --purge` — by fetching `agent-configs/`'s /// `main` from the forge. Unlike `proposed` (seeded once at first spawn and /// never touched again — see `lifecycle::setup_proposed`'s own doc /// comment), forge `main` advances with every operator-merged config PR, so /// this reflects the last merged config rather than a stale creation-time /// snapshot. A config deployed through hive-c0re without a forge merge never /// reaches it. /// /// Best-effort: returns `false` (not an error) rather than bailing when there /// is no [`reader_token`], `applied` already has a `.git` (nothing to do), or /// any git step fails — callers fall back to a less-current source or the /// original hard error. Only `true` once `applied` is /// actually seeded and tagged `deployed/0`, mirroring first-spawn's own /// `setup_applied` seed shape exactly. pub async fn fetch_config_main_into_applied(name: &str) -> bool { let Some(reader) = reader_token().await else { return false; }; let applied = crate::paths::applied_dir(name); if applied.join(".git").exists() { return false; } if let Err(e) = std::fs::create_dir_all(&applied) { tracing::warn!(%name, error = ?e, "forge: applied reseed mkdir failed"); return false; } if let Err(e) = crate::lifecycle::git(&applied, &["init", "--initial-branch=main"]).await { tracing::warn!(%name, error = ?e, "forge: applied reseed init failed"); return false; } let url = forge_git_url(&format!("{CONFIG_ORG}/{name}")); let auth = reader.auth_header(); let out = crate::lifecycle::git_command_authed(&auth) .current_dir(&applied) .args([ "fetch", "--no-tags", "--update-head-ok", &url, "refs/heads/main:refs/heads/main", ]) .output() .await; match out { Ok(o) if o.status.success() => {} Ok(o) => { tracing::warn!( %name, stderr = %String::from_utf8_lossy(&o.stderr).trim(), "forge: applied reseed fetch failed" ); return false; } Err(e) => { tracing::warn!(%name, error = ?e, "forge: applied reseed fetch failed"); return false; } } if let Err(e) = crate::lifecycle::git_read_tree_reset(&applied, "refs/heads/main").await { tracing::warn!(%name, error = ?e, "forge: applied reseed read-tree failed"); return false; } if let Err(e) = crate::lifecycle::git_tag(&applied, "deployed/0", "refs/heads/main").await { tracing::warn!(%name, error = ?e, "forge: applied reseed tag failed"); return false; } tracing::info!(%name, "forge: reseeded applied repo from agent-configs main"); true } /// Seed an agent's `proposed` repo from `agent-configs/` on the forge. /// /// When the swarm creates an agent it writes that agent's config to the forge /// before any hive is told to deploy it, so the hive's job here is to **take** /// that config rather than author a second one beside it. The local template /// is the fallback, not the default. /// /// Best-effort like [`fetch_config_main_into_applied`], and `false` is an /// ordinary answer rather than an error: there is no [`reader_token`], the /// forge is unreachable, or `agent-configs/` has no `main` — the last of /// which is the *normal* case right after `swarm-controller` creates the /// agent, before it has pushed anything into the repo it just made. /// /// `clone` rather than `init` + `fetch`: git removes a directory it created /// when the clone fails, and cloning into an existing empty dir leaves no /// `.git` behind either — so the caller's "does this repo already exist" /// check reads exactly the same before and after a failed attempt. pub async fn clone_config_into_proposed(dir: &Path, name: &str) -> bool { let Some(reader) = reader_token().await else { return false; }; let url = forge_git_url(&format!("{CONFIG_ORG}/{name}")); // `--branch main` fails outright against a repo with no commits, so an // empty repo reads as "nothing to take" instead of cloning to an unborn // HEAD that would then look like a seeded checkout. let out = crate::lifecycle::git_command_authed(&reader.auth_header()) .args([ "clone", "--branch", "main", &url, &dir.display().to_string(), ]) .output() .await; match out { Ok(o) if o.status.success() => { tracing::info!(%name, "forge: seeded proposed repo from agent-configs main"); true } Ok(o) => { // `info`, not `warn`: the normal case right after // `swarm-controller` creates the agent, before it has pushed // anything into the repo it just made, and nothing is wrong. // Logged at all because "where did this agent's config come // from" is worth answering from the journal, and proposed is // seeded exactly once. tracing::info!( %name, stderr = %String::from_utf8_lossy(&o.stderr).trim(), "forge: no config to clone for this agent; seeding the template" ); false } Err(e) => { tracing::warn!(%name, error = ?e, "forge: proposed clone failed to run"); false } } }