job_queue: split ApprovalDeploy into a four-node deploy subtree

A config-PR deploy was one opaque node that fetched, verified, merged,
built and compensated. That shape made three things impossible: the
nix-heavy phases could not take the meta window without the cheap ones
holding it too, a crash mid-build left no node to run the rollback, and
the dashboard could only ever show "deploying" for the whole thing.

Replace it with a `DeployWindow` group root over `MergeVerify ->
DeployApply` (AfterOk) plus a `DeployTail` hanging off the apply with
AfterAny, so the tail runs whether the apply succeeded, failed, or was
cancel-cascaded by a failing verify.
This commit is contained in:
atlas 2026-07-25 21:34:22 +02:00 committed by mara
commit bdf15168db
2 changed files with 99 additions and 19 deletions

View file

@ -140,10 +140,24 @@ pub fn rebuild(agent: &str, source: Source, reason: String, relock: bool) -> Dag
}
}
/// Approval-driven deploy (`MergeConfigPr`): the whole two-phase pipeline
/// stays one opaque node in v1 (design doc §9) — wire-visible as a `rebuild`
/// card like today.
/// Approval-driven deploy (`MergeConfigPr`) as a phase subtree rather than the
/// single opaque node it used to be. Structure:
/// - `DeployWindow` (0, **root**): the resource holder — global meta window,
/// agent lease, build slot — held across every child below. No work of its
/// own; it reaches `Finishing` immediately and the children run inside it.
/// - `MergeVerify` (1, child): drift-gate + fetch + eval-verify. Mutates
/// nothing, so a failure here cancel-cascades its siblings with the forge and
/// the applied repo exactly as they were.
/// - `DeployApply` (2, child, `AfterOk` `MergeVerify`): the irreversible half —
/// ff-merge, `prepare_deploy`, rebuild, `finalize_deploy`.
/// - `DeployTail` (3, child, `AfterAny` `DeployApply`): the compensation +
/// bookkeeping tail — rollback when a merge landed unfinalized, forge tag
/// mirror, PR failure comment (see [`NodeKind::DeployTail`]).
///
/// The window still spans the container build, as it must: `prepare_deploy`
/// leaves `flake.lock` staged-uncommitted for the build's whole duration.
pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec {
let a = || agent.to_owned();
DagSpec {
template: Template::Rebuild,
source: Source::Approval,
@ -151,12 +165,19 @@ pub fn approval_deploy(agent: &str, approval_id: i64, reason: String) -> DagSpec
approval_id: Some(approval_id),
inputs: Vec::new(),
transient: Some(TransientKind::Rebuilding),
nodes: vec![node(
NodeKind::ApprovalDeploy {
agent: agent.to_owned(),
},
Vec::new(),
)],
nodes: vec![
node(NodeKind::DeployWindow { agent: a() }, Vec::new()),
child(0, NodeKind::MergeVerify { agent: a() }, Vec::new()),
child(0, NodeKind::DeployApply { agent: a() }, after_ok(1)),
child(
0,
NodeKind::DeployTail { agent: a() },
vec![Dep {
on: 2,
when: DepWhen::AfterAny,
}],
),
],
}
}