From 260a16181c51dee08b516309138bbff19f19c9d7 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:12:27 +0100 Subject: [PATCH] BL7: reserved by name from the catalog half's view, builder_versions calls it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The builder's tools bundle serves reserved given a package's name: it follows the catalog half's packages view itself, finds the module from the name, answers the reserved version, or for a version no reservation holds asks the seat mesh-catalog's versions and answers released with the release's digests; a name no entry carries, two entries carry, or a version neither reserved nor released is answered error. builder_versions by name calls reserved through its invokes. The builder's process serves no call. Builds: design 47 G §572, §582 (A-70) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-builder-tools/main.go | 67 +++++++++++++++- .../mesh-delivery-builder-tools/reserved.go | 14 +++- .../reservedname_test.go | 58 ++++++++++++++ .../internal/versions/names.go | 54 ++++++++++--- .../internal/versions/reserve.go | 48 ++++++++++++ .../internal/versions/reservedname_test.go | 76 +++++++++++++++++++ .../internal/versions/tools.go | 5 ++ .../mesh-delivery-builder/manifest_test.go | 2 +- modules/mesh-delivery-builder/module.json | 1 + 9 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go create mode 100644 modules/mesh-delivery-builder/internal/versions/reservedname_test.go diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index dea457ae..221653b6 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -15,6 +15,8 @@ import ( "time" "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/sdk/buildstep" @@ -35,8 +37,11 @@ type queueAsker func(key string, value []byte) (json.RawMessage, error) // Tools are the builder's tools: stuck reads, askAgain writes the reasks value, queue writes a queues value and waits for // the builder's read, buildAgain writes a reasks value of mode build and waits for the builder's answer. +// +// builder_versions, asked by a name, answers also what the verb reserved answers for it (reserve; design 47 G §572): the +// bundle calls the seat mesh-builds through its invokes, as any caller does. Nil answers none. func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, queue, buildAgain queueAsker, since build.LogSince, now func() time.Time, vs versions.Reader, - fw, of versions.Reader) []stdio.Tool { + fw, of versions.Reader, reserve func(name, version string) (meshbuilds.ReservedAnswer, error)) []stdio.Tool { str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) } // handAct writes a reasks value of a versions hand act and answers the builder's answer, or its refusal as an error. handAct := func(key string, value []byte, err error) (any, error) { @@ -91,7 +96,16 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q "version": map[string]any{"type": "string", "description": "one version, to ask whether it is recorded"}, "digest": map[string]any{"type": "string", "description": "with version: the digest the holder received"}}}, Run: func(args map[string]any) (any, error) { - return versions.AnswerVersions(context.Background(), vs, str(args, "name"), str(args, "version"), str(args, "digest")) + out, err := versions.AnswerVersions(context.Background(), vs, str(args, "name"), str(args, "version"), str(args, "digest")) + if err != nil || reserve == nil { + return out, err + } + ra, err := reserve(out.Name, str(args, "version")) + if err != nil { + return nil, fmt.Errorf("the seat mesh-builds' reserved did not answer for %s: %w", out.Name, err) + } + out.Reserved = &ra + return out, nil }}, {Name: build.StuckTool, Description: "Every build key that waits (building or queued), oldest first: its module, fingerprint, step words and since when, and each ask it " + @@ -314,6 +328,26 @@ func written(s stdio.KeptState, key string, revision uint64) time.Time { return time.Time{} } +// The state the bundle follows, its owner's tool, and the seat verbs it calls (the manifest's invokes). +const ( + indexerModule = "mesh-delivery-indexer" + packagesState = "packages" + packagesTool = indexerModule + ".indexer_read_packages" + catalogVersionsVerb = "seat:mesh-catalog.versions" + reservedVerb = "seat:mesh-builds.reserved" +) + +// followedPackages is the catalog half's packages state as the bundle's picture of it holds it (versions.PackagesState). +type followedPackages struct{ r *views.Reader } + +func (f followedPackages) Keys(context.Context) ([]string, error) { + return f.r.Keys(indexerModule + "." + packagesState) +} + +func (f followedPackages) Get(_ context.Context, key string) ([]byte, bool, error) { + return f.r.Get(indexerModule+"."+packagesState, key) +} + func main() { steps := map[string]build.StateRead{} for _, bm := range build.StuckBuildModules { @@ -370,8 +404,33 @@ func main() { } return e.Value, true, nil }} - tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr) - tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask})...) + // The catalog half's view of the packages the bundle follows itself (design 47 G §572, A-70), as other bundles follow views. + pk := &views.Reader{Module: builds.BuilderModule, Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] "+f+"\n", a...) }, + Acts: meshhandactsclient.Recorder{Ask: stdio.Ask}} + pk.Follow(indexerModule, packagesState, packagesTool) + pk.Start(context.Background()) + carriers := versions.Carriers(followedPackages{pk}) + released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) { + raw, err := stdio.Ask(catalogVersionsVerb, meshcatalog.VersionsInput{Module: module}) + if err != nil { + return nil, err + } + var a meshcatalog.VersionsAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return nil, fmt.Errorf("the seat mesh-catalog's versions answered what does not read: %w", err) + } + return a.Versions, nil + } + reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) { + raw, err := stdio.Ask(reservedVerb, meshbuilds.ReservedInput{Name: name, Version: version}) + if err != nil { + return meshbuilds.ReservedAnswer{}, err + } + var a meshbuilds.ReservedAnswer + return a, json.Unmarshal(raw, &a) + } + tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, fr, ofr, reserve) + tools = append(tools, reservedTools(rr, buildAgain, time.Now, meshhandactsclient.Recorder{Ask: stdio.Ask}, carriers, released)...) ws := stdio.State(build.WaitsState) wr := versions.Reader{Keys: ws.Keys, Get: func(key string) (json.RawMessage, bool, error) { e, err := ws.Get(key) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go index 8e3d342c..d6a45807 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reserved.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "strings" @@ -16,7 +17,10 @@ import ( // reservedTools are builder_reserved, builder_reserved_drop and the seat mesh-builds' verb reserved (design 47 G section // 369), served as tools of the bundle. The drop is a hand act: it is recorded in the hand-act log first, then written to // reasks for the builder, which owns the reservations, to answer. -func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder) []stdio.Tool { +// +// The verb reserved given a name finds the module from the catalog half's view the bundle follows (carriers) and, for a version +// no reservation holds, asks the seat mesh-catalog's verb versions (released); the builder's process serves no call (A-70). +func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, acts meshhandactsclient.Recorder, carriers func(context.Context, string) ([]versions.Built, error), released versions.CatalogVersions) []stdio.Tool { str := func(args map[string]any, k string) string { s, _ := args[k].(string); return strings.TrimSpace(s) } return []stdio.Tool{ {Name: versions.ReservedTool, Description: versions.ReservedDescription, @@ -25,10 +29,14 @@ func reservedTools(rr versions.Reader, ask queueAsker, now func() time.Time, act Run: func(args map[string]any) (any, error) { return versions.AnswerReserved(rr, str(args, "module")) }}, {Name: versions.ReservedVerb, Description: "The seat mesh-builds' verb reserved: a module's reserved version with each artifact's digest once built " + "(design 47 G section 369), for a registry holder deciding whether to publish. Read-only.", - Input: map[string]any{"type": "object", "required": []string{"module"}, "properties": map[string]any{ - "module": map[string]any{"type": "string", "description": "the module"}, + Input: map[string]any{"type": "object", "properties": map[string]any{ + "module": map[string]any{"type": "string", "description": "the module; give this or name"}, + "name": map[string]any{"type": "string", "description": "a package's or an image's name in its ecosystem, in place of module"}, "version": map[string]any{"type": "string", "description": "the version, when asked for a particular one"}}}, Run: func(args map[string]any) (any, error) { + if name := str(args, "name"); name != "" { + return versions.AnswerReservedByName(context.Background(), rr, carriers, released, name, str(args, "version")) + } return versions.AnswerReservedVerb(rr, str(args, "module"), str(args, "version")) }}, {Name: versions.ReservedDropTool, Description: versions.ReservedDropDescription, diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go new file mode 100644 index 00000000..e23c2dba --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/reservedname_test.go @@ -0,0 +1,58 @@ +package main + +import ( + "context" + "encoding/json" + "testing" + "time" + + "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + + "mesh-delivery-builder/internal/build" + "mesh-delivery-builder/internal/versions" +) + +// Design 47 G §572, §582, A-70: the bundle's verb reserved takes a name and answers from the view it follows and the catalog's +// versions, and builder_versions, asked by a name, calls reserved and answers what it answers. +func TestReservedByNameIsTheBundlesAndBuilderVersionsCallsIt(t *testing.T) { + none := versions.Reader{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, bool, error) { return nil, false, nil }} + carriers := func(_ context.Context, name string) ([]versions.Built, error) { + if name == "@novox/mesh-sdk" { + return []versions.Built{{Module: "mesh-sdk", Artifact: "ts"}}, nil + } + return nil, nil + } + released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) { + return []meshcatalog.VersionEntry{{Module: module, Version: "0.1.2", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}}}, nil + } + acts := meshhandactsclient.Recorder{Ask: func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil }} + byName := map[string]func(map[string]any) (any, error){} + for _, tl := range reservedTools(none, nil, time.Now, acts, carriers, released) { + byName[tl.Name] = tl.Run + } + got, err := byName[versions.ReservedVerb](map[string]any{"name": "@novox/mesh-sdk", "version": "0.1.2"}) + want := meshbuilds.ReservedAnswer{Module: "mesh-sdk", Version: "0.1.2", Released: true, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{{Artifact: "mesh-sdk:ts", Digest: "sha256:dd"}}} + if err != nil || got.(meshbuilds.ReservedAnswer).Released != want.Released || got.(meshbuilds.ReservedAnswer).Artifacts[0] != want.Artifacts[0] { + t.Fatalf("reserved by name = %+v (%v), want %+v", got, err, want) + } + var called []string + reserve := func(name, version string) (meshbuilds.ReservedAnswer, error) { + called = append(called, name+"@"+version) + return want, nil + } + tools := Tools(build.StuckReader{}, nil, nil, nil, nil, time.Now, none, none, none, reserve) + for _, tl := range tools { + if tl.Name == versions.VersionsTool { + out, err := tl.Run(map[string]any{"name": "@novox/mesh-sdk", "version": "0.1.2"}) + ans, ok := out.(versions.VersionsAnswer) + if err != nil || !ok || ans.Reserved == nil || ans.Reserved.Module != "mesh-sdk" || !ans.Reserved.Released { + t.Fatalf("builder_versions by name = %+v (%v), want reserved's answer in it", out, err) + } + } + } + if len(called) != 1 || called[0] != "@novox/mesh-sdk@0.1.2" { + t.Errorf("reserved was called %v, want once, by the name and version", called) + } +} diff --git a/modules/mesh-delivery-builder/internal/versions/names.go b/modules/mesh-delivery-builder/internal/versions/names.go index e6e7d091..bb1881c0 100644 --- a/modules/mesh-delivery-builder/internal/versions/names.go +++ b/modules/mesh-delivery-builder/internal/versions/names.go @@ -2,6 +2,8 @@ package versions import ( "context" + "errors" + "fmt" "sort" "strings" @@ -71,22 +73,28 @@ func isSeriesKey(key string) bool { return ok } -// ModuleOf is the module and artifact a package's name in its ecosystem is built from, as the indexer's packages state holds -// them (the inverse of NamesIn; design 47 G §373, §465): the series key whose value names that name. -func ModuleOf(s PackagesState) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { - return func(ctx context.Context, name string) (string, string, bool, error) { +// Built is a module's artifact, which a package's name is the name of. +type Built struct{ Module, Artifact string } + +// Carriers are the module artifacts a package's name in its ecosystem is the name of, as the catalog half's packages view +// holds them (the inverse of NamesIn; design 47 G §572, §478): each distinct module and artifact of a series key whose value +// names that name, sorted. A name no entry carries answers none. +func Carriers(s PackagesState) func(ctx context.Context, name string) ([]Built, error) { + return func(ctx context.Context, name string) ([]Built, error) { keys, err := s.Keys(ctx) if err != nil { - return "", "", false, err + return nil, err } sort.Strings(keys) + seen := map[Built]bool{} + var out []Built for _, key := range keys { if !isSeriesKey(key) { continue } value, found, err := s.Get(ctx, key) if err != nil { - return "", "", false, err + return nil, err } if !found { continue @@ -95,10 +103,38 @@ func ModuleOf(s PackagesState) func(ctx context.Context, name string) (module, a if err != nil || v.Name != name { continue } - if module, artifact, cut := strings.Cut(v.Package, "/"); cut && module != "" && artifact != "" { - return module, artifact, true, nil + if module, artifact, cut := strings.Cut(v.Package, "/"); cut && module != "" && artifact != "" && !seen[Built{module, artifact}] { + seen[Built{module, artifact}] = true + out = append(out, Built{module, artifact}) } } - return "", "", false, nil + return out, nil } } + +// ModuleOf is the module and artifact a package's name is built from (Carriers), found false for a name no entry carries; more +// than one entry carrying it is an error naming them. +func ModuleOf(s PackagesState) func(ctx context.Context, name string) (module, artifact string, found bool, err error) { + carriers := Carriers(s) + return func(ctx context.Context, name string) (string, string, bool, error) { + got, err := carriers(ctx, name) + switch { + case err != nil: + return "", "", false, err + case len(got) == 0: + return "", "", false, nil + case len(got) > 1: + return "", "", false, errors.New(MoreThanOne(name, got)) + } + return got[0].Module, got[0].Artifact, true, nil + } +} + +// MoreThanOne is the words of a name that more than one entry carries. +func MoreThanOne(name string, got []Built) string { + var refs []string + for _, b := range got { + refs = append(refs, b.Module+"/"+b.Artifact) + } + return fmt.Sprintf("the name %s is carried by more than one entry: %s", name, strings.Join(refs, ", ")) +} diff --git a/modules/mesh-delivery-builder/internal/versions/reserve.go b/modules/mesh-delivery-builder/internal/versions/reserve.go index 89b0dc32..9e190a6f 100644 --- a/modules/mesh-delivery-builder/internal/versions/reserve.go +++ b/modules/mesh-delivery-builder/internal/versions/reserve.go @@ -11,6 +11,7 @@ import ( "time" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" ) // **The reservation is one key per module and MAJOR.MINOR** (design 47 G §369, §52): the builder keeps in its old state @@ -436,6 +437,53 @@ func AnswerReservedVerb(r Reader, module, version string) (meshbuilds.ReservedAn return out, nil } +// CatalogVersions answers a module's released versions, the seat mesh-catalog's verb versions (design 47 G §582). +type CatalogVersions func(ctx context.Context, module string) ([]meshcatalog.VersionEntry, error) + +// AnswerReservedByName is the verb reserved given a package's name (design 47 G §572, §582, A-70): the module is found from the +// name in the catalog half's view (carriers); that module's reserved version answers as AnswerReservedVerb does. A version +// given with no reservation of it (a reservation goes once its version is released) is asked of the catalog, and answers +// released with the artifacts and digests the release recorded. A name no entry carries, more than one entry carries, or a +// version neither reserved nor released is answered error, naming which, set alone. +func AnswerReservedByName(ctx context.Context, r Reader, carriers func(context.Context, string) ([]Built, error), released CatalogVersions, name, version string) (meshbuilds.ReservedAnswer, error) { + name, version = strings.TrimSpace(name), strings.TrimSpace(version) + got, err := carriers(ctx, name) + if err != nil { + return meshbuilds.ReservedAnswer{}, fmt.Errorf("the entries of %s cannot be read: %w", name, err) + } + switch { + case len(got) == 0: + return meshbuilds.ReservedAnswer{Error: "no entry carries the name " + name}, nil + case len(got) > 1: + return meshbuilds.ReservedAnswer{Error: MoreThanOne(name, got)}, nil + } + module := got[0].Module + rs := &Reserver{Store: readOnlyReserve{readStore{r}}} + if _, found, err := rs.Of(ctx, module, version); err != nil { + return meshbuilds.ReservedAnswer{}, err + } else if found { + return AnswerReservedVerb(r, module, version) + } + if version != "" { + entries, err := released(ctx, module) + if err != nil { + return meshbuilds.ReservedAnswer{}, fmt.Errorf("the released versions of %s cannot be read: %w", module, err) + } + for _, e := range entries { + if e.Version != version { + continue + } + out := meshbuilds.ReservedAnswer{Module: module, Version: version, Released: true, Artifacts: []meshbuilds.ReservedAnswerArtifactsItem{}} + for _, a := range e.Artifacts { + out.Artifacts = append(out.Artifacts, meshbuilds.ReservedAnswerArtifactsItem{Artifact: a.Artifact, Digest: a.Digest}) + } + return out, nil + } + return meshbuilds.ReservedAnswer{Error: fmt.Sprintf("%s (%s) has no version %s reserved or released", name, module, version)}, nil + } + return meshbuilds.ReservedAnswer{Error: fmt.Sprintf("%s (%s) has no reserved version", name, module)}, nil +} + // The descriptions of the reservation's tools. const ( ReservedDescription = "The versions the builder reserved (its state reserved, design 47 G section 369): one per module and MAJOR.MINOR, " + diff --git a/modules/mesh-delivery-builder/internal/versions/reservedname_test.go b/modules/mesh-delivery-builder/internal/versions/reservedname_test.go new file mode 100644 index 00000000..ee5b48f2 --- /dev/null +++ b/modules/mesh-delivery-builder/internal/versions/reservedname_test.go @@ -0,0 +1,76 @@ +package versions + +import ( + "context" + "encoding/json" + "errors" + "strings" + "testing" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" +) + +// TestReservedByNameAnswersFromTheViewAndTheCatalog: design 47 G §572, §582, A-70 — a package's name (an npm name or a Go +// module path) is mapped to its module from the catalog half's view; the reserved version answers as for the module, a version +// no reservation holds is asked of the catalog's versions and answers released with the release's digests, and a name no entry +// carries, one more than one carries, or a version neither reserved nor released answers error, set alone. +func TestReservedByNameAnswersFromTheViewAndTheCatalog(t *testing.T) { + ctx := context.Background() + view := packagesMem{ + "git=2enovox=2ebe/novox/mesh-sdk/go.0.1": []byte(`{"v":1,"name":"git.novox.be/novox/mesh-sdk/go","package":"mesh-sdk/go","series":"0.1"}`), + "git=2enovox=2ebe/novox/mesh-sdk/go.0.2": []byte(`{"v":1,"name":"git.novox.be/novox/mesh-sdk/go","package":"mesh-sdk/go","series":"0.2"}`), + "@novox/mesh-sdk.0.1": []byte(`{"v":1,"name":"@novox/mesh-sdk","package":"mesh-sdk/ts","series":"0.1"}`), + "twice.0.1": []byte(`{"v":1,"name":"twice","package":"a/x","series":"0.1"}`), + "twice-b.0.1": []byte(`{"v":1,"name":"twice","package":"b/y","series":"0.1"}`), + } + store := &reserveMem{mem: newMem()} + r := &Reserver{Store: store, ReleasedOf: func(context.Context, string) ([]string, error) { return []string{"0.1.3"}, nil }} + if _, err := r.Reserve(ctx, "mesh-sdk", "0.1", []string{"mesh-sdk:ts"}, Source{}); err != nil { + t.Fatal(err) + } + rd := Reader{ + Keys: func() ([]string, error) { return store.KeysOf(ctx, AllFilter) }, + Get: func(key string) (json.RawMessage, bool, error) { + raw, _, ok, err := store.Get(ctx, key) + return raw, ok, err + }} + var asked []string + released := func(_ context.Context, module string) ([]meshcatalog.VersionEntry, error) { + asked = append(asked, module) + return []meshcatalog.VersionEntry{{Module: module, Version: "0.1.2", Artifacts: []meshcatalog.VersionArtifact{{Artifact: "mesh-sdk:go", Digest: "sha256:dd"}}}}, nil + } + carriers := Carriers(view) + ask := func(name, version string) meshbuilds.ReservedAnswer { + t.Helper() + got, err := AnswerReservedByName(ctx, rd, carriers, released, name, version) + if err != nil { + t.Fatalf("%s %s: %v", name, version, err) + } + return got + } + if got := ask("@novox/mesh-sdk", ""); got.Module != "mesh-sdk" || got.Version != "0.1.4" || got.Released || got.Error != "" { + t.Errorf("an npm name = %+v, want the module's reserved version", got) + } + if got := ask("git.novox.be/novox/mesh-sdk/go", "0.1.2"); !got.Released || got.Module != "mesh-sdk" || len(got.Artifacts) != 1 || got.Artifacts[0].Digest != "sha256:dd" { + t.Errorf("a Go module path = %+v, want the release's artifacts and digests (two series of one entry are one entry)", got) + } + if len(asked) != 1 || asked[0] != "mesh-sdk" { + t.Errorf("the catalog was asked for %v, want the module's versions once, for the version no reservation holds", asked) + } + for name, c := range map[string]struct{ name, version, want string }{ + "unknown name": {"nothing", "", "no entry carries"}, + "two entries": {"twice", "", "more than one entry"}, + "neither released": {"git.novox.be/novox/mesh-sdk/go", "9.9.9", "reserved or released"}, + } { + if got := ask(c.name, c.version); !strings.Contains(got.Error, c.want) || got.Module != "" || len(got.Artifacts) != 0 { + t.Errorf("%s = %+v, want error naming %q alone", name, got, c.want) + } + } + if _, err := AnswerReservedByName(ctx, rd, Carriers(packagesMem(nil)), released, "x", ""); err == nil { + t.Error("an unreadable view was answered as if no entry carried the name") + } + if _, err := AnswerReservedByName(ctx, rd, carriers, func(context.Context, string) ([]meshcatalog.VersionEntry, error) { return nil, errors.New("down") }, "git.novox.be/novox/mesh-sdk/go", "0.1.2"); err == nil { + t.Error("an unreadable catalog was answered as if the version were not released") + } +} diff --git a/modules/mesh-delivery-builder/internal/versions/tools.go b/modules/mesh-delivery-builder/internal/versions/tools.go index 798bb0f8..a375b67d 100644 --- a/modules/mesh-delivery-builder/internal/versions/tools.go +++ b/modules/mesh-delivery-builder/internal/versions/tools.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" "strings" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" ) // The builder's tools of its versions (design 51 *Events of the builder*, *Tools of the builder*). @@ -64,6 +66,9 @@ type VersionsAnswer struct { Asked string `json:"asked,omitempty"` Recorded *bool `json:"recorded,omitempty"` Why string `json:"why,omitempty"` + // Reserved is what the seat mesh-builds' verb reserved answers for the name (and version): the reserved or released version + // with each artifact's digest, or its error (design 47 G §572, §582). + Reserved *meshbuilds.ReservedAnswer `json:"reserved,omitempty"` } // AnswerVersions is builder_versions: a name's versions in order, in one call; with a version (and a digest), whether diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 835fd25d..0a6d1dc5 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -143,7 +143,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions", "seat:git.tag", - "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove", + "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove", "seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index e43d5995..8995002a 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -82,6 +82,7 @@ "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", + "seat:mesh-builds.reserved", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence",