Watch
0
0
Fork
You've already forked hyperhive
0

swarm-builder: scope the forge token to the forge, one build at a time, opt-in

argus's review on #5177: the git credential helper answered every `get`,
so a config PR's own flake inputs could have git send the builder's forge
token to any host. The helper is now configured under
`credential.<forge-url>.helper` and itself answers only an https request
whose host is the forge's; a test runs it as git does and checks that
other hosts, http and non-get operations get nothing.

Builds are bounded: every `build_agent` node needs the one `builder` jobq
resource, so one runs at a time and the rest wait pending, and a newer
head cancels its PR's pending build (a running one finishes and writes on
its own commit). The builder also holds a one-permit semaphore around each
build.

`builder` and `review-classifier`, the forge logins swarm-controller owns,
join nix/reserved-names.nix, with a test tying the list to both constants.

The builder is now opt-in like the CI runner: `allSwarmServices` no longer
turns it on, since it runs unreviewed config PRs through the host's nix
daemon. mara had not answered atlas's question on the PR.
This commit is contained in:
atlas 2026-10-12 22:47:30 +02:00
commit cafa120d77
13 changed files with 360 additions and 70 deletions

View file

@ -4,6 +4,10 @@
//! The status is the check's only record besides the node itself: nothing is
//! pushed, and the closure the builder made stays in its host's store. A red
//! status blocks nothing; the merge deploys as before.
//!
//! Builds share one `SwarmResourceKind::Builder` slot, so one runs at a time,
//! and a newer push replaces its PR's build while that build is still
//! pending (`queue_build`).
use std::future::Future;
@ -227,6 +231,59 @@ mod tests {
);
}
/// A superseded build that finishes after the newer head's writes only on
/// its own commit, so the newer head keeps its status.
#[tokio::test]
async fn a_late_superseded_build_writes_only_on_its_own_commit() {
struct ByRevQueue;
impl BuildSink for ByRevQueue {
async fn build(&self, request: Vec<u8>) -> Result<Reply, String> {
let sent: BuildAgent = serde_json::from_slice(&request).expect("a build request");
Ok(if sent.rev == "old" {
Reply::Failed {
error_tail: "error: boom".to_owned(),
}
} else {
Reply::Built {
out_path: "/nix/store/new".to_owned(),
}
})
}
}
#[derive(Default)]
struct ByRev(Mutex<Vec<(String, CommitStatusState)>>);
impl StatusSink for ByRev {
async fn set(
&self,
_: &str,
rev: &str,
state: CommitStatusState,
_: &str,
) -> anyhow::Result<()> {
self.0.lock().unwrap().push((rev.to_owned(), state));
Ok(())
}
}
let statuses = ByRev::default();
let at = |rev: &str| BuildAgent {
rev: rev.to_owned(),
..job()
};
run(Some(&ByRevQueue), Some(&statuses), at("new")).await;
run(Some(&ByRevQueue), Some(&statuses), at("old")).await;
let written = statuses.0.into_inner().unwrap();
let last_on = |rev: &str| {
written
.iter()
.rev()
.find(|(r, _)| r == rev)
.map(|(_, s)| *s)
};
assert_eq!(last_on("new"), Some(CommitStatusState::Success));
assert_eq!(last_on("old"), Some(CommitStatusState::Failure));
}
#[tokio::test]
async fn no_builder_is_an_error_status_not_a_failure() {
let (outcome, states) =