Merge pull request 'A built module keeps its build edges: a new SDK plans the toolchain and its TypeScript bundles (hq issue 614)' (#296) from fix/614-a-built-module-keeps-its-build-edges into main
This commit was merged in pull request #296.
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