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:
parent
4697740a7e
commit
bbc6174361
4 changed files with 90 additions and 24 deletions
|
|
@ -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`;
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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<()> {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in a new issue