A built module keeps its build edges: a new SDK plans the toolchain and its TypeScript bundles (hq issue 614) #296
@@ -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 {
|
||||
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.
|
||||
@@ -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
|
||||
// the planner, never through the relation.
|
||||
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{}
|
||||
var builders []string
|
||||
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] {
|
||||
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
|
||||
@@ -121,6 +177,13 @@ func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][
|
||||
if base, _, found := strings.Cut(rest, "/"); found {
|
||||
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 != "" {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"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 TestReplay239AModuleNameIsNotTakenOverByAnotherRepository
|
||||
./cmd/mesh-controller TestAWalkThatMovedNothingSaysWhichModulesMatchedWhatRuns
|
||||
./cmd/mesh-controller TestReplay614ANewSDKPlansTheToolchainAndThenItsBundles
|
||||
|
||||
Reference in New Issue
Block a user