Read a built module's build edges from what is kept of it, so a new SDK rebuilds its toolchain and bundles (hq issue 614)
mesh/merge-gate head e658a14296f9 merged into trunk 1826b68a22b5 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head e658a14296f9 merged into trunk 1826b68a22b5 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/merge-gate head e658a14296f9 merged into trunk 1826b68a22b5 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head e658a14296f9 merged into trunk 1826b68a22b5 — pass: its merge-check.sh passed
mesh/delivery delivered
A registered manifest carries no build, so mesh-tools' build.on naming the SDK's package and every bundle's toolchain were edges only before a module was first built: mesh-sdk 0.1.14 planned "sent nowhere". The relation now reads a package base from the recorded name@version against the package the mesh holds, and the toolchain from the bundles the resolved manifest keeps. Only records the mesh already keeps are read, so it holds for every module built before it and a rollback reads the same records. Replay R614.
This commit is contained in:
@@ -0,0 +1,71 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
|
"github.com/novox/mesh-controller/internal/inventory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// **A new SDK plans the toolchain, and then every bundle compiled in it** (novox/hq issue 614), reasoned
|
||||||
|
// as `mesh-controller.delivery-plan` reasons it: the modules registered as a build registers them —
|
||||||
|
// resolved, so carrying no `build` — their builds recorded with what they stood on and what they made,
|
||||||
|
// the graph read by theGraph and the change scoped by pullScope. On 2026-10-13 mesh-sdk 0.1.14 planned
|
||||||
|
// "builds mesh-sdk, sent nowhere": mesh-tools' `build.on` naming the SDK's package and the providers'
|
||||||
|
// TypeScript bundles were edges only while their manifests were unbuilt.
|
||||||
|
//
|
||||||
|
// **The replay of issue 614** (mesh-lab R614): written only with what the controller had before its fix — a
|
||||||
|
// store, modules registered and builds recorded, theGraph and pullScope — so it is laid over the commit before
|
||||||
|
// and fails there with the plan of that day, [[mesh-sdk]]. Needs a store: MESH_TEST_POSTGRES.
|
||||||
|
func TestReplay614ANewSDKPlansTheToolchainAndThenItsBundles(t *testing.T) {
|
||||||
|
inv := inventory.ForTest(t)
|
||||||
|
ctx := t.Context()
|
||||||
|
asked := time.Now().Add(-time.Hour)
|
||||||
|
digest := func(c string) string { return "@sha256:" + strings.Repeat(c, 64) }
|
||||||
|
register := func(m catalogue.Manifest, repository, path string, against []string, made []inventory.Artifact) {
|
||||||
|
t.Helper()
|
||||||
|
if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: repository, Seat: "git", Path: path,
|
||||||
|
Ref: "main", BuiltFrom: "old", Asked: asked}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := inv.RecordBuild(ctx, inventory.Build{ID: "build-" + m.Module, Repository: repository, Ref: "main",
|
||||||
|
Module: m.Module, Commit: "old", On: "builder", Path: path, Against: against, Made: made, Asked: asked}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bundle := func(module, language string) catalogue.Manifest {
|
||||||
|
return catalogue.Manifest{Module: module, Version: "1", Bundles: []catalogue.Bundle{{
|
||||||
|
Name: "code", Language: language, Source: catalogue.ArtifactStoreScheme + module + "/code" + digest("2"),
|
||||||
|
Entrypoints: []string{"tools/index.js"}}}}
|
||||||
|
}
|
||||||
|
register(catalogue.Manifest{Module: "build-agent", Version: "1",
|
||||||
|
Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}},
|
||||||
|
"novox/mesh-catalog", "modules/build-agent", nil, nil)
|
||||||
|
register(catalogue.Manifest{Module: "mesh-sdk", Version: "1"}, "novox/mesh-sdk", "", nil,
|
||||||
|
[]inventory.Artifact{{Name: "lib", Kind: "package", Reference: "@novox/mesh-sdk@0.1.13"}})
|
||||||
|
register(catalogue.Manifest{Module: "mesh-tools", Version: "1"}, "novox/mesh-tools", "",
|
||||||
|
[]string{"@novox/mesh-sdk@0.1.13", catalogue.ArtifactStoreScheme + "upstream/docker.io/library/node" + digest("0")},
|
||||||
|
[]inventory.Artifact{{Name: "build", Kind: "image", Reference: catalogue.ArtifactStoreScheme + "mesh-tools/build" + digest("1")}})
|
||||||
|
register(catalogue.Manifest{Module: "mesh-tools-go", Version: "1"}, "novox/mesh-tools", "go", nil, nil)
|
||||||
|
register(bundle("minio", "typescript"), "novox/mesh-catalog", "modules/minio", nil, nil)
|
||||||
|
register(bundle("umami", "typescript"), "novox/mesh-catalog", "modules/umami", nil, nil)
|
||||||
|
register(bundle("dnsmasq", "go"), "novox/mesh-catalog", "modules/dnsmasq", nil, nil)
|
||||||
|
|
||||||
|
entries, read, edges, err := theGraph(ctx, inv)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := orderMember{ID: "novox/mesh-sdk@new", Repository: "novox/mesh-sdk", Head: "new",
|
||||||
|
Paths: []string{"src/provisioner/index.ts", "package.json"}}
|
||||||
|
s := pullScope(m.pull(), entries, read, edges)
|
||||||
|
want := [][]string{{"mesh-sdk"}, {"mesh-tools"}, {"minio", "umami"}}
|
||||||
|
if !reflect.DeepEqual(s.Reach.Plan.Tiers, want) {
|
||||||
|
t.Fatalf("a new SDK plans %v, want %v (edges %v)", s.Reach.Plan.Tiers, want, edges)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(s.Dependents, []string{"mesh-tools", "minio", "umami"}) {
|
||||||
|
t.Errorf("dependents %v", s.Dependents)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,42 @@ func (i *Inventory) Dependencies(ctx context.Context) ([]Edge, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return dependenciesOf(entries, against, nil), nil
|
held, err := i.Held(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return dependenciesWith(entries, against, held), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// packagesHeld is, by package name, the module whose artifact the mesh holds as that package: a
|
||||||
|
// `package` artifact's reference is `<name>@<version>` (the builder's publishPackage), never an
|
||||||
|
// artifact-store reference, so a build that stood on it recorded the package's name and version.
|
||||||
|
func packagesHeld(held map[string]string) map[string]string {
|
||||||
|
out := map[string]string{}
|
||||||
|
for key, reference := range held {
|
||||||
|
module, _, ok := strings.Cut(key, "/")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if name := packageName(reference); name != "" {
|
||||||
|
out[name] = module
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageName is the name of a `<name>@<version>` package reference, or "" for anything else: a
|
||||||
|
// kept or pinned image (`…@sha256:…`), a URL, or a reference with no version.
|
||||||
|
func packageName(reference string) string {
|
||||||
|
if strings.HasPrefix(reference, catalogue.ArtifactStoreScheme) || strings.Contains(reference, "@sha256:") ||
|
||||||
|
strings.Contains(reference, "://") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
at := strings.LastIndex(reference, "@")
|
||||||
|
if at <= 0 || at == len(reference)-1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return reference[:at]
|
||||||
}
|
}
|
||||||
|
|
||||||
// dependenciesOf is Dependencies over what was read, so a test can hand it a catalogue.
|
// dependenciesOf is Dependencies over what was read, so a test can hand it a catalogue.
|
||||||
@@ -66,6 +101,20 @@ func (i *Inventory) Dependencies(ctx context.Context) ([]Edge, error) {
|
|||||||
// `read` draws no edge (novox/hq ADR 0267 rule 4): what a build read moves it through its build source, in
|
// `read` draws no edge (novox/hq ADR 0267 rule 4): what a build read moves it through its build source, in
|
||||||
// the planner, never through the relation.
|
// the planner, never through the relation.
|
||||||
func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][]ReadRepository) []Edge {
|
func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][]ReadRepository) []Edge {
|
||||||
|
return dependenciesWith(entries, against, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// dependenciesWith is the relation with the artifacts the mesh holds, "<module>/<artifact>" to its
|
||||||
|
// reference (Held), so a package a build stood on is read as its module's.
|
||||||
|
//
|
||||||
|
// **A built manifest carries no `build`** (catalogue.Resolve drops it), so what a module declared it
|
||||||
|
// is built on is read from what is kept of it once it is built (novox/hq issue 614): the bases its
|
||||||
|
// newest build was handed (`against`) — an artifact-store reference, or a package's `<name>@<version>`
|
||||||
|
// — and the language of every bundle it compiled (`Bundles`). Read only from records the mesh already
|
||||||
|
// keeps, so it holds for every module built before it, and a controller rolled back to one without it
|
||||||
|
// reads the same records as before.
|
||||||
|
func dependenciesWith(entries []Entry, against map[string][]string, held map[string]string) []Edge {
|
||||||
|
packages := packagesHeld(held)
|
||||||
known := map[string]bool{}
|
known := map[string]bool{}
|
||||||
var builders []string
|
var builders []string
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
@@ -111,6 +160,13 @@ func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// **And once it is built**, from the bundles its resolved manifest keeps (novox/hq issue 614): the
|
||||||
|
// `build` it was declared with is gone by then, and a toolchain that moved rebuilt none of them.
|
||||||
|
for _, b := range e.Manifest.Bundles {
|
||||||
|
if chain, err := builder.ToolchainFor(b.Language); err == nil {
|
||||||
|
add(name, chain.Base, EdgeStandsOn)
|
||||||
|
}
|
||||||
|
}
|
||||||
for _, ref := range against[name] {
|
for _, ref := range against[name] {
|
||||||
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
|
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
|
||||||
// A copy of an upstream image is no module's artifact (novox/hq ADR 0257): its
|
// A copy of an upstream image is no module's artifact (novox/hq ADR 0257): its
|
||||||
@@ -121,6 +177,13 @@ func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][
|
|||||||
if base, _, found := strings.Cut(rest, "/"); found {
|
if base, _, found := strings.Cut(rest, "/"); found {
|
||||||
add(name, base, EdgeStandsOn)
|
add(name, base, EdgeStandsOn)
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// A package base (`build.on` naming a module's `package` artifact) is recorded as the
|
||||||
|
// package's name and version (novox/hq issue 614): the module that publishes that package
|
||||||
|
// is what it stood on, whichever version the mesh holds now.
|
||||||
|
if base, ok := packages[packageName(ref)]; ok {
|
||||||
|
add(name, base, EdgeStandsOn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if e.Source.Repository != "" {
|
if e.Source.Repository != "" {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package inventory
|
package inventory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/novox/mesh-controller/internal/catalogue"
|
"github.com/novox/mesh-controller/internal/catalogue"
|
||||||
@@ -174,3 +175,74 @@ func TestACopyOfAnUpstreamImageIsNoEdge(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// **A built module keeps its build edges** (novox/hq issue 614). A manifest the mesh registers from a
|
||||||
|
// build carries no `build` (catalogue.Resolve drops it), so mesh-tools' `build.on` naming mesh-sdk's
|
||||||
|
// `lib` package and a provider's TypeScript bundle drew no edge once built, and a new SDK rebuilt
|
||||||
|
// neither. The package base is read from what the build was handed (`@novox/mesh-sdk@0.1.13`, the
|
||||||
|
// package's name whichever version the mesh holds now), the toolchain from the bundles it keeps.
|
||||||
|
func TestABuiltModuleKeepsItsBuildEdges(t *testing.T) {
|
||||||
|
built := func(name string, bundles ...catalogue.Bundle) Entry {
|
||||||
|
return Entry{Manifest: catalogue.Manifest{Module: name, Bundles: bundles},
|
||||||
|
Source: Source{Repository: "http://forge/novox/" + name + ".git"}}
|
||||||
|
}
|
||||||
|
entries := []Entry{
|
||||||
|
built("mesh-sdk"),
|
||||||
|
built("mesh-tools"),
|
||||||
|
built("mesh-tools-go"),
|
||||||
|
built("minio", catalogue.Bundle{Name: "code", Language: "typescript"}),
|
||||||
|
built("umami", catalogue.Bundle{Name: "code", Language: "TypeScript"}),
|
||||||
|
built("dnsmasq", catalogue.Bundle{Name: "tools", Language: "go"}),
|
||||||
|
built("plain"),
|
||||||
|
}
|
||||||
|
against := map[string][]string{
|
||||||
|
"mesh-tools": {"@novox/mesh-sdk@0.1.13",
|
||||||
|
catalogue.ArtifactStoreScheme + "upstream/docker.io/library/node@sha256:" + strings.Repeat("0", 64)},
|
||||||
|
// A package nobody in the mesh publishes is no edge.
|
||||||
|
"plain": {"left-pad@1.3.0"},
|
||||||
|
}
|
||||||
|
held := map[string]string{
|
||||||
|
"mesh-sdk/lib": "@novox/mesh-sdk@0.1.14",
|
||||||
|
"mesh-tools/runtime": catalogue.ArtifactStoreScheme + "mesh-tools/runtime@sha256:" + strings.Repeat("1", 64),
|
||||||
|
}
|
||||||
|
got := dependenciesWith(entries, against, held)
|
||||||
|
has := func(from, to string) bool {
|
||||||
|
for _, e := range got {
|
||||||
|
if e.From == from && e.To == to && e.Kind == EdgeStandsOn {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, want := range [][2]string{
|
||||||
|
{"mesh-tools", "mesh-sdk"},
|
||||||
|
{"minio", "mesh-tools"},
|
||||||
|
{"umami", "mesh-tools"},
|
||||||
|
{"dnsmasq", "mesh-tools-go"},
|
||||||
|
} {
|
||||||
|
if !has(want[0], want[1]) {
|
||||||
|
t.Errorf("no %s stands-on %s: %+v", want[0], want[1], got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, e := range got {
|
||||||
|
if e.From == "plain" && e.Kind != EdgeBuiltBy {
|
||||||
|
t.Errorf("an unknown package drew an edge: %+v", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAPackageNameIsReadOnlyFromAPackageReference(t *testing.T) {
|
||||||
|
for ref, want := range map[string]string{
|
||||||
|
"@novox/mesh-sdk@0.1.14": "@novox/mesh-sdk",
|
||||||
|
"left-pad@1.3.0": "left-pad",
|
||||||
|
"@novox/mesh-sdk": "",
|
||||||
|
"novox.internal:5100/mesh-tools/build@sha256:" + strings.Repeat("0", 64): "",
|
||||||
|
catalogue.ArtifactStoreScheme + "mesh-tools/build@sha256:0": "",
|
||||||
|
"http://store/v2/x/blobs/sha256:0": "",
|
||||||
|
"": "",
|
||||||
|
} {
|
||||||
|
if got := packageName(ref); got != want {
|
||||||
|
t.Errorf("packageName(%q) = %q, want %q", ref, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -45,3 +45,4 @@
|
|||||||
./cmd/mesh-controller TestReplay348AFaultFromBeforeTheSendFailsNoGate
|
./cmd/mesh-controller TestReplay348AFaultFromBeforeTheSendFailsNoGate
|
||||||
./cmd/mesh-controller TestReplay239AModuleNameIsNotTakenOverByAnotherRepository
|
./cmd/mesh-controller TestReplay239AModuleNameIsNotTakenOverByAnotherRepository
|
||||||
./cmd/mesh-controller TestAWalkThatMovedNothingSaysWhichModulesMatchedWhatRuns
|
./cmd/mesh-controller TestAWalkThatMovedNothingSaysWhichModulesMatchedWhatRuns
|
||||||
|
./cmd/mesh-controller TestReplay614ANewSDKPlansTheToolchainAndThenItsBundles
|
||||||
|
|||||||
Reference in New Issue
Block a user