diff --git a/cmd/mesh-controller/release_plan.go b/cmd/mesh-controller/release_plan.go index 83976f5..438ea38 100644 --- a/cmd/mesh-controller/release_plan.go +++ b/cmd/mesh-controller/release_plan.go @@ -36,17 +36,29 @@ func tiersOf(set []string, edges []inventory.Edge) [][]string { for _, m := range set { deps[m] = map[string]bool{} } + // The build seat's holders follow the controller that defines their worker (EdgeWorkerOf, + // novox/hq issue 206), so the built-by edge from that controller to such a holder yields: the + // controller is built by whichever build machine is running, as the runtime image always was. + worker := map[string]map[string]bool{} + for _, e := range edges { + if e.Kind == inventory.EdgeWorkerOf && in[e.From] && in[e.To] { + if worker[e.To] == nil { + worker[e.To] = map[string]bool{} + } + worker[e.To][e.From] = true + } + } for _, e := range edges { // A code dependency — B packages A's source — rebuilds B with A, in the same tier: B's // build needs nothing of A's first. The other kinds order: stands-on and declared after // the base is built, built-by after the build machine is built and running — except for - // what the build machine itself stands on. The runtime image is built by the builder and - // the builder is built on the runtime image; the image comes first, built by the builder - // that is running, which is the only one there could be. + // what the build machine itself stands on, and for the controller whose worker the build + // machine binds. The runtime image is built by the builder and the builder is built on the + // runtime image; the image comes first, built by the builder that is running. if !in[e.From] || !in[e.To] || e.From == e.To || e.Kind == inventory.EdgePackages { continue } - if e.Kind == inventory.EdgeBuiltBy && isBaseOf(e.From, e.To, edges, in) { + if e.Kind == inventory.EdgeBuiltBy && (isBaseOf(e.From, e.To, edges, in) || worker[e.From][e.To]) { continue } deps[e.From][e.To] = true @@ -122,7 +134,10 @@ func reachableFrom(moved []string, edges []inventory.Edge) []string { for grew := true; grew; { grew = false for _, e := range edges { - if e.Kind == inventory.EdgeBuiltBy { + // Built-by and worker-of order a plan; neither widens it. A new build machine changes + // nothing it builds, and a new controller changes nothing about the holder it orders — + // what packages the controller's source is already a code edge. + if e.Kind == inventory.EdgeBuiltBy || e.Kind == inventory.EdgeWorkerOf { continue } if in[e.To] && !in[e.From] { diff --git a/cmd/mesh-controller/worker_order_test.go b/cmd/mesh-controller/worker_order_test.go new file mode 100644 index 0000000..f5b43f2 --- /dev/null +++ b/cmd/mesh-controller/worker_order_test.go @@ -0,0 +1,45 @@ +package main + +import ( + "testing" + + "github.com/novox/mesh-controller/internal/inventory" +) + +// The holder of the build seat follows the controller that defines its worker (novox/hq issue 206). +// On 2026-10-03 a plan put the build machine in tier 0 and the controller in tier 1; the new build +// machine could not bind the worker the old controller had defined, and nothing could build the +// controller that would have redefined it. The built-by edge from the controller to its build +// machine yields to that order: the controller is built by whichever build machine is running. +func TestTheBuildSeatsHolderFollowsTheControllerThatDefinesItsWorker(t *testing.T) { + edges := []inventory.Edge{ + {From: "build-agent", To: "mesh-controller", Kind: inventory.EdgePackages}, + {From: "build-agent", To: "mesh-controller", Kind: inventory.EdgeWorkerOf}, + {From: "mesh-controller", To: "build-agent", Kind: inventory.EdgeBuiltBy}, + {From: "route-proxy", To: "mesh-controller", Kind: inventory.EdgePackages}, + {From: "route-proxy", To: "build-agent", Kind: inventory.EdgeBuiltBy}, + } + set := reachableFrom([]string{"mesh-controller"}, edges) + if len(set) != 3 { + t.Fatalf("the controller, what packages it, and nothing more: %v", set) + } + tiers := tiersOf(set, edges) + pos := map[string]int{} + for i, tier := range tiers { + for _, m := range tier { + pos[m] = i + } + } + if pos["mesh-controller"] != 0 { + t.Fatalf("the controller first, built by the build machine that is running: %v", tiers) + } + if pos["build-agent"] <= pos["mesh-controller"] { + t.Fatalf("the build machine after the controller that defines its worker: %v", tiers) + } + if pos["route-proxy"] <= pos["build-agent"] { + t.Fatalf("what the build machine builds comes after it: %v", tiers) + } + if hasCycle(tiers, edges) { + t.Fatalf("no cycle here: %v", tiers) + } +} diff --git a/internal/inventory/dependencies.go b/internal/inventory/dependencies.go index 26274e7..580586c 100644 --- a/internal/inventory/dependencies.go +++ b/internal/inventory/dependencies.go @@ -18,8 +18,17 @@ const ( EdgeBuiltBy = "built-by" // EdgeDeclared: the manifest's own `build.on`. EdgeDeclared = "declared" + // EdgeWorkerOf: the module holds the build seat, whose worker the control plane defines + // (novox/hq issue 206). The one place a *running* order enters the graph: a build machine rolled + // before the controller that redefines its worker cannot bind it, and nothing can then build the + // controller that would end that — so the holder of the build seat follows the controller, and + // the controller is built by whichever build machine is running, as it always was. + EdgeWorkerOf = "worker-of" ) +// TheControlPlane is the module that defines every seat's worker on the bus. +const TheControlPlane = "mesh-controller" + // Edge is one dependency: From depends on To, in the way Kind says. type Edge struct { From string `json:"from"` @@ -106,6 +115,13 @@ func dependenciesOf(entries []Entry, against map[string][]string, read map[strin } } } + if known[TheControlPlane] { + for _, b := range builders { + if b != TheControlPlane { + add(b, TheControlPlane, EdgeWorkerOf) + } + } + } sort.Slice(out, func(a, b int) bool { if out[a].From != out[b].From { return out[a].From < out[b].From