diff --git a/cmd/mesh-controller/dependents_test.go b/cmd/mesh-controller/dependents_test.go new file mode 100644 index 0000000..48cf8d9 --- /dev/null +++ b/cmd/mesh-controller/dependents_test.go @@ -0,0 +1,52 @@ +package main + +import ( + "testing" + + "github.com/novox/mesh-controller/internal/catalogue" + "github.com/novox/mesh-controller/internal/inventory" +) + +// A merge that rebuilds a base rebuilds what stands on it, through every layer, and nothing else +// (novox/hq issue 186): the runtime image moving means every module built on it moves too, and a +// module built on one of those moves as well. +func TestAMergeOfABaseTakesWhatStandsOnItAlong(t *testing.T) { + entry := func(name string) inventory.Entry { + return inventory.Entry{Manifest: catalogue.Manifest{Module: name}} + } + entries := []inventory.Entry{entry("mesh-tools"), entry("shop"), entry("shop-plugin"), entry("postgres"), entry("unrelated")} + against := map[string][]string{ + "shop": {catalogue.ArtifactStoreScheme + "mesh-tools/runtime@sha256:a"}, + "shop-plugin": {catalogue.ArtifactStoreScheme + "shop/runtime@sha256:b"}, + "postgres": {catalogue.ArtifactStoreScheme + "mesh-tools/runtime@sha256:a"}, + "unrelated": {catalogue.ArtifactStoreScheme + "alpine/base@sha256:c"}, + } + got := dependentsOf([]inventory.Entry{entry("mesh-tools")}, entries, against) + var names []string + for _, e := range got { + names = append(names, e.Manifest.Module) + } + want := map[string]bool{"shop": true, "shop-plugin": true, "postgres": true} + if len(names) != len(want) { + t.Fatalf("rebuilt %v; wanted exactly the three that stand on the runtime, directly or through shop", names) + } + for _, n := range names { + if !want[n] { + t.Fatalf("%s was rebuilt and stands on nothing that moved (%v)", n, names) + } + } + // The dependents come in base order when the merge orders them: the runtime, then shop, then + // the plugin that stands on shop. + ordered := orderByBases(append([]inventory.Entry{entry("mesh-tools")}, got...), against) + pos := map[string]int{} + for i, e := range ordered { + pos[e.Manifest.Module] = i + } + if !(pos["mesh-tools"] < pos["shop"] && pos["shop"] < pos["shop-plugin"]) { + t.Fatalf("not in base order: %v", ordered) + } + // Nothing moved: nothing follows. + if more := dependentsOf(nil, entries, against); len(more) != 0 { + t.Fatalf("with nothing moved, %d module(s) were rebuilt", len(more)) + } +} diff --git a/cmd/mesh-controller/upgrades.go b/cmd/mesh-controller/upgrades.go index b058ecf..091ce02 100644 --- a/cmd/mesh-controller/upgrades.go +++ b/cmd/mesh-controller/upgrades.go @@ -299,6 +299,20 @@ func (f following) SourceMoved(ctx context.Context, m link.SourceMoved) error { if err != nil { return notNow(err) } + // And whatever stands on what moved. A base rebuilt without its dependents is a mesh half on + // the old image until somebody remembers to ask — on 2026-10-01 forty-two modules, twice, by + // hand (novox/hq issue 186). The relation is the one `build --on` reads; this is the same + // rebuild, asked by the merge that made it necessary, in base order. + standing := dependentsOf(moved, entries, against) + if len(standing) > 0 { + var on []string + for _, e := range standing { + on = append(on, e.Manifest.Module) + } + fmt.Printf(" %d module(s) stand on what moved and are rebuilt with it: %s\n", + len(standing), strings.Join(on, ", ")) + moved = append(moved, standing...) + } ordered := orderByBases(moved, against) names := make([]string, 0, len(ordered)) for _, e := range ordered { @@ -522,3 +536,37 @@ func isHistory(mergedAt string, seen time.Time) bool { } return at.Before(seen) } + +// dependentsOf is every catalogued module that stands on one of the moved modules, directly or +// through another dependent, and is not itself among them — in the catalogue's order, so the +// answer is the same each time. A module standing on nothing that moved is left alone: a merge +// rebuilds what it changed and what is built on top of that, not the catalogue. +func dependentsOf(moved, entries []inventory.Entry, against map[string][]string) []inventory.Entry { + bases := map[string]bool{} + for _, e := range moved { + bases[e.Manifest.Module] = true + } + var out []inventory.Entry + taken := map[string]bool{} + for grew := true; grew; { + grew = false + for _, e := range entries { + name := e.Manifest.Module + if bases[name] || taken[name] { + continue + } + for base := range bases { + if standsOnModule(e, base, against) { + taken[name] = true + out = append(out, e) + grew = true + break + } + } + } + for _, e := range out { + bases[e.Manifest.Module] = true + } + } + return out +}