From bbc6174361f854b8cfa77c0cb6159201e88b5369 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 24 Oct 2026 19:20:37 +0200 Subject: [PATCH] leave an agent with no config commit out of internal/meta; read config heads from main on deploy, one agent whose agent-configs repo has no commit on main is skipped and logged instead of failing the whole internal/meta render, so the other agents' pin moves go through. the controller reads each config head from main, the branch hive-c0re compares against, not the repo's default branch. --- docs/swarm/builder.md | 3 +- swarm-controller/src/forge.rs | 28 +++++++-- swarm-controller/src/forge/meta_repo.rs | 3 +- swarm-controller/src/meta_flake.rs | 80 +++++++++++++++++++------ 4 files changed, 90 insertions(+), 24 deletions(-) diff --git a/docs/swarm/builder.md b/docs/swarm/builder.md index 6de55d57..506657f3 100644 --- a/docs/swarm/builder.md +++ b/docs/swarm/builder.md @@ -127,7 +127,8 @@ commits nothing. swarm-controller sends the request. It renders `internal/meta`'s `flake.nix` with the same renderer hive-c0re uses (`hive-meta-flake`), all over `https`. Each custom agent that a hive places pins to its `agent-configs/` -`main`. Hyperhive pins to one rev, a ratchet on the controller's own +`main`; the controller omits and logs one whose repo has no commit on `main`, so it +doesn't block the rest. Hyperhive pins to one rev, a ratchet on the controller's own running rev (`services.hyperhive.c0re.hyperhiveRev`, from `self.rev`): - the first commit pins hyperhive's forge `main`; diff --git a/swarm-controller/src/forge.rs b/swarm-controller/src/forge.rs index 42cec22f..7685dc7a 100644 --- a/swarm-controller/src/forge.rs +++ b/swarm-controller/src/forge.rs @@ -158,8 +158,8 @@ pub struct PullForReview { pub narrowed: Option, } -/// Files on a repo's default branch, as [`Client::default_branch_files`] reads -/// them. +/// Files on a repo's branch, as [`Client::default_branch_files`] and +/// [`Client::branch_files`] read them. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DefaultBranchFiles { pub branch: String, @@ -1444,6 +1444,21 @@ impl Client { owner: &str, repo: &str, paths: &[&str], + ) -> Result> { + self.branch_files(owner, repo, None, paths).await + } + + /// [`Self::default_branch_files`] on `branch`, the default branch when it is + /// `None`. `None` as well when the branch has no commit. + /// + /// # Errors + /// As [`Self::default_branch_files`]. + pub async fn branch_files( + &self, + owner: &str, + repo: &str, + branch: Option<&str>, + paths: &[&str], ) -> Result> { let info = self .api @@ -1453,9 +1468,12 @@ impl Client { if info.empty.unwrap_or(false) { return Ok(None); } - let branch = info - .default_branch - .with_context(|| format!("{owner}/{repo} names no default branch"))?; + let branch = match branch { + Some(branch) => branch.to_owned(), + None => info + .default_branch + .with_context(|| format!("{owner}/{repo} names no default branch"))?, + }; let head = match self .api .call(|api| api.repo_get_branch(owner, repo, &branch).into_future()) diff --git a/swarm-controller/src/forge/meta_repo.rs b/swarm-controller/src/forge/meta_repo.rs index 8b5ef6d4..b711e5fb 100644 --- a/swarm-controller/src/forge/meta_repo.rs +++ b/swarm-controller/src/forge/meta_repo.rs @@ -17,9 +17,10 @@ impl MetaForge for Client { &self, owner: &str, repo: &str, + branch: Option<&str>, paths: &[&str], ) -> Result> { - self.default_branch_files(owner, repo, paths).await + self.branch_files(owner, repo, branch, paths).await } async fn ensure_meta_repo(&self) -> Result<()> { diff --git a/swarm-controller/src/meta_flake.rs b/swarm-controller/src/meta_flake.rs index 76ccb4db..518835ad 100644 --- a/swarm-controller/src/meta_flake.rs +++ b/swarm-controller/src/meta_flake.rs @@ -21,6 +21,10 @@ use swarm_wire::builder::{LOCK_REPLY_DEADLINE, LOCK_SUBJECT, LockMeta, LockReply use crate::forge::{CONFIG_ORG, DefaultBranchFiles}; +/// The branch an agent's config is deployed from, which hive-c0re's +/// `internal_meta` reads the config head from too. +const CONFIG_BRANCH: &str = "main"; + /// The repo in `internal` the flake is committed to. pub(crate) const META_REPO: &str = "meta"; @@ -123,10 +127,13 @@ pub(crate) struct MetaFile { /// The forge, as [`update`] reads and writes it. pub(crate) trait MetaForge { + /// The `paths` of `owner/repo` at the head of `branch`, the default + /// branch when it is `None`; `None` when the branch has no commit. fn head_files( &self, owner: &str, repo: &str, + branch: Option<&str>, paths: &[&str], ) -> impl Future>> + Send; /// The number of commits `head` has that `base` lacks, as the forge's @@ -229,6 +236,7 @@ pub(crate) async fn update( .head_files( crate::forge::objects::SHARED_ORG, META_REPO, + None, &["flake.nix", "flake.lock"], ) .await?; @@ -309,7 +317,7 @@ async fn hyperhive_pin( ) -> Result { let Some(flake) = committed_flake else { let head = forge - .head_files(HYPERHIVE_OWNER, HYPERHIVE_REPO, &[]) + .head_files(HYPERHIVE_OWNER, HYPERHIVE_REPO, None, &[]) .await? .with_context(|| format!("{HYPERHIVE_OWNER}/{HYPERHIVE_REPO} is empty or absent"))?; return Ok(HyperhivePin { @@ -379,7 +387,9 @@ async fn is_strict_descendant( } /// The `flake.nix` for `agents` at the forge's current config heads, with -/// hyperhive at `hyperhive_rev`. +/// hyperhive at `hyperhive_rev`. A custom agent whose config repo has no +/// commit on [`CONFIG_BRANCH`] is left out, so a hive builds it from its own +/// flake and no other agent waits on it. async fn render( forge: &impl MetaForge, settings: &Settings, @@ -395,15 +405,18 @@ async fn render( let pin = if agent.archetype.is_some() { None } else { - let head = forge - .head_files(CONFIG_ORG, &agent.name, &["flake.lock"]) + let Some(head) = forge + .head_files( + CONFIG_ORG, + &agent.name, + Some(CONFIG_BRANCH), + &["flake.lock"], + ) .await? - .with_context(|| { - format!( - "agent {}: {CONFIG_ORG}/{} has no commit on the forge", - agent.name, agent.name - ) - })?; + else { + tracing::warn!(agent = %agent.name, "meta flake: its config repo has no commit on {CONFIG_BRANCH}; leaving the agent out"); + continue; + }; let lock = head .files .first() @@ -507,6 +520,8 @@ mod tests { compares: Mutex>, /// Every compare fails, as a forge that is down does. compare_fails: bool, + /// Every `head_files` read, as `("owner/repo", branch)`. + head_reads: Mutex)>>, } impl FakeForge { @@ -530,8 +545,13 @@ mod tests { &self, owner: &str, repo: &str, + branch: Option<&str>, paths: &[&str], ) -> Result> { + self.head_reads + .lock() + .unwrap() + .push((format!("{owner}/{repo}"), branch.map(str::to_owned))); Ok(self.repos.get(&format!("{owner}/{repo}")).map(|h| { let mut h = h.clone(); h.files.retain(|f| paths.contains(&f.path.as_str())); @@ -780,17 +800,43 @@ mod tests { } #[tokio::test] - async fn a_custom_agent_without_a_config_commit_fails_the_render() { + async fn a_custom_agent_without_a_config_commit_is_left_out_of_the_render() { let forge = forge_with(&[]); - let agents = vec![Placed { + let mut agents = argus_and_reviewer(); + agents.push(Placed { name: "ghost".to_owned(), archetype: None, - }]; - let err = update(&forge, &FakeLocker::locked(), &settings(), &agents, &[]) + }); + let locker = FakeLocker::locked(); + let got = update(&forge, &locker, &settings(), &agents, &[]) .await - .unwrap_err(); - assert!(format!("{err:#}").contains("agent-configs/ghost")); - assert!(forge.commits.lock().unwrap().is_empty()); + .unwrap(); + assert_eq!(got.updated, Updated::Committed); + let sent = locker.sent.lock().unwrap(); + let [flake] = sent.as_slice() else { + panic!("one lock: {sent:?}") + }; + assert!( + flake.contains("\n argus = mkAgent"), + "the agents with a commit are still rendered" + ); + assert!(flake.contains("\n rev = mkAgent")); + assert!(!flake.contains("ghost")); + } + + #[tokio::test] + async fn a_config_head_is_read_from_main_not_the_default_branch() { + let forge = forge_with(&[]); + run(&forge, &FakeLocker::locked(), &settings()).await; + let reads = forge.head_reads.lock().unwrap(); + assert!( + reads.contains(&("agent-configs/argus".to_owned(), Some("main".to_owned()))), + "{reads:?}" + ); + assert!( + reads.contains(&("internal/meta".to_owned(), None)), + "the meta repo is read on its default branch, as hive-c0re reads it" + ); } #[tokio::test]