From c72f6ca8cb1574e4a0ba3a086a3b7b4fb22e08f7 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 3 Oct 2026 22:19:50 +0200 Subject: [PATCH] A bundle stands on the toolchain it is compiled in (hq issue 211) A manifest names its toolchain by language, not in build.on, so the planner did not know a bundle depends on the module that publishes its toolchain and built the two in one tier: the bundle against the old toolchain, recorded as built from the new commit. The edge is read from the manifest, so it holds before any build recorded it, and a toolchain that moves rebuilds every bundle compiled in it. --- cmd/mesh-controller/release_plan_test.go | 11 +++++++ internal/inventory/dependencies.go | 16 ++++++++++ internal/inventory/dependencies_test.go | 38 ++++++++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/cmd/mesh-controller/release_plan_test.go b/cmd/mesh-controller/release_plan_test.go index c2eeb27..5bbccbd 100644 --- a/cmd/mesh-controller/release_plan_test.go +++ b/cmd/mesh-controller/release_plan_test.go @@ -115,3 +115,14 @@ func TestACycleIsOneLastTierAndSaidSo(t *testing.T) { t.Fatalf("a cycle should be one tier of two, said: %v", tiers) } } + +// novox/hq 04-ISSUES/211: a merge moving the toolchain and a bundle compiled in it builds the +// bundle a tier after the toolchain, not beside it. +func TestABundleIsPlannedAfterTheToolchainItIsCompiledIn(t *testing.T) { + edges := []inventory.Edge{{From: "node-tools", To: "mesh-tools", Kind: inventory.EdgeStandsOn}} + p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-tools", Commit: "abc"}, + []string{"mesh-tools", "node-tools"}, edges) + if len(p.Tiers) != 2 || p.Tiers[0][0] != "mesh-tools" || p.Tiers[1][0] != "node-tools" { + t.Fatalf("the toolchain, then the bundle: %v", p.Tiers) + } +} diff --git a/internal/inventory/dependencies.go b/internal/inventory/dependencies.go index 580586c..ef38cf6 100644 --- a/internal/inventory/dependencies.go +++ b/internal/inventory/dependencies.go @@ -5,6 +5,7 @@ import ( "sort" "strings" + "github.com/novox/mesh-controller/internal/builder" "github.com/novox/mesh-controller/internal/catalogue" ) @@ -96,6 +97,21 @@ func dependenciesOf(entries []Entry, against map[string][]string, read map[strin add(name, on.Module, EdgeDeclared) } } + // **A bundle stands on the toolchain it is compiled in** (novox/hq 04-ISSUES/211). A + // manifest names its toolchain by language, not in `build.on`, so the edge was implicit + // and a merge that moved the toolchain and a bundle together built both in one tier — + // the bundle against the toolchain as it was, recorded as built from the new commit. Read + // from the manifest, so it holds before any build has recorded what it stood on; and a + // toolchain that moves rebuilds every bundle compiled in it, which is what a toolchain + // carrying a bundle's dependencies requires. + for _, a := range e.Manifest.Build.Artifacts { + if a.Kind != catalogue.ArtifactBundle { + continue + } + if chain, err := builder.ToolchainFor(a.Language); err == nil { + add(name, chain.Base, EdgeStandsOn) + } + } } for _, ref := range against[name] { if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok { diff --git a/internal/inventory/dependencies_test.go b/internal/inventory/dependencies_test.go index 1109d34..144595a 100644 --- a/internal/inventory/dependencies_test.go +++ b/internal/inventory/dependencies_test.go @@ -62,3 +62,41 @@ func TestDependenciesAreOneRelationWithTheirKinds(t *testing.T) { } } } + +// novox/hq 04-ISSUES/211: a bundle stands on the toolchain it is compiled in, so a merge moving both +// builds the toolchain first — read from the manifest, before any build recorded it. +func TestABundleStandsOnTheToolchainItIsCompiledIn(t *testing.T) { + entries := []Entry{ + {Manifest: catalogue.Manifest{Module: "mesh-tools"}, Source: Source{Repository: "novox/mesh-tools"}}, + {Manifest: catalogue.Manifest{Module: "mesh-tools-go"}, Source: Source{Repository: "novox/mesh-tools-go"}}, + {Manifest: catalogue.Manifest{Module: "node-tools", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{ + {Name: "runtime", Kind: catalogue.ArtifactBundle, Language: "go", System: "arch", From: "cmd/node-tools"}}}}, + Source: Source{Repository: "novox/mesh-tools"}}, + {Manifest: catalogue.Manifest{Module: "nftables", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{ + {Name: "tools", Kind: catalogue.ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"}}}}}, + Source: Source{Repository: "novox/mesh-catalog"}}, + {Manifest: catalogue.Manifest{Module: "photos", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{ + {Name: "server", Kind: catalogue.ArtifactImage, From: "Dockerfile"}}}}, + Source: Source{Repository: "novox/photos"}}, + } + edges := dependenciesOf(entries, nil, nil) + has := func(from, to string) bool { + for _, e := range edges { + if e.From == from && e.To == to && e.Kind == EdgeStandsOn { + return true + } + } + return false + } + if !has("nftables", "mesh-tools") { + t.Errorf("a TypeScript bundle does not stand on the TypeScript toolchain: %v", edges) + } + if !has("node-tools", "mesh-tools-go") { + t.Errorf("a Go bundle does not stand on the Go toolchain: %v", edges) + } + for _, e := range edges { + if e.From == "photos" && e.Kind == EdgeStandsOn { + t.Errorf("an image stands on a toolchain it is not compiled in: %v", e) + } + } +} -- 2.54.0