Watch
0
0
Fork
You've already forked hyperhive
0

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.
This commit is contained in:
atlas 2026-10-24 19:20:37 +02:00
commit bbc6174361
4 changed files with 90 additions and 24 deletions

View file

@ -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/<agent>`
`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`;

View file

@ -158,8 +158,8 @@ pub struct PullForReview {
pub narrowed: Option<String>,
}
/// 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<Option<DefaultBranchFiles>> {
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<Option<DefaultBranchFiles>> {
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())

View file

@ -17,9 +17,10 @@ impl MetaForge for Client {
&self,
owner: &str,
repo: &str,
branch: Option<&str>,
paths: &[&str],
) -> Result<Option<DefaultBranchFiles>> {
self.default_branch_files(owner, repo, paths).await
self.branch_files(owner, repo, branch, paths).await
}
async fn ensure_meta_repo(&self) -> Result<()> {

View file

@ -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<Output = Result<Option<DefaultBranchFiles>>> + 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<HyperhivePin> {
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<Vec<(String, String)>>,
/// Every compare fails, as a forge that is down does.
compare_fails: bool,
/// Every `head_files` read, as `("owner/repo", branch)`.
head_reads: Mutex<Vec<(String, Option<String>)>>,
}
impl FakeForge {
@ -530,8 +545,13 @@ mod tests {
&self,
owner: &str,
repo: &str,
branch: Option<&str>,
paths: &[&str],
) -> Result<Option<DefaultBranchFiles>> {
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]