122 lines
4.8 KiB
Rust
122 lines
4.8 KiB
Rust
//! Repo plumbing on the local Forgejo: seeding an agent's `applied` repo from
|
|
//! `agent-configs/<name>`. 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};
|
|
|
|
/// Seed an agent's `applied` repo, missing its `.git` entirely (first spawn,
|
|
/// or a revival after `destroy --purge`), by fetching `agent-configs/<name>`'s
|
|
/// `main` from the forge. Forge `main` advances with every operator-merged
|
|
/// config PR, so this reflects the last merged config. 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), the
|
|
/// forge has no `main` for the agent yet (the normal case right after
|
|
/// `swarm-controller` creates it, before it has pushed anything), or any git
|
|
/// step fails. Callers fall back to the template or the original hard error. A
|
|
/// failed seed leaves the directory empty, so a later "is it seeded" check reads
|
|
/// the same as before the attempt. Only `true` once `applied` is seeded and
|
|
/// tagged `deployed/0`.
|
|
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;
|
|
}
|
|
let seeded = async {
|
|
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) => {
|
|
// `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.
|
|
tracing::info!(
|
|
%name,
|
|
stderr = %String::from_utf8_lossy(&o.stderr).trim(),
|
|
"forge: no config to fetch for this agent yet"
|
|
);
|
|
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: seeded applied repo from agent-configs main");
|
|
true
|
|
}
|
|
.await;
|
|
if !seeded {
|
|
clear_dir(&applied);
|
|
}
|
|
seeded
|
|
}
|
|
|
|
/// Empty `dir`, keeping the directory itself: a failed seed may have written
|
|
/// the working tree as well as the `.git`, and the template fallback must start
|
|
/// from nothing rather than adopt files that were meant for the forge's commit.
|
|
fn clear_dir(dir: &Path) {
|
|
let Ok(entries) = std::fs::read_dir(dir) else {
|
|
return;
|
|
};
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
let _ = if path.is_dir() {
|
|
std::fs::remove_dir_all(path)
|
|
} else {
|
|
std::fs::remove_file(path)
|
|
};
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::clear_dir;
|
|
|
|
#[test]
|
|
fn clear_dir_empties_a_dir_but_keeps_it() {
|
|
let dir = tempfile::tempdir().expect("tempdir");
|
|
std::fs::create_dir(dir.path().join(".git")).expect("mkdir .git");
|
|
std::fs::write(dir.path().join(".git/HEAD"), "ref").expect("write HEAD");
|
|
std::fs::write(dir.path().join("agent.nix"), "{}").expect("write agent.nix");
|
|
|
|
clear_dir(dir.path());
|
|
|
|
assert!(dir.path().is_dir(), "the directory itself stays");
|
|
assert_eq!(std::fs::read_dir(dir.path()).expect("read_dir").count(), 0);
|
|
}
|
|
}
|