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:
parent
221c3f35a1
commit
b74011bb54
1 changed files with 212 additions and 33 deletions
|
|
@ -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")
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue