Watch
0
0
Fork
You've already forked hyperhive
0

the builds list keeps the swarm's deploy builds too

build_meta, the build of each agent from internal/meta before a deploy, now
writes its result into the same list as the config PR builds: the agent, the
internal/meta commit, how it ended and what nix printed. The builds card titles
them by agent and shows them with the PR builds.
This commit is contained in:
iris 2026-10-30 02:06:07 +01:00
commit b74011bb54

View file

@ -35,6 +35,7 @@ use swarm_wire::agent_closures::AgentClosure;
use swarm_wire::builder::{BuildFromMeta, FromMetaReply, META_SUBJECT, REPLY_DEADLINE};
use crate::agent_renewal::unix_now;
use crate::builder::runs::{BuildOutcome, BuildRecord, Builds};
use crate::forge::{self, objects::SHARED_ORG};
use crate::meta_flake::{META_REPO, MetaForge};
@ -263,10 +264,66 @@ async fn clear_failure(head: &impl MetaRepo, pr: u64, agent: &str, meta_rev: &st
}
}
/// Build `agent` from `internal/meta`'s head and record the closure.
/// Store the build of `agent` from `meta_rev` that ended as `outcome`, `text`
/// being its store path, `nix`'s tail or the reason, in the list the builds
/// page shows beside the config PRs' builds. A failed write is a warning in
/// `Builds::record` and changes nothing about the node.
async fn record_run(
builds: &Builds,
agent: &str,
meta_rev: &str,
started_unix: i64,
outcome: BuildOutcome,
text: String,
) {
let mut build = BuildRecord {
id: format!(
"meta-{agent}-{}-{started_unix}",
meta_rev.get(..12).unwrap_or(meta_rev)
),
repo: format!("{SHARED_ORG}/{META_REPO}"),
pr: None,
agent: Some(agent.to_owned()),
head_sha: meta_rev.to_owned(),
started_unix,
finished_unix: unix_now(),
outcome,
out_path: None,
error_tail: None,
detail: None,
};
*match outcome {
BuildOutcome::Built => &mut build.out_path,
BuildOutcome::Failed => &mut build.error_tail,
BuildOutcome::Error => &mut build.detail,
} = Some(text);
builds.record(&build).await;
}
/// Keep the builder's `reply` to a build of `agent` from `meta_rev` in `builds`.
async fn record_reply(
builds: &Builds,
agent: &str,
meta_rev: &str,
started_unix: i64,
reply: &Result<FromMetaReply, String>,
) {
let (outcome, text) = match reply {
Err(why) => (BuildOutcome::Error, why),
Ok(FromMetaReply::Unavailable { reason }) => (BuildOutcome::Error, reason),
Ok(FromMetaReply::Failed { error_tail }) => (BuildOutcome::Failed, error_tail),
Ok(FromMetaReply::Built { out_path, .. }) => (BuildOutcome::Built, out_path),
};
record_run(builds, agent, meta_rev, started_unix, outcome, text.clone()).await;
}
/// Build `agent` from `internal/meta`'s head and record the closure. A build
/// the builder answers, or fails to be asked, is also kept in `builds`; one that
/// never got as far as the request (no forge, no head) is not.
pub(crate) async fn run(
head: Option<&impl MetaRepo>,
queue: Option<&impl MetaBuilds>,
builds: &Builds,
agent: String,
pr: Option<u64>,
) -> Outcome {
@ -297,7 +354,10 @@ pub(crate) async fn run(
Ok(request) => request,
Err(e) => return no_verdict(format!("encoding the build request: {e}")),
};
match queue.build(request).await {
let started_unix = unix_now();
let reply = queue.build(request).await;
record_reply(builds, &agent, &meta_rev, started_unix, &reply).await;
match reply {
Err(why) => no_verdict(why),
Ok(FromMetaReply::Unavailable { reason }) => no_verdict(reason),
Ok(FromMetaReply::Failed { error_tail }) => {
@ -334,7 +394,9 @@ pub(crate) async fn run(
#[cfg(test)]
mod tests {
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
use crate::builder::runs::tests::FakeDoc;
use super::*;
@ -452,11 +514,68 @@ mod tests {
}
async fn run_with(head: &FakeHead, queue: &FakeQueue) -> Outcome {
run(Some(head), Some(queue), "atlas".to_owned(), None).await
run_recording(head, queue, &Arc::default()).await
}
/// [`run_with`], keeping the builds in `doc`.
async fn run_recording(head: &FakeHead, queue: &FakeQueue, doc: &Arc<FakeDoc>) -> Outcome {
let builds = Builds::Fake(Arc::clone(doc));
run(Some(head), Some(queue), &builds, "atlas".to_owned(), None).await
}
async fn run_for_pr(head: &FakeHead, queue: &FakeQueue, pr: u64) -> Outcome {
run(Some(head), Some(queue), "atlas".to_owned(), Some(pr)).await
let builds = Builds::Fake(Arc::default());
run(
Some(head),
Some(queue),
&builds,
"atlas".to_owned(),
Some(pr),
)
.await
}
/// The builds page lists these beside the config PRs' builds: which agent,
/// at which `internal/meta` commit, how it ended, and with what.
#[tokio::test]
async fn every_build_the_builder_answers_is_kept_for_the_builds_page() {
let head = FakeHead::reading(Ok(Some(REV)));
let green = Arc::new(FakeDoc::default());
run_recording(&head, &FakeQueue::built(), &green).await;
let builds = green.builds();
let [built] = builds.as_slice() else {
panic!("one build kept: {builds:?}")
};
assert_eq!(built.repo, "internal/meta");
assert_eq!(built.pr, None);
assert_eq!(built.agent.as_deref(), Some("atlas"));
assert_eq!(built.head_sha, REV);
assert_eq!(built.outcome, BuildOutcome::Built);
assert_eq!(built.out_path.as_deref(), Some("/nix/store/x-toplevel"));
assert!(
built.id.starts_with(&format!("meta-atlas-{}-", &REV[..12])),
"{}",
built.id
);
let red = Arc::new(FakeDoc::default());
run_recording(&head, &nix_failed("error: boom"), &red).await;
let builds = red.builds();
let [failed] = builds.as_slice() else {
panic!("one build kept: {builds:?}")
};
assert_eq!(failed.outcome, BuildOutcome::Failed);
assert_eq!(failed.error_tail.as_deref(), Some("error: boom"));
let none = Arc::new(FakeDoc::default());
let silent = FakeQueue::answering(Err("no builder is listening".to_owned()));
run_recording(&head, &silent, &none).await;
let builds = none.builds();
let [error] = builds.as_slice() else {
panic!("one build kept: {builds:?}")
};
assert_eq!(error.outcome, BuildOutcome::Error);
assert_eq!(error.detail.as_deref(), Some("no builder is listening"));
}
#[tokio::test]
@ -586,13 +705,16 @@ mod tests {
("no queue", Some(FakeHead::reading(Ok(Some(REV)))), false),
] {
let queue = FakeQueue::built();
let doc = Arc::new(FakeDoc::default());
let outcome = run(
head.as_ref(),
with_queue.then_some(&queue),
&Builds::Fake(Arc::clone(&doc)),
"atlas".to_owned(),
None,
)
.await;
assert!(doc.builds().is_empty(), "{name}: nothing was built");
let Outcome::Failed(error) = outcome else {
panic!("{name}: not failed")
};