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:
parent
b68b74d687
commit
cafa120d77
13 changed files with 360 additions and 70 deletions
|
|
@ -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) =
|
||||
|
|
|
|||
Loading…
Reference in a new issue