Indexer: the catalog's values are the seat's types and the catalog client's, the gate's, checker's and builder's states are read by their own names, the go.mod leaves the old seat paths; no go/delivery name is left (D-11: its holders and buckets are its own constants checked against its manifest; ADR 0634 §1, §2, §5; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:25:35 +01:00
parent 5125365189
commit 9869e34b8b
88 changed files with 2081 additions and 1293 deletions
@@ -3,8 +3,9 @@ package main
import (
"fmt"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -16,12 +17,12 @@ import (
func catalogRead(modules, manifests, nodes stdio.KeptState, seatDefinitions, marks views.Source) index.CatalogRead {
return index.CatalogRead{
SeatDefinitions: seatDefinitions, Marks: marks,
Modules: func() (map[string]delivery.Module, error) {
Modules: func() (map[string]meshcatalog.ModulesByModuleValue, error) {
keys, err := modules.Keys()
if err != nil {
return nil, err
}
out := map[string]delivery.Module{}
out := map[string]meshcatalog.ModulesByModuleValue{}
for _, k := range keys {
e, err := modules.Get(k)
if err != nil {
@@ -30,14 +31,14 @@ func catalogRead(modules, manifests, nodes stdio.KeptState, seatDefinitions, mar
if e == nil {
continue
}
if m, err := delivery.DecodeModule(e.Value); err == nil {
if m, err := meshcatalogclient.DecodeModule(e.Value); err == nil {
out[k] = m
}
}
return out, nil
},
Manifest: func(digest string) (string, error) {
key, err := delivery.ManifestsKey(digest)
key, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
return "", err
}
@@ -48,8 +49,8 @@ func catalogRead(modules, manifests, nodes stdio.KeptState, seatDefinitions, mar
if e == nil {
return "", fmt.Errorf("the manifests state holds no %s", digest)
}
kept, err := delivery.DecodeManifest(e.Value)
return kept.JSON, err
kept, err := meshcatalogclient.DecodeManifest(e.Value)
return kept.Manifest, err
},
Graph: func() (graph.Graph, error) {
keys, err := nodes.Keys()
@@ -10,7 +10,7 @@ import (
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"mesh-delivery-indexer/internal/index"
)
@@ -45,7 +45,7 @@ func holdsTool(read stdio.KeptState, ask asker, now func() time.Time, acts meshh
}
if release {
what := "moved the mesh-wide hold of " + module + " by hand"
if mk, err := delivery.ModulesKey(module); err == nil {
if mk, err := meshcatalogclient.ModulesKey(module); err == nil {
if e, err := read.Get(mk); err == nil && e != nil {
if h, err := index.DecodeHold(e.Value); err == nil {
what += ", " + index.Strands(h)
@@ -6,8 +6,9 @@ import (
"time"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/views"
@@ -22,14 +23,14 @@ func viewTools(getenv func(string) string, state func(string) views.Source, now
marks := state(views.MarksState)
var out []stdio.Tool
for _, t := range index.FirstViewTools {
out = append(out, views.FirstViewTool(delivery.IndexerModule, t.Tool, t.State, t.Words,
out = append(out, views.FirstViewTool(index.Module, t.Tool, t.State, t.Words,
views.ParseCallers(getenv(views.CallersEnv(t.Tool))), state(t.State), marks, now))
}
for _, t := range index.ReadTools {
out = append(out, views.ReadTool(delivery.IndexerModule, t.Tool, t.State, t.Words, state(t.State), marks, now))
out = append(out, views.ReadTool(index.Module, t.Tool, t.State, t.Words, state(t.State), marks, now))
}
// Its own first views, of the assigner's placement (DLV-F3r): read, and asked again by hand.
return append(out, views.StatusTools(delivery.IndexerModule, "indexer_first_views", "indexer_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, now)...)
return append(out, views.StatusTools(index.Module, "indexer_first_views", "indexer_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, now)...)
}
// keptSource is one of the indexer's states as its runtime reads it.
@@ -51,30 +52,30 @@ func configReadersTool(watched *stdio.WatchedStates) stdio.Tool {
if module == "" {
return nil, fmt.Errorf("%s names the module", index.ConfigReadersTool)
}
trunk := map[string]delivery.Module{}
trunk := map[string]meshcatalog.ModulesByModuleValue{}
mods, err := watched.Values("modules")
if err != nil {
return nil, fmt.Errorf("the modules state cannot be read: %w", err)
}
for k, v := range mods {
if m, err := delivery.DecodeModule(v); err == nil {
if m, err := meshcatalogclient.DecodeModule(v); err == nil {
trunk[k] = m
}
}
kept := map[string]delivery.Manifest{}
kept := map[string]meshcatalog.ManifestsByDigestValue{}
raws, err := watched.Values("manifests")
if err != nil {
return nil, fmt.Errorf("the manifests state cannot be read: %w", err)
}
for k, v := range raws {
if m, err := delivery.DecodeManifest(v); err == nil {
if m, err := meshcatalogclient.DecodeManifest(v); err == nil {
kept[k] = m
}
}
var pulls []delivery.ConfigCandidate
if cands, err := watched.Values(delivery.IndexerConfigCandidatesState); err == nil {
var pulls []meshcatalog.ConfigCandidatesByPullValue
if cands, err := watched.Values(index.ConfigCandidatesState); err == nil {
for _, v := range cands {
var c delivery.ConfigCandidate
var c meshcatalog.ConfigCandidatesByPullValue
if json.Unmarshal(v, &c) == nil {
pulls = append(pulls, c)
}
@@ -140,7 +141,7 @@ func packageUsersTool(state func(string) views.Source) stdio.Tool {
"package": map[string]any{"type": "string", "description": "one package by its name in its own ecosystem (a Go module path, an npm package name); all when absent"}}},
Run: func(args map[string]any) (any, error) {
only, _ := args["package"].(string)
got, err := index.PackageUsersOf(state("modules"), state("manifests"), state(delivery.PackagesState), only)
got, err := index.PackageUsersOf(state("modules"), state("manifests"), state(index.PackagesState), only)
if err != nil {
return nil, err
}
@@ -5,13 +5,13 @@ import (
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/index"
)
// mergedTool is the forge bridge's tool that answers a merge's merged message again (issue 1585): arguments repository
// and merge, answer {message}, the message whole as the bridge writes it on FORGE. The indexer's manifest lists it in
// invokes, which is the grant to call it.
const mergedTool = delivery.ForgeBridgeModule + ".bridge_merged_message"
const mergedTool = index.ForgeBridgeModule + ".bridge_merged_message"
// messageVia asks the forge bridge for a merge's merged message, for a held merge the indexer does not hold in memory (a
// restart, a new build). An error is the bridge's own words.
@@ -17,14 +17,25 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -74,20 +85,20 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
builds.end(t, "novox/mesh-catalog", "c", "modules/c")
}, func(ctx context.Context, js jetstream.JetStream) {
// The states and the stream the controller makes from the indexer's manifest (old sections, ADR 0595 §11).
for bucket, history := range map[string]uint8{index.PendingBucket: 1, delivery.VerdictsBucket: delivery.VerdictsHistory,
graph.Bucket: 1, delivery.TakenBucket: 1, delivery.PackagesBucket: 1,
for bucket, history := range map[string]uint8{index.PendingBucket: 1, index.VerdictsBucket: index.VerdictsHistory,
graph.Bucket: 1, index.TakenBucket: 1, index.PackagesBucket: 1,
index.SeatDefinitionsBucket: 1, index.RequirementsBucket: 1, findingsBucket: 1, index.HoldsBucket: 1} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: bucket, History: history}); err != nil {
t.Fatal(err)
}
}
// The take-in events are the indexer's ordinary events (ADR 0633 §5), on the path manifest-accepted takes.
tc, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket("mesh-build-go"), History: 1})
tc, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.ToolchainsBucket("mesh-build-go"), History: 1})
if err != nil {
t.Fatal(err)
}
key, _ := delivery.ToolchainKey("n1", "go")
value, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: time.Now()})
key, _ := index.ToolchainKey("n1", "go")
value, err := index.EncodeToolchain(nodebuildgo.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: ptr(time.Now())})
if err != nil {
t.Fatal(err)
}
@@ -137,18 +148,18 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
}
}
hash, _ := delivery.HeadsHash([]delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}, {Repository: "novox/mesh-catalog", Number: 300, Head: h2}})
var c delivery.Candidate
hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}, {Repository: "novox/mesh-catalog", Number: 300, Head: h2}})
var c meshcatalog.CandidatesByGroupValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
e, err := candidates.Get(ctx, "feat/x")
if err != nil {
continue
}
if c, err = delivery.DecodeCandidate(e.Value()); err == nil && len(c.Pulls) == 2 {
if c, err = index.DecodeCandidate(e.Value()); err == nil && len(c.Pulls) == 2 {
break
}
}
if c.Group != "feat/x" || c.Base != 0 || c.HeadsHash != hash || len(c.Pulls) != 2 {
if c.Group != "feat/x" || c.Base != 0 || c.Heads != hash || len(c.Pulls) != 2 {
t.Fatalf("candidate = %+v within 20 s", c)
}
for _, p := range c.Pulls {
@@ -167,9 +178,9 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
t.Errorf("views event = %+v, bucket at %d (%v)", views0, cur.Revision(), err)
}
marks, _ := js.KeyValue(ctx, index.MarksBucket)
f := &views.Follower{Owner: delivery.IndexerModule, Tool: "indexer_read_candidates", State: "candidates",
f := &views.Follower{Owner: index.Module, Tool: "indexer_read_candidates", State: "candidates",
Ask: func(context.Context, string) (views.Answer, error) {
return views.FirstView(delivery.IndexerModule, "candidates", jsSource{ctx, candidates}, jsSource{ctx, marks}, "", time.Now())
return views.FirstView(index.Module, "candidates", jsSource{ctx, candidates}, jsSource{ctx, marks}, "", time.Now())
}}
if err := f.Start(ctx); err != nil {
t.Fatal(err)
@@ -191,7 +202,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
if err != nil {
t.Fatal(err)
}
const ix = delivery.IndexerModule
const ix = index.Module
m1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1,
Merge: "1111111111111111111111111111111111111111", Root: "r0",
Trees: map[string]string{"modules/" + ix: "tree-indexer-2", "modules/a": "tree-a-2"},
@@ -262,35 +273,35 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
return e.Value(), true
}
// Only what building and moving the named indexer needs is written.
raw, ok := get(delivery.ModulesBucket, ix)
own, err := delivery.DecodeModule(raw)
if !ok || err != nil || own.FP2 != named || own.Commit != m1.Merge {
raw, ok := get(index.ModulesBucket, ix)
own, err := meshcatalogclient.DecodeModule(raw)
if !ok || err != nil || own.Fp2 != named || own.Commit != m1.Merge {
t.Fatalf("modules.%s = %+v (%v), want the named build %s at the merge", ix, own, err, named)
}
if _, ok := get(delivery.ManifestsBucket, own.Manifest); !ok {
if _, ok := get(index.ManifestsBucket, own.Manifest); !ok {
t.Errorf("manifests.%s is not written", own.Manifest)
}
if _, ok := get(delivery.VerdictsBucket, own.Manifest); !ok {
if _, ok := get(index.VerdictsBucket, own.Manifest); !ok {
t.Errorf("the verdict on the indexer's manifest %s is not written", own.Manifest)
}
if gk, _ := graph.Key(ix); !func() bool { _, ok := get(graph.Bucket, gk); return ok }() {
t.Errorf("the indexer's key %s of the module graph is not written", gk)
}
repoKey, _ := delivery.RepositoryKey(m1.Repository)
if _, ok := get(delivery.ModulesBucket, "a"); ok {
repoKey, _ := trunk.RepositoryKey(m1.Repository)
if _, ok := get(index.ModulesBucket, "a"); ok {
t.Errorf("modules.a is written while the revision is pending")
}
if _, ok := get(delivery.TakenBucket, repoKey); ok {
if _, ok := get(index.TakenBucket, repoKey); ok {
t.Errorf("taken.%s is written while the revision is pending", repoKey)
}
if e, err := candidates.Get(ctx, "feat/x"); err != nil {
t.Errorf("the candidate feat/x is gone while the merge of #12 is pending: %v", err)
} else if c, err := delivery.DecodeCandidate(e.Value()); err != nil || len(c.Pulls) != 2 {
} else if c, err := index.DecodeCandidate(e.Value()); err != nil || len(c.Pulls) != 2 {
t.Errorf("the candidate feat/x = %+v while the merge of #12 is pending, want its two pull requests", c)
}
// A group's first member arrives meanwhile; its end is still to come (the knowledge base's remark: it stays across the restart).
members := []delivery.Head{{Repository: "novox/mesh-catalog", Number: 40, Head: "4040404040404040404040404040404040404040"},
members := []meshdeliveryclient.Head{{Repository: "novox/mesh-catalog", Number: 40, Head: "4040404040404040404040404040404040404040"},
{Repository: "novox/mesh-controller", Number: 41, Head: "4141414141414141414141414141414141414141"}}
g1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/g", Number: 40, Head: members[0].Head,
Merge: "4000000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/c": "tree-c-2"},
@@ -337,16 +348,16 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
break
}
}
if raw, ok := get(delivery.ModulesBucket, "a"); !ok || !strings.Contains(string(raw), m1.Merge) {
if raw, ok := get(index.ModulesBucket, "a"); !ok || !strings.Contains(string(raw), m1.Merge) {
t.Errorf("modules.a = %s after the take-in at the start, want it at the merge %s", raw, m1.Merge)
}
if _, ok := get(delivery.TakenBucket, repoKey); !ok {
if _, ok := get(index.TakenBucket, repoKey); !ok {
t.Errorf("taken.%s is not written by the take-in at the start", repoKey)
}
if len(w) != 1 || w[0].Key != groupKey || len(w[0].Keys) != 1 || w[0].Keys[0] != g1Key {
t.Fatalf("indexer_pending after the start = %+v, want only the group gathered, its first member %s", w, g1Key)
}
if _, ok := get(delivery.ModulesBucket, "c"); ok {
if _, ok := get(index.ModulesBucket, "c"); ok {
t.Errorf("modules.c is written while its group is gathered")
}
@@ -365,8 +376,8 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15
}
var hasC, hasK bool
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline) && !(hasC && hasK && len(w) == 0); time.Sleep(50 * time.Millisecond) {
_, hasC = get(delivery.ModulesBucket, "c")
_, hasK = get(delivery.ModulesBucket, "k")
_, hasC = get(index.ModulesBucket, "c")
_, hasK = get(index.ModulesBucket, "k")
w = waits()
}
if !hasC || !hasK || len(w) != 0 {
@@ -511,21 +522,21 @@ func startIndexerRun(t *testing.T, put func(context.Context, buildLog), before .
t.Fatal(err)
}
}
candidates, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.CandidatesBucket, History: delivery.CandidatesHistory})
candidates, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.CandidatesBucket, History: index.CandidatesHistory})
if err != nil {
t.Fatal(err)
}
modules, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ModulesBucket, History: delivery.ModulesHistory})
modules, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.ModulesBucket, History: index.ModulesHistory})
if err != nil {
t.Fatal(err)
}
// The bucket of the manifests, made by the controller from the manifest's state entry manifests (history 1, issue 1449).
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ManifestsBucket, History: delivery.ManifestsHistory}); err != nil {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.ManifestsBucket, History: index.ManifestsHistory}); err != nil {
t.Fatal(err)
}
// The builder's module log, as the runtime raises it (ADR 0297), filled by put before the indexer starts.
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: buscompose.LogStreamName(delivery.BuilderModule, delivery.BuilderLog),
Subjects: []string{buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog) + ".>"}, AllowDirect: true}); err != nil {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: buscompose.LogStreamName(builds.BuilderModule, builds.BuilderLog),
Subjects: []string{buscompose.LogSubject(builds.BuilderModule, builds.BuilderLog) + ".>"}, AllowDirect: true}); err != nil {
t.Fatal(err)
}
// The indexer's marks (ADR 0615 §5), and a waiter on manifest-accepted, which the views family never wakes (its events
@@ -586,13 +597,13 @@ type buildLog struct {
// end appends the end of a done trunk build of module from repo's directory path.
func (l buildLog) end(t *testing.T, repo, module, path string) {
t.Helper()
raw, err := delivery.EncodeBuildLogEntry(delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: module, FP: "src2:" + module,
raw, err := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: module, FP: "src2:" + module,
Repository: repo, Directory: path, Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Node: "n1",
At: time.Date(2026, 10, 19, 9, 38, 0, 0, time.UTC), Outcome: delivery.BuildDone})
At: time.Date(2026, 10, 19, 9, 38, 0, 0, time.UTC), Outcome: builds.BuildDone})
if err != nil {
t.Fatal(err)
}
if _, err := l.js.Publish(l.ctx, buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog)+"."+module, raw); err != nil {
if _, err := l.js.Publish(l.ctx, buscompose.LogSubject(builds.BuilderModule, builds.BuilderLog)+"."+module, raw); err != nil {
t.Fatal(err)
}
}
@@ -614,15 +625,15 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m
if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.merged", data); err != nil {
t.Fatal(err)
}
var m delivery.Module
var m meshcatalog.ModulesByModuleValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "a"); err == nil {
if m, err = delivery.DecodeModule(e.Value()); err == nil {
if m, err = meshcatalogclient.DecodeModule(e.Value()); err == nil {
break
}
}
}
if m.Repository != "novox/mesh-delivery" || m.Commit != merge || m.Path != "modules/a" || m.Manifest == "" || m.FP != "" || m.None != "" { // 1606/AC2
if m.Repository != "novox/mesh-delivery" || m.Commit != merge || m.Path != "modules/a" || m.Manifest == "" || m.Fp != "" || m.None != "" { // 1606/AC2
t.Errorf("module = %+v within 20 s, want path modules/a, its manifest, and no value of today's form", m)
}
}
@@ -652,7 +663,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
builds.end(t, "novox/mesh-catalog", "avahi", "modules/avahi")
}, func(ctx context.Context, js jetstream.JetStream) {
// The verdicts state and the stream of the indexer's events, as the controller makes them from its manifest.
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.VerdictsBucket, History: delivery.VerdictsHistory}); err != nil {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.VerdictsBucket, History: index.VerdictsHistory}); err != nil {
t.Fatal(err)
}
})
@@ -671,20 +682,20 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-catalog.merged", data); err != nil {
t.Fatal(err)
}
var m delivery.Module
var m meshcatalog.ModulesByModuleValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err = delivery.DecodeModule(e.Value()); err == nil {
if m, err = meshcatalogclient.DecodeModule(e.Value()); err == nil {
break
}
}
}
digest := delivery.ManifestDigest(manifest)
if m.Manifest != digest || m.None2 != "toolchain not yet reported" || m.FP2 != "" || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" ||
digest := meshcatalogclient.ManifestDigest(manifest)
if m.Manifest != digest || m.None2 != "toolchain not yet reported" || m.Fp2 != "" || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" ||
m.Inputs2.Locks["tools"] != goSum || m.Path != "modules/avahi" { // 1606/AC2
t.Errorf("module = %+v within 20 s, want its path modules/avahi, its manifest digest %s, its inputs and none2 'toolchain not yet reported'", m, digest)
}
kv, err := js.KeyValue(ctx, delivery.ManifestsBucket)
kv, err := js.KeyValue(ctx, index.ManifestsBucket)
if err != nil {
t.Fatal(err)
}
@@ -692,20 +703,20 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
if err != nil {
t.Fatalf("manifests.%s: %v", digest, err)
}
if got, err := delivery.DecodeManifest(e.Value()); err != nil || got.Module != "avahi" || got.JSON != string(manifest) {
if got, err := meshcatalogclient.DecodeManifest(e.Value()); err != nil || got.Module != "avahi" || got.Manifest != string(manifest) {
t.Errorf("manifest = %+v err %v, want avahi and the file's bytes", got, err)
}
verdicts, err := js.KeyValue(ctx, delivery.VerdictsBucket)
verdicts, err := js.KeyValue(ctx, index.VerdictsBucket)
if err != nil {
t.Fatal(err)
}
want := map[string]string{"good": delivery.VerdictAccepted, "bad": delivery.VerdictRefused, "later": delivery.VerdictManifestWaits}
got := map[string]delivery.ManifestVerdict{}
want := map[string]string{"good": meshcatalogclient.VerdictAccepted, "bad": meshcatalogclient.VerdictRefused, "later": meshcatalogclient.VerdictManifestWaits}
got := map[string]meshcatalog.VerdictsByDigestValue{}
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && len(got) < len(want); time.Sleep(50 * time.Millisecond) {
for name := range want {
if e, err := verdicts.Get(ctx, delivery.ManifestDigest([]byte(judged[name]))); err == nil {
if v, err := delivery.DecodeManifestVerdict(e.Value()); err == nil {
if e, err := verdicts.Get(ctx, meshcatalogclient.ManifestDigest([]byte(judged[name]))); err == nil {
if v, err := meshcatalogclient.DecodeVerdict(e.Value()); err == nil {
got[name] = v
}
}
@@ -732,8 +743,8 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
if err != nil {
break
}
var r delivery.ManifestRefused
if msg.Subject == "mesh.seat.mesh-catalog.event."+delivery.ManifestRefusedEvent && json.Unmarshal(msg.Data, &r) == nil {
var r meshcatalog.ManifestRefusedEvent
if msg.Subject == "mesh.seat.mesh-catalog.event."+meshcatalog.EventManifestRefused && json.Unmarshal(msg.Data, &r) == nil {
said[r.Module] = true
}
}
@@ -745,9 +756,9 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
}
// TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain: the bucket mesh-build-go_toolchains exists (the state
// toolchains of the build module, delivery.ToolchainsBucket) and holds no key; a merged message of mesh-catalog carrying
// toolchains of the build module, index.ToolchainsBucket) and holds no key; a merged message of mesh-catalog carrying
// avahi's real manifest makes modules.avahi with none2 'toolchain not yet reported'; the bucket made after the indexer started, a put of the key n1.go and the build module's event toolchain.reported (the build
// module's own layout, delivery.ToolchainKey and EncodeToolchain) makes, within 20 s and with no merge, its fingerprint:
// module's own layout, index.ToolchainKey and EncodeToolchain) makes, within 20 s and with no merge, its fingerprint:
// no builder input is waited for (ADR 0530 §1; issue 1887 I1).
func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *testing.T) { // 1444/AC10
manifest, err := os.ReadFile("../../internal/index/testdata/catalogue-a67f0196-avahi.module.json")
@@ -773,7 +784,7 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes
var got string
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err := delivery.DecodeModule(e.Value()); err == nil {
if m, err := meshcatalogclient.DecodeModule(e.Value()); err == nil {
if got = m.None2; got == want {
break
}
@@ -782,20 +793,20 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes
}
return got
}
if got := none2(delivery.ToolchainNoReport); got != delivery.ToolchainNoReport {
t.Fatalf("none2 = %q within 20 s, want %q", got, delivery.ToolchainNoReport)
if got := none2(sdkstep.ToolchainNoReport); got != sdkstep.ToolchainNoReport {
t.Fatalf("none2 = %q within 20 s, want %q", got, sdkstep.ToolchainNoReport)
}
// The bucket is made only now, as when the build module is assigned after the indexer started (review finding 9): the
// indexer finds it on its own.
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket("mesh-build-go"), History: 1})
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.ToolchainsBucket("mesh-build-go"), History: 1})
if err != nil {
t.Fatal(err)
}
key, err := delivery.ToolchainKey("n1", "go")
key, err := index.ToolchainKey("n1", "go")
if err != nil {
t.Fatal(err)
}
value, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: time.Now()})
value, err := index.EncodeToolchain(nodebuildgo.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: ptr(time.Now())})
if err != nil {
t.Fatal(err)
}
@@ -833,15 +844,15 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
if _, err := js.CreateOrUpdateConsumer(ctx, PreviewsStream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-indexer", AckPolicy: jetstream.AckExplicitPolicy}); err != nil {
t.Fatal(err)
}
source, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.SourceBucket, History: 4})
source, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.SourceBucket, History: 4})
if err != nil {
t.Fatal(err)
}
data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: map[string]string{"novox/mesh-catalog": trunk}})
data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{"novox/mesh-catalog": trunk}})
if err != nil {
t.Fatal(err)
}
if _, err := source.Put(ctx, delivery.SourceMainKey, data); err != nil {
if _, err := source.Put(ctx, meshdeliveryclient.SourceMainKey, data); err != nil {
t.Fatal(err)
}
})
@@ -857,7 +868,7 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
var got string
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := candidates.Get(ctx, "feat/x"); err == nil {
if c, err := delivery.DecodeCandidate(e.Value()); err == nil && len(c.Pulls) == 1 {
if c, err := index.DecodeCandidate(e.Value()); err == nil && len(c.Pulls) == 1 {
got = c.Pulls[0].Fps["avahi"].None2
if got == want {
break
@@ -871,14 +882,14 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
t.Fatalf("none2 = %q within 20 s of the pull request, want 'no merge preview is computed'", got)
}
// The checker's bucket is made only now, as when the checker is assigned after the indexer started.
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.PreviewsBucket, History: delivery.PreviewsHistory})
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PreviewsBucket, History: index.PreviewsHistory})
if err != nil {
t.Fatal(err)
}
pv := delivery.Preview{Repository: "novox/mesh-catalog", Head: head, Trunk: trunk, Merge: "6666666666666666666666666666666666666666",
Modules: map[string]delivery.PreviewModule{"modules/avahi": {Tree: "newtree", Manifest: delivery.ManifestDigest(manifest), Locks: map[string]string{"go.sum": goSum}}}}
key, _ := delivery.PreviewKey(pv.Repository, pv.Head, pv.Trunk)
data, err := delivery.EncodePreview(pv)
pv := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/mesh-catalog", Head: head, Trunk: trunk, Merge: "6666666666666666666666666666666666666666",
Modules: map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{"modules/avahi": {Tree: "newtree", Manifest: meshcatalogclient.ManifestDigest(manifest), Locks: map[string]string{"go.sum": goSum}}}}
key, _ := index.PreviewKey(pv.Repository, pv.Head, pv.Trunk)
data, err := index.EncodePreview(pv)
if err != nil {
t.Fatal(err)
}
@@ -888,8 +899,8 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc
if _, err := js.Publish(ctx, previewWrittenSubject, []byte(`{"key":"`+key+`"}`)); err != nil {
t.Fatal(err)
}
if got := none2(delivery.ToolchainNoReport); got != delivery.ToolchainNoReport {
t.Errorf("none2 = %q within 20 s of the preview, want %q (computed from the preview)", got, delivery.ToolchainNoReport)
if got := none2(sdkstep.ToolchainNoReport); got != sdkstep.ToolchainNoReport {
t.Errorf("none2 = %q within 20 s of the preview, want %q (computed from the preview)", got, sdkstep.ToolchainNoReport)
}
}
@@ -903,45 +914,45 @@ func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC
t.Fatal(err)
}
const goSum = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"
digest := delivery.ManifestDigest(manifest)
digest := meshcatalogclient.ManifestDigest(manifest)
_, ctx, _, modules := startIndexer(t, func(context.Context, buildLog) {}, func(ctx context.Context, js jetstream.JetStream) {
mkey, err := delivery.ManifestsKey(digest)
mkey, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
t.Fatal(err)
}
mdata, err := delivery.EncodeManifest(delivery.Manifest{Module: "avahi", JSON: string(manifest)})
mdata, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "avahi", Manifest: string(manifest)})
if err != nil {
t.Fatal(err)
}
manifests, err := js.KeyValue(ctx, delivery.ManifestsBucket)
manifests, err := js.KeyValue(ctx, index.ManifestsBucket)
if err != nil {
t.Fatal(err)
}
if _, err := manifests.Put(ctx, mkey, mdata); err != nil {
t.Fatal(err)
}
stale, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: "89abcdef0123456789abcdef0123456789abcdef", Path: "modules/avahi",
stale, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: "89abcdef0123456789abcdef0123456789abcdef", Path: "modules/avahi",
Manifest: digest, None2: "the manifest cannot be read: claims node-build-go, which is a seat in the mesh's own namespace that it does not define",
Inputs2: &delivery.KeptInputs2{Tree: "tree-avahi", Locks: map[string]string{"go.sum": goSum}}})
Inputs2: &meshcatalog.ModulesByModuleValueInputs2{Tree: "tree-avahi", Locks: map[string]string{"go.sum": goSum}}})
if err != nil {
t.Fatal(err)
}
held, err := js.KeyValue(ctx, delivery.ModulesBucket)
held, err := js.KeyValue(ctx, index.ModulesBucket)
if err != nil {
t.Fatal(err)
}
if _, err := held.Put(ctx, "avahi", stale); err != nil {
t.Fatal(err)
}
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket("mesh-build-go"), History: 1})
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.ToolchainsBucket("mesh-build-go"), History: 1})
if err != nil {
t.Fatal(err)
}
key, err := delivery.ToolchainKey("n1", "go")
key, err := index.ToolchainKey("n1", "go")
if err != nil {
t.Fatal(err)
}
value, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: time.Now()})
value, err := index.EncodeToolchain(nodebuildgo.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: ptr(time.Now())})
if err != nil {
t.Fatal(err)
}
@@ -949,18 +960,18 @@ func TestIndexerJudgesAnUnreadableVerdictAgainAtStart(t *testing.T) { // 1923/AC
t.Fatal(err)
}
})
var got delivery.Module
var got meshcatalog.ModulesByModuleValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err := delivery.DecodeModule(e.Value()); err == nil {
if got = m; got.FP2 != "" {
if m, err := meshcatalogclient.DecodeModule(e.Value()); err == nil {
if got = m; got.Fp2 != "" {
break
}
}
}
}
if got.FP2 == "" || got.None2 != "" {
t.Errorf("avahi = fp2 %q none2 %q within 20 s of the start, want a fingerprint and no reason", got.FP2, got.None2)
if got.Fp2 == "" || got.None2 != "" {
t.Errorf("avahi = fp2 %q none2 %q within 20 s of the start, want a fingerprint and no reason", got.Fp2, got.None2)
}
}
@@ -978,16 +989,16 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
const goSum = "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"
var placement, toolchains jetstream.KeyValue
place := func(ctx context.Context, t *testing.T, node string, modules ...string) {
p := delivery.Placement{Node: node}
p := meshmoduleassigner.Placement{Node: node}
for _, m := range modules {
p.Modules = append(p.Modules, delivery.PlacedModule{Module: m})
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
}
// Its node key, the commit that names the modules placed (research 072 row 37).
values, err := delivery.PlacementValues(p)
values, err := meshmoduleassignerclient.PlacementValues(p)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.PlacementNodeKey(node)
key := meshmoduleassigner.PlacementByNodeNodeKey(node)
if _, err := placement.Put(ctx, key, values[key]); err != nil {
t.Fatal(err)
}
@@ -998,14 +1009,14 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
assignerLate = up
t.Cleanup(func() { assignerLate = nil })
js, ctx, _, modules := startIndexer(t, func(context.Context, buildLog) {}, func(ctx context.Context, js jetstream.JetStream) {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket("mesh-build-go"), History: 1})
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.ToolchainsBucket("mesh-build-go"), History: 1})
if err != nil {
t.Fatal(err)
}
toolchains = kv
for node, version := range map[string]string{"n1": "go1.27.3", "n2": "go1.27.2"} {
key, _ := delivery.ToolchainKey(node, "go")
value, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: version, Recipe: "1", At: time.Now()})
key, _ := index.ToolchainKey(node, "go")
value, err := index.EncodeToolchain(nodebuildgo.Toolchain{Language: "go", Version: version, Recipe: "1", At: ptr(time.Now())})
if err != nil {
t.Fatal(err)
}
@@ -1014,7 +1025,7 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
}
}
// The seat mesh-placement's placement, as the assigner writes it (ADR 0567): n1 places mesh-build-go, n2 nothing.
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.PlacementBucket, History: delivery.PlacementHistory}); err != nil {
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PlacementBucket, History: index.PlacementHistory}); err != nil {
t.Fatal(err)
}
place(ctx, t, "n1", "mesh-build-go")
@@ -1032,11 +1043,11 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-catalog.merged", data); err != nil {
t.Fatal(err)
}
read := func(ok func(delivery.Module) bool) delivery.Module {
var got delivery.Module
read := func(ok func(meshcatalog.ModulesByModuleValue) bool) meshcatalog.ModulesByModuleValue {
var got meshcatalog.ModulesByModuleValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, "avahi"); err == nil {
if m, err := delivery.DecodeModule(e.Value()); err == nil {
if m, err := meshcatalogclient.DecodeModule(e.Value()); err == nil {
if got = m; ok(m) {
break
}
@@ -1045,14 +1056,16 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
}
return got
}
agreed := func(m delivery.Module) bool { return m.FP2 != "" && m.Toolchains["go"].Version == "go1.27.3" }
agreed := func(m meshcatalog.ModulesByModuleValue) bool {
return m.Fp2 != "" && m.Toolchains["go"].Version == "go1.27.3"
}
held := read(agreed)
if !agreed(held) {
t.Fatalf("within 20 s avahi has fp2 %q, none2 %q, toolchains %v: want a fingerprint of n1's go1.27.3 alone", held.FP2, held.None2, held.Toolchains)
t.Fatalf("within 20 s avahi has fp2 %q, none2 %q, toolchains %v: want a fingerprint of n1's go1.27.3 alone", held.Fp2, held.None2, held.Toolchains)
}
place(ctx, t, "n2", "mesh-build-go")
key, _ := delivery.ToolchainKey("n2", "go")
value, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.2", Recipe: "1", At: time.Now().Add(time.Second)})
key, _ := index.ToolchainKey("n2", "go")
value, err := index.EncodeToolchain(nodebuildgo.Toolchain{Language: "go", Version: "go1.27.2", Recipe: "1", At: ptr(time.Now().Add(time.Second))})
if err != nil {
t.Fatal(err)
}
@@ -1064,8 +1077,8 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
time.Sleep(500 * time.Millisecond)
if e, err := modules.Get(ctx, "avahi"); err != nil {
t.Fatal(err)
} else if m, err := delivery.DecodeModule(e.Value()); err != nil || m.FP2 != held.FP2 {
t.Fatalf("after n2's placement avahi has fp2 %q, none2 %q: want its fingerprint %q kept", m.FP2, m.None2, held.FP2)
} else if m, err := meshcatalogclient.DecodeModule(e.Value()); err != nil || m.Fp2 != held.Fp2 {
t.Fatalf("after n2's placement avahi has fp2 %q, none2 %q: want its fingerprint %q kept", m.Fp2, m.None2, held.Fp2)
}
moved := strings.Replace(string(data), `"newtree"`, `"movedtree"`, 1)
if moved == string(data) {
@@ -1074,15 +1087,15 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) {
if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-catalog.merged", []byte(moved)); err != nil {
t.Fatal(err)
}
disagree := func(m delivery.Module) bool {
return m.FP2 == "" && strings.HasPrefix(m.None2, "builders disagree: go1.27.2 recipe 1 on n2")
disagree := func(m meshcatalog.ModulesByModuleValue) bool {
return m.Fp2 == "" && strings.HasPrefix(m.None2, "builders disagree: go1.27.2 recipe 1 on n2")
}
if m := read(disagree); !disagree(m) {
t.Fatalf("within 20 s of a merge after n2's placement avahi has fp2 %q, none2 %q: want the builders to disagree", m.FP2, m.None2)
t.Fatalf("within 20 s of a merge after n2's placement avahi has fp2 %q, none2 %q: want the builders to disagree", m.Fp2, m.None2)
}
place(ctx, t, "n2")
if m := read(agreed); !agreed(m) {
t.Fatalf("within 20 s of n2's placement taken away avahi has fp2 %q, none2 %q", m.FP2, m.None2)
t.Fatalf("within 20 s of n2's placement taken away avahi has fp2 %q, none2 %q", m.Fp2, m.None2)
}
}
@@ -1095,23 +1108,23 @@ var assignerLate chan struct{}
// and its views family said by the views emitter onto ASSIGNER_VIEWS, which the indexer's durable consumer reads.
func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetStream, nc *nats.Conn) {
t.Helper()
placement, err := js.KeyValue(ctx, delivery.PlacementBucket)
placement, err := js.KeyValue(ctx, index.PlacementBucket)
if err != nil {
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.PlacementBucket, History: delivery.PlacementHistory}); err != nil {
if placement, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PlacementBucket, History: index.PlacementHistory}); err != nil {
t.Fatal(err)
}
}
marks, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.AssignerModule + "_" + views.MarksState, History: 1})
marks, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.AssignerModule + "_" + views.MarksState, History: 1})
if err != nil {
t.Fatal(err)
}
if _, err := js.CreateOrUpdateConsumer(ctx, "EVENTS", jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-indexer", AckPolicy: jetstream.AckExplicitPolicy,
FilterSubjects: []string{"mesh.mod." + delivery.AssignerModule + ".event.views.>"}}); err != nil {
FilterSubjects: []string{"mesh.mod." + index.AssignerModule + ".event.views.>"}}); err != nil {
t.Fatal(err)
}
em := views.Emitter{Owner: delivery.AssignerModule, Subject: process.Account{Node: "n1", Module: delivery.AssignerModule}.EventSubject,
em := views.Emitter{Owner: index.AssignerModule, Subject: process.Account{Node: "n1", Module: index.AssignerModule}.EventSubject,
Publish: views.JetStreamPublish(js), Marks: marks}
if err := em.Start(ctx, delivery.PlacementState, placement, nil); err != nil {
if err := em.Start(ctx, index.PlacementState, placement, nil); err != nil {
t.Fatal(err)
}
late := assignerLate
@@ -1123,7 +1136,7 @@ func serveAssignerPlacement(t *testing.T, ctx context.Context, js jetstream.JetS
return
}
}
a, err := views.FirstView(delivery.AssignerModule, delivery.PlacementState, jsSource{ctx, placement}, jsSource{ctx, marks}, "", time.Now())
a, err := views.FirstView(index.AssignerModule, index.PlacementState, jsSource{ctx, placement}, jsSource{ctx, marks}, "", time.Now())
out := map[string]any{"result": a, "node": "n0"}
if err != nil {
out = map[string]any{"error": err.Error()}
@@ -26,9 +26,9 @@ import (
"github.com/nats-io/nats.go/jetstream"
"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/modules/mesh-delivery-forge-bridge/forgemsg"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -190,7 +190,7 @@ type delivered interface {
// groupEndSubject is the whole subject of a delivery group's end on the forge bridge's stream: compared exactly, so a
// repository named forge/group, whose merged message ends the same way, is never taken for one.
var groupEndSubject = process.ModuleStreamSubject(delivery.ForgeBridgeModule, forgemsg.ForgeGroupEndEvent)
var groupEndSubject = process.ModuleStreamSubject(index.ForgeBridgeModule, forgemsg.ForgeGroupEndEvent)
// handleGroupEnd takes a group's end in (index.EndGroup): a body that does not read is terminated and said; an end
// whose members are not all held yet, or while the build this program runs is not known, is left for redelivery.
@@ -359,21 +359,21 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return err
}
defer conn.Close()
candidates, err := process.EssentialBucket(ctx, js, "indexer", delivery.CandidatesBucket, "write a candidate's fingerprints")
candidates, err := process.EssentialBucket(ctx, js, "indexer", index.CandidatesBucket, "write a candidate's fingerprints")
if err != nil {
return err
}
modules, err := process.EssentialBucket(ctx, js, "indexer", delivery.ModulesBucket, "write a module's fingerprint")
modules, err := process.EssentialBucket(ctx, js, "indexer", index.ModulesBucket, "write a module's fingerprint")
if err != nil {
return err
}
manifests, err := process.EssentialBucket(ctx, js, "indexer", delivery.ManifestsBucket, "keep a module's manifest")
manifests, err := process.EssentialBucket(ctx, js, "indexer", index.ManifestsBucket, "keep a module's manifest")
if err != nil {
return err
}
// Its taken state (ADR 0556 clause 1) is bound when it appears, as every state a delivery adds (issue 1790): until then
// a merge's taken key is said and not written, and the rest of the indexer runs.
taken := process.AwaitBucket(ctx, js, conn, delivery.TakenBucket, "no repository's taken key is written", logger.Printf)
taken := process.AwaitBucket(ctx, js, conn, index.TakenBucket, "no repository's taken key is written", logger.Printf)
// The control records (ADR 0565 §2), new with this delivery and bound when they appear: until then nothing is written to them.
predefined := process.AwaitBucket(ctx, js, conn, index.PredefinedBucket, "no predefined list is written", logger.Printf)
configCandidates := process.AwaitBucket(ctx, js, conn, index.ConfigCandidatesBucket, "no pull request's config declarations are written", logger.Printf)
@@ -394,7 +394,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
verdicts := process.AwaitBucket(ctx, js, conn, index.VerdictsBucket, "no verdict on a manifest is written", logger.Printf)
// The published packages (ADR 0593 §2): a state this delivery brings, bound when it appears; until then every
// fingerprint takes the version line.
packages := process.AwaitBucket(ctx, js, conn, delivery.PackagesBucket, "no package is published, so every fingerprint takes the version line", logger.Printf)
packages := process.AwaitBucket(ctx, js, conn, index.PackagesBucket, "no package is published, so every fingerprint takes the version line", logger.Printf)
// How many modules use each old section (issue 1966 D4): a state this delivery brings, bound when it appears; until then
// the judge refuses no use of an old section.
oldSections := process.AwaitBucket(ctx, js, conn, index.OldSectionsBucket, "the count of the old sections is not kept, so the judge refuses no use of one", logger.Printf)
@@ -425,9 +425,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// new one indexes the module it names when that module is registered and the bucket holds none (registered.go).
var started atomic.Bool
var startEnds []stdio.LogEntry // the log's entries read at the start, whose imports are kept once the packages are published
if err := calls.ReadLog(delivery.BuilderModule, delivery.BuilderLog).Watch("", stdio.LogFromAll, func(le stdio.LogEntry) error {
if err := calls.ReadLog(builds.BuilderModule, builds.BuilderLog).Watch("", stdio.LogFromAll, func(le stdio.LogEntry) error {
if err := known.Put(le.Value); err != nil {
logger.Printf("%s entry %d is not read: %v", delivery.BuildLogRead, le.Sequence, err)
logger.Printf("%s entry %d is not read: %v", builds.BuildLogRead, le.Sequence, err)
return nil
}
if started.Load() {
@@ -438,7 +438,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
ix.ApplyOutOfStepBuild(ctx, le.Value)
// A done build's imports are kept on its module (ADR 0593 §2); at the start they are kept once the packages are.
if err := ix.ApplyBuildEnd(ctx, le.Value); err != nil {
logger.Printf("%s entry %d: its imports are not kept: %v", delivery.BuildLogRead, le.Sequence, err)
logger.Printf("%s entry %d: its imports are not kept: %v", builds.BuildLogRead, le.Sequence, err)
}
} else {
startEnds = append(startEnds, le)
@@ -446,21 +446,21 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
return nil
}); err != nil {
logger.Printf("%s is not read (%v): a module whose key names no directory is not given one, and no registered module is "+
"indexed from it, until the indexer starts again", delivery.BuildLogRead, err)
"indexed from it, until the indexer starts again", builds.BuildLogRead, err)
}
started.Store(true)
// The gate's source bucket (mesh-delivery-gate_source) comes with the gate (release 3e, novox/hq ADR 0450 §1): its key
// main is the mesh's source revision, which stamps the modules (issue 1344). Where it does not exist yet the indexer does
// not wait for it (that needs a timer, ADR 0437 §8): it says so, and is started again with the gate.
sourceErrs := make(chan error, 1)
if source := process.OptionalBucket(ctx, js, delivery.SourceBucket, "no source revision is read until the indexer starts again", logger.Printf); source != nil {
if source := process.OptionalBucket(ctx, js, index.SourceBucket, "no source revision is read until the indexer starts again", logger.Printf); source != nil {
each := func(e jetstream.KeyValueEntry) {
if opOf(e) {
applySource(ctx, ix, e.Key(), e.Revision(), e.Value(), logger)
}
}
if sw, err := settle(ctx, source, each); err != nil {
process.NotWatched(logger.Printf, delivery.SourceBucket, err, "no source revision is read until the indexer starts again")
process.NotWatched(logger.Printf, index.SourceBucket, err, "no source revision is read until the indexer starts again")
} else {
go func() { sourceErrs <- follow(ctx, sw, each) }()
}
@@ -542,7 +542,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
for _, le := range startEnds {
ix.ApplyOutOfStepBuild(ctx, le.Value)
if err := ix.ApplyBuildEnd(ctx, le.Value); err != nil {
logger.Printf("%s entry %d: its imports are not kept: %v", delivery.BuildLogRead, le.Sequence, err)
logger.Printf("%s entry %d: its imports are not kept: %v", builds.BuildLogRead, le.Sequence, err)
}
}
startEnds = nil
@@ -673,8 +673,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
startViews(ctx, js, cred, marks,
findings,
map[string]readyOf{"manifests": boundNow(manifests), "modules": boundNow(modules), "candidates": boundNow(candidates),
index.VerdictsState: awaited(verdicts), delivery.IndexerPredefinedState: awaited(predefined),
delivery.IndexerConfigCandidatesState: awaited(configCandidates), delivery.PackagesState: awaited(packages),
index.VerdictsState: awaited(verdicts), index.PredefinedState: awaited(predefined),
index.ConfigCandidatesState: awaited(configCandidates), index.PackagesState: awaited(packages),
graphState: awaited(graph), index.HeardState: awaited(heard.b), "taken": awaited(taken), "findings": awaited(findings),
index.SeatDefinitionsState: awaited(seatDefs)}, errs, logger.Printf)
cc, err := process.HearEvents(cons, func(m jetstream.Msg) { handle(ctx, m, ix, heard, logger); ix.KickFill(ctx) }, logger.Printf)
@@ -11,8 +11,9 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-indexer/internal/index"
@@ -110,7 +111,7 @@ func TestAMessageIsIndexedAndAcked(t *testing.T) { // 1238/AC2 (unit part)
m := &fakeMsg{subject: "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull", data: data}
heard := &memStore{vals: map[string][]byte{}}
handle(context.Background(), m, ix, heard, log.New(&bytes.Buffer{}, "", 0))
c, err := delivery.DecodeCandidate(st.vals["feat/x"])
c, err := index.DecodeCandidate(st.vals["feat/x"])
if err != nil || len(c.Pulls) != 1 || c.Pulls[0].Modules[0] != "a" || m.acked != 1 || m.doubled != 1 || m.termd != 0 {
t.Errorf("candidate %+v err %v acked %d terminated %d", c, err, m.acked, m.termd)
}
@@ -157,18 +158,20 @@ func TestTheSourceMainKeyIsTakenAtItsRevision(t *testing.T) { // 1344/AC1 (the w
logger := log.New(&logs, "", 0)
mods := &memStore{vals: map[string][]byte{}}
ix := &index.Indexer{Store: &memStore{vals: map[string][]byte{}}, Modules: mods, Builds: index.NewBuilds()}
value, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: map[string]string{"novox/mesh-delivery": head}})
value, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{"novox/mesh-delivery": head}})
if err != nil {
t.Fatal(err)
}
applySource(context.Background(), ix, "other", 3, value, logger) // not the key of the mesh's source
applySource(context.Background(), ix, delivery.SourceMainKey, 4, []byte(`{"v":1`), logger)
applySource(context.Background(), ix, meshdeliveryclient.SourceMainKey, 4, []byte(`{"v":1`), logger)
if !strings.Contains(logs.String(), "source") {
t.Errorf("a source that cannot be read was not said: %q", logs.String())
}
logs.Reset()
applySource(context.Background(), ix, delivery.SourceMainKey, 5, value, logger)
applySource(context.Background(), ix, meshdeliveryclient.SourceMainKey, 5, value, logger)
if logs.Len() != 0 {
t.Errorf("a good source said %q", logs.String())
}
}
func ptr[T any](v T) *T { return &v }
@@ -9,8 +9,9 @@ import (
"path"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"mesh-delivery-indexer/internal/fingerprint"
"mesh-delivery-indexer/internal/index"
@@ -29,7 +30,7 @@ type toolCaller func(key string, body any) (json.RawMessage, error)
// asked is an error naming it.
func namedVia(call toolCaller) func(ctx context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error) {
return func(_ context.Context, repository, sha string, paths map[string][]string) (map[string]map[string]string, error) {
owner, repo, ok := strings.Cut(delivery.RepositoryName(repository), "/")
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
@@ -58,7 +59,7 @@ func namedVia(call toolCaller) func(ctx context.Context, repository, sha string,
// answers without paths, one per directory that holds a module.json (the root as ""; the tool lists no other).
func trunkVia(call toolCaller) func(ctx context.Context, repository, sha string) ([]string, error) {
return func(_ context.Context, repository, sha string) ([]string, error) {
owner, repo, ok := strings.Cut(delivery.RepositoryName(repository), "/")
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
@@ -85,7 +86,7 @@ func trunkVia(call toolCaller) func(ctx context.Context, repository, sha string)
// directory asked is an error naming it; a cut listing fails the call in the tool.
func filesVia(call toolCaller) func(ctx context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error) {
return func(_ context.Context, repository, sha string, dirs []string) (map[string]map[string]string, error) {
owner, repo, ok := strings.Cut(delivery.RepositoryName(repository), "/")
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
@@ -124,7 +125,7 @@ func goModVia(call toolCaller) func(ctx context.Context, repository, sha, dir st
// not hold is none.
func goFileVia(call toolCaller, name string) func(ctx context.Context, repository, sha, dir string) ([]byte, error) {
return func(_ context.Context, repository, sha, dir string) ([]byte, error) {
owner, repo, ok := strings.Cut(delivery.RepositoryName(repository), "/")
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return nil, fmt.Errorf("%q is not <owner>/<name>", repository)
}
@@ -132,7 +133,7 @@ func goFileVia(call toolCaller, name string) func(ctx context.Context, repositor
switch {
case name == "": // readFileVia: dir is the file's whole path
p = dir
case dir != "" && dir != delivery.RootPath:
case dir != "" && dir != meshcatalogclient.RootPath:
p = dir + "/" + name
}
raw, err := call(getFileTool, map[string]string{"owner": owner, "repo": repo, "path": p, "ref": sha})
@@ -157,7 +158,7 @@ func goFileVia(call toolCaller, name string) func(ctx context.Context, repositor
// containsVia asks the forge whether a commit is in another (gitea.gitea_contains: contains is true for the same commit or an ancestor).
func containsVia(call toolCaller) func(ctx context.Context, repository, commit, in string) (bool, error) {
return func(_ context.Context, repository, commit, in string) (bool, error) {
owner, repo, ok := strings.Cut(delivery.RepositoryName(repository), "/")
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return false, fmt.Errorf("%q is not <owner>/<name>", repository)
}
@@ -187,7 +188,7 @@ var lockFiles = []string{"go.sum", "package-lock.json"}
// forge does not have is an error; a directory with no module.json in the trees is answered with no manifest; a lock file it does not have is left out.
func readVia(call toolCaller) func(ctx context.Context, repository, sha, dir string) (index.RepoRead, error) {
return func(_ context.Context, repository, sha, dir string) (index.RepoRead, error) {
owner, repo, ok := strings.Cut(delivery.RepositoryName(repository), "/")
owner, repo, ok := strings.Cut(trunk.RepositoryName(repository), "/")
if !ok {
return index.RepoRead{}, fmt.Errorf("%q is not <owner>/<name>", repository)
}
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/index"
)
// compositionVerb is the node-engine seat's read verb that says what runs of a module on a machine (its Runs.FP), asked of
@@ -17,7 +17,7 @@ const compositionVerb = "seat:node-engine.composition"
// ownBuildVia asks the engine of node which fingerprint of the indexer runs there.
func ownBuildVia(call toolCaller, node string) func(ctx context.Context) (string, error) {
return func(context.Context) (string, error) {
raw, err := call(compositionVerb+"@"+node, map[string]any{"module": delivery.IndexerModule})
raw, err := call(compositionVerb+"@"+node, map[string]any{"module": index.Module})
if err != nil {
return "", fmt.Errorf("%s: %w", compositionVerb, err)
}
@@ -11,17 +11,22 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"mesh-delivery-indexer/internal/fingerprint"
"mesh-delivery-indexer/internal/index"
)
@@ -59,20 +64,20 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
var packages, seatDefs, findings, takeIns jetstream.KeyValue
js, ctx, _, modules := startIndexer(t, func(context.Context, buildLog) {}, func(ctx context.Context, js jetstream.JetStream) {
var err error
if packages, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.PackagesBucket, History: delivery.PackagesHistory}); err != nil {
if packages, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.PackagesBucket, History: index.PackagesHistory}); err != nil {
t.Fatal(err)
}
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.TakenBucket, History: 1}); err != nil {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.TakenBucket, History: 1}); err != nil {
t.Fatal(err)
}
// Both languages' toolchains, so mesh-sdk (a TypeScript package) gets a fingerprint that moves with its source.
for module, language := range map[string]string{"mesh-build-go": "go", "mesh-build-node": "typescript"} {
tc, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket(module), History: 1})
tc, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.ToolchainsBucket(module), History: 1})
if err != nil {
t.Fatal(err)
}
key, _ := delivery.ToolchainKey("n1", language)
value, _ := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: "v1", Recipe: "1", At: time.Now()})
key, _ := index.ToolchainKey("n1", language)
value, _ := index.EncodeToolchain(nodebuildgo.Toolchain{Language: language, Version: "v1", Recipe: "1", At: ptr(time.Now())})
if _, err := tc.Put(ctx, key, value); err != nil {
t.Fatal(err)
}
@@ -90,7 +95,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
if takeIns, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.TakeInsBucket, History: 1}); err != nil {
t.Fatal(err)
}
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: checkerAsksStream, Subjects: []string{meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule)}}); err != nil {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: checkerAsksStream, Subjects: []string{meshdeliverychecker.HolderSubjectVerdict(index.CheckerModule)}}); err != nil {
t.Fatal(err)
}
if _, err := js.CreateOrUpdateConsumer(ctx, checkerAsksStream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-indexer", AckPolicy: jetstream.AckExplicitPolicy}); err != nil {
@@ -108,12 +113,12 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
t.Fatal(err)
}
}
module := func(name string, ok func(delivery.Module) bool) delivery.Module {
module := func(name string, ok func(meshcatalog.ModulesByModuleValue) bool) meshcatalog.ModulesByModuleValue {
t.Helper()
var m delivery.Module
var m meshcatalog.ModulesByModuleValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := modules.Get(ctx, name); err == nil {
if m, err = delivery.DecodeModule(e.Value()); err == nil && ok(m) {
if m, err = meshcatalogclient.DecodeModule(e.Value()); err == nil && ok(m) {
return m
}
}
@@ -129,7 +134,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
// mesh-sdk at m1, then the catalogue's two modules: their go.sum names mesh-sdk, read by the indexer itself.
merged("novox/mesh-sdk", 70, m1, forgemsg.Forge{Root: sdkRoot1, Trees: map[string]string{"": sdkRoot1},
Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("6666666666666666666666666666666666666666")})
module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m1 })
module("mesh-sdk", func(m meshcatalog.ModulesByModuleValue) bool { return m.Commit == m1 })
merged("novox/mesh-catalog", 301, catalogMerge, forgemsg.Forge{Root: "r0",
Trees: map[string]string{"modules/importer": "t1", "modules/plain": "t2"},
Manifests: map[string]string{"modules/importer": named("importer"), "modules/plain": named("plain")},
@@ -140,26 +145,28 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
t.Fatal(err)
}
}
hasFP := func(m delivery.Module) bool { return m.FP2 != "" && m.Packages[sdkGo] == m1 }
hasFP := func(m meshcatalog.ModulesByModuleValue) bool { return m.Fp2 != "" && m.Packages[sdkGo] == m1 }
importer, plain := module("importer", hasFP), module("plain", hasFP)
// Each build ends reporting its imports: kept, no fingerprint moving.
for _, b := range []struct {
m delivery.Module
m meshcatalog.ModulesByModuleValue
name string
imports []string
}{{importer, "importer", []string{sdkGo, sdkGo + "/buildstep"}}, {plain, "plain", []string{sdkGo, sdkGo + "/announce"}}} {
raw, err := delivery.EncodeBuildLogEntry(delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: b.name, FP: b.m.FP2,
raw, err := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: b.name, FP: b.m.Fp2,
Repository: "novox/mesh-catalog", Directory: "modules/" + b.name, Commit: catalogMerge, Ref: "main", Node: "n1",
At: time.Now().UTC(), Outcome: delivery.BuildDone, Imports: b.imports})
At: time.Now().UTC(), Outcome: builds.BuildDone, Imports: b.imports})
if err != nil {
t.Fatal(err)
}
if _, err := js.Publish(ctx, buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog)+"."+b.name, raw); err != nil {
if _, err := js.Publish(ctx, buscompose.LogSubject(builds.BuilderModule, builds.BuilderLog)+"."+b.name, raw); err != nil {
t.Fatal(err)
}
fp := b.m.FP2
module(b.name, func(m delivery.Module) bool { return m.Inputs2 != nil && m.Inputs2.ImportForm != "" && m.FP2 == fp })
fp := b.m.Fp2
module(b.name, func(m meshcatalog.ModulesByModuleValue) bool {
return m.Inputs2 != nil && m.Inputs2.ImportForm != "" && m.Fp2 == fp
})
}
// mesh-sdk merges a change of go/ alone that leaves its own named paths' objects: its value, a TypeScript package's
@@ -172,8 +179,8 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
// mesh-sdk merges a change of buildstep alone (its version moves: src changed too).
merged("novox/mesh-sdk", 71, m2, forgemsg.Forge{Root: sdk2, Trees: map[string]string{"": sdk2},
Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("7777777777777777777777777777777777777777")})
sdkAt2 := module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m2 })
if sdkAt2.Inputs2 == nil || !strings.HasPrefix(sdkAt2.Inputs2.Tree, delivery.NamedTreePrefix) {
sdkAt2 := module("mesh-sdk", func(m meshcatalog.ModulesByModuleValue) bool { return m.Commit == m2 })
if sdkAt2.Inputs2 == nil || !strings.HasPrefix(sdkAt2.Inputs2.Tree, fingerprint.NamedTreePrefix) {
t.Errorf("mesh-sdk's kept inputs %+v, want a named tree", sdkAt2.Inputs2)
}
history, err := modules.History(ctx, "mesh-sdk")
@@ -181,15 +188,15 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
t.Fatal(err)
}
for _, e := range history {
if m, err := delivery.DecodeModule(e.Value()); err == nil && m.Commit == goOnly {
if m, err := meshcatalogclient.DecodeModule(e.Value()); err == nil && m.Commit == goOnly {
t.Errorf("the go/-only merge wrote mesh-sdk's value: %+v", m)
}
}
key, _ := delivery.PackagesKey(sdkGo)
var pub delivery.PublishedPackages
key, _ := index.PackagesKey(sdkGo)
var pub meshcatalog.PackagesByModuleValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
if e, err := packages.Get(ctx, key); err == nil {
if pub, err = delivery.DecodePublishedPackages(e.Value()); err == nil && pub.Commit == m2 {
if pub, err = index.DecodePublishedPackages(e.Value()); err == nil && pub.Commit == m2 {
break
}
}
@@ -197,13 +204,13 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
if pub.Commit != m2 || len(pub.Packages) != 3 {
t.Fatalf("packages.%s = %+v within 20 s, want the merge %s with a digest of go, announce and buildstep", key, pub, m2)
}
moved := module("importer", func(m delivery.Module) bool { return m.FP2 != importer.FP2 })
moved := module("importer", func(m meshcatalog.ModulesByModuleValue) bool { return m.Fp2 != importer.Fp2 })
if moved.Packages[sdkGo] != m2 {
t.Errorf("importer moved to %+v, want it built against %s", moved.Packages, m2)
}
// plain imports nothing that changed: its value keeps its fingerprint (one recompute has run: importer moved in it).
if now := module("plain", func(delivery.Module) bool { return true }); now.FP2 != plain.FP2 {
t.Errorf("plain moved from %s to %s, though it imports nothing the merge changed", plain.FP2, now.FP2)
if now := module("plain", func(meshcatalog.ModulesByModuleValue) bool { return true }); now.Fp2 != plain.Fp2 {
t.Errorf("plain moved from %s to %s, though it imports nothing the merge changed", plain.Fp2, now.Fp2)
}
// DLV-I17, I18 (research 081 test 21, design 26): mesh-contracts merges a valid definition, said once; a pull
@@ -281,13 +288,13 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
// naming them raises generated-types-out-of-step, and only a clean build end of the same module clears it. DLV-I19 [i]:
// indexer_out_of_step answers both, from the states the indexer wrote.
verdict, _ := json.Marshal(meshdeliverychecker.VerdictHolderEvent{Repository: "novox/mesh-contracts", Pull: 82, Head: badHead, Verdict: "fail", OutOfStep: []string{"go/seats/git/types.go"}})
if _, err := js.Publish(ctx, meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule), verdict); err != nil {
if _, err := js.Publish(ctx, meshdeliverychecker.HolderSubjectVerdict(index.CheckerModule), verdict); err != nil {
t.Fatal(err)
}
logEnd := func(outcome string, files ...string) {
t.Helper()
raw, err := delivery.EncodeBuildLogEntry(delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: "mesh-contracts", FP: "src2:mesh-contracts",
Repository: "novox/mesh-contracts", Directory: delivery.RootPath, Commit: contractsMerge, Node: "n1", At: time.Now().UTC(), Outcome: outcome})
raw, err := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "mesh-contracts", FP: "src2:mesh-contracts",
Repository: "novox/mesh-contracts", Directory: meshcatalogclient.RootPath, Commit: contractsMerge, Node: "n1", At: time.Now().UTC(), Outcome: outcome})
if err != nil {
t.Fatal(err)
}
@@ -297,11 +304,11 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
e["out-of-step"] = files // BuildLogEntry.OutOfStep, the builder's field (J3), written by its name here
}
raw, _ = json.Marshal(e)
if _, err := js.Publish(ctx, buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog)+".mesh-contracts", raw); err != nil {
if _, err := js.Publish(ctx, buscompose.LogSubject(builds.BuilderModule, builds.BuilderLog)+".mesh-contracts", raw); err != nil {
t.Fatal(err)
}
}
logEnd(delivery.BuildFailed, "go/seats/git/types.go")
logEnd(builds.BuildFailed, "go/seats/git/types.go")
outOfStep := func(want int) []index.OutOfStepRow {
t.Helper()
var rows []index.OutOfStepRow
@@ -316,7 +323,7 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
if rows := outOfStep(2); rows[0].Module != "mesh-contracts" || !strings.Contains(rows[0].Summary, "types.go") || rows[1].Pull != "novox/mesh-contracts#82" {
t.Errorf("indexer_out_of_step = %+v, want the trunk's finding and the pull request's files", rows)
}
logEnd(delivery.BuildDone)
logEnd(builds.BuildDone)
if rows := outOfStep(1); rows[0].Source != index.SourcePull {
t.Errorf("after a clean build end indexer_out_of_step = %+v, want the pull request's files alone", rows)
}
@@ -11,8 +11,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/index"
)
@@ -21,7 +19,7 @@ import (
const PreviewsStream = "CHECKS"
// previewWrittenSubject is the subject of the checker's preview.written event.
const previewWrittenSubject = "mesh.stream." + delivery.CheckerModule + ".event.preview.written"
const previewWrittenSubject = "mesh.stream." + index.CheckerModule + ".event.preview.written"
// previewWatch reads the merge previews the checker writes (mesh-delivery-checker_previews) and follows them: every change
// writes the candidates' fingerprints again (index.Indexer.ApplyPreview). The checker's bucket is made when it is assigned,
@@ -50,26 +48,26 @@ func (w *previewWatch) ensure() error {
if w.watched {
return nil
}
kv, err := w.js.KeyValue(w.ctx, delivery.PreviewsBucket)
kv, err := w.js.KeyValue(w.ctx, index.PreviewsBucket)
switch {
case errors.Is(err, jetstream.ErrBucketNotFound):
w.logger.Printf("the bucket %s does not exist yet: it is watched when the checker says it wrote a preview", delivery.PreviewsBucket)
w.logger.Printf("the bucket %s does not exist yet: it is watched when the checker says it wrote a preview", index.PreviewsBucket)
return nil
case err != nil:
// Refused or not answered (its grant not on the bus yet, issue 1945): said, and opened again at the checker's next preview.
w.logger.Printf("the bucket %s cannot be read (%v): it is watched when the checker says it wrote a preview", delivery.PreviewsBucket, err)
w.logger.Printf("the bucket %s cannot be read (%v): it is watched when the checker says it wrote a preview", index.PreviewsBucket, err)
return nil
}
watcher, err := settle(w.ctx, kv, func(e jetstream.KeyValueEntry) { w.ix.PutPreview(e.Key(), e.Value(), !opOf(e)) })
if err != nil {
process.NotWatched(w.logger.Printf, delivery.PreviewsBucket, err, "it is watched when the checker says it wrote a preview")
process.NotWatched(w.logger.Printf, index.PreviewsBucket, err, "it is watched when the checker says it wrote a preview")
return nil
}
w.watched = true
go func() {
w.errs <- follow(w.ctx, watcher, func(e jetstream.KeyValueEntry) {
if err := w.ix.ApplyPreview(w.ctx, e.Key(), e.Value(), !opOf(e)); err != nil {
w.logger.Printf("%s.%s could not be taken into the candidates: %v", delivery.PreviewsBucket, e.Key(), err)
w.logger.Printf("%s.%s could not be taken into the candidates: %v", index.PreviewsBucket, e.Key(), err)
}
})
}()
@@ -8,8 +8,7 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-indexer/internal/index"
@@ -65,7 +64,7 @@ func saySeatDefinition(js jetstream.JetStream, cred process.Account) func(contex
// heardVerdict takes the checker's verdict event: a pull request's out-of-step files are kept (a clean verdict removes
// them). Another event of the stream is acknowledged and passed over; a write that failed is delivered again.
func heardVerdict(ctx context.Context, ix *index.Indexer, m jetstream.Msg, logf func(string, ...any)) {
if m.Subject() != meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule) {
if m.Subject() != meshdeliverychecker.HolderSubjectVerdict(index.CheckerModule) {
_ = m.Ack()
return
}
@@ -6,9 +6,9 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"mesh-delivery-indexer/internal/index"
)
@@ -21,7 +21,7 @@ var (
gopackageregistry.SubjectPublished: gopackageregistry.Seat,
npmpackageregistry.SubjectPublished: npmpackageregistry.Seat,
}
numberedSubject = "mesh.mod." + delivery.BuilderModule + ".event.version.numbered"
numberedSubject = "mesh.mod." + builds.BuilderModule + ".event.version.numbered"
)
// numbered is the builder's version.numbered body as the indexer reads it.
@@ -4,7 +4,7 @@ import (
"context"
"log"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"mesh-delivery-indexer/internal/index"
)
@@ -13,10 +13,10 @@ import (
// source revision (mesh-sdk go/delivery SourceMainKey, Source). Any other key is not the mesh's source. A value that is no
// source is said; so is a write that failed, and it is tried again only at the next revision (novox/hq ADR 0437 §8).
func applySource(ctx context.Context, ix *index.Indexer, key string, rev uint64, value []byte, logger *log.Logger) {
if key != delivery.SourceMainKey {
if key != meshdeliveryclient.SourceMainKey {
return
}
s, err := delivery.DecodeSource(value)
s, err := meshdeliveryclient.DecodeSource(value)
if err != nil {
logger.Printf("source.%s at revision %d is not a source, and is left: %v", key, rev, err)
return
@@ -14,8 +14,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/index"
)
@@ -33,7 +31,7 @@ type bucketOpener interface {
}
// toolchainWatch reads the toolchains each build module reports (mesh-build-go_toolchains, mesh-build-node_toolchains: the
// module's state toolchains, delivery.ToolchainsBucket; novox/hq ADR 0465 clause 2, issue 1444) and follows them: every
// module's state toolchains, index.ToolchainsBucket; novox/hq ADR 0465 clause 2, issue 1444) and follows them: every
// change of a language's agreed toolchain writes the modules of that language again (index.Indexer.Recompute). A bucket
// is made when its build module is assigned, after the indexer started, and the indexer holds no timer (ADR 0437 §8):
// it watches the buckets that exist at start, and a build module says each time it wrote its toolchains key
@@ -67,7 +65,7 @@ func (w *toolchainWatch) start() error {
func (w *toolchainWatch) ensure(module string) error {
w.mu.Lock()
defer w.mu.Unlock()
name := delivery.ToolchainsBucket(module)
name := index.ToolchainsBucket(module)
if !w.watched[module] {
kv, err := w.js.KeyValue(w.ctx, name)
switch {
@@ -10,9 +10,11 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/index"
)
// A build module's bucket is made when the module is assigned, after the indexer started (issue 1444, review finding 9).
@@ -81,7 +83,7 @@ func (e fakeEntry) Operation() jetstream.KeyValueOp { return jetstream.KeyValueP
func TestABuildModulesBucketThatAppearsLaterIsWatchedWhenItsEventArrives(t *testing.T) { // 1444 (review finding 9)
ctx, stop := context.WithCancel(context.Background())
defer stop()
value, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: time.Now()})
value, err := index.EncodeToolchain(nodebuildgo.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "1", At: ptr(time.Now())})
if err != nil {
t.Fatal(err)
}
@@ -120,7 +122,7 @@ func TestABuildModulesBucketThatAppearsLaterIsWatchedWhenItsEventArrives(t *test
}
// The bucket of mesh-build-go is made and already holds a key, and the module says it reported.
bucket := &fakeBucket{held: []jetstream.KeyValueEntry{fakeEntry{key: "novox.go", value: value}}}
buckets.make(delivery.ToolchainsBucket("mesh-build-go"), bucket)
buckets.make(index.ToolchainsBucket("mesh-build-go"), bucket)
msg := &fakeMsg{subject: "mesh.stream.mesh-build-go.event.toolchain.reported"}
w.Reported(msg)
if msg.acked != 1 || len(puts) != 1 || puts[0] != "mesh-build-go novox.go" || recomputes != 1 {
@@ -140,7 +142,7 @@ func TestABuildModulesBucketThatAppearsLaterIsWatchedWhenItsEventArrives(t *test
t.Errorf("applies = %v, want the key put later", applies)
}
w.Reported(&fakeMsg{subject: "mesh.stream.mesh-build-go.event.toolchain.reported"})
if len(puts) != 1 || recomputes != 2 || buckets.asked[delivery.ToolchainsBucket("mesh-build-go")] != 2 {
if len(puts) != 1 || recomputes != 2 || buckets.asked[index.ToolchainsBucket("mesh-build-go")] != 2 {
t.Errorf("puts %v recomputes %d asked %v, want the bucket opened again only at the first report", puts, recomputes, buckets.asked)
}
// An event of no build module is terminated.
@@ -6,8 +6,8 @@ import (
"fmt"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
@@ -15,14 +15,14 @@ import (
// sayRefused says the indexer's event manifest-refused (novox/hq ADR 0578 §1): mesh.seat.mesh-catalog.event.manifest-refused,
// with the SDK's envelope headers and the digest and judge as the bus's dedup id, so one refusal is said once.
func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string, ...any)) func(context.Context, delivery.ManifestRefused) {
return func(ctx context.Context, r delivery.ManifestRefused) {
func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string, ...any)) func(context.Context, meshcatalog.ManifestRefusedEvent) {
return func(ctx context.Context, r meshcatalog.ManifestRefusedEvent) {
data, err := json.Marshal(r)
if err != nil {
logf("judge: %s for %s is not said: %v", delivery.ManifestRefusedEvent, r.Module, err)
logf("judge: %s for %s is not said: %v", meshcatalog.EventManifestRefused, r.Module, err)
return
}
msg := nats.NewMsg(seatEventSubject(delivery.ManifestRefusedEvent))
msg := nats.NewMsg(seatEventSubject(meshcatalog.EventManifestRefused))
id := "manifest-refused-" + r.Digest + "-" + r.Judge
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", cred.Module)
@@ -33,21 +33,21 @@ func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string,
pub, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
defer cancel()
if _, err := js.PublishMsg(pub, msg, jetstream.WithMsgID(id)); err != nil {
logf("judge: %s", fmt.Errorf("saying %s for %s: %w", delivery.ManifestRefusedEvent, r.Module, err))
logf("judge: %s", fmt.Errorf("saying %s for %s: %w", meshcatalog.EventManifestRefused, r.Module, err))
}
}
}
// sayAccepted says the indexer's event manifest-accepted (issue 1966 D3, novox/hq ADR 0595 §4):
// mesh.seat.mesh-catalog.event.manifest-accepted, carrying the module's bus-state entries whole, with the digest and judge as
// the bus's dedup id (delivery.ManifestAccepted.EventID), so one acceptance is said once.
func sayAccepted(js jetstream.JetStream, cred process.Account) func(context.Context, delivery.ManifestAccepted, string) error {
return func(ctx context.Context, a delivery.ManifestAccepted, id string) error {
// the bus's dedup id (meshcatalog.ManifestAcceptedEvent.EventID), so one acceptance is said once.
func sayAccepted(js jetstream.JetStream, cred process.Account) func(context.Context, meshcatalog.ManifestAcceptedEvent, string) error {
return func(ctx context.Context, a meshcatalog.ManifestAcceptedEvent, id string) error {
data, err := json.Marshal(a)
if err != nil {
return err
}
msg := nats.NewMsg(seatEventSubject(delivery.ManifestAcceptedEvent))
msg := nats.NewMsg(seatEventSubject(meshcatalog.EventManifestAccepted))
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", cred.Module)
msg.Header.Set("x-node", cred.Node)
@@ -9,7 +9,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/process"
@@ -17,11 +16,13 @@ import (
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-delivery/sdk/heard"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-indexer/internal/index"
)
// findingsBucket is the indexer's own findings, which the condition keeper raises and clears (ADR 0564 §2): a value too
// large for its views event, and its first view of the assigner's placement unanswered past its bound.
var findingsBucket = health.FindingsBucket(delivery.IndexerModule)
var findingsBucket = health.FindingsBucket(index.Module)
// readyOf binds a state's bucket: now for one bound at the start, once it appears for one awaited.
type readyOf func(func(jetstream.KeyValue))
@@ -38,7 +39,7 @@ func startViews(ctx context.Context, js jetstream.JetStream, cred process.Accoun
if kv == nil {
return
}
if err := sdkfindings.Writer(ctx, findings.KV).Put(views.TooLargeFinding(delivery.IndexerModule, state, key, size, time.Now())); err != nil {
if err := sdkfindings.Writer(ctx, findings.KV).Put(views.TooLargeFinding(index.Module, state, key, size, time.Now())); err != nil {
logf("views: the finding of %s.%s too large is not written: %v", state, key, err)
}
}
@@ -58,7 +59,7 @@ func startViews(ctx context.Context, js jetstream.JetStream, cred process.Accoun
// view-unsaid, in its own findings once bound (review round 2).
func viewsEmitter(ctx context.Context, js jetstream.JetStream, cred process.Account, mk jetstream.KeyValue, findings *process.AwaitedBucket,
tooLarge func(string, string, int), logf func(string, ...any)) views.Emitter {
return views.Emitter{Owner: delivery.IndexerModule, Subject: cred.EventSubject, Publish: views.JetStreamPublish(js),
return views.Emitter{Owner: index.Module, Subject: cred.EventSubject, Publish: views.JetStreamPublish(js),
Marks: mk, TooLarge: tooLarge, Findings: sdkfindings.Writer(ctx, findings.KV), Logf: logf}
}
@@ -79,7 +80,7 @@ func sortedStates(states map[string]readyOf) []string {
// notHeardWriter writes the indexer's finding of a family it does not hear into its findings state, once that is bound.
func notHeardWriter(ctx context.Context, findings *process.AwaitedBucket, logf func(string, ...any)) heard.Writer {
return heard.Writer{Observer: delivery.IndexerModule, Logf: logf,
return heard.Writer{Observer: index.Module, Logf: logf,
Put: func(key string, value []byte) error {
findings.Ready(func(kv jetstream.KeyValue) {
if _, err := kv.Put(ctx, key, value); err != nil {
@@ -106,10 +107,10 @@ const graphState = graph.State
// firstViewsBucket is the indexer's own state of its first views (views.StatusState): what indexer_first_views reads and
// where indexer_first_view_again asks.
var firstViewsBucket = delivery.IndexerModule + "_" + views.StatusState
var firstViewsBucket = index.Module + "_" + views.StatusState
// placementTool is the assigner's first-view tool of its placement, as the indexer's invokes names it (ADR 0621 §2).
const placementTool = delivery.AssignerModule + "." + delivery.PlacementState
const placementTool = index.AssignerModule + "." + index.PlacementState
// followPlacement reads the seat mesh-placement's placement through the assigner's first view and then its views
// family, never its bucket (ADR 0615 §1, DLV-F1r), into gate: each key as it changes, settled once answered, with
@@ -120,7 +121,7 @@ func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Ac
gate *buildrun.ToolchainGate, changed func(context.Context), findings, status *process.AwaitedBucket, logf func(string, ...any)) *views.Follower {
finding := func(s views.Status, raised bool) {
w := sdkfindings.Writer(ctx, findings.KV)
f := views.UnansweredFinding(delivery.IndexerModule, s, time.Now())
f := views.UnansweredFinding(index.Module, s, time.Now())
var err error
if raised {
err = w.Put(f)
@@ -132,7 +133,7 @@ func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Ac
}
}
var settled atomic.Bool // the first view answered: the gate's nodes are the counted ones from now on
f := &views.Follower{Owner: delivery.AssignerModule, Tool: placementTool, State: delivery.PlacementState,
f := &views.Follower{Owner: index.AssignerModule, Tool: placementTool, State: index.PlacementState,
Ask: views.AskVia(call, placementTool), Logf: logf,
Raise: func(s views.Status) { finding(s, true) }, Clear: func(s views.Status) { finding(s, false) },
Changed: func(key string, value json.RawMessage, deleted bool, at time.Time) {
@@ -166,7 +167,7 @@ func followPlacement(ctx context.Context, hub *process.EventHub, cred process.Ac
// The assigner's views family, on the module's one hub (the host starts it once every handler is registered); a family the
// hub cannot register is the finding family-not-heard until the indexer starts again (ADR 0615 §5).
_ = notHeardWriter(ctx, findings, logf).Bind(process.EventsStream, "the placement is the first view's alone, asked again by hand, until the indexer starts again", func() error {
return hub.Handle("mesh.mod."+delivery.AssignerModule+".event."+views.EventPrefix+"*", views.Hear(ctx, map[string]*views.Follower{delivery.PlacementState: f}, logf))
return hub.Handle("mesh.mod."+index.AssignerModule+".event."+views.EventPrefix+"*", views.Hear(ctx, map[string]*views.Follower{index.PlacementState: f}, logf))
})
wait, cancel := context.WithTimeout(ctx, views.Bound) // a context's bound, no timer (ADR 0437 §8)
defer cancel()
+14 -4
View File
@@ -3,9 +3,20 @@ module mesh-delivery-indexer
go 1.26.0
require (
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
@@ -35,9 +46,6 @@ require (
require (
git.novox.be/novox/mesh-contracts/go/seats/meshbroker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104174344-ae5266e77639
)
require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect
@@ -47,3 +55,5 @@ replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ./takein
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg => ../mesh-delivery-forge-bridge/forgemsg
replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds => ../mesh-delivery-builder/builds
@@ -1,7 +1,7 @@
// Package fingerprint is the one computation of a module's source fingerprint, of a named tree, of a mesh package's
// content digest and of a package's published version (novox/hq ADR 0593 §2): the indexer's own, and nobody else's. It
// left mesh-sdk's delivery package, which keeps only the fingerprint's form (delivery.SourcePrefix2, the key part) and
// the shapes the indexer writes (delivery.Module, delivery.PublishedPackages). No other part computes any of it: the
// left mesh-sdk's delivery package, which keeps only the fingerprint's form (meshcatalogclient.SourcePrefix2, the key part) and
// the shapes the indexer writes (meshcatalog.ModulesByModuleValue, meshcatalog.PackagesByModuleValue). No other part computes any of it: the
// builder, the checker and the build modules read what the indexer writes.
package fingerprint
@@ -15,7 +15,10 @@ import (
"sort"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// NoPackageVersion opens the reason a module has no fingerprint while a mesh package it uses has no published version
@@ -25,7 +28,7 @@ const NoPackageVersion = "no published version of the package "
// Inputs are what a module's source fingerprint is of (novox/hq ADR 0429 clause 5 as ADR 0437 clause 5 changed it; ADR
// 0453 clause 6; ADR 0593 §2). A base enters by its module's fingerprint, an artifact that resolves packages when it is
// built by the hash of its lock file (an entry with an empty hash is such an artifact with no lock, which has no
// fingerprint), an artifact's toolchain as delivery.Toolchain2.
// fingerprint), an artifact's toolchain as meshcatalogclient.Toolchain2.
//
// The mesh packages enter in one of two forms. **The import form** (ADR 0593 §2): Imports, the content digest of each
// mesh package the module's programs import (by import path), and Requires, the digest of each mesh Go module's go.mod
@@ -38,7 +41,7 @@ type Inputs struct {
Builder string
Contexts map[string]string
Locks map[string]string
Toolchains map[string]delivery.Toolchain2
Toolchains map[string]meshcatalogclient.Toolchain2
Packages map[string]string
Imports map[string]string
Requires map[string]string
@@ -102,16 +105,16 @@ func Of(in Inputs) (fp, none string) {
}
sort.Strings(lines[3:])
sum := sha256.Sum256([]byte(strings.Join(lines, "\n")))
return delivery.SourcePrefix2 + hex.EncodeToString(sum[:]), ""
return meshcatalogclient.SourcePrefix2 + hex.EncodeToString(sum[:]), ""
}
// NamedTree is the tree input of a module whose every artifact names the paths it is built from (novox/hq ADR 0499
// clause 2): `named:` and the hex sha256 of the lines "manifest <digest>" and, sorted, "path <path> <object>", joined by
// a newline. manifest is the module's delivery.ManifestDigest; paths are the named paths, each clean and relative to
// a newline. manifest is the module's meshcatalogclient.ManifestDigest; paths are the named paths, each clean and relative to
// the module's directory, with the git object id each has at the commit. It refuses no path, a named path with no
// object, a path that is not clean, and a manifest that is not a digest, naming them: the module then has none.
func NamedTree(manifest string, paths map[string]string) (string, error) {
if _, err := delivery.ManifestsKey(manifest); err != nil {
if _, err := meshcatalogclient.ManifestsKey(manifest); err != nil {
return "", err
}
if len(paths) == 0 {
@@ -129,20 +132,20 @@ func NamedTree(manifest string, paths map[string]string) (string, error) {
}
sort.Strings(lines)
sum := sha256.Sum256([]byte("manifest " + manifest + "\n" + strings.Join(lines, "\n")))
return delivery.NamedTreePrefix + hex.EncodeToString(sum[:]), nil
return NamedTreePrefix + hex.EncodeToString(sum[:]), nil
}
// MeshPackagesOf are the mesh Go modules a go.sum names (the module path of each line under
// delivery.ForgeModulePrefix), sorted and each once; nil for none. They are what a module with no reported import set
// trunk.ForgeModulePrefix), sorted and each once; nil for none. They are what a module with no reported import set
// takes the version line of (ADR 0593 §2).
func MeshPackagesOf(goSum []byte) []string {
seen := map[string]bool{}
for _, line := range strings.Split(string(goSum), "\n") {
f := strings.Fields(line)
if len(f) < 2 || !strings.HasPrefix(f[0], delivery.ForgeModulePrefix) {
if len(f) < 2 || !strings.HasPrefix(f[0], trunk.ForgeModulePrefix) {
continue
}
if _, _, ok := delivery.PackageRepository(f[0]); ok {
if _, _, ok := trunk.PackageRepository(f[0]); ok {
seen[f[0]] = true
}
}
@@ -156,7 +159,7 @@ func MeshPackagesOf(goSum []byte) []string {
func OtherPackages(packages []string, repository string) []string {
var out []string
for _, p := range packages {
if repo, _, ok := delivery.PackageRepository(p); ok && !delivery.SameRepository(repo, repository) {
if repo, _, ok := trunk.PackageRepository(p); ok && !trunk.SameRepository(repo, repository) {
out = append(out, p)
}
}
@@ -195,7 +198,7 @@ func ReplacedSiblings(goMod []byte, repository string) []string {
if len(from) == 0 || len(to) != 1 || !(strings.HasPrefix(to[0], "./") || strings.HasPrefix(to[0], "../")) {
continue
}
if repo, _, ok := delivery.PackageRepository(from[0]); ok && delivery.SameRepository(repo, repository) {
if repo, _, ok := trunk.PackageRepository(from[0]); ok && trunk.SameRepository(repo, repository) {
seen[from[0]] = true
}
}
@@ -210,14 +213,14 @@ func ReplacedSiblings(goMod []byte, repository string) []string {
// (ReplacedSiblings), by the content its directory publishes, `content:` and the hex sha256 of its requirements' digest
// and each package's digest (empty while it is not published, which gives no fingerprint until it is, as a version
// none publishes does).
func VersionLine(repository string, packages []string, versions delivery.PackageVersions, published map[string]delivery.PublishedPackages) map[string]string {
func VersionLine(repository string, packages []string, versions sdkstep.PackageVersions, published map[string]meshcatalog.PackagesByModuleValue) map[string]string {
if len(packages) == 0 {
return nil
}
out := map[string]string{}
for _, p := range packages {
repo, _, ok := delivery.PackageRepository(p)
if !ok || !delivery.SameRepository(repo, repository) {
repo, _, ok := trunk.PackageRepository(p)
if !ok || !trunk.SameRepository(repo, repository) {
out[p] = versions[p]
continue
}
@@ -243,7 +246,7 @@ func VersionLine(repository string, packages []string, versions delivery.Package
type Resolved struct {
Digests map[string]string
Requires map[string]string
Versions delivery.PackageVersions
Versions sdkstep.PackageVersions
Missing []string
}
@@ -251,17 +254,17 @@ type Resolved struct {
// import is held by the published module with the longest path that publishes it. A module of repository itself is
// built from its directory, so it is given no version (a same-repository replace, ADR 0593 §1); it still enters by
// its digests.
func Resolve(repository string, imports []string, published map[string]delivery.PublishedPackages) Resolved {
func Resolve(repository string, imports []string, published map[string]meshcatalog.PackagesByModuleValue) Resolved {
r := Resolved{Digests: map[string]string{}, Requires: map[string]string{}}
for _, imp := range imports {
var holder *delivery.PublishedPackages
var holder *meshcatalog.PackagesByModuleValue
digest := ""
for _, m := range slices.Sorted(maps.Keys(published)) {
p := published[m]
if imp != m && !strings.HasPrefix(imp, m+"/") {
continue
}
d, ok := p.DigestOf(imp)
d, ok := digestOf(p, imp)
if !ok {
continue
}
@@ -275,9 +278,9 @@ func Resolve(repository string, imports []string, published map[string]delivery.
}
r.Digests[imp] = digest
r.Requires[holder.Module] = holder.Requires
if !delivery.SameRepository(holder.Repository, repository) {
if !trunk.SameRepository(holder.Repository, repository) {
if r.Versions == nil {
r.Versions = delivery.PackageVersions{}
r.Versions = sdkstep.PackageVersions{}
}
r.Versions[holder.Module] = holder.Commit
}
@@ -296,7 +299,7 @@ func Resolve(repository string, imports []string, published map[string]delivery.
// leaving out `*_test.go` and everything under a sub-directory that is itself a package or holds a go.mod: a file
// //go:embed can reach counts, a test or a sub-package does not. The requirements' digest is the hex sha256 of the
// lines "go.mod <object>" and "go.sum <object>" (the second absent with no go.sum).
func Packages(module string, files map[string]string) (requires string, packages []delivery.PackageDigest, err error) {
func Packages(module string, files map[string]string) (requires string, packages []meshcatalog.PackagesByModuleValuePackagesItem, err error) {
goMod, ok := files["go.mod"]
if !ok {
return "", nil, fmt.Errorf("the module %s has no go.mod at its directory", module)
@@ -351,9 +354,11 @@ func Packages(module string, files map[string]string) (requires string, packages
if dir != "" {
imp = module + "/" + dir
}
packages = append(packages, delivery.PackageDigest{Package: imp, Digest: hex.EncodeToString(s[:])})
packages = append(packages, meshcatalog.PackagesByModuleValuePackagesItem{Package: imp, Digest: hex.EncodeToString(s[:])})
}
slices.SortFunc(packages, func(a, b delivery.PackageDigest) int { return strings.Compare(a.Package, b.Package) })
slices.SortFunc(packages, func(a, b meshcatalog.PackagesByModuleValuePackagesItem) int {
return strings.Compare(a.Package, b.Package)
})
return requires, packages, nil
}
@@ -398,21 +403,21 @@ func sortedKeys[V any](m map[string]V) []string { return slices.Sorted(maps.Keys
// commit of the held module of the package's repository whose directory holds the package's directory (the deepest, a
// module at the repository's root holding every directory). A package no held module holds, or whose module names no
// commit or no directory, is in missing, sorted; versions is nil when packages is empty.
func HeldVersionsOf(packages []string, modules map[string]delivery.Module) (versions delivery.PackageVersions, missing []string) {
func HeldVersionsOf(packages []string, modules map[string]meshcatalog.ModulesByModuleValue) (versions sdkstep.PackageVersions, missing []string) {
for _, pkg := range packages {
repository, dir, ok := delivery.PackageRepository(pkg)
repository, dir, ok := trunk.PackageRepository(pkg)
commit, depth := "", -1
if ok {
for _, key := range slices.Sorted(maps.Keys(modules)) {
m := modules[key]
if !delivery.SameRepository(m.Repository, repository) || m.Commit == "" {
if !trunk.SameRepository(m.Repository, repository) || m.Commit == "" {
continue
}
at := strings.Trim(m.Path, "/")
switch {
case at == "":
continue // a value that names no directory says nothing of where the package lies
case at == delivery.RootPath:
case at == meshcatalogclient.RootPath:
at = ""
case dir != at && !strings.HasPrefix(dir, at+"/"):
continue
@@ -427,10 +432,26 @@ func HeldVersionsOf(packages []string, modules map[string]delivery.Module) (vers
continue
}
if versions == nil {
versions = delivery.PackageVersions{}
versions = sdkstep.PackageVersions{}
}
versions[pkg] = commit
}
slices.Sort(missing)
return versions, missing
}
// digestOf is the content digest of the package at import path pkg in a published module, and whether it holds it
// (its packages are sorted by import path).
func digestOf(p meshcatalog.PackagesByModuleValue, pkg string) (string, bool) {
i, found := slices.BinarySearchFunc(p.Packages, pkg, func(d meshcatalog.PackagesByModuleValuePackagesItem, s string) int {
return strings.Compare(d.Package, s)
})
if !found {
return "", false
}
return p.Packages[i].Digest, true
}
// NamedTreePrefix opens a module's tree when it is computed over the paths its artifacts name, not the directory's own
// git tree (novox/hq ADR 0499 clause 2): the indexer alone computes it (NamedTree).
const NamedTreePrefix = "named:"
@@ -4,7 +4,9 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
const sdk = "git.novox.be/novox/mesh-sdk/go"
@@ -29,11 +31,11 @@ func versionLineTakesASiblingDirectoryByItsContent(t *testing.T) {
if got := ReplacedSiblings(goMod, repo); len(got) != 1 || got[0] != buildrun {
t.Fatalf("siblings %v, want buildrun alone (mesh-sdk is another repository, replaced by a version)", got)
}
published := func(digest string) map[string]delivery.PublishedPackages {
return map[string]delivery.PublishedPackages{buildrun: {Module: buildrun, Repository: repo, Requires: "r1",
Packages: []delivery.PackageDigest{{Package: buildrun, Digest: digest}}}}
published := func(digest string) map[string]meshcatalog.PackagesByModuleValue {
return map[string]meshcatalog.PackagesByModuleValue{buildrun: {Module: buildrun, Repository: repo, Requires: "r1",
Packages: []meshcatalog.PackagesByModuleValuePackagesItem{{Package: buildrun, Digest: digest}}}}
}
versions := delivery.PackageVersions{sdk: "c1"}
versions := sdkstep.PackageVersions{sdk: "c1"}
line := func(digest string) string {
in := base()
in.Packages = VersionLine(repo, []string{sdk, buildrun}, versions, published(digest))
@@ -53,7 +55,7 @@ func versionLineTakesASiblingDirectoryByItsContent(t *testing.T) {
func base() Inputs {
return Inputs{Module: "netcheck", Tree: "t1", Bases: []string{"src2:b"}, Contexts: map[string]string{"c": "x"},
Locks: map[string]string{"bin": "l1"}, Toolchains: map[string]delivery.Toolchain2{"bin": {Language: "go", Version: "1.26.0", Recipe: "7"}}}
Locks: map[string]string{"bin": "l1"}, Toolchains: map[string]meshcatalogclient.Toolchain2{"bin": {Language: "go", Version: "1.26.0", Recipe: "7"}}}
}
// The form moved out of mesh-sdk computes what mesh-sdk's SourceFingerprint2 and NamedTree computed for the same
@@ -77,10 +79,10 @@ func movedComputationGivesTheValuesItGaveInMeshSDK(t *testing.T) {
}
}
func published(module, repo, dir, commit, requires string, digests map[string]string) delivery.PublishedPackages {
p := delivery.PublishedPackages{Module: module, Repository: repo, Dir: dir, Commit: commit, Requires: requires}
func published(module, repo, dir, commit, requires string, digests map[string]string) meshcatalog.PackagesByModuleValue {
p := meshcatalog.PackagesByModuleValue{Module: module, Repository: repo, Dir: dir, Commit: commit, Requires: requires}
for _, pkg := range sortedKeys(digests) {
p.Packages = append(p.Packages, delivery.PackageDigest{Package: pkg, Digest: digests[pkg]})
p.Packages = append(p.Packages, meshcatalog.PackagesByModuleValuePackagesItem{Package: pkg, Digest: digests[pkg]})
}
return p
}
@@ -90,13 +92,13 @@ func published(module, repo, dir, commit, requires string, digests map[string]st
func importFormMovesOnlyWithWhatTheModuleImports(t *testing.T) {
const sibling = "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
imports := []string{sdk, sdk + "/announce", sibling}
pubs := func(announce, buildstep, sib, requires string) map[string]delivery.PublishedPackages {
return map[string]delivery.PublishedPackages{
pubs := func(announce, buildstep, sib, requires string) map[string]meshcatalog.PackagesByModuleValue {
return map[string]meshcatalog.PackagesByModuleValue{
sdk: published(sdk, "novox/mesh-sdk", "go", "c1", requires, map[string]string{sdk: "r", sdk + "/announce": announce, sdk + "/buildstep": buildstep}),
sibling: published(sibling, "novox/mesh-catalog", "modules/mesh-build-go/buildstep", "c9", "q", map[string]string{sibling: sib}),
}
}
fpOf := func(p map[string]delivery.PublishedPackages) string {
fpOf := func(p map[string]meshcatalog.PackagesByModuleValue) string {
t.Helper()
r := Resolve("novox/mesh-catalog", imports, p)
if len(r.Missing) > 0 {
@@ -8,9 +8,10 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/keys"
"mesh-delivery-indexer/judge"
)
@@ -29,33 +30,30 @@ const (
// acceptedOf is the event body for the verdict v (its revision in the verdicts state) on the manifest raw of digest, its
// entries an empty list when it asks for none.
func acceptedOf(digest string, v delivery.ManifestVerdict, revision uint64, raw []byte) delivery.ManifestAccepted {
func acceptedOf(digest string, v meshcatalog.VerdictsByDigestValue, revision uint64, raw []byte) meshcatalog.ManifestAcceptedEvent {
m, _ := judge.Manifest(raw, nil)
a := delivery.AcceptedOf(digest, v, m)
if a.BusState == nil {
a.BusState = []catalogue.BusStateEntry{}
}
a.Revision = revision
a := acceptedEvent(digest, v, m)
a.Revision = int64(revision)
return a
}
// leftOf is the empty manifest-accepted for module, which left the trunk, judged by judgeID; revision is the modules
// state's revision of the value that left (0 when none is held), so its event id (delivery.ManifestAccepted.EventID, the
// state's revision of the value that left (0 when none is held), so its event id (acceptedEventID, the
// module and the revision, since its body names no digest) differs for each leave.
func leftOf(module string, revision uint64, judgeID string) (delivery.ManifestAccepted, string) {
a := delivery.ManifestAccepted{V: delivery.Version, Module: module, Judge: judgeID, BusState: []catalogue.BusStateEntry{}, Revision: revision}
return a, a.EventID()
func leftOf(module string, revision uint64, judgeID string) (meshcatalog.ManifestAcceptedEvent, string) {
a := meshcatalog.ManifestAcceptedEvent{V: keys.Version, Module: module, Judge: judgeID, BusState: busStateOf(nil), Revision: int64(revision)}
return a, acceptedEventID(a)
}
// sayAccepted says a with the event id id, once more when the first publish fails; the module's finding is written when
// both fail and deleted when it is said. Nil SayAccepted says nothing.
func (ix *Indexer) sayAccepted(ctx context.Context, a delivery.ManifestAccepted, id string) error {
func (ix *Indexer) sayAccepted(ctx context.Context, a meshcatalog.ManifestAcceptedEvent, id string) error {
if ix.SayAccepted == nil {
return nil
}
err := ix.SayAccepted(ctx, a, id)
if err != nil {
ix.logf("judge: %s for %s failed, published again: %v", delivery.ManifestAcceptedEvent, a.Module, err)
ix.logf("judge: %s for %s failed, published again: %v", meshcatalog.EventManifestAccepted, a.Module, err)
err = ix.SayAccepted(ctx, a, id)
}
ix.unsaid(ctx, a.Module, err)
@@ -64,7 +62,7 @@ func (ix *Indexer) sayAccepted(ctx context.Context, a delivery.ManifestAccepted,
// unsaidKey is the key of module's unsaid finding.
func unsaidKey(module string) string {
return health.FindingKey(health.ScopeModule, delivery.IndexerModule+".accepted."+module, UnsaidKind)
return health.FindingKey(health.ScopeModule, Module+".accepted."+module, UnsaidKind)
}
// unsaid writes module's finding for err, or deletes it with none.
@@ -78,19 +76,19 @@ func (ix *Indexer) unsaid(ctx context.Context, module string, err error) {
return nil, found, nil
}
at := ix.now().UTC()
summary := fmt.Sprintf("%s of %s was not published: %v", delivery.ManifestAcceptedEvent, module, err)
summary := fmt.Sprintf("%s of %s was not published: %v", meshcatalog.EventManifestAccepted, module, err)
data, ferr := health.EncodeFinding(health.Finding{Key: key, Kind: UnsaidKind,
Subject: health.Subject{Scope: health.ScopeModule, ID: delivery.IndexerModule + ".accepted." + module}, Severity: health.Warning,
Subject: health.Subject{Scope: health.ScopeModule, ID: Module + ".accepted." + module}, Severity: health.Warning,
Summary: summary,
Headline: "The indexer could not tell the bus module that " + module + "'s manifest was accepted",
Explanation: "The bus module creates and removes a module's bus state from this event. The indexer says it again at its " +
"next judging pass and clears this once it is said; indexer_say_accepted says it now.",
Resolved: "manifest-accepted of " + module + " was said.",
Evidence: []health.Evidence{{At: at, Said: summary}}, Source: UnsaidKind, Observer: delivery.IndexerModule, At: at})
Evidence: []health.Evidence{{At: at, Said: summary}}, Source: UnsaidKind, Observer: Module, At: at})
return data, false, ferr
})
if werr != nil {
ix.logf("judge: the finding of %s for %s is not written: %v", delivery.ManifestAcceptedEvent, module, werr)
ix.logf("judge: the finding of %s for %s is not written: %v", meshcatalog.EventManifestAccepted, module, werr)
}
}
@@ -104,7 +102,7 @@ func (ix *Indexer) sayFor(ctx context.Context, module string, allowLeft, again b
return "", fmt.Errorf("the modules state cannot be read: %w", err)
}
m, ok := held[module]
var a delivery.ManifestAccepted
var a meshcatalog.ManifestAcceptedEvent
var id string
switch {
case !ok && !allowLeft:
@@ -114,7 +112,7 @@ func (ix *Indexer) sayFor(ctx context.Context, module string, allowLeft, again b
case m.Manifest == "" || ix.Verdicts == nil || ix.Manifests == nil:
return "", fmt.Errorf("%s has no accepted verdict the indexer holds", module)
default:
key, err := delivery.VerdictsKey(m.Manifest)
key, err := meshcatalogclient.VerdictsKey(m.Manifest)
if err != nil {
return "", err
}
@@ -122,11 +120,11 @@ func (ix *Indexer) sayFor(ctx context.Context, module string, allowLeft, again b
if err != nil {
return "", fmt.Errorf("the verdict on %s cannot be read: %w", module, err)
}
var v delivery.ManifestVerdict
var v meshcatalog.VerdictsByDigestValue
if found {
v, err = delivery.DecodeManifestVerdict(value)
v, err = meshcatalogclient.DecodeVerdict(value)
}
if !found || err != nil || v.Refused() || v.Waits() {
if !found || err != nil || meshcatalogclient.VerdictHeld(v) {
if !allowLeft {
return "", fmt.Errorf("%s has no accepted verdict on its manifest %s", module, m.Manifest)
}
@@ -134,18 +132,18 @@ func (ix *Indexer) sayFor(ctx context.Context, module string, allowLeft, again b
return "none: its manifest is not accepted", nil
}
raw, _, found, err := ix.Manifests.Get(ctx, m.Manifest)
kept, derr := delivery.DecodeManifest(raw)
kept, derr := meshcatalogclient.DecodeManifest(raw)
if err != nil || !found || derr != nil {
return "", fmt.Errorf("the manifest %s of %s cannot be read", m.Manifest, module)
}
a = acceptedOf(m.Manifest, v, revision, []byte(kept.JSON))
id = a.EventID()
a = acceptedOf(m.Manifest, v, revision, []byte(kept.Manifest))
id = acceptedEventID(a)
}
if again {
id += "-again-" + ix.now().UTC().Format(time.RFC3339Nano)
}
if err := ix.sayAccepted(ctx, a, id); err != nil {
return "", fmt.Errorf("%s of %s was not published: %w", delivery.ManifestAcceptedEvent, module, err)
return "", fmt.Errorf("%s of %s was not published: %w", meshcatalog.EventManifestAccepted, module, err)
}
if a.Digest == "" {
return "said empty: the trunk holds no module " + module, nil
@@ -174,7 +172,7 @@ func (ix *Indexer) SayUnsaid(ctx context.Context) {
if err != nil || f.Kind != UnsaidKind {
continue
}
module := strings.TrimPrefix(f.Subject.ID, delivery.IndexerModule+".accepted.")
module := strings.TrimPrefix(f.Subject.ID, Module+".accepted.")
if _, err := ix.sayFor(ctx, module, true, false); err != nil {
ix.logf("judge: %v; its finding stands", err)
}
@@ -9,7 +9,8 @@ import (
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildrecord"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// Builds holds the builder's log (mesh-delivery-builder.build-log, novox/hq ADR 0571 clause 1) for what the indexer reads
@@ -22,11 +23,11 @@ import (
// (ADR 0501 clause 6, issue 1851). Safe for use from a watch and from the message handler at once.
type Builds struct {
mu sync.Mutex
records map[string]delivery.BuildRecord // by the entry's event, module, fingerprint and time
records map[string]buildrecord.BuildRecord // by the entry's event, module, fingerprint and time
}
// NewBuilds is an empty Builds.
func NewBuilds() *Builds { return &Builds{records: map[string]delivery.BuildRecord{}} }
func NewBuilds() *Builds { return &Builds{records: map[string]buildrecord.BuildRecord{}} }
// Put takes one entry of the builder's log. A build's end is an outcome: done registers, failed or queued does not; its
// start is a request, which registers nothing either. An entry of another kind (the old reference form's close, design 51
@@ -36,21 +37,21 @@ func (b *Builds) Put(value []byte) error {
if !builds.IsBuildEntry(value) {
return nil
}
e, err := delivery.DecodeBuildLogEntry(value)
e, err := builds.DecodeBuildLogEntry(value)
if err != nil {
return err
}
if e.Module == "" || e.Repository == "" {
return fmt.Errorf("a build log entry naming no module or repository: %s", value)
}
r := delivery.BuildRecord{ID: e.At.UTC().Format(time.RFC3339Nano) + "." + e.FP, Repository: e.Repository, Ref: e.Ref, Module: e.Module,
r := buildrecord.BuildRecord{ID: e.At.UTC().Format(time.RFC3339Nano) + "." + e.FP, Repository: e.Repository, Ref: e.Ref, Module: e.Module,
Path: e.Directory, Commit: e.Commit, At: e.At}
switch {
case e.Event != delivery.BuildEndedEvent:
case e.Event != builds.BuildEndedEvent:
r.State = e.Event
case e.Outcome == delivery.BuildFailed:
case e.Outcome == builds.BuildFailed:
r.Failed = "the build failed"
case e.Outcome != delivery.BuildDone:
case e.Outcome != builds.BuildDone:
r.State = e.Outcome
}
b.mu.Lock()
@@ -59,7 +60,7 @@ func (b *Builds) Put(value []byte) error {
return nil
}
func newer(a, b delivery.BuildRecord) bool {
func newer(a, b buildrecord.BuildRecord) bool {
return a.At.After(b.At) || (a.At.Equal(b.At) && a.ID > b.ID)
}
@@ -67,18 +68,18 @@ func newer(a, b delivery.BuildRecord) bool {
// newest successful trunk build's first ("." for the repository's root), and false when no record of the repository
// names the module (issue 1614).
func (b *Builds) PathOf(module, repository string) (string, bool) {
var best *delivery.BuildRecord
trunk := false
var best *buildrecord.BuildRecord
onTrunk := false
b.mu.Lock()
defer b.mu.Unlock()
for _, r := range b.records {
if r.Module != module || !delivery.SameRepository(r.Repository, repository) {
if r.Module != module || !trunk.SameRepository(r.Repository, repository) {
continue
}
t := registered(r) && trunkLike[r.Ref]
if best == nil || (t && !trunk) || (t == trunk && newer(r, *best)) {
if best == nil || (t && !onTrunk) || (t == onTrunk && newer(r, *best)) {
c := r
best, trunk = &c, t
best, onTrunk = &c, t
}
}
if best == nil {
@@ -97,7 +98,7 @@ type Registration struct {
// registered is whether the record is an outcome that did not fail: what registers a module. It asks for no particular branch: a
// repository's trunk is named main, master, or what its owner chose (the registered photos, de-spiegel and link2pay were
// built from nox-mesh, and every record of them names that ref).
func registered(r delivery.BuildRecord) bool {
func registered(r buildrecord.BuildRecord) bool {
return r.State == "" && r.Failed == ""
}
@@ -107,11 +108,11 @@ func registered(r delivery.BuildRecord) bool {
// first that holds the module's manifest is the one indexed.
func (b *Builds) RegistrationsOf(module string) []Registration {
type place struct{ repository, path string }
byPlace := map[place][]delivery.BuildRecord{}
byPlace := map[place][]buildrecord.BuildRecord{}
b.mu.Lock()
for _, r := range b.records {
if r.Module == module && registered(r) {
k := place{delivery.RepositoryName(r.Repository), pathOf(strings.Trim(r.Path, "/"))}
k := place{trunk.RepositoryName(r.Repository), pathOf(strings.Trim(r.Path, "/"))}
byPlace[k] = append(byPlace[k], r)
}
}
@@ -119,9 +120,9 @@ func (b *Builds) RegistrationsOf(module string) []Registration {
// Within one place the record is that of the ref a registration comes from, never a newer pull request's or try's: a
// trunk's ref (main, master, trunk, none), else the ref most of the place's records name (the registered photos, de-spiegel
// and link2pay were built from nox-mesh), the newer of two equal; then its newest record.
best := map[place]delivery.BuildRecord{}
best := map[place]buildrecord.BuildRecord{}
for k, recs := range byPlace {
count, latest := map[string]int{}, map[string]delivery.BuildRecord{}
count, latest := map[string]int{}, map[string]buildrecord.BuildRecord{}
for _, r := range recs {
count[r.Ref]++
if cur, ok := latest[r.Ref]; !ok || newer(r, cur) {
@@ -149,7 +150,7 @@ func (b *Builds) RegistrationsOf(module string) []Registration {
best[k] = latest[pick]
}
recs := slices.Collect(maps.Values(best))
slices.SortFunc(recs, func(x, y delivery.BuildRecord) int {
slices.SortFunc(recs, func(x, y buildrecord.BuildRecord) int {
switch {
case trunkLike[x.Ref] != trunkLike[y.Ref]:
if trunkLike[x.Ref] {
@@ -4,8 +4,8 @@ import (
"fmt"
"slices"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -58,11 +58,11 @@ func BusStateOf(modules, verdicts, manifests, marks views.Source) (BusStateAnswe
if !ok {
continue
}
mod, err := delivery.DecodeModule(e.Value)
mod, err := meshcatalogclient.DecodeModule(e.Value)
if err != nil || mod.Manifest == "" {
continue // no manifest held for it, so no verdict
}
key, err := delivery.VerdictsKey(mod.Manifest)
key, err := meshcatalogclient.VerdictsKey(mod.Manifest)
if err != nil {
continue
}
@@ -73,8 +73,8 @@ func BusStateOf(modules, verdicts, manifests, marks views.Source) (BusStateAnswe
if !ok {
continue
}
v, err := delivery.DecodeManifestVerdict(ve.Value)
if err != nil || v.State != delivery.VerdictAccepted {
v, err := meshcatalogclient.DecodeVerdict(ve.Value)
if err != nil || v.State != meshcatalogclient.VerdictAccepted {
continue
}
me, ok, err := manifests.Get(mod.Manifest)
@@ -84,11 +84,11 @@ func BusStateOf(modules, verdicts, manifests, marks views.Source) (BusStateAnswe
if !ok {
continue
}
kept, err := delivery.DecodeManifest(me.Value)
kept, err := meshcatalogclient.DecodeManifest(me.Value)
if err != nil {
return BusStateAnswer{}, fmt.Errorf("the manifest of %s: %w", name, err)
}
m, err := catalogue.Decode([]byte(kept.JSON))
m, err := catalogue.Decode([]byte(kept.Manifest))
if err != nil {
return BusStateAnswer{}, fmt.Errorf("the accepted manifest of %s does not read: %w", name, err)
}
@@ -7,12 +7,14 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
func manifestPull(t *testing.T, number int, head, manifest string) forgemsg.Forge {
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: number, Head: head,
f := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: number, Head: head,
Action: forgemsg.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true}
if manifest != "" {
f.Manifests = map[string]string{"modules/avahi": manifest}
@@ -20,9 +22,9 @@ func manifestPull(t *testing.T, number int, head, manifest string) forgemsg.Forg
return f
}
func candidateOf(t *testing.T, st *memStore) delivery.Candidate {
func candidateOf(t *testing.T, st *memStore) meshcatalog.CandidatesByGroupValue {
t.Helper()
key, err := delivery.CandidatesKey("feat/x")
key, err := CandidatesKey("feat/x")
if err != nil {
t.Fatal(err)
}
@@ -30,7 +32,7 @@ func candidateOf(t *testing.T, st *memStore) delivery.Candidate {
if !ok {
t.Fatalf("candidates holds %d keys, none %s", len(st.vals), key)
}
c, err := delivery.DecodeCandidate(raw)
c, err := DecodeCandidate(raw)
if err != nil {
t.Fatal(err)
}
@@ -44,12 +46,12 @@ func TestACandidateGivesEachTouchedModuleItsManifestAndTheReasonItHasNoPreview(t
t.Fatal(err)
}
c := candidateOf(t, ix.Store.(*memStore)) // DecodeCandidate has run Candidate.check
want := delivery.CandidateModule{Manifest: delivery.ManifestDigest([]byte(avahi)), None2: "no merge preview is computed"}
want := meshcatalog.CandidatesByGroupValuePullsItemFpsValue{Manifest: meshcatalogclient.ManifestDigest([]byte(avahi)), None2: "no merge preview is computed"}
if len(c.Pulls) != 1 || len(c.Pulls[0].Fps) != 1 || !reflect.DeepEqual(c.Pulls[0].Fps["avahi"], want) {
t.Fatalf("pulls = %+v, want Fps[avahi] = %+v", c.Pulls, want)
}
if c.FPSet == "" || c.FPSet != delivery.FingerprintSet(c.Pulls) {
t.Errorf("fpset = %q, want %q", c.FPSet, delivery.FingerprintSet(c.Pulls))
if c.Fpset == "" || c.Fpset != meshcatalogclient.FingerprintSet(c.Pulls) {
t.Errorf("fpset = %q, want %q", c.Fpset, meshcatalogclient.FingerprintSet(c.Pulls))
}
// The same pull request heard again without its directories (the head unchanged) keeps what it had.
again := manifestPull(t, 301, h1, "")
@@ -57,8 +59,8 @@ func TestACandidateGivesEachTouchedModuleItsManifestAndTheReasonItHasNoPreview(t
if err := ix.Apply(context.Background(), again); err != nil {
t.Fatal(err)
}
if c2 := candidateOf(t, ix.Store.(*memStore)); !reflect.DeepEqual(c2.Pulls[0].Fps["avahi"], want) || c2.FPSet != c.FPSet {
t.Errorf("after a message without directories: %+v fpset %q", c2.Pulls[0].Fps, c2.FPSet)
if c2 := candidateOf(t, ix.Store.(*memStore)); !reflect.DeepEqual(c2.Pulls[0].Fps["avahi"], want) || c2.Fpset != c.Fpset {
t.Errorf("after a message without directories: %+v fpset %q", c2.Pulls[0].Fps, c2.Fpset)
}
}
@@ -68,7 +70,7 @@ func TestAPullWithoutManifestsWritesTheCandidateAsToday(t *testing.T) { // 1451/
t.Fatal(err)
}
c := candidateOf(t, ix.Store.(*memStore))
if len(c.Pulls) != 1 || c.Pulls[0].Fps != nil || c.FPSet != "" || len(c.Pulls[0].Modules) != 1 || c.Pulls[0].Modules[0] != "avahi" {
if len(c.Pulls) != 1 || c.Pulls[0].Fps != nil || c.Fpset != "" || len(c.Pulls[0].Modules) != 1 || c.Pulls[0].Modules[0] != "avahi" {
t.Errorf("candidate = %+v, want the module avahi and no Fps and no fpset", c)
}
}
@@ -93,15 +95,15 @@ func TestTwoPullsGivingOneModuleDifferentManifestsAreRefusedNamingBoth(t *testin
}
}
// Candidate.check refuses the group too, which is what the refusal stands for.
if _, encErr := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: "feat.x", HeadsHash: before.HeadsHash, Pulls: []delivery.CandidatePull{before.Pulls[0], {
Head: delivery.Head{Repository: catalogueRepo, Number: 302, Head: h2}, Modules: []string{"avahi"},
Fps: map[string]delivery.CandidateModule{"avahi": {Manifest: delivery.ManifestDigest([]byte(other)), None2: NoneNoPreview}}}}}); encErr == nil {
if _, encErr := EncodeCandidate(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat.x", Heads: before.Heads, Pulls: []meshcatalog.CandidatesByGroupValuePullsItem{before.Pulls[0], {
Repository: catalogueRepo, Number: 302, Head: h2, Modules: []string{"avahi"},
Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"avahi": {Manifest: meshcatalogclient.ManifestDigest([]byte(other)), None2: NoneNoPreview}}}}}); encErr == nil {
t.Error("the candidate check accepts two manifests for one module")
}
if ix.Store.(*memStore).writes != writes {
t.Error("a partial write was made")
}
if after := candidateOf(t, ix.Store.(*memStore)); len(after.Pulls) != 1 || after.FPSet != before.FPSet {
if after := candidateOf(t, ix.Store.(*memStore)); len(after.Pulls) != 1 || after.Fpset != before.Fpset {
t.Errorf("the candidate changed: %+v", after)
}
}
@@ -0,0 +1,132 @@
package index
import (
"encoding/json"
"fmt"
"path"
"reflect"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/keys"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// The values of the state candidates (novox/hq ADR 0437 §5, ADR 0453 clauses 6 and 16): the contract's types, written by
// the indexer alone, so their key and writing rules are the indexer's (ADR 0634 §1).
// CandidateNoneNoModule is the None2 of a candidate's module whose directory the merge preview names no manifest for: the
// change removes the module. The indexer writes it and the assigner reads it as a removal (issue 1297).
const CandidateNoneNoModule = "the merge preview names no manifest for the module's directory"
// CandidatesKey is the key, in the candidates state, of a group: `<group>`, the branch name the group's pull requests share
// (a slash is allowed), escaped so a branch such as `fix/1.2` keys; a lone pull request is a group of one under its branch.
func CandidatesKey(group string) (string, error) {
if group == "" {
return "", fmt.Errorf("index: the group name is empty")
}
return meshcatalog.CandidatesByGroupKey(keys.EscapeName(group)), nil
}
func checkCandidateModule(m meshcatalog.CandidatesByGroupValuePullsItemFpsValue) error {
switch {
case m.Fp2 != "" && m.None2 != "":
return fmt.Errorf("index: a module has both fp2 and none2")
case m.Fp2 == "" && m.None2 == "":
return fmt.Errorf("index: a module has neither fp2 nor none2")
}
for p, object := range m.Paths {
if p == "" || p != path.Clean(p) || strings.HasPrefix(p, "/") || p == ".." || strings.HasPrefix(p, "../") {
return fmt.Errorf("index: the named path %q is not a clean path inside the module's directory", p)
}
if object == "" {
return fmt.Errorf("index: the named path %q names no git object", p)
}
}
if err := sdkstep.PackageVersions(m.Packages).Check(); err != nil {
return err
}
if m.Fp2 != "" {
if _, err := meshcatalogclient.KeyPart(m.Fp2); err != nil {
return err
}
}
if m.Manifest != "" {
if _, err := meshcatalogclient.ManifestsKey(m.Manifest); err != nil {
return err
}
}
return nil
}
func checkCandidate(c meshcatalog.CandidatesByGroupValue) error {
if len(c.Pulls) == 0 {
return fmt.Errorf("index: a candidate names no pull request")
}
heads := make([]meshdeliveryclient.Head, len(c.Pulls))
for i, p := range c.Pulls {
heads[i] = meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
want, _ := meshdeliveryclient.HeadsHash(heads)
if c.Heads != want {
return fmt.Errorf("index: the candidate's heads hash %q is not the hash %q of its pull requests' heads", c.Heads, want)
}
seen := map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{}
for _, p := range c.Pulls {
touched := map[string]bool{}
for _, m := range p.Modules {
touched[m] = true
}
for module, m := range p.Fps {
if !touched[module] {
return fmt.Errorf("index: the candidate gives a fingerprint of the module %q, which pull request %s#%d does not touch", module, p.Repository, p.Number)
}
if err := checkCandidateModule(m); err != nil {
return fmt.Errorf("index: the candidate's module %q: %w", module, err)
}
if before, ok := seen[module]; ok && !reflect.DeepEqual(before, m) {
return fmt.Errorf("index: two pull requests of the candidate give the module %q different fingerprints", module)
}
seen[module] = m
}
}
if wantSet := meshcatalogclient.FingerprintSet(c.Pulls); c.Fpset != wantSet {
return fmt.Errorf("index: the candidate's fpset %q is not the hash %q of its fingerprints", c.Fpset, wantSet)
}
return nil
}
// EncodeCandidate is c as JSON at the kept values' version; it refuses what a reader could not take.
func EncodeCandidate(c meshcatalog.CandidatesByGroupValue) ([]byte, error) {
if err := checkCandidate(c); err != nil {
return nil, err
}
c.V = keys.Version
return json.Marshal(c)
}
// DecodeCandidate reads a candidate; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeCandidate refuses.
func DecodeCandidate(data []byte) (meshcatalog.CandidatesByGroupValue, error) {
if err := keys.CheckVersion(data); err != nil {
return meshcatalog.CandidatesByGroupValue{}, err
}
var c meshcatalog.CandidatesByGroupValue
if err := json.Unmarshal(data, &c); err != nil {
return meshcatalog.CandidatesByGroupValue{}, fmt.Errorf("index: a candidate does not fit its shape: %w", err)
}
if err := checkCandidate(c); err != nil {
return meshcatalog.CandidatesByGroupValue{}, err
}
return c, nil
}
// SameCandidate says whether two candidates are the same value.
func SameCandidate(a, b meshcatalog.CandidatesByGroupValue) bool { return reflect.DeepEqual(a, b) }
// headOf is a candidate's pull request as the head the heads hash is over.
func headOf(p meshcatalog.CandidatesByGroupValuePullsItem) meshdeliveryclient.Head {
return meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
@@ -1,6 +1,7 @@
package index
import (
"cmp"
"context"
"errors"
"fmt"
@@ -11,9 +12,15 @@ import (
"sync/atomic"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// ErrConflict is what a Store says when a write found the key changed or created since it was read.
@@ -67,15 +74,15 @@ type Indexer struct {
// JudgeID names the judge a verdict says made it; empty is judge.Identity, the running program's.
JudgeID string
// SayRefused says the event manifest-refused for a refusal newly written; nil says nothing.
SayRefused func(ctx context.Context, r delivery.ManifestRefused)
SayRefused func(ctx context.Context, r meshcatalog.ManifestRefusedEvent)
// SayAccepted publishes the event manifest-accepted a under the bus's duplicate id id (accepted.go; issue 1966 D3,
// DLV-I9): for every accepted verdict newly written, carrying the entries whole, and an empty one for a module leaving
// the trunk; nil says nothing.
SayAccepted func(ctx context.Context, a delivery.ManifestAccepted, id string) error
SayAccepted func(ctx context.Context, a meshcatalog.ManifestAcceptedEvent, id string) error
// Findings is the indexer's findings state, where a manifest-accepted whose publish failed twice is kept until said
// (accepted.go, DLV-I9b); nil writes none.
Findings Store
// Packages is the packages state (delivery.PackagesBucket, packages.go; novox/hq ADR 0593 §2); nil publishes none,
// Packages is the packages state (PackagesBucket, packages.go; novox/hq ADR 0593 §2); nil publishes none,
// and every fingerprint takes the version line.
Packages Store
// SeatDefinitions is the state seat-definitions (seatdefs.go; design 26, DLV-I17); nil keeps none.
@@ -133,19 +140,19 @@ type Indexer struct {
mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach
source sourceNow
previews map[string]delivery.Preview // by previews key (previews.go)
toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go)
tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go)
unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread
moving map[string]forgemsg.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
retrying atomic.Bool // a RetryUnread runs in the background (kickRetry)
retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one
retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on
recompute sync.Mutex // one Recompute at a time
untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go)
filling atomic.Bool // a FillTaken runs in the background (KickFill)
fillAgain atomic.Bool // a kick arrived while it ran
atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished)
previews map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue // by previews key (previews.go)
toolchains map[string]map[string]nodebuildgo.Toolchain // language, then node (toolchains.go)
tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go)
unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread
moving map[string]forgemsg.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go)
retrying atomic.Bool // a RetryUnread runs in the background (kickRetry)
retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one
retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on
recompute sync.Mutex // one Recompute at a time
untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go)
filling atomic.Bool // a FillTaken runs in the background (KickFill)
fillAgain atomic.Bool // a kick arrived while it ran
atHeads map[string]map[string]meshcatalog.PackagesByModuleValue // a member head's published Go modules, by repository@head (candidatePublished)
// OwnBuild answers the fingerprint of the build this program runs, from the node-engine's seat; nil knows none.
OwnBuild func(ctx context.Context) (string, error)
judgeMu sync.Mutex // one judge pass at a time (verdicts.go)
@@ -232,7 +239,7 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f forgemsg.Forge) (Applied,
}
}
var digests map[string]string
var held map[string]delivery.Module
var held map[string]meshcatalog.ModulesByModuleValue
if f.Kind == forgemsg.ForgePull {
var err error
if digests, err = ix.keepManifests(ctx, f); err != nil {
@@ -249,7 +256,7 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f forgemsg.Forge) (Applied,
break
}
if value, _, found, err := ix.Manifests.Get(ctx, digest); err == nil && found {
if kept, err := delivery.DecodeManifest(value); err == nil {
if kept, err := meshcatalogclient.DecodeManifest(value); err == nil {
over[kept.Module] = digest
}
}
@@ -272,19 +279,19 @@ func (ix *Indexer) removeCandidate(ctx context.Context, f forgemsg.Forge) error
}
// writeCandidate writes the group's candidate with the pull request f names (taken out for a merged or closed one).
func (ix *Indexer) writeCandidate(ctx context.Context, f forgemsg.Forge, digests map[string]string, held map[string]delivery.Module) error {
func (ix *Indexer) writeCandidate(ctx context.Context, f forgemsg.Forge, digests map[string]string, held map[string]meshcatalog.ModulesByModuleValue) error {
if trunkLike[f.Branch] {
ix.logf("%s#%d: its branch %q is no delivery group, so no candidate", f.Repository, f.Number, f.Branch)
return nil
}
key, err := delivery.CandidatesKey(f.Branch)
key, err := CandidatesKey(f.Branch)
if err != nil {
return fmt.Errorf("%w: %s#%d: %v", ErrUnreadable, f.Repository, f.Number, err)
}
return write(ctx, ix.Store, key, func(value []byte, found bool) ([]byte, bool, error) {
var cur *delivery.Candidate
var cur *meshcatalog.CandidatesByGroupValue
if found {
c, err := delivery.DecodeCandidate(value)
c, err := DecodeCandidate(value)
if err != nil {
return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err)
}
@@ -298,12 +305,12 @@ func (ix *Indexer) writeCandidate(ctx context.Context, f forgemsg.Forge, digests
}
}
switch {
case next == nil && cur == nil, next != nil && cur != nil && delivery.SameCandidate(*cur, *next):
case next == nil && cur == nil, next != nil && cur != nil && SameCandidate(*cur, *next):
return nil, false, nil
case next == nil:
return nil, true, nil
}
data, err := delivery.EncodeCandidate(*next)
data, err := EncodeCandidate(*next)
return data, false, err
})
}
@@ -325,12 +332,12 @@ func (ix *Indexer) applyOpen(ctx context.Context, f forgemsg.Forge) error {
if !found {
return nil, false, nil
}
c, err := delivery.DecodeCandidate(value)
c, err := DecodeCandidate(value)
if err != nil {
return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err)
}
kept := slices.DeleteFunc(slices.Clone(c.Pulls), func(p delivery.CandidatePull) bool {
return !named[forgemsg.OpenPull{Repository: p.Repository, Number: p.Number}]
kept := slices.DeleteFunc(slices.Clone(c.Pulls), func(p meshcatalog.CandidatesByGroupValuePullsItem) bool {
return !named[forgemsg.OpenPull{Repository: p.Repository, Number: int(p.Number)}]
})
switch {
case len(kept) == len(c.Pulls):
@@ -339,16 +346,16 @@ func (ix *Indexer) applyOpen(ctx context.Context, f forgemsg.Forge) error {
ix.logf("%s: no pull request of the group is open, so no candidate", key)
return nil, true, nil
}
heads := make([]delivery.Head, len(kept))
heads := make([]meshdeliveryclient.Head, len(kept))
for i, p := range kept {
heads[i] = p.Head
heads[i] = meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
hash, err := delivery.HeadsHash(heads)
hash, err := meshdeliveryclient.HeadsHash(heads)
if err != nil {
return nil, false, fmt.Errorf("%w: candidates.%s: %v", ErrUnreadable, key, err)
}
c.Pulls, c.HeadsHash = kept, hash
data, err := delivery.EncodeCandidate(c)
c.Pulls, c.Heads = kept, hash
data, err := EncodeCandidate(c)
return data, false, err
})
if err != nil {
@@ -401,20 +408,22 @@ func write(ctx context.Context, st Store, key string, compute func(value []byte,
// its manifest and why it has no fingerprint of the new form (NoneNoPreview), and the candidate its FPSet (issue 1451); a
// pull request whose head is held again without its directories keeps the Fps it had. digests are the manifests' digests by
// directory; held are the modules the modules bucket holds, which name the module of a changed directory (moduleOfDir).
func (ix *Indexer) next(cur *delivery.Candidate, f forgemsg.Forge, digests map[string]string, held map[string]delivery.Module) *delivery.Candidate {
var pulls []delivery.CandidatePull
var base uint64
func (ix *Indexer) next(cur *meshcatalog.CandidatesByGroupValue, f forgemsg.Forge, digests map[string]string, held map[string]meshcatalog.ModulesByModuleValue) *meshcatalog.CandidatesByGroupValue {
var pulls []meshcatalog.CandidatesByGroupValuePullsItem
var base int64
if cur != nil {
pulls, base = slices.Clone(cur.Pulls), cur.Base
}
at := slices.IndexFunc(pulls, func(p delivery.CandidatePull) bool { return p.Repository == f.Repository && p.Number == f.Number })
at := slices.IndexFunc(pulls, func(p meshcatalog.CandidatesByGroupValuePullsItem) bool {
return p.Repository == f.Repository && p.Number == int64(f.Number)
})
if f.Kind == forgemsg.ForgeMerged || f.Action == forgemsg.PullClosed {
if at < 0 {
return cur
}
pulls = slices.Delete(pulls, at, at+1)
} else {
entry := delivery.CandidatePull{Head: delivery.Head{Repository: f.Repository, Number: f.Number, Head: f.Head}, Modules: []string{}}
entry := meshcatalog.CandidatesByGroupValuePullsItem{Repository: f.Repository, Number: int64(f.Number), Head: f.Head, Modules: []string{}}
switch {
case f.DirsKnown:
for _, dir := range f.Dirs {
@@ -424,12 +433,12 @@ func (ix *Indexer) next(cur *delivery.Candidate, f forgemsg.Forge, digests map[s
}
if digest, ok := digests[dir]; ok {
if entry.Fps == nil {
entry.Fps = map[string]delivery.CandidateModule{}
entry.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{}
}
entry.Fps[m] = delivery.CandidateModule{Manifest: digest, None2: NoneNoPreview}
entry.Fps[m] = meshcatalog.CandidatesByGroupValuePullsItemFpsValue{Manifest: digest, None2: NoneNoPreview}
}
}
case at >= 0 && pulls[at].Head.Head == f.Head:
case at >= 0 && pulls[at].Head == f.Head:
// Its directories are not known now, for the head the group already holds: what it knew stands.
entry.Modules, entry.Fps = pulls[at].Modules, pulls[at].Fps
}
@@ -442,18 +451,18 @@ func (ix *Indexer) next(cur *delivery.Candidate, f forgemsg.Forge, digests map[s
if len(pulls) == 0 {
return nil
}
slices.SortFunc(pulls, func(a, b delivery.CandidatePull) int {
slices.SortFunc(pulls, func(a, b meshcatalog.CandidatesByGroupValuePullsItem) int {
if c := strings.Compare(a.Repository, b.Repository); c != 0 {
return c
}
return a.Number - b.Number
return cmp.Compare(a.Number, b.Number)
})
heads := make([]delivery.Head, len(pulls))
heads := make([]meshdeliveryclient.Head, len(pulls))
for i, p := range pulls {
heads[i] = p.Head
heads[i] = meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
hash, _ := delivery.HeadsHash(heads)
return &delivery.Candidate{V: delivery.Version, Group: f.Branch, Base: base, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}
hash, _ := meshdeliveryclient.HeadsHash(heads)
return &meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: f.Branch, Base: base, Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}
}
// NoneNoPreview is why a candidate's touched module has no fingerprint of the new form: the merge preview is not
@@ -464,8 +473,8 @@ const NoneNoPreview = "no merge preview is computed"
// candidate's own check does (mesh-sdk Candidate.check), naming both pull requests so the log says which; nothing is
// written, and the message is dropped as unreadable and said in the log (main.go handle): the next message of either pull
// request is judged again.
func (ix *Indexer) refuseDisagreement(c delivery.Candidate) error {
first := map[string]delivery.CandidatePull{}
func (ix *Indexer) refuseDisagreement(c meshcatalog.CandidatesByGroupValue) error {
first := map[string]meshcatalog.CandidatesByGroupValuePullsItem{}
for _, p := range c.Pulls {
for module, m := range p.Fps {
before, ok := first[module]
@@ -9,7 +9,6 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
@@ -25,7 +24,7 @@ import (
// CatalogRead reads the states the seat's verbs answer from.
type CatalogRead struct {
// Modules is every value of the modules state, by module.
Modules func() (map[string]delivery.Module, error)
Modules func() (map[string]meshcatalog.ModulesByModuleValue, error)
// Manifest is the manifest kept under a digest in the manifests state, as its JSON.
Manifest func(digest string) (string, error)
// Graph is the module graph, by module.
@@ -35,8 +34,8 @@ type CatalogRead struct {
SeatDefinitions, Marks views.Source
}
func entryOf(name string, m delivery.Module) catalogue.CatalogEntry {
return catalogue.CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.FP2, None: m.None2}
func entryOf(name string, m meshcatalog.ModulesByModuleValue) catalogue.CatalogEntry {
return catalogue.CatalogEntry{Module: name, Repository: m.Repository, Path: m.Path, Commit: m.Commit, FP: m.Fp2, None: m.None2}
}
// CatalogModulesAnswer is the verb modules: every module the modules state holds, by name.
@@ -104,7 +103,7 @@ func (c CatalogRead) CatalogProvidesAnswer(provision string) (catalogue.CatalogP
}
// scopeOf is the scope the module's manifest offers provision at, "" when its manifest cannot be read.
func (c CatalogRead) scopeOf(m delivery.Module, provision string) string {
func (c CatalogRead) scopeOf(m meshcatalog.ModulesByModuleValue, provision string) string {
if m.Manifest == "" {
return ""
}
@@ -142,7 +141,7 @@ func (c CatalogRead) CatalogDependentsAnswer(module string) (catalogue.CatalogDe
return catalogue.CatalogDependents{Module: module, Dependents: out}, nil
}
func (c CatalogRead) graphAndModules() (graph.Graph, map[string]delivery.Module, error) {
func (c CatalogRead) graphAndModules() (graph.Graph, map[string]meshcatalog.ModulesByModuleValue, error) {
g, err := c.Graph()
if err != nil {
return nil, nil, fmt.Errorf("the module graph cannot be read: %w", err)
@@ -10,7 +10,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
@@ -57,8 +57,8 @@ func catalogVerbsAnswerFromTheStates(t *testing.T) {
mod{"ca-trust", catalogueManifest(t, "ca-trust")}, mod{"acme", withAcme})
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
c := CatalogRead{
Modules: func() (map[string]delivery.Module, error) {
out := map[string]delivery.Module{}
Modules: func() (map[string]meshcatalog.ModulesByModuleValue, error) {
out := map[string]meshcatalog.ModulesByModuleValue{}
for k := range mods.vals {
m, _ := moduleOf(t, mods, k)
out[k] = m
@@ -4,7 +4,7 @@ import (
"encoding/json"
"slices"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
// ConfigReadersTool is the indexer's tool that says who reads a module's config keys (design 32 *A value no longer
@@ -51,7 +51,7 @@ func configKeys(raw string) (values, secrets []string) {
// ConfigReaders are the rows of module's config keys: every key any manifest of the module the indexer keeps ever
// declared (so a key no manifest declares any more is a row with no reader), each with who reads it now. trunk is the
// modules state, manifests the manifests state by digest, candidates the open pull requests' config declarations.
func ConfigReaders(module string, trunk map[string]delivery.Module, manifests map[string]delivery.Manifest, candidates []delivery.ConfigCandidate) []ConfigReaderRow {
func ConfigReaders(module string, trunk map[string]meshcatalog.ModulesByModuleValue, manifests map[string]meshcatalog.ManifestsByDigestValue, candidates []meshcatalog.ConfigCandidatesByPullValue) []ConfigReaderRow {
rows := map[string]*ConfigReaderRow{}
add := func(kind, key, reader string) {
id := kind + "|" + key
@@ -76,7 +76,7 @@ func ConfigReaders(module string, trunk map[string]delivery.Module, manifests ma
if digest == current {
reader = module
}
values, secrets := configKeys(kept.JSON)
values, secrets := configKeys(kept.Manifest)
for _, k := range values {
add(ConfigKindValue, k, reader)
}
@@ -10,8 +10,10 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -22,8 +24,8 @@ import (
// The two states' buckets.
var (
PredefinedBucket = delivery.IndexerModule + "_" + delivery.IndexerPredefinedState
ConfigCandidatesBucket = delivery.IndexerModule + "_" + delivery.IndexerConfigCandidatesState
PredefinedBucket = Module + "_" + PredefinedState
ConfigCandidatesBucket = Module + "_" + ConfigCandidatesState
)
// writePredefined keeps the predefined list a merged message carries, unless the key names a later merge: a merge older
@@ -32,9 +34,12 @@ func (ix *Indexer) writePredefined(ctx context.Context, f forgemsg.Forge) error
if ix.Predefined == nil || len(f.Predefined) == 0 {
return nil
}
return ix.notYet(PredefinedBucket, write(ctx, ix.Predefined, delivery.PredefinedListKey, func(value []byte, found bool) ([]byte, bool, error) {
return ix.notYet(PredefinedBucket, write(ctx, ix.Predefined, PredefinedListKey, func(value []byte, found bool) ([]byte, bool, error) {
if found {
var cur delivery.PredefinedKept
var cur struct { // the list is compared as it was kept, byte for byte
Commit string `json:"commit"`
List json.RawMessage `json:"list"`
}
if err := json.Unmarshal(value, &cur); err == nil {
if cur.Commit == f.Merge && string(cur.List) == string(f.Predefined) {
return nil, false, nil
@@ -49,7 +54,7 @@ func (ix *Indexer) writePredefined(ctx context.Context, f forgemsg.Forge) error
}
}
}
next, err := json.Marshal(delivery.PredefinedKept{V: delivery.Version, Commit: f.Merge, At: ix.now(), List: f.Predefined})
next, err := json.Marshal(meshcatalog.PredefinedByListValue{V: keys.Version, Commit: f.Merge, At: ptr(ix.now()), List: f.Predefined})
return next, false, err
}))
}
@@ -76,10 +81,10 @@ func PullName(repository string, number int) string { return repository + "#" +
// DeclarationsOf is a manifest's config declarations by key, in the layout of the controller's declared-config mirror: the
// config form's `config`, else the settings form's `settings`, each default and enum encoded the one way.
func DeclarationsOf(raw string) (map[string]delivery.ConfigDeclaration, error) {
func DeclarationsOf(raw string) (map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue, error) {
var m struct {
Config map[string]delivery.ConfigDeclaration `json:"config"`
Settings map[string]delivery.ConfigDeclaration `json:"settings"`
Config map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue `json:"config"`
Settings map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue `json:"settings"`
}
if err := json.Unmarshal([]byte(raw), &m); err != nil {
return nil, err
@@ -88,30 +93,17 @@ func DeclarationsOf(raw string) (map[string]delivery.ConfigDeclaration, error) {
if declared == nil {
declared = m.Settings
}
out := map[string]delivery.ConfigDeclaration{}
// A default and an enum decoded into the contract's `any` are written again in the one canonical way (keys sorted).
out := make(map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue, len(declared))
for key, d := range declared {
for _, field := range []*json.RawMessage{&d.Default, &d.Enum} {
if len(*field) == 0 {
continue
}
var v any
if err := json.Unmarshal(*field, &v); err != nil {
return nil, fmt.Errorf("%s: %w", key, err)
}
canonical, err := json.Marshal(v)
if err != nil {
return nil, err
}
*field = canonical
}
out[key] = d
}
return out, nil
}
// changedDeclarations are the declarations head adds or changes against trunk.
func changedDeclarations(head, trunk map[string]delivery.ConfigDeclaration) map[string]delivery.ConfigDeclaration {
out := map[string]delivery.ConfigDeclaration{}
func changedDeclarations(head, trunk map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue) map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue {
out := map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue{}
for key, d := range head {
if before, there := trunk[key]; !there || !reflect.DeepEqual(before, d) {
out[key] = d
@@ -121,7 +113,7 @@ func changedDeclarations(head, trunk map[string]delivery.ConfigDeclaration) map[
}
// trunkManifest is the manifest the indexer holds for a module on the trunk, "" when it holds none.
func (ix *Indexer) trunkManifest(ctx context.Context, held map[string]delivery.Module, module string) (string, error) {
func (ix *Indexer) trunkManifest(ctx context.Context, held map[string]meshcatalog.ModulesByModuleValue, module string) (string, error) {
m, ok := held[module]
if !ok || m.Manifest == "" || ix.Manifests == nil {
return "", nil
@@ -130,17 +122,17 @@ func (ix *Indexer) trunkManifest(ctx context.Context, held map[string]delivery.M
if err != nil || !found {
return "", err
}
kept, err := delivery.DecodeManifest(raw)
kept, err := meshcatalogclient.DecodeManifest(raw)
if err != nil {
return "", err
}
return kept.JSON, nil
return kept.Manifest, nil
}
// candidateDeclares are, per module of a pull request's head, the declarations its manifest adds or changes against the
// trunk's (an empty set for a module that adds none): heads are the head's manifests by module.
func (ix *Indexer) candidateDeclares(ctx context.Context, held map[string]delivery.Module, heads map[string]string) (map[string]map[string]delivery.ConfigDeclaration, error) {
out := map[string]map[string]delivery.ConfigDeclaration{}
func (ix *Indexer) candidateDeclares(ctx context.Context, held map[string]meshcatalog.ModulesByModuleValue, heads map[string]string) (map[string]map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue, error) {
out := map[string]map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue{}
for module, raw := range heads {
head, err := DeclarationsOf(raw)
if err != nil {
@@ -150,7 +142,7 @@ func (ix *Indexer) candidateDeclares(ctx context.Context, held map[string]delive
if err != nil {
return nil, err
}
trunk := map[string]delivery.ConfigDeclaration{}
trunk := map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue{}
if trunkRaw != "" {
if trunk, err = DeclarationsOf(trunkRaw); err != nil {
return nil, fmt.Errorf("the trunk's manifest of %s: %w", module, err)
@@ -165,12 +157,12 @@ func (ix *Indexer) candidateDeclares(ctx context.Context, held map[string]delive
// writeConfigCandidate writes a pull request's key of config-candidates from its head's manifests by module: written when
// what it says differs (its time apart), never for the same head and declarations twice.
func (ix *Indexer) writeConfigCandidate(ctx context.Context, repository string, number int, head string, held map[string]delivery.Module, heads map[string]string) error {
func (ix *Indexer) writeConfigCandidate(ctx context.Context, repository string, number int, head string, held map[string]meshcatalog.ModulesByModuleValue, heads map[string]string) error {
if ix.ConfigCandidates == nil {
return nil
}
pull := PullName(repository, number)
key, err := delivery.ConfigCandidateKey(pull)
key, err := ConfigCandidateKey(pull)
if err != nil {
return fmt.Errorf("%w: %v", ErrUnreadable, err)
}
@@ -180,12 +172,12 @@ func (ix *Indexer) writeConfigCandidate(ctx context.Context, repository string,
}
return ix.notYet(ConfigCandidatesBucket, write(ctx, ix.ConfigCandidates, key, func(value []byte, found bool) ([]byte, bool, error) {
if found {
var cur delivery.ConfigCandidate
var cur meshcatalog.ConfigCandidatesByPullValue
if json.Unmarshal(value, &cur) == nil && cur.Head == head && reflect.DeepEqual(cur.Declares, declares) {
return nil, false, nil
}
}
next, err := json.Marshal(delivery.ConfigCandidate{V: delivery.Version, Pull: pull, Head: head, Declares: declares, At: ix.now()})
next, err := json.Marshal(meshcatalog.ConfigCandidatesByPullValue{V: keys.Version, Pull: pull, Head: head, Declares: declares, At: ptr(ix.now())})
return next, false, err
}))
}
@@ -195,7 +187,7 @@ func (ix *Indexer) dropConfigCandidate(ctx context.Context, repository string, n
if ix.ConfigCandidates == nil {
return nil
}
key, err := delivery.ConfigCandidateKey(PullName(repository, number))
key, err := ConfigCandidateKey(PullName(repository, number))
if err != nil {
return nil
}
@@ -204,7 +196,7 @@ func (ix *Indexer) dropConfigCandidate(ctx context.Context, repository string, n
// configCandidateOfPull is a pull message's part in config-candidates: a head whose manifests the message carries is written,
// a closed one deleted. A head held again without its directories leaves the key as it is.
func (ix *Indexer) configCandidateOfPull(ctx context.Context, f forgemsg.Forge, held map[string]delivery.Module) error {
func (ix *Indexer) configCandidateOfPull(ctx context.Context, f forgemsg.Forge, held map[string]meshcatalog.ModulesByModuleValue) error {
if f.Action == forgemsg.PullClosed {
return ix.dropConfigCandidate(ctx, f.Repository, f.Number)
}
@@ -248,7 +240,7 @@ func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f forgemsg.Fo
if err != nil || !found {
continue
}
c, err := delivery.DecodeCandidate(raw)
c, err := DecodeCandidate(raw)
if err != nil {
continue
}
@@ -262,11 +254,11 @@ func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f forgemsg.Fo
if err != nil || !found {
continue
}
if kept, err := delivery.DecodeManifest(mraw); err == nil {
heads[module] = kept.JSON
if kept, err := meshcatalogclient.DecodeManifest(mraw); err == nil {
heads[module] = kept.Manifest
}
}
if err := ix.writeConfigCandidate(ctx, p.Repository, p.Number, p.Head.Head, held, heads); err != nil {
if err := ix.writeConfigCandidate(ctx, p.Repository, int(p.Number), p.Head, held, heads); err != nil {
errs = append(errs, err)
}
}
@@ -274,7 +266,7 @@ func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f forgemsg.Fo
return errors.Join(errs...)
}
func touchesAny(p delivery.CandidatePull, modules []string) bool {
func touchesAny(p meshcatalog.CandidatesByGroupValuePullsItem, modules []string) bool {
for _, m := range modules {
if _, ok := p.Fps[m]; ok {
return true
@@ -291,3 +283,16 @@ func writtenModules(written []string) []string {
}
return out
}
func ptr[T any](v T) *T { return &v }
// ConfigCandidateKey is a pull request's key in config-candidates, `owner/repository#number` with `/` and `#` as dots, the form
// mesh-config keeps a pull request's values under.
func ConfigCandidateKey(pull string) (string, error) {
owner, rest, ok := strings.Cut(strings.TrimSpace(pull), "/")
repo, number, ok2 := strings.Cut(rest, "#")
if !ok || !ok2 || owner == "" || repo == "" || number == "" || strings.ContainsAny(owner+repo+number, ".*> ") {
return "", fmt.Errorf("index: %q is not a pull request, owner/repository#number", pull)
}
return meshcatalog.ConfigCandidatesByPullKey(owner + "." + repo + "." + number), nil
}
@@ -9,9 +9,13 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/views"
@@ -26,13 +30,13 @@ func TestTheControlRecords(t *testing.T) {
// manifest declares now has no reader.
t.Run("indexer_config_readers", func(t *testing.T) {
d1, d2 := strings.Repeat("1", 64), strings.Repeat("2", 64)
manifests := map[string]delivery.Manifest{
d1: {Module: "web", JSON: `{"module":"web","config":{"old":{"kind":"value","why":"w"},"port":{"kind":"value","why":"w"}},"own-secrets":{"key":"x"}}`},
d2: {Module: "web", JSON: `{"module":"web","config":{"port":{"kind":"value","why":"w"}}}`},
strings.Repeat("3", 64): {Module: "db", JSON: `{"module":"db","config":{"old":{"kind":"value","why":"w"}}}`},
manifests := map[string]meshcatalog.ManifestsByDigestValue{
d1: {Module: "web", Manifest: `{"module":"web","config":{"old":{"kind":"value","why":"w"},"port":{"kind":"value","why":"w"}},"own-secrets":{"key":"x"}}`},
d2: {Module: "web", Manifest: `{"module":"web","config":{"port":{"kind":"value","why":"w"}}}`},
strings.Repeat("3", 64): {Module: "db", Manifest: `{"module":"db","config":{"old":{"kind":"value","why":"w"}}}`},
}
trunk := map[string]delivery.Module{"web": {Manifest: d2}}
pulls := []delivery.ConfigCandidate{{Pull: "novox/web#7", Declares: map[string]map[string]delivery.ConfigDeclaration{"web": {"new": {}}}}}
trunk := map[string]meshcatalog.ModulesByModuleValue{"web": {Manifest: d2}}
pulls := []meshcatalog.ConfigCandidatesByPullValue{{Pull: "novox/web#7", Declares: map[string]map[string]meshcatalog.ConfigCandidatesByPullValueDeclaresValueValue{"web": {"new": {}}}}}
got := ConfigReaders("web", trunk, manifests, pulls)
want := []ConfigReaderRow{{Module: "web", Key: "key", Kind: ConfigKindSecret, Readers: []string{}},
{Module: "web", Key: "new", Kind: ConfigKindValue, Readers: []string{"novox/web#7"}},
@@ -56,15 +60,15 @@ func TestTheControlRecords(t *testing.T) {
s[key] = &views.Entry{Value: raw, Revision: rev}
}
modules, verdicts, manifests, marks := memSource{}, memSource{}, memSource{}, src
putVerdict := func(digest string, rev uint64, v delivery.ManifestVerdict) {
raw, err := delivery.EncodeManifestVerdict(v)
putVerdict := func(digest string, rev uint64, v meshcatalog.VerdictsByDigestValue) {
raw, err := meshcatalogclient.EncodeVerdict(v)
if err != nil {
t.Fatal(err)
}
verdicts[digest] = &views.Entry{Value: raw, Revision: rev}
}
for name, digest := range map[string]string{"cache": dAsk, "plain": dNone, "bad": dRefused, "bare": ""} {
raw, err := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: digest,
raw, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("a", 64), Manifest: digest,
Repository: "novox/mesh-catalog", Commit: "c"})
if err != nil {
t.Fatal(err)
@@ -73,11 +77,11 @@ func TestTheControlRecords(t *testing.T) {
}
asks := `{"module":"cache","version":"1","requires":[{"name":"c","storage":"key-value","template":"state"}]}`
for digest, raw := range map[string]string{dAsk: asks, dNone: `{"module":"plain","version":"1"}`, dRefused: asks} {
put(manifests, digest, 1, delivery.Manifest{V: delivery.Version, Module: "m", JSON: raw})
put(manifests, digest, 1, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: "m", Manifest: raw})
}
putVerdict(dAsk, 7, delivery.ManifestVerdict{Module: "cache", State: delivery.VerdictAccepted, Judge: "j"})
putVerdict(dNone, 8, delivery.ManifestVerdict{Module: "plain", State: delivery.VerdictAccepted, Judge: "j"})
putVerdict(dRefused, 9, delivery.ManifestVerdict{Module: "bad", State: delivery.VerdictRefused, Judge: "j", Problems: []string{"p"}})
putVerdict(dAsk, 7, meshcatalog.VerdictsByDigestValue{Module: "cache", State: meshcatalogclient.VerdictAccepted, Judge: "j"})
putVerdict(dNone, 8, meshcatalog.VerdictsByDigestValue{Module: "plain", State: meshcatalogclient.VerdictAccepted, Judge: "j"})
putVerdict(dRefused, 9, meshcatalog.VerdictsByDigestValue{Module: "bad", State: meshcatalogclient.VerdictRefused, Judge: "j", Problems: []string{"p"}})
put(marks, VerdictsState, 1, views.Mark{V: views.Version, State: VerdictsState, Revision: 9})
got, err := BusStateOf(modules, verdicts, manifests, marks)
if err != nil || got.Revision != 9 || len(got.Modules) != 1 || got.Modules[0].Module != "cache" ||
@@ -157,7 +161,7 @@ func TestTheControlRecords(t *testing.T) {
merged := func(merge string) {
t.Helper()
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repo, Number: 1, Merge: merge,
Manifests: map[string]string{delivery.RootPath: manifest, "modules/other": `{"module":"other","version":"1"}`}}); err != nil {
Manifests: map[string]string{meshcatalogclient.RootPath: manifest, "modules/other": `{"module":"other","version":"1"}`}}); err != nil {
t.Fatal(err)
}
}
@@ -213,7 +217,7 @@ func TestTheControlRecords(t *testing.T) {
mergedOther := func(merge string) {
t.Helper()
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: other, Number: 2, Merge: merge,
Manifests: map[string]string{delivery.RootPath: otherManifest}}); err != nil {
Manifests: map[string]string{meshcatalogclient.RootPath: otherManifest}}); err != nil {
t.Fatal(err)
}
}
@@ -235,7 +239,7 @@ func TestTheControlRecords(t *testing.T) {
}
// A pull request's head: its own entry, the trunk's answered unless it is named.
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgePull, Action: forgemsg.PullUpdated, Repository: repo, Number: 9, Head: head,
Dirs: []string{delivery.RootPath}, Manifests: map[string]string{delivery.RootPath: manifest}}); err != nil {
Dirs: []string{meshcatalogclient.RootPath}, Manifests: map[string]string{meshcatalogclient.RootPath: manifest}}); err != nil {
t.Fatal(err)
}
pk := SeatDefinitionKey("git", SourcePull, repo, 9)
@@ -257,7 +261,7 @@ func TestTheControlRecords(t *testing.T) {
t.Fatal(err)
}
end := func(outcome string, files ...string) []byte {
raw, _ := json.Marshal(map[string]any{"v": 1, "event": delivery.BuildEndedEvent, "module": "mesh-contracts", "ref": "main",
raw, _ := json.Marshal(map[string]any{"v": 1, "event": builds.BuildEndedEvent, "module": "mesh-contracts", "ref": "main",
"commit": m3, "outcome": outcome, "out-of-step": files})
return raw
}
@@ -272,7 +276,7 @@ func TestTheControlRecords(t *testing.T) {
rows[1].Pull != repo+"#9" || !reflect.DeepEqual(rows[1].Files, []string{"go/a.go", "go/b.go"}) {
t.Errorf("out of step %+v, %v; want the trunk's module and the pull request's sorted files", rows, err)
}
ix.ApplyOutOfStepBuild(ctx, end(delivery.BuildDone))
ix.ApplyOutOfStepBuild(ctx, end(builds.BuildDone))
if len(findings.vals) != 0 {
t.Errorf("findings %v stand after a clean build end", findings.vals)
}
@@ -280,10 +284,10 @@ func TestTheControlRecords(t *testing.T) {
// generated-from: a trunk that names none, a head that names it, is read as the contracts module the head is.
ix.Modules, ix.Manifests = newMem(), newMem()
plain := `{"module":"mesh-contracts","version":"1"}`
kept, _ := delivery.EncodeManifest(delivery.Manifest{Module: "mesh-contracts", JSON: plain})
mkey, _ := delivery.ManifestsKey(delivery.ManifestDigest([]byte(plain)))
kept, _ := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mesh-contracts", Manifest: plain})
mkey, _ := meshcatalogclient.ManifestsKey(meshcatalogclient.ManifestDigest([]byte(plain)))
ix.Manifests.(*memStore).vals[mkey] = kept
mod, _ := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: delivery.ManifestDigest([]byte(plain)), Repository: repo, Commit: m3, Path: delivery.RootPath})
mod, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("a", 64), Manifest: meshcatalogclient.ManifestDigest([]byte(plain)), Repository: repo, Commit: m3, Path: meshcatalogclient.RootPath})
ix.Modules.(*memStore).vals["mesh-contracts"] = mod
files["h2"] = map[string]string{"module.json": manifest, "seats/git/seat.json": strings.Replace(git, "ADR 0111", "ADR 0400", 1)}
if r := ix.SeatDefinitionsAgain(ctx, "mesh-contracts", 9, "h2"); !r.Stored {
@@ -335,7 +339,7 @@ func TestTheControlRecords(t *testing.T) {
files["p1"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
files["i1"] = map[string]string{"seats/vault/seat.json": seat("vault", "Identity and access")}
domainManifest := func(module string) map[string]string {
return map[string]string{delivery.RootPath: strings.Replace(manifest, `"mesh-contracts"`, `"`+module+`"`, 1)}
return map[string]string{meshcatalogclient.RootPath: strings.Replace(manifest, `"mesh-contracts"`, `"`+module+`"`, 1)}
}
mergeAt := func(repository, merge string, manifests map[string]string) {
t.Helper()
@@ -348,7 +352,7 @@ func TestTheControlRecords(t *testing.T) {
before = len(said)
// mesh-contracts defines wants and vault, seats of other domains: refused with a finding naming both modules, the
// held entries stand, and lone (Core) is taken.
mergeAt(repo, "c1", map[string]string{delivery.RootPath: manifest})
mergeAt(repo, "c1", map[string]string{meshcatalogclient.RootPath: manifest})
if v, ok := findings.vals[outside]; !ok || !strings.Contains(string(v), "mesh-placement-contracts") || !strings.Contains(string(v), "mesh-identity-and-access-contracts") ||
!strings.Contains(string(v), "mesh-contracts defines it") {
t.Errorf("finding %s = %s, want it naming both modules", outside, v)
@@ -365,7 +369,7 @@ func TestTheControlRecords(t *testing.T) {
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" || w.Commit != "p1" {
t.Errorf("the domain's module did not take wants over: %+v", w)
}
mergeAt(repo, "c2", map[string]string{delivery.RootPath: manifest})
mergeAt(repo, "c2", map[string]string{meshcatalogclient.RootPath: manifest})
if w, _ := entry("wants.trunk"); w.Module != "mesh-placement-contracts" {
t.Errorf("mesh-contracts dropping wants removed or replaced the domain module's entry: %+v", w)
}
@@ -390,7 +394,7 @@ func TestTheControlRecords(t *testing.T) {
files["h2"] = map[string]string{"seats/wants/seat.json": seat("wants", "Placement")}
ix.ReadFile = func(_ context.Context, _, sha, p string) ([]byte, error) { return []byte(files[sha][p]), nil }
if err := ix.ApplySeatDefinitions(ctx, forgemsg.Forge{Kind: forgemsg.ForgePull, Action: forgemsg.PullUpdated, Repository: repo, Number: 11, Head: "h2",
Dirs: []string{delivery.RootPath}, Manifests: map[string]string{delivery.RootPath: manifest}}); err != nil {
Dirs: []string{meshcatalogclient.RootPath}, Manifests: map[string]string{meshcatalogclient.RootPath: manifest}}); err != nil {
t.Fatal(err)
}
if e, _ := entry(SeatDefinitionKey("wants", SourcePull, repo, 11)); !strings.Contains(e.Unparsed, "mesh-placement-contracts") || e.Definition != nil {
@@ -423,7 +427,7 @@ func TestTheControlRecords(t *testing.T) {
sdk := `{"module":"mesh-sdk","version":"1","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]},` +
`{"name":"ts","kind":"package","language":"typescript","paths":["src","package.json"],"series":"0.3"}]}}`
if err := ix.KeepPackageNames(ctx, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Merge: "m1",
Manifests: map[string]string{delivery.RootPath: sdk}}); err != nil {
Manifests: map[string]string{meshcatalogclient.RootPath: sdk}}); err != nil {
t.Fatal(err)
}
named := ix.SeriesAll(ctx)
@@ -487,13 +491,13 @@ func TestTheControlRecords(t *testing.T) {
"none": {"novox/mesh-catalog", `{"module":"none","version":"1"}`, nil, nil, ""},
} {
digest := fmt.Sprintf("%064x", len(name)*16+int(name[0]))
putJSON(manifests, digest, delivery.Manifest{V: delivery.Version, Module: name, JSON: u.manifest})
mod := delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: digest, Repository: u.repo, Commit: "c",
Inputs2: &delivery.KeptInputs2{Packages: u.pkgs, Imports: u.imports}}
putJSON(manifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: name, Manifest: u.manifest})
mod := meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("a", 64), Manifest: digest, Repository: u.repo, Commit: "c",
Inputs2: &meshcatalog.ModulesByModuleValueInputs2{Packages: u.pkgs, Imports: u.imports}}
if u.pinned != "" {
mod.Packages = delivery.PackageVersions{sdkGo: u.pinned}
mod.Packages = sdkstep.PackageVersions{sdkGo: u.pinned}
}
raw, err := delivery.EncodeModule(mod)
raw, err := meshcatalogclient.EncodeModule(mod)
if err != nil {
t.Fatal(err)
}
@@ -528,8 +532,8 @@ func TestTheControlRecords(t *testing.T) {
if err := ix.Apply(ctx, f); err != nil {
t.Fatal(err)
}
var kept delivery.PredefinedKept
if err := json.Unmarshal(pre.vals[delivery.PredefinedListKey], &kept); err != nil || kept.Commit != merge || string(kept.List) != string(f.Predefined) {
var kept meshcatalog.PredefinedByListValue
if err := json.Unmarshal(pre.vals[PredefinedListKey], &kept); err != nil || kept.Commit != merge || string(rawList(pre.vals[PredefinedListKey])) != string(f.Predefined) {
t.Fatalf("kept = %+v (%v)", kept, err)
}
writes := pre.writes
@@ -561,9 +565,9 @@ func TestTheControlRecords(t *testing.T) {
if err := ix.Apply(ctx, p); err != nil {
t.Fatal(err)
}
var c delivery.ConfigCandidate
var c meshcatalog.ConfigCandidatesByPullValue
if err := json.Unmarshal(cands.vals[key], &c); err != nil || c.Pull != "novox/mesh-catalog#7" || c.Head != h1 || len(c.Declares["avahi"]) != 1 ||
string(c.Declares["avahi"]["name"].Default) != `"x"` {
c.Declares["avahi"]["name"].Default != "x" {
t.Fatalf("candidate = %+v (%v)", c, err)
}
writes := cands.writes
@@ -574,7 +578,7 @@ func TestTheControlRecords(t *testing.T) {
if err := ix.Apply(ctx, same); err != nil {
t.Fatal(err)
}
c = delivery.ConfigCandidate{}
c = meshcatalog.ConfigCandidatesByPullValue{}
if err := json.Unmarshal(cands.vals[key], &c); err != nil || len(c.Declares) != 0 || c.Declares == nil {
t.Errorf("a head declaring nothing new = %+v, want an empty set", c)
}
@@ -0,0 +1,50 @@
package index
import (
"encoding/json"
"strconv"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The bodies of the seat's events manifest-accepted and manifest-refused are the contract's types; the indexer, who says
// them, makes them from a verdict (novox/hq ADR 0634 §1: the logic of one holder stays with it).
// refusedOf is the body of manifest-refused for the verdict v on the manifest of digest.
func refusedOf(digest string, v meshcatalog.VerdictsByDigestValue) meshcatalog.ManifestRefusedEvent {
return meshcatalog.ManifestRefusedEvent{V: keys.Version, Module: v.Module, Digest: digest, Judge: v.Judge, Problems: v.Problems}
}
// acceptedEvent is the body of manifest-accepted for the verdict v accepting the manifest m of digest, its bus-state
// entries whole (novox/hq ADR 0595 §4); an empty list when m asks for none.
func acceptedEvent(digest string, v meshcatalog.VerdictsByDigestValue, m catalogue.Manifest) meshcatalog.ManifestAcceptedEvent {
return meshcatalog.ManifestAcceptedEvent{V: keys.Version, Module: v.Module, Digest: digest, Judge: v.Judge, BusState: busStateOf(m.BusState)}
}
// busStateOf is the manifest's bus-state entries in the event's form (the same JSON).
func busStateOf(entries []catalogue.BusStateEntry) []meshcatalog.ManifestAcceptedEventBusStateItem {
out := []meshcatalog.ManifestAcceptedEventBusStateItem{}
if len(entries) == 0 {
return out
}
raw, err := json.Marshal(entries)
if err == nil {
err = json.Unmarshal(raw, &out)
}
if err != nil {
return []meshcatalog.ManifestAcceptedEventBusStateItem{}
}
return out
}
// acceptedEventID is the event's id, one per manifest digest and judge, so the bus takes it once:
// `manifest-accepted-<digest>-<judge>`. The empty one of a module leaving the trunk carries no digest, so it is named by
// its module and the indexer's revision: `manifest-accepted-left-<module>-<revision>`, so two leaves are both kept.
func acceptedEventID(a meshcatalog.ManifestAcceptedEvent) string {
if a.Digest == "" {
return meshcatalog.EventManifestAccepted + "-left-" + a.Module + "-" + strconv.FormatInt(a.Revision, 10)
}
return meshcatalog.EventManifestAccepted + "-" + a.Digest + "-" + a.Judge
}
@@ -4,7 +4,9 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"mesh-delivery-indexer/internal/fingerprint"
)
@@ -13,7 +15,7 @@ import (
// its build is pinned to.
type form struct {
fp, none string
versions delivery.PackageVersions
versions sdkstep.PackageVersions
}
// formOf is the module's fingerprint from its kept inputs (bases already its bases' fingerprints) and toolchains, in the
@@ -35,7 +37,7 @@ type form struct {
// digests of that version's commit.
//
// It sets kept.ImportForm when it writes the import form.
func formOf(module, repository string, kept *delivery.KeptInputs2, toolchains map[string]delivery.Toolchain2, prev delivery.Module, held map[string]delivery.Module, published map[string]delivery.PublishedPackages, pins delivery.PackageVersions, logf func(string, ...any)) form {
func formOf(module, repository string, kept *meshcatalog.ModulesByModuleValueInputs2, toolchains map[string]meshcatalogclient.Toolchain2, prev meshcatalog.ModulesByModuleValue, held map[string]meshcatalog.ModulesByModuleValue, published map[string]meshcatalog.PackagesByModuleValue, pins sdkstep.PackageVersions, logf func(string, ...any)) form {
in := inputsFrom(module, *kept, toolchains)
if len(kept.Imports) > 0 {
r := fingerprint.Resolve(repository, kept.Imports, published)
@@ -45,8 +47,8 @@ func formOf(module, repository string, kept *delivery.KeptInputs2, toolchains ma
switch {
case fp == "":
return form{none: none}
case kept.ImportForm == fp && prev.FP2 != "":
return form{fp: prev.FP2, versions: prev.Packages} // the build that reported the imports stands
case kept.ImportForm == fp && prev.Fp2 != "":
return form{fp: prev.Fp2, versions: prev.Packages} // the build that reported the imports stands
}
kept.ImportForm = fp
return form{fp: fp, versions: r.Versions}
@@ -60,7 +62,7 @@ func formOf(module, repository string, kept *delivery.KeptInputs2, toolchains ma
for p, v := range pins { // an accepted series' newest published version (acceptedSeries, DLV-I6)
if slices.Contains(others, p) {
if versions == nil {
versions = delivery.PackageVersions{}
versions = sdkstep.PackageVersions{}
}
versions[p] = v
unpublished = slices.DeleteFunc(unpublished, func(u string) bool { return u == p })
@@ -79,7 +81,7 @@ func formOf(module, repository string, kept *delivery.KeptInputs2, toolchains ma
// inputsFrom is the fingerprint's inputs of module m from its kept inputs and the given toolchains, no package of
// either form yet.
func inputsFrom(module string, k delivery.KeptInputs2, toolchains map[string]delivery.Toolchain2) fingerprint.Inputs {
func inputsFrom(module string, k meshcatalog.ModulesByModuleValueInputs2, toolchains map[string]meshcatalogclient.Toolchain2) fingerprint.Inputs {
return fingerprint.Inputs{Module: module, Tree: k.Tree, Bases: k.Bases, Builder: k.Builder, Contexts: k.Contexts,
Locks: k.Locks, Toolchains: toolchains}
}
@@ -7,8 +7,9 @@ import (
"maps"
"slices"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -28,11 +29,11 @@ const GraphMost = 2000
// graphEdges is what a recompute knows of one module for its graph node.
type graphEdges struct {
module string
cur delivery.Module
cur meshcatalog.ModulesByModuleValue
manifest catalogue.Manifest
fp, none string
tcs map[string]delivery.ToolchainUsed
pkgs delivery.PackageVersions
tcs map[string]meshcatalog.ToolchainUsed
pkgs sdkstep.PackageVersions
}
// graphNode is the graph node of e: its edges, the awaited provisions judged against providers (catalogue.Awaited, as
@@ -4,8 +4,6 @@ import (
"context"
"encoding/json"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// What the indexer heard of the forge bridge's stream FORGE (issue 1126 note 76): no tool read FORGE's messages, so a merge
@@ -15,7 +13,7 @@ import (
// the last 64 values (its history); mesh-delivery-view's indexer_heard reads them, newest first, with their times.
const (
HeardState = "heard"
HeardBucket = delivery.IndexerModule + "_" + HeardState
HeardBucket = Module + "_" + HeardState
HeardKey = "forge"
// What the indexer did with a message.
HeardApplied = "applied"
@@ -0,0 +1,12 @@
package index
import "encoding/json"
// rawList is the list a kept predefined value holds, as it was written.
func rawList(value []byte) json.RawMessage {
var kept struct {
List json.RawMessage `json:"list"`
}
_ = json.Unmarshal(value, &kept)
return kept.List
}
@@ -12,8 +12,10 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -33,12 +35,12 @@ import (
const (
// HoldsState is the indexer's state of held entries: the bucket mesh-delivery-indexer_holds, keyed as the modules state.
HoldsState = "holds"
HoldsBucket = delivery.IndexerModule + "_" + HoldsState
HoldsBucket = Module + "_" + HoldsState
// HoldsTool reads the holds, asks one again now, or moves one by a person's word.
HoldsTool = "indexer_holds"
// EngineReadsState is the indexer's state of what each counted node's engine states it reads (sdk/enginereads).
EngineReadsState = "engine-reads"
EngineReadsBucket = delivery.IndexerModule + "_" + EngineReadsState
EngineReadsBucket = Module + "_" + EngineReadsState
// EngineReadsTool lists, asks again and drops those statements.
EngineReadsTool = "indexer_engine_reads"
)
@@ -58,7 +60,7 @@ const (
HoldStandingKind = "manifest-held"
HoldBound = 30 * time.Minute
HoldBoundsState = "hold-bounds"
HoldBoundsBucket = delivery.IndexerModule + "_" + HoldBoundsState
HoldBoundsBucket = Module + "_" + HoldBoundsState
)
// Bounds is the state hold-bounds: Arm writes key to expire at ends, a standing key keeping its end; Disarm deletes it.
@@ -161,7 +163,7 @@ func lackingWords(lacking map[string][]string, unknown string) string {
// a changed manifest of a module the modules state holds an entry of: a new module has no previous entry to keep, and is
// held on each node by the assigner (ADR 0614 §4). An error is a hold that could not be kept: the merge is left, and
// delivered again, rather than writing what a counted engine cannot read.
func (ix *Indexer) holdBack(ctx context.Context, key string, next, cur delivery.Module, has bool) (bool, error) {
func (ix *Indexer) holdBack(ctx context.Context, key string, next, cur meshcatalog.ModulesByModuleValue, has bool) (bool, error) {
if ix.Holds == nil || ix.Counted == nil {
return false, nil
}
@@ -176,7 +178,7 @@ func (ix *Indexer) holdBack(ctx context.Context, key string, next, cur delivery.
if len(lacking) == 0 && unknown == "" {
return false, ix.dropHold(ctx, key, "its merge "+next.Commit+" brought a manifest every counted node-engine reads")
}
raw, err := delivery.EncodeModule(next)
raw, err := meshcatalogclient.EncodeModule(next)
if err != nil {
return false, err
}
@@ -297,7 +299,7 @@ func (ix *Indexer) moveHold(ctx context.Context, key, by string) (bool, string,
ix.logf("holds.%s holds what is not a hold, left: %v", key, err)
return moved, "", fmt.Errorf("holds.%s: %w", key, err)
}
next, err := delivery.DecodeModule(h.Next)
next, err := meshcatalogclient.DecodeModule(h.Next)
if err != nil {
return moved, "", fmt.Errorf("holds.%s: its entry: %w", key, err)
}
@@ -334,8 +336,8 @@ func (ix *Indexer) moveHold(ctx context.Context, key, by string) (bool, string,
ix.recompute.Unlock()
return moved, "", fmt.Errorf("modules.%s: %w", key, err)
}
cur, derr := delivery.DecodeModule(curRaw)
if !has || derr != nil || !delivery.SameRepository(cur.Repository, next.Repository) {
cur, derr := meshcatalogclient.DecodeModule(curRaw)
if !has || derr != nil || !trunk.SameRepository(cur.Repository, next.Repository) {
err := ix.Holds.Delete(ctx, key, rev)
ix.recompute.Unlock()
if errors.Is(err, ErrConflict) {
@@ -347,9 +349,9 @@ func (ix *Indexer) moveHold(ctx context.Context, key, by string) (bool, string,
ix.logf("holds.%s dropped: the modules state no longer holds the module of %s", key, next.Repository)
return moved, "dropped: the modules state no longer holds the module of " + next.Repository, err
}
next.Src = ix.srcOf(next.Repository, next.Commit)
next.Src = int64(ix.srcOf(next.Repository, next.Commit))
w := &mergeWrite{repository: next.Repository, merge: next.Commit}
err = ix.recomputeLocked(ctx, map[string]delivery.Module{key: next}, w)
err = ix.recomputeLocked(ctx, map[string]meshcatalog.ModulesByModuleValue{key: next}, w)
if err == nil {
err = ix.Holds.Delete(ctx, key, rev)
}
@@ -382,8 +384,8 @@ func (ix *Indexer) moveHold(ctx context.Context, key, by string) (bool, string,
}
// sameHeld says a held entry is next but for where it came from: its first merge stands.
func sameHeld(held json.RawMessage, next delivery.Module) bool {
prev, err := delivery.DecodeModule(held)
func sameHeld(held json.RawMessage, next meshcatalog.ModulesByModuleValue) bool {
prev, err := meshcatalogclient.DecodeModule(held)
return err == nil && sameButWhere(prev, next)
}
@@ -453,7 +455,7 @@ func cmpOr(a, b string) string {
// lack what it needs once more and weighs it now; by moves it by that person's word, whatever the engines state.
func (ix *Indexer) HoldAgain(ctx context.Context, module string, release bool, by string) TakenIn {
r := TakenIn{Module: module}
key, err := delivery.ModulesKey(module)
key, err := meshcatalogclient.ModulesKey(module)
if err != nil {
r.Answer = err.Error()
return r
@@ -504,7 +506,7 @@ func (ix *Indexer) HoldAgain(ctx context.Context, module string, release bool, b
// ListHolds is every hold the state holds, by module, or one module's; a value that does not read is named with why.
func ListHolds(keys []string, get func(key string) ([]byte, error), module string) (map[string]any, error) {
if module != "" {
key, err := delivery.ModulesKey(module)
key, err := meshcatalogclient.ModulesKey(module)
if err != nil {
return nil, err
}
@@ -7,8 +7,8 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
)
@@ -58,7 +58,7 @@ func storeHoldersNewManifestWaitsForEveryCountedEngine(t *testing.T) {
}
kept, _ := moduleOf(t, mods, "distribution")
keptDocker, _ := moduleOf(t, mods, "docker")
if kept.Manifest != delivery.ManifestDigest([]byte(old)) || len(holds.vals) != 0 {
if kept.Manifest != meshcatalogclient.ManifestDigest([]byte(old)) || len(holds.vals) != 0 {
t.Fatalf("after the first merge modules.distribution names %s with %d holds, want the old manifest and none", kept.Manifest, len(holds.vals))
}
second := withManifests(mergedMsg(catalogueRepo, map[string]string{dir: "tree2", dockerDir: "d2"}), map[string]string{dir: next, dockerDir: docker}, nil)
@@ -70,12 +70,12 @@ func storeHoldersNewManifestWaitsForEveryCountedEngine(t *testing.T) {
stays := func(when string) {
t.Helper()
m, _ := moduleOf(t, mods, "distribution")
if m.Manifest != kept.Manifest || m.FP2 != kept.FP2 || m.Commit != kept.Commit || m.Inputs2 == nil || m.Inputs2.Tree != "tree1" {
if m.Manifest != kept.Manifest || m.Fp2 != kept.Fp2 || m.Commit != kept.Commit || m.Inputs2 == nil || m.Inputs2.Tree != "tree1" {
t.Fatalf("%s modules.distribution = manifest %s fp2 %q commit %s, want the old entry kept whole (%s, %q, %s)",
when, m.Manifest, m.FP2, m.Commit, kept.Manifest, kept.FP2, kept.Commit)
when, m.Manifest, m.Fp2, m.Commit, kept.Manifest, kept.Fp2, kept.Commit)
}
h, err := DecodeHold(holds.vals["distribution"])
if err != nil || h.Manifest != delivery.ManifestDigest([]byte(next)) || h.Kept != kept.Manifest || !slices.Equal(h.Lacking["n2"], []string{"form:seat-reach"}) || len(h.Lacking) != 1 {
if err != nil || h.Manifest != meshcatalogclient.ManifestDigest([]byte(next)) || h.Kept != kept.Manifest || !slices.Equal(h.Lacking["n2"], []string{"form:seat-reach"}) || len(h.Lacking) != 1 {
t.Fatalf("%s holds.distribution = %+v (%v), want the new manifest held, n2 alone lacking form:seat-reach", when, h, err)
}
}
@@ -108,7 +108,7 @@ func storeHoldersNewManifestWaitsForEveryCountedEngine(t *testing.T) {
t.Fatal(err)
}
m, _ := moduleOf(t, mods, "distribution")
if m.Manifest != delivery.ManifestDigest([]byte(next)) || m.Commit != second.Merge || m.Inputs2 == nil || m.Inputs2.Tree != "tree2" || len(holds.vals) != 0 || findings.vals[heldKey] != nil || len(bounds) != 0 {
if m.Manifest != meshcatalogclient.ManifestDigest([]byte(next)) || m.Commit != second.Merge || m.Inputs2 == nil || m.Inputs2.Tree != "tree2" || len(holds.vals) != 0 || findings.vals[heldKey] != nil || len(bounds) != 0 {
t.Fatalf("once every counted engine states form:seat-reach modules.distribution = manifest %s commit %s, %d holds, finding %s, bounds %v; want the new entry and none",
m.Manifest, m.Commit, len(holds.vals), findings.vals[heldKey], bounds)
}
@@ -7,13 +7,15 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"mesh-delivery-indexer/internal/fingerprint"
)
// ApplyBuildEnd keeps the imports a done build reports at its end (delivery.BuildLogEntry.Imports; novox/hq ADR 0593
// ApplyBuildEnd keeps the imports a done build reports at its end (builds.BuildLogEntry.Imports; novox/hq ADR 0593
// §2) on the module it built, when the build's fingerprint is the one the module holds: the import set of the module's
// last trunk build. With them it keeps ImportForm, the import-form fingerprint of exactly what the build was built from:
// the module's kept inputs and the toolchains its fingerprint was computed with, and each imported package's digest at
@@ -28,14 +30,14 @@ func (ix *Indexer) ApplyBuildEnd(ctx context.Context, value []byte) error {
if !builds.IsBuildEntry(value) {
return nil
}
e, err := delivery.DecodeBuildLogEntry(value)
e, err := builds.DecodeBuildLogEntry(value)
if err != nil {
return err
}
if e.Event != delivery.BuildEndedEvent || e.Outcome != delivery.BuildDone || len(e.Imports) == 0 || ix.Modules == nil || e.FP == "" {
if e.Event != builds.BuildEndedEvent || e.Outcome != builds.BuildDone || len(e.Imports) == 0 || ix.Modules == nil || e.FP == "" {
return nil
}
key, err := delivery.ModulesKey(e.Module)
key, err := meshcatalogclient.ModulesKey(e.Module)
if err != nil {
return nil
}
@@ -45,8 +47,8 @@ func (ix *Indexer) ApplyBuildEnd(ctx context.Context, value []byte) error {
if err != nil || !found {
return err
}
cur, err := delivery.DecodeModule(raw)
if err != nil || cur.FP2 != e.FP || cur.Inputs2 == nil || cur.Manifest == "" {
cur, err := meshcatalogclient.DecodeModule(raw)
if err != nil || cur.Fp2 != e.FP || cur.Inputs2 == nil || cur.Manifest == "" {
return nil // a build of a fingerprint the module no longer holds says nothing of its inputs now
}
if slices.Equal(cur.Inputs2.Imports, e.Imports) && cur.Inputs2.ImportForm != "" {
@@ -56,14 +58,14 @@ func (ix *Indexer) ApplyBuildEnd(ctx context.Context, value []byte) error {
if err != nil {
return fmt.Errorf("modules.%s: its manifest: %w", key, err)
}
toolchains := map[string]delivery.Toolchain2{}
toolchains := map[string]meshcatalogclient.Toolchain2{}
for artifact, language := range languagesOf(m) {
used, ok := cur.Toolchains[language]
if !ok {
ix.logf("%s: its fingerprint names no %s toolchain, so the imports of its build are not kept", e.Module, language)
return nil
}
toolchains[artifact] = delivery.Toolchain2{Language: language, Version: used.Version, Recipe: used.Recipe}
toolchains[artifact] = meshcatalogclient.Toolchain2{Language: language, Version: used.Version, Recipe: used.Recipe}
}
kept := *cur.Inputs2
kept.Imports, kept.ImportForm = slices.Clone(e.Imports), ""
@@ -82,14 +84,14 @@ func (ix *Indexer) ApplyBuildEnd(ctx context.Context, value []byte) error {
if !found {
return nil, false, nil
}
now, err := delivery.DecodeModule(value)
if err != nil || now.FP2 != e.FP || now.Manifest != cur.Manifest || now.Inputs2 == nil {
now, err := meshcatalogclient.DecodeModule(value)
if err != nil || now.Fp2 != e.FP || now.Manifest != cur.Manifest || now.Inputs2 == nil {
return nil, false, nil
}
in := *now.Inputs2
in.Imports, in.ImportForm = kept.Imports, kept.ImportForm
now.Inputs2 = &in
data, err := delivery.EncodeModule(now)
data, err := meshcatalogclient.EncodeModule(now)
return data, false, err
})
}
@@ -98,7 +100,7 @@ func (ix *Indexer) ApplyBuildEnd(ctx context.Context, value []byte) error {
// it: a module of the built module's own repository at the commit built, one of another repository at the version the
// module's fingerprint pins (cur.Packages), else its latest published version. A module whose published commit is the
// one built is taken from the packages state; any other is read from the forge's listing at that commit.
func (ix *Indexer) publishedAsBuilt(ctx context.Context, cur delivery.Module, e delivery.BuildLogEntry) (map[string]delivery.PublishedPackages, error) {
func (ix *Indexer) publishedAsBuilt(ctx context.Context, cur meshcatalog.ModulesByModuleValue, e builds.BuildLogEntry) (map[string]meshcatalog.PackagesByModuleValue, error) {
published := ix.publishedAll(ctx)
if r := fingerprint.Resolve(cur.Repository, e.Imports, published); len(r.Missing) > 0 {
if err := ix.FillPackages(ctx); err != nil {
@@ -107,27 +109,27 @@ func (ix *Indexer) publishedAsBuilt(ctx context.Context, cur delivery.Module, e
published = ix.publishedAll(ctx)
}
// A pin that is an accepted series' version (DLV-I6) is taken by the contents its series keeps for it.
byVersion := map[string]delivery.PublishedPackages{}
byVersion := map[string]meshcatalog.PackagesByModuleValue{}
for _, s := range ix.SeriesAll(ctx) {
if s.GoModule != "" && s.Newest != nil && s.Newest.Contents != nil {
byVersion[s.GoModule+"@v"+s.Newest.Version] = *s.Newest.Contents
byVersion[s.GoModule+"@v"+s.Newest.Version] = publishedOfContents(*s.Newest.Contents)
}
}
type at struct{ repository, commit string }
reread := map[at][]string{}
var asVersion map[string]delivery.PublishedPackages
var asVersion map[string]meshcatalog.PackagesByModuleValue
for _, imp := range e.Imports {
r := fingerprint.Resolve(cur.Repository, []string{imp}, published)
for mod := range r.Requires {
p := published[mod]
commit := p.Commit
switch pin := cur.Packages[mod]; {
case delivery.SameRepository(p.Repository, cur.Repository) && e.Commit != "":
case trunk.SameRepository(p.Repository, cur.Repository) && e.Commit != "":
commit = e.Commit
case strings.HasPrefix(pin, "v"):
if c, ok := byVersion[mod+"@"+pin]; ok {
if asVersion == nil {
asVersion = map[string]delivery.PublishedPackages{}
asVersion = map[string]meshcatalog.PackagesByModuleValue{}
}
c.Commit, asVersion[mod] = pin, c
} else {
@@ -6,7 +6,9 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
const meshSDK = "git.novox.be/novox/mesh-sdk/go"
@@ -57,7 +59,7 @@ func TestImportsReportedMoveTheFingerprintOnlyWithAnImportedPackage(t *testing.T
ix.Packages, ix.Files = newMem(), listings.files
// avahi's go.sum names mesh-sdk (the version line), as every Go module's does; mesh-sdk's module is held at c1.
setPackages(t, mods, "avahi", []string{meshSDK})
sdkModule, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-sdk", Path: ".", Commit: c1, None2: "a TypeScript package"})
sdkModule, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-sdk", Path: ".", Commit: c1, None2: "a TypeScript package"})
if err != nil {
t.Fatal(err)
}
@@ -74,12 +76,12 @@ func TestImportsReportedMoveTheFingerprintOnlyWithAnImportedPackage(t *testing.T
t.Fatal(err)
}
before, _ := moduleOf(t, mods, "avahi")
if before.FP2 == "" {
if before.Fp2 == "" {
t.Fatalf("no fingerprint to start from: %q", before.None2)
}
// The build of that fingerprint ends, reporting its imports: kept, and the fingerprint does not move.
end := buildEnd(t, "avahi", before.FP2, meshSDK, meshSDK+"/announce")
end := buildEnd(t, "avahi", before.Fp2, meshSDK, meshSDK+"/announce")
if err := ix.ApplyBuildEnd(ctx, end); err != nil {
t.Fatal(err)
}
@@ -87,9 +89,9 @@ func TestImportsReportedMoveTheFingerprintOnlyWithAnImportedPackage(t *testing.T
t.Fatal(err)
}
after, _ := moduleOf(t, mods, "avahi")
if after.FP2 != before.FP2 || strings.Join(after.Inputs2.Imports, ",") != meshSDK+","+meshSDK+"/announce" || after.Inputs2.ImportForm == "" {
if after.Fp2 != before.Fp2 || strings.Join(after.Inputs2.Imports, ",") != meshSDK+","+meshSDK+"/announce" || after.Inputs2.ImportForm == "" {
t.Fatalf("after the build's imports: fp2 %q (was %q), imports %v, import form %q; want the same fingerprint and the imports kept",
after.FP2, before.FP2, after.Inputs2.Imports, after.Inputs2.ImportForm)
after.Fp2, before.Fp2, after.Inputs2.Imports, after.Inputs2.ImportForm)
}
// mesh-sdk merges a change of buildstep only: avahi does not import it.
@@ -100,8 +102,8 @@ func TestImportsReportedMoveTheFingerprintOnlyWithAnImportedPackage(t *testing.T
if err := ix.Recompute(ctx); err != nil {
t.Fatal(err)
}
if m, _ := moduleOf(t, mods, "avahi"); m.FP2 != before.FP2 {
t.Fatalf("a mesh-sdk merge changing only buildstep moved avahi's fingerprint to %q", m.FP2)
if m, _ := moduleOf(t, mods, "avahi"); m.Fp2 != before.Fp2 {
t.Fatalf("a mesh-sdk merge changing only buildstep moved avahi's fingerprint to %q", m.Fp2)
}
// mesh-sdk merges a change of announce: avahi moves, to the import form, pinned to the new version.
@@ -113,9 +115,9 @@ func TestImportsReportedMoveTheFingerprintOnlyWithAnImportedPackage(t *testing.T
t.Fatal(err)
}
moved, _ := moduleOf(t, mods, "avahi")
if moved.FP2 == before.FP2 || moved.FP2 != moved.Inputs2.ImportForm || moved.Packages[meshSDK] != c3 {
if moved.Fp2 == before.Fp2 || moved.Fp2 != moved.Inputs2.ImportForm || moved.Packages[meshSDK] != c3 {
t.Fatalf("a change of an imported package: fp2 %q (was %q), import form %q, versions %v; want the import form at c3",
moved.FP2, before.FP2, moved.Inputs2.ImportForm, moved.Packages)
moved.Fp2, before.Fp2, moved.Inputs2.ImportForm, moved.Packages)
}
}
@@ -158,7 +160,7 @@ func aSiblingDirectoryAloneMovesItsImporter(t *testing.T) {
return out, nil
}
setPackages(t, mods, "avahi", []string{sibling})
fpAt := func(commit string) delivery.Module {
fpAt := func(commit string) meshcatalog.ModulesByModuleValue {
t.Helper()
held, _ := ix.heldModules(ctx)
if err := ix.publish(ctx, catalogueRepo, commit, held, false); err != nil {
@@ -168,18 +170,18 @@ func aSiblingDirectoryAloneMovesItsImporter(t *testing.T) {
t.Fatal(err)
}
m, _ := moduleOf(t, mods, "avahi")
if m.FP2 == "" {
if m.Fp2 == "" {
t.Fatalf("no fingerprint at %s: %q", commit, m.None2)
}
return m
}
first := fpAt(cat1)
moved := fpAt(strings.Repeat("b", 40))
if moved.FP2 == first.FP2 {
t.Fatalf("a change of the sibling directory alone left avahi at %q", first.FP2)
if moved.Fp2 == first.Fp2 {
t.Fatalf("a change of the sibling directory alone left avahi at %q", first.Fp2)
}
if again := fpAt(strings.Repeat("c", 40)); again.FP2 != moved.FP2 {
t.Errorf("a merge changing nothing avahi reaches moved it from %q to %q", moved.FP2, again.FP2)
if again := fpAt(strings.Repeat("c", 40)); again.Fp2 != moved.Fp2 {
t.Errorf("a merge changing nothing avahi reaches moved it from %q to %q", moved.Fp2, again.Fp2)
}
if _, pinned := moved.Packages[sibling]; pinned {
t.Errorf("the build is pinned to a version of its sibling directory: %v", moved.Packages)
@@ -188,9 +190,9 @@ func aSiblingDirectoryAloneMovesItsImporter(t *testing.T) {
func buildEnd(t *testing.T, module, fp string, imports ...string) []byte {
t.Helper()
raw, err := delivery.EncodeBuildLogEntry(delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: module, FP: fp,
raw, err := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: module, FP: fp,
Repository: catalogueRepo, Directory: "modules/" + module, Commit: cat1, Ref: "main", Node: "n1", At: time.Now(),
Outcome: delivery.BuildDone, Imports: imports})
Outcome: builds.BuildDone, Imports: imports})
if err != nil {
t.Fatal(err)
}
@@ -204,7 +206,7 @@ func setPackages(t *testing.T, mods *memStore, module string, packages []string)
in := *m.Inputs2
in.Packages = packages
m.Inputs2 = &in
raw, err := delivery.EncodeModule(m)
raw, err := meshcatalogclient.EncodeModule(m)
if err != nil {
t.Fatal(err)
}
@@ -221,12 +223,12 @@ func anAcceptedSeriesPinsItsNewest(t *testing.T) {
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
ix.Packages, ix.Files = newMem(), forge{c1: sdkFiles("a1", "b1"), c2: sdkFiles("a1", "b2"), c3: sdkFiles("a2", "b2")}.files
setPackages(t, mods, "avahi", []string{meshSDK})
sdkModule, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-sdk", Path: ".", Commit: c1, None2: "a TypeScript package"})
sdkModule, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-sdk", Path: ".", Commit: c1, None2: "a TypeScript package"})
if err != nil {
t.Fatal(err)
}
mods.put("mesh-sdk", sdkModule)
step := func(what string, act func() error) delivery.Module {
step := func(what string, act func() error) meshcatalog.ModulesByModuleValue {
t.Helper()
if err := act(); err != nil {
t.Fatalf("%s: %v", what, err)
@@ -255,16 +257,16 @@ func anAcceptedSeriesPinsItsNewest(t *testing.T) {
t.Fatalf("with no series published, versions %v, want the commit form c1", before.Packages)
}
step("the build reports its imports", func() error {
return ix.ApplyBuildEnd(ctx, buildEnd(t, "avahi", before.FP2, meshSDK, meshSDK+"/announce"))
return ix.ApplyBuildEnd(ctx, buildEnd(t, "avahi", before.Fp2, meshSDK, meshSDK+"/announce"))
})
pinned := step("0.1.15 published from c3", hear("0.1.15", c3))
if pinned.FP2 == before.FP2 || pinned.Packages[meshSDK] != "v0.1.15" {
t.Fatalf("after 0.1.15: fp2 %q (was %q), versions %v; want moved by c3's announce, pinned to v0.1.15", pinned.FP2, before.FP2, pinned.Packages)
if pinned.Fp2 == before.Fp2 || pinned.Packages[meshSDK] != "v0.1.15" {
t.Fatalf("after 0.1.15: fp2 %q (was %q), versions %v; want moved by c3's announce, pinned to v0.1.15", pinned.Fp2, before.Fp2, pinned.Packages)
}
if m := step("0.2.0 published from c1", hear("0.2.0", c1)); m.FP2 != pinned.FP2 || m.Packages[meshSDK] != "v0.1.15" {
t.Errorf("a publish of a series avahi does not accept moved it: %q %v", m.FP2, m.Packages)
if m := step("0.2.0 published from c1", hear("0.2.0", c1)); m.Fp2 != pinned.Fp2 || m.Packages[meshSDK] != "v0.1.15" {
t.Errorf("a publish of a series avahi does not accept moved it: %q %v", m.Fp2, m.Packages)
}
if m := step("trunk at c2", trunk(c2)); m.FP2 != pinned.FP2 || m.Packages[meshSDK] != "v0.1.15" {
t.Errorf("a trunk merge moved avahi off its accepted series' newest: %q %v", m.FP2, m.Packages)
if m := step("trunk at c2", trunk(c2)); m.Fp2 != pinned.Fp2 || m.Packages[meshSDK] != "v0.1.15" {
t.Errorf("a trunk merge moved avahi off its accepted series' newest: %q %v", m.Fp2, m.Packages)
}
}
@@ -6,8 +6,10 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
const (
@@ -120,7 +122,7 @@ func buildRecord(t *testing.T, repo, module, path, at string) []byte {
}
func pull(repo string, number int, branch, head string, dirs ...string) forgemsg.Forge {
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: repo, Branch: branch, Number: number, Head: head,
return forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: repo, Branch: branch, Number: number, Head: head,
Action: forgemsg.PullUpdated, Dirs: dirs, DirsKnown: dirs != nil}
}
@@ -137,13 +139,13 @@ func newIndexer(t *testing.T) (*Indexer, *memStore, *strings.Builder) {
}}, st, logs
}
func read(t *testing.T, st *memStore, key string) (delivery.Candidate, bool) {
func read(t *testing.T, st *memStore, key string) (meshcatalog.CandidatesByGroupValue, bool) {
t.Helper()
v, ok := st.vals[key]
if !ok {
return delivery.Candidate{}, false
return meshcatalog.CandidatesByGroupValue{}, false
}
c, err := delivery.DecodeCandidate(v)
c, err := DecodeCandidate(v)
if err != nil {
t.Fatal(err)
}
@@ -163,8 +165,8 @@ func TestTwoRepositoriesFormOneGroupByBranch(t *testing.T) { // 1238/AC2 (the gr
if !ok {
t.Fatal("no candidate under feat/x")
}
hash, _ := delivery.HeadsHash([]delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}, {Repository: "novox/mesh-catalog", Number: 300, Head: h2}})
if c.Group != "feat/x" || c.Base != 0 || c.HeadsHash != hash || len(c.Pulls) != 2 {
hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}, {Repository: "novox/mesh-catalog", Number: 300, Head: h2}})
if c.Group != "feat/x" || c.Base != 0 || c.Heads != hash || len(c.Pulls) != 2 {
t.Fatalf("candidate = %+v", c)
}
got := map[string][]string{}
@@ -194,7 +196,7 @@ func TestModuleOfADirectory(t *testing.T) { // 1238/AC3, as R4e has it: the modu
if strings.Join(c.Pulls[0].Modules, ",") != "odd-name,plain" {
t.Errorf("modules = %v, want the held module for modules/odd, the last element for modules/plain, no repeats", c.Pulls[0].Modules)
}
held := map[string]delivery.Module{"x": {Repository: "novox/other", Path: "modules/a"}}
held := map[string]meshcatalog.ModulesByModuleValue{"x": {Repository: "novox/other", Path: "modules/a"}}
if name, ok := moduleAt(held, "novox/mesh-delivery", "modules/a"); ok {
t.Errorf("modules/a of novox/mesh-delivery is %q, held only of novox/other", name)
}
@@ -223,7 +225,7 @@ func TestDirsNotKnownKeepTheModulesOfTheSameHead(t *testing.T) { // 1238/AC4
t.Fatal(err)
}
c, _ = read(t, st, "feat/x")
if c.Pulls[0].Head.Head != h3 || len(c.Pulls[0].Modules) != 0 {
if c.Pulls[0].Head != h3 || len(c.Pulls[0].Modules) != 0 {
t.Errorf("pull = %+v, want the new head with no modules", c.Pulls[0])
}
// Known and empty is no modules, not unknown.
@@ -246,7 +248,7 @@ func TestClosedAndMergedTakeAPullOutAndTheLastDeletesTheKey(t *testing.T) { // 1
f.Action = forgemsg.PullClosed
return f
}
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: n, Head: head, Merge: h3, Root: "r", Trees: map[string]string{"a": "t"}}
return forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: n, Head: head, Merge: h3, Root: "r", Trees: map[string]string{"a": "t"}}
}
if err := ix.Apply(ctx, gone("novox/mesh-delivery", 12, h1)); err != nil {
t.Fatal(err)
@@ -255,9 +257,9 @@ func TestClosedAndMergedTakeAPullOutAndTheLastDeletesTheKey(t *testing.T) { // 1
if !ok || len(c.Pulls) != 1 || c.Pulls[0].Repository != "novox/mesh-catalog" {
t.Fatalf("%s: candidate = %+v (kept %v)", how, c, ok)
}
hash, _ := delivery.HeadsHash([]delivery.Head{c.Pulls[0].Head})
if c.HeadsHash != hash {
t.Errorf("%s: heads hash %q is not the remaining pull request's %q", how, c.HeadsHash, hash)
hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{headOf(c.Pulls[0])})
if c.Heads != hash {
t.Errorf("%s: heads hash %q is not the remaining pull request's %q", how, c.Heads, hash)
}
if err := ix.Apply(ctx, gone("novox/mesh-catalog", 300, h2)); err != nil {
t.Fatal(err)
@@ -321,11 +323,11 @@ func TestAConflictReadsAgain(t *testing.T) { // 1238 (compare-and-set: Update wi
_ = ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"))
// Between the indexer's read and its write another writer adds #300 to the group.
st.beforeWrite = func() {
other := delivery.Candidate{V: delivery.Version, Group: "feat/x", Pulls: []delivery.CandidatePull{
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: h1}, Modules: []string{"a"}},
{Head: delivery.Head{Repository: "novox/mesh-catalog", Number: 300, Head: h2}, Modules: []string{"c"}}}}
other.HeadsHash, _ = delivery.HeadsHash([]delivery.Head{other.Pulls[0].Head, other.Pulls[1].Head})
data, _ := delivery.EncodeCandidate(other)
other := meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Pulls: []meshcatalog.CandidatesByGroupValuePullsItem{
{Repository: "novox/mesh-delivery", Number: 12, Head: h1, Modules: []string{"a"}},
{Repository: "novox/mesh-catalog", Number: 300, Head: h2, Modules: []string{"c"}}}}
other.Heads, _ = meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{headOf(other.Pulls[0]), headOf(other.Pulls[1])})
data, _ := EncodeCandidate(other)
st.put("feat/x", data)
}
if err := ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h3, "modules/a")); err != nil {
@@ -336,8 +338,8 @@ func TestAConflictReadsAgain(t *testing.T) { // 1238 (compare-and-set: Update wi
t.Fatalf("pulls = %+v, want #12 moved and #300 kept (the other writer's)", c.Pulls)
}
for _, p := range c.Pulls {
if p.Number == 12 && p.Head.Head != h3 {
t.Errorf("#12 head = %s", p.Head.Head)
if p.Number == 12 && p.Head != h3 {
t.Errorf("#12 head = %s", p.Head)
}
}
}
@@ -349,7 +351,7 @@ func TestAnOddBranchNameKeysEscaped(t *testing.T) {
if err := ix.Apply(context.Background(), pull("novox/mesh-delivery", 12, "feat x?", h1, "modules/a")); err != nil {
t.Fatal(err)
}
key, err := delivery.CandidatesKey("feat x?")
key, err := CandidatesKey("feat x?")
if err != nil {
t.Fatal(err)
}
@@ -359,7 +361,7 @@ func TestAnOddBranchNameKeysEscaped(t *testing.T) {
}
func open(pulls ...forgemsg.OpenPull) forgemsg.Forge {
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeOpen, Open: pulls}
return forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeOpen, Open: pulls}
}
// 1279/AC7 (ADR 0443 §5): on an open message, candidates lose every pull request it does not name, a group left empty is
@@ -383,8 +385,8 @@ func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/A
t.Fatal(err)
}
c, ok := read(t, st, "feat/x")
hash, _ := delivery.HeadsHash([]delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}})
if !ok || len(c.Pulls) != 1 || c.Pulls[0].Number != 12 || c.Pulls[0].Head.Head != h1 || strings.Join(c.Pulls[0].Modules, ",") != "a" || c.HeadsHash != hash || c.HeadsHash == before.HeadsHash {
hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: h1}})
if !ok || len(c.Pulls) != 1 || c.Pulls[0].Number != 12 || c.Pulls[0].Head != h1 || strings.Join(c.Pulls[0].Modules, ",") != "a" || c.Heads != hash || c.Heads == before.Heads {
t.Fatalf("feat/x after the open message = %+v, want only mesh-delivery#12 with its modules and the heads hash of it alone", c)
}
for _, gone := range []string{"feat/y", "feat/old"} {
@@ -7,15 +7,18 @@ import (
"sort"
"strings"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/fingerprint"
"mesh-delivery-indexer/judge"
)
// The reasons a module has no fingerprint of the new form while the indexer writes none for any module (novox/hq
// ADR 0465 clause 2; issue 1444). Toolchains missing is delivery.ToolchainNoReport.
// ADR 0465 clause 2; issue 1444). Toolchains missing is sdkstep.ToolchainNoReport.
const (
// None2NoManifest is for a module whose directory the merged message carries no manifest of (an older bridge).
None2NoManifest = "the message carries no manifest"
@@ -34,14 +37,14 @@ var lockFile = map[string]string{"go": "go.sum", "typescript": "package-lock.jso
// moduleInputs is what a module's manifest and lock hashes give the indexer at a merge (issue 1450).
type moduleInputs struct {
// Kept is the new-form inputs the merge gave the module; nil when the manifest could not be read or gave none.
Kept *delivery.KeptInputs2
Kept *meshcatalog.ModulesByModuleValueInputs2
// Languages is the language of each artifact that has a toolchain (a bundle or a package of go or typescript), by
// artifact name. The toolchain versions are joined to them when the build modules report them (issue 1444, the
// watching half); the SDK's KeptInputs2 keeps no languages, so they are read from the manifest again then.
Languages map[string]string
// None2 is why the module has no fingerprint of the new form.
None2 string
// ArtifactPaths are the paths each artifact naming them names, as its manifest names them (delivery.Module.ArtifactPaths).
// ArtifactPaths are the paths each artifact naming them names, as its manifest names them (meshcatalog.ModulesByModuleValue.ArtifactPaths).
ArtifactPaths map[string][]string
}
@@ -105,14 +108,14 @@ func inputsOfNamed(module, raw, tree string, locks, objects map[string]string, p
if len(missing) > 0 {
return moduleInputs{None2: "the message carries no git object of the named paths " + strings.Join(missing, ", "), ArtifactPaths: artifactPaths}
}
if tree, err = fingerprint.NamedTree(delivery.ManifestDigest([]byte(raw)), named); err != nil {
if tree, err = fingerprint.NamedTree(meshcatalogclient.ManifestDigest([]byte(raw)), named); err != nil {
return moduleInputs{None2: err.Error(), ArtifactPaths: artifactPaths}
}
}
if tree == "" {
return moduleInputs{None2: "the merge names no tree for the module's directory", ArtifactPaths: artifactPaths}
}
kept := delivery.KeptInputs2{Tree: tree, Paths: named}
kept := meshcatalog.ModulesByModuleValueInputs2{Tree: tree, Paths: named}
contexts := false
var missing []string
langs := languagesOf(m)
@@ -155,7 +158,7 @@ func inputsOfNamed(module, raw, tree string, locks, objects map[string]string, p
case len(missing) > 0:
out.None2 = strings.Join(missing, "; ")
case len(langs) > 0:
out.None2 = delivery.ToolchainNoReport
out.None2 = sdkstep.ToolchainNoReport
default:
out.None2 = None2Pending
}
@@ -6,7 +6,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"mesh-delivery-indexer/internal/fingerprint"
)
@@ -25,7 +26,7 @@ func noBases(string) (string, string) { return "", "none are held in this test"
func TestAGoModuleKeepsItsTreeAndTheHashOfItsLockFile(t *testing.T) { // 1450/AC1
in := inputsOf("avahi", catalogueManifest(t, "avahi"), "avahitree", map[string]string{"go.sum": avahiGoSum}, noBases)
want := &delivery.KeptInputs2{Tree: "avahitree", Locks: map[string]string{"tools": avahiGoSum}}
want := &meshcatalog.ModulesByModuleValueInputs2{Tree: "avahitree", Locks: map[string]string{"tools": avahiGoSum}}
if !reflect.DeepEqual(in.Kept, want) {
t.Errorf("kept = %+v, want %+v", in.Kept, want)
}
@@ -40,7 +41,7 @@ func TestAGoModuleKeepsItsTreeAndTheHashOfItsLockFile(t *testing.T) { // 1450/AC
t.Fatal(err)
}
m, ok := moduleOf(t, mods, "avahi")
if !ok || !reflect.DeepEqual(m.Inputs2, want) || m.Manifest != delivery.ManifestDigest([]byte(catalogueManifest(t, "avahi"))) {
if !ok || !reflect.DeepEqual(m.Inputs2, want) || m.Manifest != meshcatalogclient.ManifestDigest([]byte(catalogueManifest(t, "avahi"))) {
t.Errorf("module = %+v (%v), want inputs %+v and the manifest digest", m, ok, want)
}
}
@@ -55,7 +56,7 @@ func TestAnArtifactWhoseLockFileTheMergeDidNotCarryHasNoLocksEntry(t *testing.T)
t.Errorf("languages = %v, want tools: go (the toolchain still applies)", in.Languages)
}
// With the toolchain known, mesh-sdk gives such inputs a src2: value, never 'no lock'.
if fp, none := fingerprint.Of(inputsFrom("avahi", *in.Kept, map[string]delivery.Toolchain2{"tools": {Language: "go", Version: "1.25.0", Recipe: "1"}})); fp == "" || none != "" {
if fp, none := fingerprint.Of(inputsFrom("avahi", *in.Kept, map[string]meshcatalogclient.Toolchain2{"tools": {Language: "go", Version: "1.25.0", Recipe: "1"}})); fp == "" || none != "" {
t.Errorf("fingerprint = %q none %q, want a value", fp, none)
}
// A TypeScript bundle without package-lock.json (go.sum of another language does not count).
@@ -113,8 +114,8 @@ func TestABaseEntersByTheFingerprintOfItsModuleOrNamesItsAbsence(t *testing.T) {
}
// Through the indexer: modules.mesh-tools holds a fingerprint of the new form (a record written by a later release).
ix, mods, _ := manifestsIndexer(t, rec(t, "build-1", "avahi", catalogueRepo, "modules/avahi", "main", "", "2026-10-19T09:00:00Z", aInputs))
mods.put("mesh-tools", mustEncode(t, delivery.Module{Repository: "novox/mesh-tools", // a base held of another repository stays when the catalogue merges (issue 1900)
Commit: merge, None: "x", FP2: fp, Manifest: strings.Repeat("a", 64)}))
mods.put("mesh-tools", mustEncode(t, meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-tools", // a base held of another repository stays when the catalogue merges (issue 1900)
Commit: merge, None: "x", Fp2: fp, Manifest: strings.Repeat("a", 64)}))
f := withManifests(mergedMsg(catalogueRepo, map[string]string{"modules/avahi": "t"}), map[string]string{"modules/avahi": aModuleWithABase(t)}, nil)
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
@@ -124,9 +125,9 @@ func TestABaseEntersByTheFingerprintOfItsModuleOrNamesItsAbsence(t *testing.T) {
}
}
func mustEncode(t *testing.T, m delivery.Module) []byte {
func mustEncode(t *testing.T, m meshcatalog.ModulesByModuleValue) []byte {
t.Helper()
b, err := delivery.EncodeModule(m)
b, err := meshcatalogclient.EncodeModule(m)
if err != nil {
t.Fatal(err)
}
@@ -4,7 +4,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
// The build records below are the live ones, as `mesh-controller.command builds` showed them on 2026-10-19 at about
@@ -25,8 +25,8 @@ const (
func liveRecord(id, module, repository, ref, commit, path, at, source string) [2]any {
when, _ := time.Parse(time.RFC3339, at)
raw, _ := delivery.EncodeBuildLogEntry(delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: module, FP: source, Repository: repository,
Directory: path, Commit: commit, Ref: ref, Node: "n1", At: when, Outcome: delivery.BuildDone})
raw, _ := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: module, FP: source, Repository: repository,
Directory: path, Commit: commit, Ref: ref, Node: "n1", At: when, Outcome: builds.BuildDone})
return [2]any{module, raw}
}
@@ -5,8 +5,10 @@ import (
"errors"
"fmt"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// keepManifest writes manifests.<digest> for a manifest as the forge answered it, create-once (issue 1449): a key that
@@ -15,15 +17,15 @@ import (
// digest. A manifest that cannot be a value (empty, not UTF-8, not JSON) is an error the caller says; it is never tried
// again. With no manifests store nothing is written and the digest is still answered.
func (ix *Indexer) keepManifest(ctx context.Context, module, raw string) (string, error) {
digest := delivery.ManifestDigest([]byte(raw))
digest := meshcatalogclient.ManifestDigest([]byte(raw))
if ix.Manifests == nil {
return digest, nil
}
key, err := delivery.ManifestsKey(digest)
key, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
return "", err
}
data, err := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw})
data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: module, Manifest: raw})
if err != nil {
return "", fmt.Errorf("%w: manifests.%s: %v", errCannotBeAValue, key, err)
}
@@ -41,7 +43,7 @@ func (ix *Indexer) keepManifest(ctx context.Context, module, raw string) (string
if !found {
return "", fmt.Errorf("manifests.%s was there and is not: written again at the next delivery", key)
}
if cur, err := delivery.DecodeManifest(held); err == nil && delivery.SameManifest(cur, delivery.Manifest{V: delivery.Version, Module: module, JSON: raw}) {
if cur, err := meshcatalogclient.DecodeManifest(held); err == nil && cur == (meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: module, Manifest: raw}) {
return digest, nil
}
return "", fmt.Errorf("manifests.%s holds another value than the manifest of %s, which hashes to it", key, module)
@@ -5,8 +5,9 @@ import (
"os"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The manifests these tests carry are real files, copied unchanged into testdata: the module.json of modules of
@@ -57,13 +58,13 @@ func mergedManifestIsKeptByItsDigestAndNamedByTheModule(t *testing.T) { // 1449/
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
digest := delivery.ManifestDigest([]byte(avahi))
digest := meshcatalogclient.ManifestDigest([]byte(avahi))
raw, ok := manifests.vals[digest]
if !ok || len(manifests.vals) != 1 {
t.Fatalf("manifests holds %d keys, want only %s", len(manifests.vals), digest)
}
m, err := delivery.DecodeManifest(raw)
if err != nil || m.Module != "avahi" || m.JSON != avahi {
m, err := meshcatalogclient.DecodeManifest(raw)
if err != nil || m.Module != "avahi" || m.Manifest != avahi {
t.Errorf("manifest = %+v err %v, want avahi and the bytes of the file", m, err)
}
if mod, ok := moduleOf(t, mods, "avahi"); !ok || mod.Manifest != digest {
@@ -83,7 +84,7 @@ func sameManifestAppliedAgainIsWrittenOnce(t *testing.T) { // 1449/AC2
if err := ix.Apply(context.Background(), f); err != nil {
t.Errorf("a second merge: %v", err)
}
pull := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: 301, Head: h1,
pull := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: 301, Head: h1,
Action: forgemsg.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true, Manifests: map[string]string{"modules/avahi": avahi}}
if err := ix.Apply(context.Background(), pull); err != nil {
t.Errorf("a pull message: %v", err)
@@ -7,9 +7,12 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"mesh-delivery-indexer/internal/fingerprint"
"mesh-delivery-indexer/judge"
@@ -61,7 +64,7 @@ func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool,
return w, err
}
objects, namedErr := ix.namedObjects(ctx, f)
overlay := map[string]delivery.Module{}
overlay := map[string]meshcatalog.ModulesByModuleValue{}
dirOf := map[string]string{}
named, every := map[string]bool{}, true // every module the message's manifests name, and whether each names one
for _, dir := range slices.Sorted(maps.Keys(f.Manifests)) {
@@ -72,7 +75,7 @@ func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool,
every = false
continue
}
key, err := delivery.ModulesKey(name)
key, err := meshcatalogclient.ModulesKey(name)
if err != nil {
every = false
ix.logf("%s: the module %q has no usable key, skipped: %v", f.Repository, name, err)
@@ -80,7 +83,7 @@ func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool,
}
named[key] = true
cur, has := held[key]
if has && cur.Repository != "" && !delivery.SameRepository(cur.Repository, f.Repository) {
if has && cur.Repository != "" && !trunk.SameRepository(cur.Repository, f.Repository) {
if !moves[key] {
ix.waitToMove(key, f, dir)
ix.logf("%s at %s: %q names the module %s, which the indexer holds of %s; left as it is until %s's trunk drops it, and then moved here (a move between repositories is the merge of the group that moves it, ADR 0420 clause 7)", f.Repository, f.Merge, dir, name, cur.Repository, cur.Repository)
@@ -96,7 +99,7 @@ func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool,
ix.logf("%s at %s: %q and %q both name the module %s; %q, which the indexer holds, is written", f.Repository, f.Merge, other, dir, name, dir)
}
dirOf[name] = dir
next := delivery.Module{Repository: f.Repository, Commit: f.Merge, Src: ix.srcOf(f.Repository, f.Merge), Manifest: digests[dir], Path: pathOf(dir)}
next := meshcatalog.ModulesByModuleValue{Repository: f.Repository, Commit: f.Merge, Src: int64(ix.srcOf(f.Repository, f.Merge)), Manifest: digests[dir], Path: pathOf(dir)}
// The reader capabilities a changed manifest needs are written with it, before the judge's pass names them with its
// verdict, so no reader takes a new manifest as needing nothing (ADR 0614 §4).
next.Needs, _ = catalogue.ManifestNeeds([]byte(raw))
@@ -108,7 +111,7 @@ func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool,
} else {
packages := ix.goSumPackages(ctx, f, dir, cur, has)
in := inputsOfNamed(name, raw, f.Trees[dir], f.Locks[dir], objects[dir], packages, ix.baseFP(ctx))
next.Inputs2, next.None2, next.ArtifactPaths = in.Kept, in.None2, in.ArtifactPaths
next.Inputs2, next.None2, next.Paths = in.Kept, in.None2, in.ArtifactPaths
if in.Kept != nil && has && cur.Inputs2 != nil {
// The imports its last trunk build reported stand until its next build reports again (ADR 0593 §2, §4).
in.Kept.Imports, in.Kept.ImportForm = cur.Inputs2.Imports, cur.Inputs2.ImportForm
@@ -138,7 +141,7 @@ func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool,
// repository goes (issue 1126 note 94: a repository that removed its last module). A message with trees but no
// manifests (an older writer) or one that cannot be read whole says nothing.
if drop && (len(f.Manifests) > 0 || len(f.Trees) == 0) && every {
if err := ix.dropRemoved(ctx, f.Repository, held, func(key string, _ delivery.Module) bool { return named[key] }); err != nil {
if err := ix.dropRemoved(ctx, f.Repository, held, func(key string, _ meshcatalog.ModulesByModuleValue) bool { return named[key] }); err != nil {
return w, err
}
}
@@ -156,7 +159,7 @@ func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool,
// baseFP answers the new-form fingerprint of a base module as modules.<base> holds it, or why it has none.
func (ix *Indexer) baseFP(ctx context.Context) baseFP {
return func(module string) (string, string) {
key, err := delivery.ModulesKey(module)
key, err := meshcatalogclient.ModulesKey(module)
if err != nil {
return "", err.Error()
}
@@ -167,12 +170,12 @@ func (ix *Indexer) baseFP(ctx context.Context) baseFP {
if !found {
return "", "the indexer holds no record of it"
}
cur, err := delivery.DecodeModule(value)
cur, err := meshcatalogclient.DecodeModule(value)
switch {
case err != nil:
return "", "its record cannot be read: " + err.Error()
case cur.FP2 != "":
return cur.FP2, ""
case cur.Fp2 != "":
return cur.Fp2, ""
case cur.None2 != "":
return "", cur.None2
}
@@ -183,12 +186,15 @@ func (ix *Indexer) baseFP(ctx context.Context) baseFP {
// pathOf is the module's directory as Module.Path holds it: "." for the repository's root, since empty means not known.
func pathOf(dir string) string {
if dir == "" {
return delivery.RootPath
return meshcatalogclient.RootPath
}
return dir
}
func withV(m delivery.Module) delivery.Module { m.V = delivery.Version; return m }
func withV(m meshcatalog.ModulesByModuleValue) meshcatalog.ModulesByModuleValue {
m.V = keys.Version
return m
}
// namedObjects are, by module directory, the git objects of the paths each manifest of the merge names for every artifact
// (catalogue Manifest.NamedPaths): those the message carries, else asked of the forge at the merge commit in one call
@@ -231,12 +237,12 @@ func (ix *Indexer) namedObjects(ctx context.Context, f forgemsg.Forge) (map[stri
// dropRemoved deletes, from the modules the bucket holds of the repository, each one the trunk no longer holds (issue 1900):
// held(key, module) says whether the trunk holds it. A module of another repository, or of none known, is never touched.
func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[string]delivery.Module, trunkHolds func(key string, m delivery.Module) bool) error {
func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[string]meshcatalog.ModulesByModuleValue, trunkHolds func(key string, m meshcatalog.ModulesByModuleValue) bool) error {
var handed []string
elsewhere := map[string]forgemsg.Forge{}
for _, key := range slices.Sorted(maps.Keys(held)) {
cur := held[key]
if cur.Repository == "" || !delivery.SameRepository(cur.Repository, repository) || trunkHolds(key, cur) {
if cur.Repository == "" || !trunk.SameRepository(cur.Repository, repository) || trunkHolds(key, cur) {
continue
}
if ix.movingTo(key, repository) {
@@ -254,7 +260,7 @@ func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[
if !found {
return nil, false, nil
}
if m, err := delivery.DecodeModule(value); err != nil || !delivery.SameRepository(m.Repository, repository) {
if m, err := meshcatalogclient.DecodeModule(value); err != nil || !trunk.SameRepository(m.Repository, repository) {
return nil, false, nil
}
return nil, true, nil
@@ -297,9 +303,9 @@ func (ix *Indexer) Reconcile(ctx context.Context) error {
return err
}
for _, repository := range slices.Sorted(maps.Keys(heads)) {
mine := map[string]delivery.Module{}
mine := map[string]meshcatalog.ModulesByModuleValue{}
for key, m := range held {
if m.Repository != "" && delivery.SameRepository(m.Repository, repository) {
if m.Repository != "" && trunk.SameRepository(m.Repository, repository) {
mine[key] = m
}
}
@@ -322,7 +328,7 @@ func (ix *Indexer) Reconcile(ctx context.Context) error {
head := heads[repository]
// The source can lag a merge already applied: a key written at a commit the head read does not hold (a merge heard
// first) is not read as removed, only one whose commit the head is, or contains.
stays := func(_ string, m delivery.Module) bool {
stays := func(_ string, m meshcatalog.ModulesByModuleValue) bool {
if m.Path == "" || there[m.Path] {
return true
}
@@ -353,11 +359,11 @@ const None2Unreadable = "the manifest cannot be read: "
// hashes by file name (the artifacts that own them are not known without the manifest) and the objects of the named
// paths the message carried; nil when the message carried no tree. Judging the manifest again (rejudged) turns them into
// the inputs of the module (issue 1923).
func unreadInputs(tree string, locks, paths map[string]string, packages []string) *delivery.KeptInputs2 {
func unreadInputs(tree string, locks, paths map[string]string, packages []string) *meshcatalog.ModulesByModuleValueInputs2 {
if tree == "" {
return nil
}
in := &delivery.KeptInputs2{Tree: tree}
in := &meshcatalog.ModulesByModuleValueInputs2{Tree: tree}
if len(locks) > 0 {
in.Locks = maps.Clone(locks)
}
@@ -371,7 +377,7 @@ func unreadInputs(tree string, locks, paths map[string]string, packages []string
// goSumPackages are the version line's packages of the merge's directory dir (ADR 0573 §2, ADR 0593 §2): the mesh Go
// modules of other repositories its go.sum names, and those of its own repository its go.mod replaces by a directory
// (siblingPackages), which no go.sum line names. A directory with no go.sum is no Go module and has none.
func (ix *Indexer) goSumPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string {
func (ix *Indexer) goSumPackages(ctx context.Context, f forgemsg.Forge, dir string, cur meshcatalog.ModulesByModuleValue, has bool) []string {
others := ix.otherPackages(ctx, f, dir, cur, has)
if f.Locks[dir]["go.sum"] == "" && len(f.Packages[dir]) == 0 {
return others
@@ -385,14 +391,14 @@ func (ix *Indexer) goSumPackages(ctx context.Context, f forgemsg.Forge, dir stri
// siblingPackages are the mesh Go modules of the merge's own repository the go.mod of dir replaces by a directory, read
// from the forge (GoMod); where it cannot be read, the held module's own-repository packages stand, said.
func (ix *Indexer) siblingPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string {
func (ix *Indexer) siblingPackages(ctx context.Context, f forgemsg.Forge, dir string, cur meshcatalog.ModulesByModuleValue, has bool) []string {
held := func() []string {
if !has || cur.Inputs2 == nil {
return nil
}
var out []string
for _, p := range cur.Inputs2.Packages {
if repo, _, ok := delivery.PackageRepository(p); ok && delivery.SameRepository(repo, f.Repository) {
if repo, _, ok := trunk.PackageRepository(p); ok && trunk.SameRepository(repo, f.Repository) {
out = append(out, p)
}
}
@@ -412,7 +418,7 @@ func (ix *Indexer) siblingPackages(ctx context.Context, f forgemsg.Forge, dir st
// otherPackages are the mesh Go modules of other repositories the go.sum of the merge's directory dir names: those the
// message carries (an older bridge), else the held module's while its go.sum's hash is unchanged, else the go.sum read
// from the forge (GoSum).
func (ix *Indexer) otherPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string {
func (ix *Indexer) otherPackages(ctx context.Context, f forgemsg.Forge, dir string, cur meshcatalog.ModulesByModuleValue, has bool) []string {
if carried := f.Packages[dir]; len(carried) > 0 {
return fingerprint.OtherPackages(carried, f.Repository) // its own repository's come with its tree
}
@@ -420,7 +426,7 @@ func (ix *Indexer) otherPackages(ctx context.Context, f forgemsg.Forge, dir stri
if hash == "" {
return nil
}
if has && cur.Inputs2 != nil && delivery.SameRepository(cur.Repository, f.Repository) && ix.requirementsKept(f.Repository, dir) {
if has && cur.Inputs2 != nil && trunk.SameRepository(cur.Repository, f.Repository) && ix.requirementsKept(f.Repository, dir) {
for _, h := range cur.Inputs2.Locks {
if h == hash {
return fingerprint.OtherPackages(cur.Inputs2.Packages, f.Repository)
@@ -10,9 +10,16 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -28,18 +35,18 @@ func logged(t *testing.T, id, module, repo, path, ref, outcome, at string) []byt
t.Fatal(err)
}
if path == "" {
path = delivery.RootPath
path = meshcatalogclient.RootPath
}
e := delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: module, FP: "src2:" + id, Repository: repo, Directory: path,
Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Ref: ref, Node: "n1", At: when, Outcome: delivery.BuildDone}
e := builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: module, FP: "src2:" + id, Repository: repo, Directory: path,
Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Ref: ref, Node: "n1", At: when, Outcome: builds.BuildDone}
switch outcome {
case "":
case "failed":
e.Outcome = delivery.BuildFailed
e.Outcome = builds.BuildFailed
default:
e.Event, e.Outcome = delivery.BuildStartedEvent, ""
e.Event, e.Outcome = builds.BuildStartedEvent, ""
}
raw, err := delivery.EncodeBuildLogEntry(e)
raw, err := builds.EncodeBuildLogEntry(e)
if err != nil {
t.Fatal(err)
}
@@ -77,7 +84,7 @@ func mergedMsg(repo string, trees map[string]string) forgemsg.Forge {
for dir := range trees {
manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1"}`
}
return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests}
return forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests}
}
func nameOfDir(dir string) string {
@@ -87,13 +94,13 @@ func nameOfDir(dir string) string {
return path.Base(dir)
}
func moduleOf(t *testing.T, st *memStore, key string) (delivery.Module, bool) {
func moduleOf(t *testing.T, st *memStore, key string) (meshcatalog.ModulesByModuleValue, bool) {
t.Helper()
v, ok := st.vals[key]
if !ok {
return delivery.Module{}, false
return meshcatalog.ModulesByModuleValue{}, false
}
m, err := delivery.DecodeModule(v)
m, err := meshcatalogclient.DecodeModule(v)
if err != nil {
t.Fatal(err)
}
@@ -145,7 +152,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
{name: "a merge heard while the taken state was missing is filled when the state appears", writes: 2, commit: map[string]string{"a": next, "b": merge, "c": merge}, taken: next,
then: func(ix *Indexer) error { // issue 1280: the key was missing, the merge was taken in without it
on := ix.Taken.(*memStore)
key, _ := delivery.RepositoryKey(repo)
key, _ := trunk.RepositoryKey(repo)
delete(on.vals, key)
ix.Taken = &memStore{unbound: process.ErrNotYet}
m := mergedMsg(repo, map[string]string{"modules/a": "treeA2", "modules/b": "treeB", "modules/c": "treeC"})
@@ -293,7 +300,7 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
}},
{name: "a later source revision of another repository's merge", writes: 0, commit: map[string]string{"a": merge, "b": merge, "c": merge}, taken: merge,
then: func(ix *Indexer) error {
return ix.ApplySource(context.Background(), 9, delivery.Source{Group: "g", Heads: map[string]string{repo: merge, "novox/mesh-catalog": otherMerge}})
return ix.ApplySource(context.Background(), 9, meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{repo: merge, "novox/mesh-catalog": otherMerge}})
}},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -338,11 +345,11 @@ func TestAnUnchangedModuleWritesNothing(t *testing.T) { // 1239 (unchanged value
t.Errorf("%s = %+v (%v), want commit %.8s", module, m, ok, want)
}
}
key, _ := delivery.RepositoryKey(repo)
key, _ := trunk.RepositoryKey(repo)
if _, held := taken.vals[key]; !held && tc.taken == "" {
return
}
if got, err := delivery.DecodeTaken(taken.vals[key]); err != nil || got.Merge != tc.taken || got.Repository != repo {
if got, err := takein.DecodeTaken(taken.vals[key]); err != nil || got.Merge != tc.taken || got.Repository != repo {
t.Errorf("taken.%s = %+v (%v), want merge %.8s", key, got, err, tc.taken)
}
})
@@ -367,7 +374,7 @@ func TestAMergeWritesTheModulesDirectoryAndKeepsItThroughSourceAndRecompute(t *t
if err := ix.Apply(ctx, mergedMsg("novox/mesh-delivery", map[string]string{"modules/a": "newtree", "": "roottree"})); err != nil {
t.Fatal(err)
}
for module, want := range map[string]string{"a": "modules/a", "r": delivery.RootPath} {
for module, want := range map[string]string{"a": "modules/a", "r": meshcatalogclient.RootPath} {
if m, ok := moduleOf(t, mods, module); !ok || m.Path != want {
t.Errorf("after the merge %s = %+v (%v), want path %q", module, m, ok, want)
}
@@ -392,8 +399,8 @@ func TestARootModuleHasTheDirectoryDot(t *testing.T) { // 1615/AC3: empty means
if err := ix.Apply(context.Background(), mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree"})); err != nil {
t.Fatal(err)
}
if m, ok := moduleOf(t, mods, "r"); !ok || m.Path != delivery.RootPath {
t.Errorf("module = %+v (%v), want path %q", m, ok, delivery.RootPath)
if m, ok := moduleOf(t, mods, "r"); !ok || m.Path != meshcatalogclient.RootPath {
t.Errorf("module = %+v (%v), want path %q", m, ok, meshcatalogclient.RootPath)
}
}
@@ -406,13 +413,13 @@ func TestAMergeAddingAModuleDirectoryWritesItsValueWithNoBuildRecordAnywhere(t *
t.Fatal(err)
}
m, ok := moduleOf(t, mods, "avahi")
if !ok || m.Path != "modules/avahi" || m.Repository != catalogueRepo || m.Commit != merge || m.Manifest != delivery.ManifestDigest([]byte(avahi)) || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" {
if !ok || m.Path != "modules/avahi" || m.Repository != catalogueRepo || m.Commit != merge || m.Manifest != meshcatalogclient.ManifestDigest([]byte(avahi)) || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" {
t.Fatalf("module = %+v (%v), want its directory, merge, manifest and inputs from the message alone", m, ok)
}
if m.FP != "" || m.None != "" {
if m.Fp != "" || m.None != "" {
t.Errorf("module = %+v, want no value of today's form (I4: it came from the build records)", m)
}
if m.FP2 == "" && m.None2 == "" {
if m.Fp2 == "" && m.None2 == "" {
t.Errorf("module = %+v, want a new-form fingerprint or its reason", m)
}
// ADR 0614 §4: the merge writes the reader capabilities its manifest needs, so no reader takes it as needing nothing
@@ -567,6 +574,6 @@ func TestTwoDirectoriesNamingOneModuleWriteOneAndSayTheOther(t *testing.T) { //
}
func takenKey(repo string) string {
key, _ := delivery.RepositoryKey(repo)
key, _ := trunk.RepositoryKey(repo)
return key
}
@@ -8,7 +8,8 @@ import (
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// heldByAnotherTrunk reads, for a module the trunk of repository no longer holds, whether the trunk of another repository
@@ -24,7 +25,7 @@ func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository st
ix.mu.Unlock()
for _, other := range slices.Sorted(maps.Keys(heads)) {
head := heads[other]
if head == "" || delivery.SameRepository(other, repository) {
if head == "" || trunk.SameRepository(other, repository) {
continue
}
dirs, err := ix.Trunk(ctx, other, head)
@@ -46,7 +47,7 @@ func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository st
if read.Trees[dir] == "" || manifestModule(raw) != module {
continue
}
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: other, Merge: head, Root: read.Root,
f := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: other, Merge: head, Root: read.Root,
Trees: map[string]string{dir: read.Trees[dir]}, Manifests: map[string]string{dir: raw}}
if l := read.Locks[dir]; len(l) > 0 {
f.Locks = map[string]map[string]string{dir: l}
@@ -102,7 +103,7 @@ func (ix *Indexer) movingTo(key, from string) bool {
ix.mu.Lock()
defer ix.mu.Unlock()
f, ok := ix.moving[key]
return ok && !delivery.SameRepository(f.Repository, from)
return ok && !trunk.SameRepository(f.Repository, from)
}
// handOver writes the module at key from the merge waiting to take it, and forgets the wait.
@@ -7,7 +7,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"mesh-delivery-indexer/internal/fingerprint"
)
@@ -31,15 +32,15 @@ func TestAModuleNamingItsPathsTakesItsTreeFromThemAndKeepsTheirObjects(t *testin
if !ok || m.Inputs2 == nil {
t.Fatalf("module = %+v (%v)", m, ok)
}
want, err := fingerprint.NamedTree(delivery.ManifestDigest([]byte(raw)), sdkObjects)
want, err := fingerprint.NamedTree(meshcatalogclient.ManifestDigest([]byte(raw)), sdkObjects)
if err != nil {
t.Fatal(err)
}
if m.Inputs2.Tree != want || !reflect.DeepEqual(m.Inputs2.Paths, sdkObjects) {
t.Errorf("inputs = %+v, want the named tree %s and the objects kept", m.Inputs2, want)
}
if got := m.ArtifactPaths["lib"]; strings.Join(got, ",") != "src,test,package.json,package-lock.json,tsconfig.json" {
t.Errorf("artifact paths = %v, want lib's paths as its manifest names them", m.ArtifactPaths)
if got := m.Paths["lib"]; strings.Join(got, ",") != "src,test,package.json,package-lock.json,tsconfig.json" {
t.Errorf("artifact paths = %v, want lib's paths as its manifest names them", m.Paths)
}
// Recomputed from what is kept, with no merge, the module is the same (ADR 0499 clause 2).
before := string(mods.vals["mesh-sdk"])
@@ -59,7 +60,7 @@ func TestAMessageWithoutTheNamedPathsObjectsGivesTheModuleNoneNamingThem(t *test
t.Fatal(err)
}
m, _ := moduleOf(t, mods, "mesh-sdk")
if m.FP2 != "" || !strings.Contains(m.None2, "named paths") || !strings.Contains(m.None2, "tsconfig.json") {
if m.Fp2 != "" || !strings.Contains(m.None2, "named paths") || !strings.Contains(m.None2, "tsconfig.json") {
t.Errorf("module = %+v, want none naming the paths the message did not carry", m)
}
}
@@ -79,8 +80,8 @@ func TestACandidatesModuleTakesTheNamedPathsObjectsFromThePreview(t *testing.T)
t.Fatal(err)
}
named, _ := fingerprint.NamedTree(digest, sdkObjects)
with := delivery.Preview{Modules: map[string]delivery.PreviewModule{".": {Tree: named, Manifest: digest, Paths: sdkObjects}}}
without := delivery.Preview{Modules: map[string]delivery.PreviewModule{".": {Tree: "roottree", Manifest: digest}}}
with := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Modules: map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{".": {Tree: named, Manifest: digest, Paths: sdkObjects}}}
without := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Modules: map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{".": {Tree: "roottree", Manifest: digest}}}
if got := ix.candidateModule(context.Background(), "mesh-sdk", ".", without, nil, nil); !strings.Contains(got.None2, "named paths") {
t.Errorf("without the objects: %+v, want none naming the named paths", got)
}
@@ -88,7 +89,7 @@ func TestACandidatesModuleTakesTheNamedPathsObjectsFromThePreview(t *testing.T)
if strings.Contains(got.None2, "named paths") || got.Manifest != digest {
t.Errorf("with the objects: %+v, want them taken (none only for what else is missing)", got)
}
if got.FP2 != "" && !reflect.DeepEqual(got.Paths, sdkObjects) {
if got.Fp2 != "" && !reflect.DeepEqual(got.Paths, sdkObjects) {
t.Errorf("with a fingerprint: %+v, want the named paths' objects carried for the builder's fetch", got)
}
}
@@ -114,7 +115,7 @@ func TestTheIndexerAsksTheForgeForTheNamedPathsObjectsOfAMerge(t *testing.T) { /
t.Fatalf("asked %+v, want the root's named paths of the merge commit, once, and nothing for a module naming none", asked)
}
m, _ := moduleOf(t, mods, "mesh-sdk")
want, _ := fingerprint.NamedTree(delivery.ManifestDigest([]byte(raw)), sdkObjects)
want, _ := fingerprint.NamedTree(meshcatalogclient.ManifestDigest([]byte(raw)), sdkObjects)
if m.Inputs2 == nil || m.Inputs2.Tree != want {
t.Errorf("module = %+v, want the named tree %s", m, want)
}
@@ -129,7 +130,7 @@ func TestAForgeThatCannotSayTheObjectsGivesTheModuleNoneWithItsWords(t *testing.
if err := ix.Apply(context.Background(), withManifests(mergedMsg("novox/mesh-sdk", map[string]string{"": "roottree"}), map[string]string{"": raw}, nil)); err != nil {
t.Fatal(err)
}
if m, _ := moduleOf(t, mods, "mesh-sdk"); m.FP2 != "" || !strings.Contains(m.None2, "is not at the commit") {
if m, _ := moduleOf(t, mods, "mesh-sdk"); m.Fp2 != "" || !strings.Contains(m.None2, "is not at the commit") {
t.Errorf("module = %+v, want none with the forge's words", m)
}
}
@@ -0,0 +1,46 @@
package index
// The holders, states and buckets the indexer names, taken from its own manifest (module.json: its name, its `state`, its
// `reads`) and from the manifests of the holders it reads; a unit row checks them against the manifest
// (TestTheNamesAreTheManifests). No contract names a holder (novox/hq ADR 0625 §4, ADR 0634 §1).
const (
// Module is the indexer's own name; the catalog client's Module is the readers' name for it.
Module = "mesh-delivery-indexer"
// The indexer's own states, with the revisions each keeps per key (module.json `state`).
CandidatesState = "candidates"
CandidatesBucket = Module + "_" + CandidatesState
CandidatesHistory = 10
ModulesState = "modules"
ModulesBucket = Module + "_" + ModulesState
ModulesHistory = 20
ManifestsState = "manifests"
ManifestsBucket = Module + "_" + ManifestsState
ManifestsHistory = 1
// VerdictsState and VerdictsBucket are in verdicts.go.
VerdictsHistory = 5
PredefinedState = "predefined"
ConfigCandidatesState = "config-candidates"
PackagesState = "packages"
PackagesBucket = Module + "_" + PackagesState
PackagesHistory = 5
TakenState = "taken"
TakenBucket = Module + "_" + TakenState
PredefinedListKey = "list"
ForgeBridgeModule = "mesh-delivery-forge-bridge"
AssignerModule = "mesh-delivery-assigner"
GateModule = "mesh-delivery-gate"
CheckerModule = "mesh-delivery-checker"
BuildGoModule = "mesh-build-go"
BuildNodeModule = "mesh-build-node"
PlacementState = "placement"
PlacementBucket = AssignerModule + "_" + PlacementState
PlacementHistory = 5
SourceBucket = GateModule + "_source"
PreviewsBucket = CheckerModule + "_previews"
PreviewsHistory = 5
toolchainsState = "toolchains"
)
// ToolchainsBucket is the toolchains state of a build module (reads: mesh-build-go.toolchains, mesh-build-node.toolchains).
func ToolchainsBucket(module string) string { return module + "_" + toolchainsState }
@@ -4,7 +4,7 @@ import (
"context"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
)
func TestAGoModuleAtAMergeHasItsManifestItsInputsAndNoFingerprintOfTheNewForm(t *testing.T) { // 1444/AC1
@@ -20,14 +20,14 @@ func TestAGoModuleAtAMergeHasItsManifestItsInputsAndNoFingerprintOfTheNewForm(t
if !ok {
t.Fatal("no module avahi")
}
if m.Manifest != delivery.ManifestDigest([]byte(avahi)) || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" || m.Inputs2.Locks["tools"] != avahiGoSum {
if m.Manifest != meshcatalogclient.ManifestDigest([]byte(avahi)) || m.Inputs2 == nil || m.Inputs2.Tree != "newtree" || m.Inputs2.Locks["tools"] != avahiGoSum {
t.Errorf("module = %+v, want its manifest digest and inputs", m)
}
if m.None2 != "toolchain not yet reported" || m.FP2 != "" {
t.Errorf("none2 = %q fp2 = %q, want none2 'toolchain not yet reported'", m.None2, m.FP2)
if m.None2 != "toolchain not yet reported" || m.Fp2 != "" {
t.Errorf("none2 = %q fp2 = %q, want none2 'toolchain not yet reported'", m.None2, m.Fp2)
}
if m.FP != "" || m.None != "" {
t.Errorf("fp = %q none = %q, want no value of today's form (I4)", m.FP, m.None)
if m.Fp != "" || m.None != "" {
t.Errorf("fp = %q none = %q, want no value of today's form (I4)", m.Fp, m.None)
}
}
@@ -39,7 +39,7 @@ func TestAModuleWithNoToolchainArtifactGetsItsFingerprintAtOnce(t *testing.T) {
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
if m, ok := moduleOf(t, mods, "ca-trust"); !ok || m.FP2 == "" || m.None2 != "" || m.Inputs2 == nil || m.Inputs2.Builder != "" {
if m, ok := moduleOf(t, mods, "ca-trust"); !ok || m.Fp2 == "" || m.None2 != "" || m.Inputs2 == nil || m.Inputs2.Builder != "" {
t.Errorf("module = %+v (%v), want its fingerprint with no builder input", m, ok)
}
}
@@ -6,8 +6,6 @@ import (
"fmt"
"maps"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/judge"
)
@@ -19,7 +17,7 @@ const (
OldSectionsTool = "indexer_old_sections"
// OldSectionsState is the indexer's state of the count: the bucket mesh-delivery-indexer_oldsections.
OldSectionsState = "oldsections"
OldSectionsBucket = delivery.IndexerModule + "_" + OldSectionsState
OldSectionsBucket = Module + "_" + OldSectionsState
// OldSectionsKey is the one key it holds.
OldSectionsKey = "counts"
)
@@ -7,8 +7,10 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -81,7 +83,7 @@ func (ix *Indexer) lateMerge(ctx context.Context, w *mergeWrite) error {
if ix.Taken == nil || w.merge == "" {
return nil
}
key, err := delivery.RepositoryKey(w.repository)
key, err := trunk.RepositoryKey(w.repository)
if err != nil {
return nil // writeTaken says it
}
@@ -92,7 +94,7 @@ func (ix *Indexer) lateMerge(ctx context.Context, w *mergeWrite) error {
if err != nil {
return fmt.Errorf("taken.%s: %w", key, err)
}
cur, err := delivery.DecodeTaken(value)
cur, err := takein.DecodeTaken(value)
if err != nil || cur.Merge == "" || cur.Merge == w.merge {
return nil
}
@@ -7,8 +7,10 @@ import (
"path"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// From R4e the indexer lists a repository's modules from what it holds itself (novox/hq ADR 0501 clause 6, issue 1851):
@@ -30,8 +32,8 @@ func manifestModule(raw string) string {
// heldModules are the values the modules bucket holds, by key (the module's name). A value that is not a module is left
// out and said.
func (ix *Indexer) heldModules(ctx context.Context) (map[string]delivery.Module, error) {
out := map[string]delivery.Module{}
func (ix *Indexer) heldModules(ctx context.Context) (map[string]meshcatalog.ModulesByModuleValue, error) {
out := map[string]meshcatalog.ModulesByModuleValue{}
if ix.Modules == nil {
return out, nil
}
@@ -47,7 +49,7 @@ func (ix *Indexer) heldModules(ctx context.Context) (map[string]delivery.Module,
if !found {
continue
}
m, err := delivery.DecodeModule(value)
m, err := meshcatalogclient.DecodeModule(value)
if err != nil {
ix.logf("modules.%s holds what is not a module, left: %v", key, err)
continue
@@ -58,10 +60,10 @@ func (ix *Indexer) heldModules(ctx context.Context) (map[string]delivery.Module,
}
// moduleAt is the module whose directory in the repository is dir, as the modules bucket holds it.
func moduleAt(held map[string]delivery.Module, repository, dir string) (string, bool) {
func moduleAt(held map[string]meshcatalog.ModulesByModuleValue, repository, dir string) (string, bool) {
want := pathOf(strings.Trim(dir, "/"))
for name, m := range held {
if m.Path == want && delivery.SameRepository(m.Repository, repository) {
if m.Path == want && trunk.SameRepository(m.Repository, repository) {
return name, true
}
}
@@ -71,7 +73,7 @@ func moduleAt(held map[string]delivery.Module, repository, dir string) (string,
// moduleOfDir is the module of a changed directory of a pull request: the module its manifest in the message names, else
// the one the modules bucket holds at that directory of the repository, else the directory's last element (the
// catalogue's rule for nearly every manifest).
func moduleOfDir(held map[string]delivery.Module, f forgemsg.Forge, dir string) string {
func moduleOfDir(held map[string]meshcatalog.ModulesByModuleValue, f forgemsg.Forge, dir string) string {
if name := manifestModule(f.Manifests[dir]); name != "" {
return name
}
@@ -6,8 +6,10 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -62,7 +64,7 @@ func PackageUsersOf(modules, manifests, packages views.Source, only string) ([]P
if !ok {
continue
}
s, err := delivery.DecodeSeriesPackage(e.Value)
s, err := DecodeSeriesPackage(e.Value)
if err != nil {
return nil, fmt.Errorf("packages.%s holds what is not a series: %w", key, err)
}
@@ -75,12 +77,12 @@ func PackageUsersOf(modules, manifests, packages views.Source, only string) ([]P
p.Series[s.Series] = s.Newest.Version
}
}
type trunk struct {
mod delivery.Module
type trunkMod struct {
mod meshcatalog.ModulesByModuleValue
manifest catalogue.Manifest
}
var mods []string
held := map[string]trunk{}
held := map[string]trunkMod{}
names, err := modules.Keys()
if err != nil {
return nil, fmt.Errorf("the modules state cannot be listed: %w", err)
@@ -93,7 +95,7 @@ func PackageUsersOf(modules, manifests, packages views.Source, only string) ([]P
if !ok {
continue
}
mod, err := delivery.DecodeModule(e.Value)
mod, err := meshcatalogclient.DecodeModule(e.Value)
if err != nil || mod.Manifest == "" {
continue // no manifest held for it
}
@@ -104,25 +106,25 @@ func PackageUsersOf(modules, manifests, packages views.Source, only string) ([]P
if !ok {
continue
}
kept, err := delivery.DecodeManifest(me.Value)
kept, err := meshcatalogclient.DecodeManifest(me.Value)
if err != nil {
return nil, fmt.Errorf("the manifest of %s: %w", name, err)
}
m, err := catalogue.Decode([]byte(kept.JSON))
m, err := catalogue.Decode([]byte(kept.Manifest))
if err != nil {
return nil, fmt.Errorf("the manifest of %s does not read: %w", name, err)
}
for pkg := range m.Packages {
get(pkg)
}
held[name], mods = trunk{mod, m}, append(mods, name)
held[name], mods = trunkMod{mod, m}, append(mods, name)
}
slices.Sort(mods)
reaches := func(mod delivery.Module, goModule string) bool {
reaches := func(mod meshcatalog.ModulesByModuleValue, goModule string) bool {
if goModule == "" || mod.Inputs2 == nil {
return false
}
if repo, _, ok := delivery.PackageRepository(goModule); !ok || delivery.SameRepository(repo, mod.Repository) {
if repo, _, ok := trunk.PackageRepository(goModule); !ok || trunk.SameRepository(repo, mod.Repository) {
return false
}
for _, p := range append(slices.Clone(mod.Inputs2.Packages), mod.Inputs2.Imports...) {
@@ -9,14 +9,18 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"mesh-delivery-indexer/internal/fingerprint"
)
// The published packages (novox/hq ADR 0593 §2): the indexer alone reads, from the forge's listing, the content digest
// of each package of every mesh Go module a held module reaches, and writes the module's latest published version and
// digests in its state `packages` (delivery.PackagesBucket), one key per Go module. The builder, the checker and the
// digests in its state `packages` (PackagesBucket), one key per Go module. The builder, the checker and the
// build modules read it there; nobody else computes a digest or a version.
//
// A Go module is reached when a held module's go.sum names it (KeptInputs2.Packages) or holds an import a build
@@ -25,8 +29,8 @@ import (
// reached module the state does not hold yet (FillPackages).
// publishedAll is every value of the packages state, by Go module path; empty with no state or one that does not answer.
func (ix *Indexer) publishedAll(ctx context.Context) map[string]delivery.PublishedPackages {
out := map[string]delivery.PublishedPackages{}
func (ix *Indexer) publishedAll(ctx context.Context) map[string]meshcatalog.PackagesByModuleValue {
out := map[string]meshcatalog.PackagesByModuleValue{}
if ix.Packages == nil {
return out
}
@@ -43,7 +47,7 @@ func (ix *Indexer) publishedAll(ctx context.Context) map[string]delivery.Publish
if err != nil || !found {
continue
}
p, err := delivery.DecodePublishedPackages(value)
p, err := DecodePublishedPackages(value)
if err != nil {
ix.logf("packages.%s holds what is not a published module, left out: %v", key, err)
continue
@@ -55,10 +59,10 @@ func (ix *Indexer) publishedAll(ctx context.Context) map[string]delivery.Publish
// reached are, by repository, the mesh Go module paths the held modules' go.sum names and the imports their builds
// reported.
func reached(held map[string]delivery.Module) (modules, imports map[string][]string) {
func reached(held map[string]meshcatalog.ModulesByModuleValue) (modules, imports map[string][]string) {
modules, imports = map[string][]string{}, map[string][]string{}
add := func(to map[string][]string, p string) {
repo, _, ok := delivery.PackageRepository(p)
repo, _, ok := trunk.PackageRepository(p)
if ok && !slices.Contains(to[repo], p) {
to[repo] = append(to[repo], p)
}
@@ -86,13 +90,13 @@ func reached(held map[string]delivery.Module) (modules, imports map[string][]str
// computed from the forge's listing (Files), by module path. An import is held by a known module when one holds its
// directory, else by the Go module whose go.mod is at its directory or the nearest above it (asked of the forge
// directory by directory). A module the listing has no go.mod for is said and left out.
func (ix *Indexer) packagesAt(ctx context.Context, repository, commit string, modules, imports []string) (map[string]delivery.PublishedPackages, error) {
func (ix *Indexer) packagesAt(ctx context.Context, repository, commit string, modules, imports []string) (map[string]meshcatalog.PackagesByModuleValue, error) {
if ix.Files == nil || commit == "" {
return nil, nil
}
dirOf := map[string]string{} // module path to its directory in the repository
for _, m := range modules {
if _, dir, ok := delivery.PackageRepository(m); ok {
if _, dir, ok := trunk.PackageRepository(m); ok {
dirOf[m] = dir
}
}
@@ -128,13 +132,13 @@ func (ix *Indexer) packagesAt(ctx context.Context, repository, commit string, mo
if holds(imp) {
continue
}
_, dir, _ := delivery.PackageRepository(imp)
_, dir, _ := trunk.PackageRepository(imp)
for d := dir; ; d = parent(d) {
if err := list([]string{d}); err != nil {
return nil, err
}
if _, ok := listings[d]["go.mod"]; ok {
m := strings.TrimSuffix(delivery.ForgeModulePrefix+strings.TrimPrefix(repository, "novox/")+"/"+d, "/")
m := strings.TrimSuffix(trunk.ForgeModulePrefix+strings.TrimPrefix(repository, "novox/")+"/"+d, "/")
dirOf[m] = d
break
}
@@ -147,7 +151,7 @@ func (ix *Indexer) packagesAt(ctx context.Context, repository, commit string, mo
if err := list(slices.Sorted(maps.Values(dirOf))); err != nil {
return nil, err
}
out := map[string]delivery.PublishedPackages{}
out := map[string]meshcatalog.PackagesByModuleValue{}
for _, m := range slices.Sorted(maps.Keys(dirOf)) {
dir := dirOf[m]
requires, pkgs, err := fingerprint.Packages(m, listings[dir])
@@ -157,9 +161,9 @@ func (ix *Indexer) packagesAt(ctx context.Context, repository, commit string, mo
}
d := dir
if d == "" {
d = delivery.RootPath
d = meshcatalogclient.RootPath
}
out[m] = delivery.PublishedPackages{Module: m, Repository: delivery.RepositoryName(repository), Dir: d, Commit: commit,
out[m] = meshcatalog.PackagesByModuleValue{Module: m, Repository: trunk.RepositoryName(repository), Dir: d, Commit: commit,
Requires: requires, Packages: pkgs}
}
return out, nil
@@ -179,12 +183,12 @@ func parent(d string) string {
// publish writes the packages state for the Go modules of repository the held modules (and the merge's overlay) reach,
// as the merge commit holds them: a key whose commit, requirements and digests are what it holds already is left.
func (ix *Indexer) publish(ctx context.Context, repository, commit string, held map[string]delivery.Module, onlyMissing bool) error {
func (ix *Indexer) publish(ctx context.Context, repository, commit string, held map[string]meshcatalog.ModulesByModuleValue, onlyMissing bool) error {
if ix.Packages == nil || ix.Files == nil {
return nil
}
modules, imports := reached(held)
repo := delivery.RepositoryName(repository)
repo := trunk.RepositoryName(repository)
if len(modules[repo]) == 0 && len(imports[repo]) == 0 {
return nil
}
@@ -209,7 +213,7 @@ func (ix *Indexer) publish(ctx context.Context, repository, commit string, held
}
// An import a published module of the repository already holds is resolved by its module's directory.
for m, p := range have {
if delivery.SameRepository(p.Repository, repo) && !slices.Contains(modules[repo], m) && !onlyMissing {
if trunk.SameRepository(p.Repository, repo) && !slices.Contains(modules[repo], m) && !onlyMissing {
modules[repo] = append(modules[repo], m)
}
}
@@ -219,20 +223,20 @@ func (ix *Indexer) publish(ctx context.Context, repository, commit string, held
}
for _, m := range slices.Sorted(maps.Keys(got)) {
next := got[m]
key, err := delivery.PackagesKey(m)
key, err := PackagesKey(m)
if err != nil {
ix.logf("%s: %v", m, err)
continue
}
err = write(ctx, ix.Packages, key, func(value []byte, found bool) ([]byte, bool, error) {
if found {
if cur, err := delivery.DecodePublishedPackages(value); err == nil && cur.Commit == next.Commit &&
if cur, err := DecodePublishedPackages(value); err == nil && cur.Commit == next.Commit &&
cur.Requires == next.Requires && reflect.DeepEqual(cur.Packages, next.Packages) {
return nil, false, nil
}
}
next.At = ix.now().UTC()
data, err := delivery.EncodePublishedPackages(next)
next.At = ptr(ix.now().UTC())
data, err := EncodePublishedPackages(next)
return data, false, err
})
if err != nil {
@@ -262,7 +266,7 @@ func (ix *Indexer) FillPackages(ctx context.Context) error {
repos[r] = true
}
for _, repo := range slices.Sorted(maps.Keys(repos)) {
key, err := delivery.RepositoryKey(repo)
key, err := trunk.RepositoryKey(repo)
if err != nil {
continue
}
@@ -270,7 +274,7 @@ func (ix *Indexer) FillPackages(ctx context.Context) error {
if err != nil || !found {
continue
}
t, err := delivery.DecodeTaken(value)
t, err := takein.DecodeTaken(value)
if err != nil {
continue
}
@@ -285,7 +289,7 @@ func (ix *Indexer) FillPackages(ctx context.Context) error {
// member's repository publishes at the member's head (members, by repository), every other at its latest published
// version. A head's modules are read from the forge once and kept while the head is a member somewhere; one the forge
// cannot list is said and left at its published version.
func (ix *Indexer) candidatePublished(ctx context.Context, members map[string]string, held map[string]delivery.Module) map[string]delivery.PublishedPackages {
func (ix *Indexer) candidatePublished(ctx context.Context, members map[string]string, held map[string]meshcatalog.ModulesByModuleValue) map[string]meshcatalog.PackagesByModuleValue {
out := ix.publishedAll(ctx)
if len(members) == 0 {
return out
@@ -304,7 +308,7 @@ func (ix *Indexer) candidatePublished(ctx context.Context, members map[string]st
var ms []string
ms = append(ms, modules[repo]...)
for m, p := range out {
if delivery.SameRepository(p.Repository, repo) && !slices.Contains(ms, m) {
if trunk.SameRepository(p.Repository, repo) && !slices.Contains(ms, m) {
ms = append(ms, m)
}
}
@@ -315,7 +319,7 @@ func (ix *Indexer) candidatePublished(ctx context.Context, members map[string]st
}
ix.mu.Lock()
if ix.atHeads == nil || len(ix.atHeads) >= maxHeadsKept {
ix.atHeads = map[string]map[string]delivery.PublishedPackages{}
ix.atHeads = map[string]map[string]meshcatalog.PackagesByModuleValue{}
}
ix.atHeads[at] = got
ix.mu.Unlock()
@@ -0,0 +1,189 @@
package index
import (
"encoding/json"
"fmt"
"regexp"
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The values of the state packages (novox/hq ADR 0593 §2, ADR 0606 §1): the contract's types, written by the indexer
// alone, so their writing rules are the indexer's (ADR 0634 §1: a holder's own logic stays with it).
// PackagesKey is the key of a mesh Go module (its module path) in the packages state: the path as a key part. It refuses
// a path that is no mesh Go module's.
func PackagesKey(module string) (string, error) {
if _, _, ok := trunk.PackageRepository(module); !ok {
return "", fmt.Errorf("index: %q is no mesh Go module's path (it starts with %s)", module, trunk.ForgeModulePrefix)
}
return keys.EscapeName(module), nil
}
func checkPublished(p meshcatalog.PackagesByModuleValue) error {
pkgs := make([][2]string, len(p.Packages))
for i, d := range p.Packages {
pkgs[i] = [2]string{d.Package, d.Digest}
}
return checkPublishedFields(p.Module, p.Repository, p.Dir, p.Commit, p.Requires, p.At, pkgs)
}
var commitFull = regexp.MustCompile(`^[0-9a-f]{40}$`)
// checkPublishedFields is the rule of a published module's value, over its fields (a series' contents hold the same).
func checkPublishedFields(module, repository, dir, commit, requires string, at *time.Time, packages [][2]string) error {
want, _, ok := trunk.PackageRepository(module)
switch {
case !ok:
return fmt.Errorf("index: %q is no mesh Go module's path (it starts with %s)", module, trunk.ForgeModulePrefix)
case !trunk.SameRepository(repository, want):
return fmt.Errorf("index: the module %s is published from %s, and the value names the repository %q", module, want, repository)
case dir == "":
return fmt.Errorf("index: the published module %s names no directory (%q for the repository's root)", module, meshcatalogclient.RootPath)
case !commitFull.MatchString(commit):
return fmt.Errorf("index: the published module %s names the commit %q, which is no full commit", module, commit)
case requires == "":
return fmt.Errorf("index: the published module %s names no digest of its requirements", module)
case at == nil || at.IsZero():
return fmt.Errorf("index: the published module %s names no time", module)
}
for i, d := range packages {
if d[0] != module && !strings.HasPrefix(d[0], module+"/") {
return fmt.Errorf("index: the package %q is not in the module %s", d[0], module)
}
if d[1] == "" {
return fmt.Errorf("index: the package %s names no digest", d[0])
}
if i > 0 && packages[i-1][0] >= d[0] {
return fmt.Errorf("index: the packages of %s are not sorted and each once (%s after %s)", module, d[0], packages[i-1][0])
}
}
return nil
}
// EncodePublishedPackages is p as JSON at the kept values' version; it refuses what a reader could not take.
func EncodePublishedPackages(p meshcatalog.PackagesByModuleValue) ([]byte, error) {
if err := checkPublished(p); err != nil {
return nil, err
}
p.V = keys.Version
return json.Marshal(p)
}
// DecodePublishedPackages reads a per-module packages value; it refuses an unknown version (keys.ErrUnknownVersion) and
// what EncodePublishedPackages refuses.
func DecodePublishedPackages(data []byte) (meshcatalog.PackagesByModuleValue, error) {
if err := keys.CheckVersion(data); err != nil {
return meshcatalog.PackagesByModuleValue{}, err
}
var p meshcatalog.PackagesByModuleValue
if err := json.Unmarshal(data, &p); err != nil {
return meshcatalog.PackagesByModuleValue{}, fmt.Errorf("index: a packages value does not fit its shape: %w", err)
}
if err := checkPublished(p); err != nil {
return meshcatalog.PackagesByModuleValue{}, err
}
return p, nil
}
// SeriesPackageKey is the key of a package's series in the packages state: its ecosystem name as a key part and the series.
func SeriesPackageKey(name, series string) string { return keys.EscapeName(name) + "." + series }
// IsSeriesPackageKey says whether a key of the packages state is a series key, not a per-module one (a series key always
// holds a dot, which keys.EscapeName never leaves in a per-module key).
func IsSeriesPackageKey(key string) bool { return strings.Contains(key, ".") }
// builtFrom says whether pkg is `<module>/<artifact>`: a module name (no slash) and an artifact named after it.
func builtFrom(pkg string) bool {
module, artifact, ok := strings.Cut(pkg, "/")
return ok && module != "" && artifact != ""
}
var seriesForm = regexp.MustCompile(`^[0-9]+\.[0-9]+$`)
func checkSeries(s meshcatalog.PackagesByPackageSeriesValue) error {
switch {
case s.Name == "":
return fmt.Errorf("index: a series of %q names no package in its ecosystem", s.Package)
case !seriesForm.MatchString(s.Series):
return fmt.Errorf("index: the series %q of %s is no <major>.<minor>", s.Series, s.Name)
case s.Package != "" && !builtFrom(s.Package):
return fmt.Errorf("index: the series %s of %s is built from %q, which is no <module>/<artifact>", s.Series, s.Name, s.Package)
}
if v := s.Newest; v != nil {
if v.Version == "" {
return fmt.Errorf("index: the newest version of %s %s names no version", s.Name, s.Series)
}
if c := v.Contents; c != nil {
pkgs := make([][2]string, len(c.Packages))
for i, d := range c.Packages {
pkgs[i] = [2]string{d.Package, d.Digest}
}
if err := checkPublishedFields(c.Module, c.Repository, c.Dir, c.Commit, c.Requires, c.At, pkgs); err != nil {
return err
}
}
}
if v := s.Numbered; v != nil {
if v.Version == "" {
return fmt.Errorf("index: the numbered version of %s %s names no version", s.Name, s.Series)
}
if c := v.Contents; c != nil {
pkgs := make([][2]string, len(c.Packages))
for i, d := range c.Packages {
pkgs[i] = [2]string{d.Package, d.Digest}
}
if err := checkPublishedFields(c.Module, c.Repository, c.Dir, c.Commit, c.Requires, c.At, pkgs); err != nil {
return err
}
}
}
return nil
}
// EncodeSeriesPackage is s as JSON at the kept values' version; it refuses what a reader could not take.
func EncodeSeriesPackage(s meshcatalog.PackagesByPackageSeriesValue) ([]byte, error) {
if err := checkSeries(s); err != nil {
return nil, err
}
s.V = keys.Version
return json.Marshal(s)
}
// DecodeSeriesPackage reads a series key's value; it refuses an unknown version and what EncodeSeriesPackage refuses.
func DecodeSeriesPackage(data []byte) (meshcatalog.PackagesByPackageSeriesValue, error) {
if err := keys.CheckVersion(data); err != nil {
return meshcatalog.PackagesByPackageSeriesValue{}, err
}
var s meshcatalog.PackagesByPackageSeriesValue
if err := json.Unmarshal(data, &s); err != nil {
return meshcatalog.PackagesByPackageSeriesValue{}, fmt.Errorf("index: a series value does not fit its shape: %w", err)
}
if err := checkSeries(s); err != nil {
return meshcatalog.PackagesByPackageSeriesValue{}, err
}
return s, nil
}
// publishedOfContents is a series version's contents as the value of a per-module packages key (the same fields).
func publishedOfContents(c meshcatalog.PackagesByPackageSeriesValueNewestContents) meshcatalog.PackagesByModuleValue {
p := meshcatalog.PackagesByModuleValue{At: c.At, Commit: c.Commit, Dir: c.Dir, Module: c.Module, Repository: c.Repository, Requires: c.Requires, V: c.V}
for _, d := range c.Packages {
p.Packages = append(p.Packages, meshcatalog.PackagesByModuleValuePackagesItem{Package: d.Package, Digest: d.Digest})
}
return p
}
// contentsOfPublished is the value of a per-module packages key as a series version's contents.
func contentsOfPublished(p meshcatalog.PackagesByModuleValue) *meshcatalog.PackagesByPackageSeriesValueNewestContents {
c := &meshcatalog.PackagesByPackageSeriesValueNewestContents{At: p.At, Commit: p.Commit, Dir: p.Dir, Module: p.Module, Repository: p.Repository, Requires: p.Requires, V: p.V}
for _, d := range p.Packages {
c.Packages = append(c.Packages, meshcatalog.PackagesByPackageSeriesValueNewestContentsPackagesItem{Package: d.Package, Digest: d.Digest})
}
return c
}
@@ -4,7 +4,9 @@ import (
"context"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/buildrecord"
)
// Issue 1614: a modules key written before the indexer learned paths (mesh-tools and node-tools live) gets its directory
@@ -13,8 +15,8 @@ import (
func heldValue(t *testing.T, st *memStore, name, path string) {
t.Helper()
data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-tools", Commit: "e0fadc43d9dda0bc24bd19bb05b021dd87da8582",
FP2: "src2:" + hex64('2'), Manifest: hex64('9'), Path: path})
data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-tools", Commit: "e0fadc43d9dda0bc24bd19bb05b021dd87da8582",
Fp2: "src2:" + hex64('2'), Manifest: hex64('9'), Path: path})
if err != nil {
t.Fatal(err)
}
@@ -31,17 +33,17 @@ func hex64(c byte) string {
func TestAnEmptyPathIsFilledOnceFromTheModulesNewestBuildRecord(t *testing.T) { // 1614
ix, mods := modulesIndexer(t,
[2]any{delivery.BuildKey("mesh-tools", "build-1"), buildRecord(t, "novox/mesh-tools", "mesh-tools", "", "2026-10-19T09:00:00Z")},
[2]any{delivery.BuildKey("node-tools", "build-2"), buildRecord(t, "novox/mesh-tools", "node-tools", "node-tools", "2026-10-19T09:00:00Z")})
[2]any{buildrecord.BuildKey("mesh-tools", "build-1"), buildRecord(t, "novox/mesh-tools", "mesh-tools", "", "2026-10-19T09:00:00Z")},
[2]any{buildrecord.BuildKey("node-tools", "build-2"), buildRecord(t, "novox/mesh-tools", "node-tools", "node-tools", "2026-10-19T09:00:00Z")})
heldValue(t, mods, "mesh-tools", "")
heldValue(t, mods, "node-tools", "")
before, _ := moduleOf(t, mods, "node-tools")
if err := ix.Recompute(context.Background()); err != nil {
t.Fatal(err)
}
for name, want := range map[string]string{"mesh-tools": delivery.RootPath, "node-tools": "node-tools"} {
for name, want := range map[string]string{"mesh-tools": meshcatalogclient.RootPath, "node-tools": "node-tools"} {
m, _ := moduleOf(t, mods, name)
if m.Path != want || m.FP2 != before.FP2 || m.Commit != before.Commit {
if m.Path != want || m.Fp2 != before.Fp2 || m.Commit != before.Commit {
t.Errorf("%s = %+v, want path %q and nothing else changed", name, m, want)
}
}
@@ -55,7 +57,7 @@ func TestAnEmptyPathIsFilledOnceFromTheModulesNewestBuildRecord(t *testing.T) {
}
func TestASetPathIsNeverOverwrittenByABuildRecord(t *testing.T) { // 1614
ix, mods := modulesIndexer(t, [2]any{delivery.BuildKey("node-tools", "build-2"), buildRecord(t, "novox/mesh-tools", "node-tools", "elsewhere", "2026-10-19T09:00:00Z")})
ix, mods := modulesIndexer(t, [2]any{buildrecord.BuildKey("node-tools", "build-2"), buildRecord(t, "novox/mesh-tools", "node-tools", "elsewhere", "2026-10-19T09:00:00Z")})
heldValue(t, mods, "node-tools", "node-tools")
writes := mods.writes
if err := ix.Recompute(context.Background()); err != nil {
@@ -10,9 +10,14 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -26,7 +31,7 @@ const (
// takein.PendingKey, so the key order is the merge order. An old `state` entry (the indexer is foundation, ADR
// 0595 §11), history 1, reached by the indexer alone.
PendingState = "pending"
PendingBucket = delivery.IndexerModule + "_" + PendingState
PendingBucket = Module + "_" + PendingState
// PendingTool reads it (ADR 0633 §5); TakeInPendingTool and DropPendingTool steer it.
PendingTool = "indexer_pending"
TakeInPendingTool = "indexer_take_in_pending"
@@ -52,8 +57,8 @@ type PendingRef struct {
Number int `json:"number,omitempty"`
Merge string `json:"merge"`
// Branch is the pull request's branch, which names its candidate: a merge covered by a newer one is taken out of it.
Branch string `json:"branch,omitempty"`
Members []delivery.Head `json:"members,omitempty"`
Branch string `json:"branch,omitempty"`
Members []meshdeliveryclient.Head `json:"members,omitempty"`
// Holds are the modules the merge's manifests name, which indexer_pending says it holds back.
Holds []string `json:"holds,omitempty"`
}
@@ -107,7 +112,7 @@ func DecodePending(data []byte) (Pending, forgemsg.Forge, error) {
}
if len(p.Forge) == 0 && p.Ref != nil {
r := p.Ref
return p, forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: r.Repository, Number: r.Number,
return p, forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: r.Repository, Number: r.Number,
Merge: r.Merge, Branch: r.Branch, Group: p.Group, Members: r.Members}, nil
}
f, err := forgemsg.DecodeForge(p.Forge)
@@ -462,7 +467,7 @@ func (ix *Indexer) EndGroup(ctx context.Context, end forgemsg.ForgeGroupEnd) (Ap
}
isHeld := func(m forgemsg.GroupMerge) bool {
return slices.ContainsFunc(held, func(e PendingEntry) bool {
return delivery.SameRepository(e.Forge.Repository, m.Repository) && e.Forge.Number == m.Number
return trunk.SameRepository(e.Forge.Repository, m.Repository) && e.Forge.Number == m.Number
})
}
var missing []string
@@ -505,7 +510,7 @@ func (ix *Indexer) takenAlready(ctx context.Context, m forgemsg.GroupMerge) (boo
if ix.Taken == nil {
return false, nil
}
key, err := delivery.RepositoryKey(m.Repository)
key, err := trunk.RepositoryKey(m.Repository)
if err != nil {
return false, err
}
@@ -516,7 +521,7 @@ func (ix *Indexer) takenAlready(ctx context.Context, m forgemsg.GroupMerge) (boo
if err != nil {
return false, fmt.Errorf("taken.%s: %w", key, err)
}
cur, err := delivery.DecodeTaken(value)
cur, err := takein.DecodeTaken(value)
if err != nil || cur.Merge == "" {
return false, nil
}
@@ -706,7 +711,7 @@ func (ix *Indexer) dry() *dryRun {
ix.mu.Lock()
x.source = sourceNow{rev: ix.source.rev, heads: maps.Clone(ix.source.heads)}
x.previews = maps.Clone(ix.previews)
x.toolchains = map[string]map[string]delivery.Toolchain{}
x.toolchains = map[string]map[string]nodebuildgo.Toolchain{}
for l, by := range ix.toolchains {
x.toolchains[l] = maps.Clone(by)
}
@@ -737,7 +742,7 @@ func (ix *Indexer) computeOver(ctx context.Context, ready []PendingEntry) (strin
return "", d, fmt.Errorf("computing the take-in of %s (%s#%d): %w", e.Key, e.Forge.Repository, e.Forge.Number, err)
}
}
key, _ := delivery.ModulesKey(delivery.IndexerModule)
key, _ := meshcatalogclient.ModulesKey(Module)
value, _, found, err := d.ix.Modules.Get(ctx, key)
if err != nil {
return "", d, err
@@ -745,14 +750,14 @@ func (ix *Indexer) computeOver(ctx context.Context, ready []PendingEntry) (strin
if !found {
return NoBuild + "the indexer holds no record of itself", d, nil
}
m, err := delivery.DecodeModule(value)
m, err := meshcatalogclient.DecodeModule(value)
if err != nil {
return "", d, nil
}
if m.FP2 == "" {
if m.Fp2 == "" {
return NoBuild + m.None2, d, nil
}
return m.FP2, d, nil
return m.Fp2, d, nil
}
// claim marks the entries as taken in by this take-in, by compare-and-set on the revisions read. A lost one (another
@@ -911,18 +916,18 @@ func saidWaits(entries []PendingEntry) []takein.TakeIn {
// manifest, from the dry run; the manifests and the packages are written by the dry run itself. The revision is said
// pending under its oldest merge's key, and a wait it was said under before by another key is said taken.
func (ix *Indexer) holdFor(ctx context.Context, ready []PendingEntry, names, own string, d *dryRun) error {
key, _ := delivery.ModulesKey(delivery.IndexerModule)
key, _ := meshcatalogclient.ModulesKey(Module)
if err := copyKey(ctx, d.modules, ix.Modules, key); err != nil {
return fmt.Errorf("the indexer's own module entry: %w", err)
}
if gk, err := graph.Key(delivery.IndexerModule); err == nil {
if gk, err := graph.Key(Module); err == nil {
if err := copyKey(ctx, d.graph, ix.Graph, gk); err != nil && !errors.Is(err, process.ErrNotYet) {
return fmt.Errorf("the indexer's key of the module graph: %w", err)
}
}
if value, _, found, err := d.ix.Modules.Get(ctx, key); err == nil && found {
if m, err := delivery.DecodeModule(value); err == nil && m.Manifest != "" {
if vk, err := delivery.VerdictsKey(m.Manifest); err == nil {
if m, err := meshcatalogclient.DecodeModule(value); err == nil && m.Manifest != "" {
if vk, err := meshcatalogclient.VerdictsKey(m.Manifest); err == nil {
if err := copyKey(ctx, d.verdicts, ix.Verdicts, vk); err != nil && !errors.Is(err, process.ErrNotYet) {
return fmt.Errorf("the verdict on the indexer's manifest: %w", err)
}
@@ -9,10 +9,10 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/buscompose"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -31,7 +31,7 @@ type takeInBed struct {
func newTakeInBed(t *testing.T) *takeInBed {
t.Helper()
ix, mods, f := newFormIndexerUnmerged(t, mod{"avahi", catalogueManifest(t, "avahi")},
mod{delivery.IndexerModule, renamed(t, "avahi", delivery.IndexerModule)})
mod{Module, renamed(t, "avahi", Module)})
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
@@ -55,7 +55,7 @@ func newTakeInBed(t *testing.T) *takeInBed {
logf(format, args...)
}
}
own := b.fp(t, delivery.IndexerModule)
own := b.fp(t, Module)
if own == "" || b.fp(t, "avahi") == "" {
t.Fatalf("the bed's modules have no fingerprint")
}
@@ -66,7 +66,7 @@ func newTakeInBed(t *testing.T) *takeInBed {
func (b *takeInBed) fp(t *testing.T, module string) string {
t.Helper()
m, _ := moduleOf(t, b.mods, module)
return m.FP2
return m.Fp2
}
func (b *takeInBed) commit(t *testing.T, module string) string {
@@ -87,7 +87,7 @@ func (b *takeInBed) merge(number int, c, avahi, indexer string) forgemsg.Forge {
f.Trees["modules/avahi"] = avahi
}
if indexer != "" {
f.Trees["modules/"+delivery.IndexerModule] = indexer
f.Trees["modules/"+Module] = indexer
}
return f
}
@@ -147,7 +147,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
})
t.Run("T7 and T3 a merge alone, an indexer change, a merge behind it and a revert", func(t *testing.T) {
b := newTakeInBed(t)
own, avahi0 := b.fp(t, delivery.IndexerModule), b.fp(t, "avahi")
own, avahi0 := b.fp(t, Module), b.fp(t, "avahi")
// A merge with no change to the indexer and nothing pending is taken in alone, as before.
b.arrive(t, 1, b.merge(20, sha('b'), "tree-avahi-2", ""))
if avahi1 := b.fp(t, "avahi"); avahi1 == avahi0 || len(b.keys()) != 0 || len(b.said) != 0 {
@@ -156,11 +156,11 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
avahi1, verdicts := b.fp(t, "avahi"), len(b.ix.Verdicts.(*memStore).vals)
// T4: a change to the indexer writes only its entry (with its graph key and verdict); the rest is pending.
b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2"))
named := b.fp(t, delivery.IndexerModule)
named := b.fp(t, Module)
if named == own || b.fp(t, "avahi") != avahi1 || b.commit(t, "avahi") != sha('b') {
t.Fatalf("indexer change: indexer %s (own %s), avahi %s at %s: want only the indexer written", named, own, b.fp(t, "avahi"), b.commit(t, "avahi"))
}
if g := graphOf(t, b.ix, delivery.IndexerModule); g.FP != named {
if g := graphOf(t, b.ix, Module); g.FP != named {
t.Errorf("graph of the indexer names %s, want %s", g.FP, named)
}
if n := len(b.ix.Verdicts.(*memStore).vals); n > verdicts+1 {
@@ -180,9 +180,9 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
t.Fatalf("behind: pending %v, avahi at %s, said %v: want both pending and nothing said again", b.keys(), b.commit(t, "avahi"), b.events())
}
// A revert brings the indexer back to the running build: everything is taken in, oldest first.
b.arrive(t, 4, b.merge(23, sha('e'), "tree-avahi-5", b.base.Trees["modules/"+delivery.IndexerModule]))
if len(b.keys()) != 0 || b.fp(t, delivery.IndexerModule) != own || b.commit(t, "avahi") != sha('e') {
t.Fatalf("revert: pending %v, indexer %s, avahi at %s: want all taken in", b.keys(), b.fp(t, delivery.IndexerModule), b.commit(t, "avahi"))
b.arrive(t, 4, b.merge(23, sha('e'), "tree-avahi-5", b.base.Trees["modules/"+Module]))
if len(b.keys()) != 0 || b.fp(t, Module) != own || b.commit(t, "avahi") != sha('e') {
t.Fatalf("revert: pending %v, indexer %s, avahi at %s: want all taken in", b.keys(), b.fp(t, Module), b.commit(t, "avahi"))
}
if want := []string{"pending " + takein.PendingKey(2), "taken " + takein.PendingKey(2)}; !slices.Equal(b.events(), want) {
t.Errorf("said %v, want %v", b.events(), want)
@@ -193,9 +193,9 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2"))
g := b.merge(30, sha('f'), "", "")
g.Repository, g.Trees, g.Manifests, g.Locks, g.Group = "novox/mesh-sdk", map[string]string{}, nil, nil, "feat/x"
g.Members = []delivery.Head{{Repository: "novox/mesh-sdk", Number: 30, Head: g.Head}, {Repository: "novox/mesh-host", Number: 4, Head: sha('9')}}
g.Members = []meshdeliveryclient.Head{{Repository: "novox/mesh-sdk", Number: 30, Head: g.Head}, {Repository: "novox/mesh-host", Number: 4, Head: sha('9')}}
b.arrive(t, 5, g)
named := b.fp(t, delivery.IndexerModule)
named := b.fp(t, Module)
// The new indexer starts; its build is not known at first: a hand ask is answered meanwhile (R5), and the start waits.
next := &Indexer{}
*next = Indexer{Store: b.ix.Store, Modules: b.ix.Modules, Manifests: b.ix.Manifests, Graph: b.ix.Graph, Verdicts: b.ix.Verdicts,
@@ -234,7 +234,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
member := func(repo string, number int, c string) forgemsg.Forge {
f := b.merge(number, c, "", "")
f.Group = "feat/take-in"
f.Members = []delivery.Head{{Repository: catalogueRepo, Number: 40, Head: sha('1')}, {Repository: "novox/mesh-sdk", Number: 41, Head: sha('2')}}
f.Members = []meshdeliveryclient.Head{{Repository: catalogueRepo, Number: 40, Head: sha('1')}, {Repository: "novox/mesh-sdk", Number: 41, Head: sha('2')}}
if repo != catalogueRepo {
f.Repository, f.Trees, f.Manifests, f.Locks = repo, map[string]string{}, nil, nil
}
@@ -287,7 +287,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
t.Run("T5 two take-ins of one pending revision: the one that loses the claim writes nothing", func(t *testing.T) {
b := newTakeInBed(t)
b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2"))
named := b.fp(t, delivery.IndexerModule)
named := b.fp(t, Module)
b.ix.ownMu.Lock()
b.ix.ownFP = named
b.ix.ownMu.Unlock()
@@ -343,7 +343,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
b := newTakeInBed(t)
b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2"))
b.arrive(t, 3, b.merge(22, sha('d'), "tree-avahi-4", "tree-indexer-2"))
named := b.fp(t, delivery.IndexerModule)
named := b.fp(t, Module)
b.ix.ownMu.Lock()
b.ix.ownFP = named
b.ix.ownMu.Unlock()
@@ -369,7 +369,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
// The build the revision names under the next toolchain, worked out on a dry indexer; this one runs it.
x := b.ix.dry().ix
for _, node := range []string{"novox", "ace"} {
key, _ := delivery.ToolchainKey(node, "go")
key, _ := ToolchainKey(node, "go")
x.PutToolchain("mesh-build-go", key, toolchainValue(t, "go", "go1.27.2", "1"), false)
}
ready, _, _ := b.ix.loadNewest(ctx, WaitsOf(entries).Ready)
@@ -403,7 +403,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
answer = out.(takein.PendingAnswer)
}
}
if len(answer.Waits) != 1 || answer.Waits[0].Key != takein.PendingKey(2) || !slices.Equal(answer.Waits[0].Holds, []string{"avahi", delivery.IndexerModule}) ||
if len(answer.Waits) != 1 || answer.Waits[0].Key != takein.PendingKey(2) || !slices.Equal(answer.Waits[0].Holds, []string{"avahi", Module}) ||
answer.Waits[0].Said != takein.TakeInPendingEvent || len(answer.Waits[0].Keys) != 2 {
t.Fatalf("indexer_pending = %+v", answer)
}
@@ -447,7 +447,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
t.Fatal(err)
}
}
key, _ := delivery.CandidatesKey("feat/g")
key, _ := CandidatesKey("feat/g")
if _, _, found, _ := b.ix.Store.Get(ctx, key); !found {
t.Fatal("the group has no candidate")
}
@@ -457,7 +457,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
if len(b.keys()) != 2 || len(raw) > 4096 || b.commit(t, "avahi") != sha('b') {
t.Fatalf("a big merge held: pending %v, value %d bytes, avahi at %s: want two small values", b.keys(), len(raw), b.commit(t, "avahi"))
}
named := b.fp(t, delivery.IndexerModule)
named := b.fp(t, Module)
// The new indexer starts with nothing in memory and the bridge down: the newest merge is said lost with the bridge's
// words (event, state), the older one is covered and not lost, and nothing is taken in.
next := &Indexer{Store: b.ix.Store, Modules: b.ix.Modules, Manifests: b.ix.Manifests, Graph: b.ix.Graph, Verdicts: b.ix.Verdicts,
@@ -497,7 +497,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
}
// A hand take-in of a merge by its commit asks the bridge and runs the message through Arrive.
b.ix.Message = answers
b.publish(t, big(b.merge(30, sha('e'), "tree-avahi-6", b.base.Trees["modules/"+delivery.IndexerModule])))
b.publish(t, big(b.merge(30, sha('e'), "tree-avahi-6", b.base.Trees["modules/"+Module])))
if r := b.ix.TakeInMerge(ctx, catalogueRepo, sha('e'), 9, "lead@shanks", "held by the cap"); !r.Stored || b.commit(t, "avahi") != sha('e') {
t.Fatalf("merge take-in: %+v, avahi at %s: want it taken in", r, b.commit(t, "avahi"))
}
@@ -505,9 +505,9 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633)
t.Errorf("merge take-in of a merge the bridge does not know: %+v", r)
}
// An old value that holds the message whole reads as before.
old := b.merge(31, sha('f'), "tree-avahi-7", b.base.Trees["modules/"+delivery.IndexerModule])
old := b.merge(31, sha('f'), "tree-avahi-7", b.base.Trees["modules/"+Module])
rawOld, _ := forgemsg.EncodeForge(old)
data, _ := encodePending(Pending{Forge: rawOld, Since: b.ix.now(), LastWriter: b.fp(t, delivery.IndexerModule)})
data, _ := encodePending(Pending{Forge: rawOld, Since: b.ix.now(), LastWriter: b.fp(t, Module)})
if err := b.pending.Create(ctx, takein.PendingKey(11), data); err != nil {
t.Fatal(err)
}
@@ -8,7 +8,11 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"mesh-delivery-indexer/internal/fingerprint"
)
@@ -27,7 +31,7 @@ const (
NonePreviewConflict = "does not merge: the head conflicts with the trunk in "
// NonePreviewNoModule is for a module whose directory the preview names no manifest for: mesh-sdk's words, which the
// assigner reads as the change removing the module (issue 1297).
NonePreviewNoModule = delivery.CandidateNoneNoModule
NonePreviewNoModule = CandidateNoneNoModule
// NonePreviewNoManifest is for a module whose manifest at the merge the indexer does not hold.
NonePreviewNoManifest = "the manifest of the merge is not kept"
// NoneNoDirectory is for a module whose directory in its repository neither the modules bucket nor the preview tells.
@@ -35,10 +39,10 @@ const (
)
// PutPreview takes a change of the previews bucket in without writing any candidate: a put of a preview (key
// delivery.PreviewKey) or its delete. It answers whether the previews held changed. A key or a value that is not a
// PreviewKey) or its delete. It answers whether the previews held changed. A key or a value that is not a
// preview is said in the log and left out.
func (ix *Indexer) PutPreview(key string, value []byte, deleted bool) bool {
if _, _, _, err := delivery.ParsePreviewKey(key); err != nil {
if _, _, _, err := ParsePreviewKey(key); err != nil {
ix.logf("previews.%s is not a preview key, left out: %v", key, err)
return false
}
@@ -51,15 +55,15 @@ func (ix *Indexer) PutPreview(key string, value []byte, deleted bool) bool {
delete(ix.previews, key)
return true
}
pv, err := delivery.DecodePreview(value)
pv, err := DecodePreview(value)
if err != nil {
ix.logf("previews.%s is not a preview, left out: %v", key, err)
return false
}
if ix.previews == nil {
ix.previews = map[string]delivery.Preview{}
ix.previews = map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}
}
if cur, had := ix.previews[key]; had && delivery.SamePreview(cur, pv) {
if cur, had := ix.previews[key]; had && SamePreview(cur, pv) {
return false
}
ix.previews[key] = pv
@@ -96,7 +100,7 @@ func (ix *Indexer) RecomputeCandidates(ctx context.Context) error {
if !found {
return nil, false, nil
}
c, err := delivery.DecodeCandidate(value)
c, err := DecodeCandidate(value)
if err != nil {
ix.logf("candidates.%s holds what is not a candidate, left: %v", key, err)
return nil, false, nil
@@ -104,7 +108,7 @@ func (ix *Indexer) RecomputeCandidates(ctx context.Context) error {
if !ix.withPreviews(ctx, &c) {
return nil, false, nil
}
data, err := delivery.EncodeCandidate(c)
data, err := EncodeCandidate(c)
return data, false, err
})
if err != nil {
@@ -116,24 +120,24 @@ func (ix *Indexer) RecomputeCandidates(ctx context.Context) error {
// previewOf is the preview of a head merged onto the trunk head the source heads name: found, or stale when only a preview
// onto another trunk head exists, or neither (found false, stale false) when the source is not known or none exists.
func (ix *Indexer) previewOf(heads map[string]string, repository, head string) (pv delivery.Preview, found, stale bool) {
func (ix *Indexer) previewOf(heads map[string]string, repository, head string) (pv meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue, found, stale bool) {
ix.mu.Lock()
defer ix.mu.Unlock()
trunk := heads[repository]
if trunk == "" {
return delivery.Preview{}, false, false
trunkHead := heads[repository]
if trunkHead == "" {
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}, false, false
}
if key, err := delivery.PreviewKey(repository, head, trunk); err == nil {
if key, err := PreviewKey(repository, head, trunkHead); err == nil {
if pv, ok := ix.previews[key]; ok {
return pv, true, false
}
}
for _, pv := range ix.previews {
if pv.Repository == repository && pv.Head == head {
return delivery.Preview{}, false, true
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}, false, true
}
}
return delivery.Preview{}, false, false
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}, false, false
}
// withPreviews gives each touched module of the candidate's pull requests the fingerprint its preview gives it, or the
@@ -144,7 +148,7 @@ func (ix *Indexer) previewOf(heads map[string]string, repository, head string) (
// whenever the fingerprints are, so the builder merges a preview onto the trunk head its fingerprint was computed on (the head
// of the pull request's repository in that revision), never onto a module value's commit, which stays at the commit its
// fingerprint first appeared at.
func (ix *Indexer) withPreviews(ctx context.Context, c *delivery.Candidate) bool {
func (ix *Indexer) withPreviews(ctx context.Context, c *meshcatalog.CandidatesByGroupValue) bool {
ix.mu.Lock()
rev, heads := ix.source.rev, ix.source.heads
ix.mu.Unlock()
@@ -153,13 +157,13 @@ func (ix *Indexer) withPreviews(ctx context.Context, c *delivery.Candidate) bool
// latest published version (novox/hq ADR 0573 §4): a group's members build against the group's own package.
members := map[string]string{}
for _, p := range c.Pulls {
members[delivery.RepositoryName(p.Repository)] = p.Head.Head
members[trunk.RepositoryName(p.Repository)] = p.Head
}
var held map[string]delivery.Module
var held map[string]meshcatalog.ModulesByModuleValue
listed := false
for i := range c.Pulls {
p := &c.Pulls[i]
pv, found, stale := ix.previewOf(heads, p.Repository, p.Head.Head)
pv, found, stale := ix.previewOf(heads, p.Repository, p.Head)
if !found && !stale {
continue
}
@@ -175,7 +179,7 @@ func (ix *Indexer) withPreviews(ctx context.Context, c *delivery.Candidate) bool
// whose manifest is the one the pull request gave the module (a module the pull request adds; ADR 0501 clause 6).
dirs := map[string]string{}
for name, m := range held {
if delivery.SameRepository(m.Repository, p.Repository) && m.Path != "" {
if trunk.SameRepository(m.Repository, p.Repository) && m.Path != "" {
dirs[name] = m.Path
}
}
@@ -191,7 +195,7 @@ func (ix *Indexer) withPreviews(ctx context.Context, c *delivery.Candidate) bool
}
fps := maps.Clone(p.Fps)
for _, m := range p.Modules {
next := delivery.CandidateModule{Manifest: p.Fps[m].Manifest}
next := meshcatalog.CandidatesByGroupValuePullsItemFpsValue{Manifest: p.Fps[m].Manifest}
switch {
case stale:
next.None2 = NonePreviewStale
@@ -206,22 +210,22 @@ func (ix *Indexer) withPreviews(ctx context.Context, c *delivery.Candidate) bool
next = ix.candidateModule(ctx, m, dir, pv, held, members)
}
if fps == nil {
fps = map[string]delivery.CandidateModule{}
fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{}
}
if !reflect.DeepEqual(fps[m], next) {
fps[m] = next
changed = true
}
}
if changed || !maps.EqualFunc(fps, p.Fps, func(a, b delivery.CandidateModule) bool { return reflect.DeepEqual(a, b) }) {
if changed || !maps.EqualFunc(fps, p.Fps, func(a, b meshcatalog.CandidatesByGroupValuePullsItemFpsValue) bool { return reflect.DeepEqual(a, b) }) {
p.Fps = fps
}
}
if changed {
c.FPSet = delivery.FingerprintSet(c.Pulls)
c.Fpset = meshcatalogclient.FingerprintSet(c.Pulls)
}
if read && rev != 0 && c.Base != rev && (changed || c.Base == 0) {
c.Base, changed = rev, true
if read && rev != 0 && c.Base != int64(rev) && (changed || c.Base == 0) {
c.Base, changed = int64(rev), true
}
return changed
}
@@ -230,18 +234,18 @@ func (ix *Indexer) withPreviews(ctx context.Context, c *delivery.Candidate) bool
// merge (tree, manifest, locks), its bases' and the builder's fingerprints as the trunk's modules hold them, the toolchains
// the build modules agree on, and the version of each mesh package it uses (candidatePackages), by the rules Recompute
// applies to a module of the trunk.
func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delivery.Preview, held map[string]delivery.Module, members map[string]string) delivery.CandidateModule {
func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue, held map[string]meshcatalog.ModulesByModuleValue, members map[string]string) meshcatalog.CandidatesByGroupValuePullsItemFpsValue {
pm, ok := pv.Modules[dir]
if !ok && dir == "" {
pm, ok = pv.Modules[delivery.RootPath]
pm, ok = pv.Modules[meshcatalogclient.RootPath]
}
if !ok && dir == delivery.RootPath {
if !ok && dir == meshcatalogclient.RootPath {
pm, ok = pv.Modules[""]
}
if !ok {
return delivery.CandidateModule{None2: NonePreviewNoModule}
return meshcatalog.CandidatesByGroupValuePullsItemFpsValue{None2: NonePreviewNoModule}
}
out := delivery.CandidateModule{Manifest: pm.Manifest}
out := meshcatalog.CandidatesByGroupValuePullsItemFpsValue{Manifest: pm.Manifest}
if ix.Manifests == nil {
out.None2 = NonePreviewNoManifest
return out
@@ -251,7 +255,7 @@ func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delive
out.None2 = NonePreviewNoManifest
return out
}
kept, err := delivery.DecodeManifest(value)
kept, err := meshcatalogclient.DecodeManifest(value)
if err != nil {
out.None2 = NonePreviewNoManifest
return out
@@ -259,31 +263,31 @@ func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delive
// The preview carries the directory's raw inputs (its tree, the named paths' objects, the lock hashes): the indexer
// computes the named tree (ADR 0593 §2). The version line's packages are the trunk module's, unless an older build
// module still names them.
trunk := held[m]
trunkMod := held[m]
packages := fingerprint.OtherPackages(pm.Packages, pv.Repository)
if trunk.Inputs2 != nil {
if trunkMod.Inputs2 != nil {
if len(pm.Packages) == 0 {
packages = trunk.Inputs2.Packages
packages = trunkMod.Inputs2.Packages
} else {
// The siblings its go.mod replaces by a directory are the trunk module's: no go.sum names them.
for _, p := range trunk.Inputs2.Packages {
if repo, _, ok := delivery.PackageRepository(p); ok && delivery.SameRepository(repo, pv.Repository) {
for _, p := range trunkMod.Inputs2.Packages {
if repo, _, ok := trunk.PackageRepository(p); ok && trunk.SameRepository(repo, pv.Repository) {
packages = append(packages, p)
}
}
}
}
in := inputsOfNamed(m, kept.JSON, pm.Tree, pm.Locks, pm.Paths, packages, ix.baseFP(ctx))
in := inputsOfNamed(m, kept.Manifest, pm.Tree, pm.Locks, pm.Paths, packages, ix.baseFP(ctx))
if in.Kept == nil {
out.None2 = in.None2
return out
}
if in.None2 != "" && in.None2 != delivery.ToolchainNoReport && in.None2 != None2Pending {
if in.None2 != "" && in.None2 != sdkstep.ToolchainNoReport && in.None2 != None2Pending {
out.None2 = in.None2 // a context, or a base with no fingerprint
return out
}
tools := ix.agreed()
toolchains := map[string]delivery.Toolchain2{}
toolchains := map[string]meshcatalogclient.Toolchain2{}
for _, artifact := range slices.Sorted(maps.Keys(in.Languages)) {
t := tools[in.Languages[artifact]]
if t.none != "" {
@@ -294,27 +298,27 @@ func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delive
}
inputs := *in.Kept // no builder input (ADR 0530 §1)
var fp, none string
var versions delivery.PackageVersions
if trunk.Inputs2 != nil && len(trunk.Inputs2.Imports) > 0 {
inputs.Imports, inputs.ImportForm = trunk.Inputs2.Imports, trunk.Inputs2.ImportForm
var versions sdkstep.PackageVersions
if trunkMod.Inputs2 != nil && len(trunkMod.Inputs2.Imports) > 0 {
inputs.Imports, inputs.ImportForm = trunkMod.Inputs2.Imports, trunkMod.Inputs2.ImportForm
}
// The import form against the group's own heads of the packages it publishes (ADR 0573 §4, ADR 0593 §2), as the trunk
// computes it once merged, the trunk's held fingerprint standing where nothing the build was built from changed.
published := ix.candidatePublished(ctx, members, held)
// A package of a repository outside the group by the series the candidate's manifest accepts (DLV-I6); a member's by the
// group's head.
var pins delivery.PackageVersions
var pins sdkstep.PackageVersions
if _, man, err := ix.manifestRaw(ctx, pm.Manifest); err == nil {
series := ix.SeriesAll(ctx)
for key, s := range series {
if repo, _, _ := delivery.PackageRepository(s.GoModule); members[repo] != "" {
if repo, _, _ := trunk.PackageRepository(s.GoModule); members[repo] != "" {
delete(series, key)
}
}
published, pins = acceptedSeries(series, man, pv.Repository, published)
}
if len(inputs.Imports) > 0 && len(fingerprint.Resolve(pv.Repository, inputs.Imports, published).Missing) == 0 {
f := formOf(m, pv.Repository, &inputs, toolchains, trunk, held, published, pins, nil)
f := formOf(m, pv.Repository, &inputs, toolchains, trunkMod, held, published, pins, nil)
fp, none, versions = f.fp, f.none, f.versions
} else {
inputs.Imports, inputs.ImportForm = nil, ""
@@ -322,7 +326,7 @@ func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delive
for p, v := range pins {
if _, named := versions[p]; named || slices.Contains(inputs.Packages, p) {
if versions == nil {
versions = delivery.PackageVersions{}
versions = sdkstep.PackageVersions{}
}
versions[p] = v
}
@@ -335,7 +339,7 @@ func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delive
out.None2 = none
return out
}
out.FP2, out.Packages = fp, versions
out.Fp2, out.Packages = fp, versions
if len(inputs.Paths) > 0 {
out.Paths = inputs.Paths // the builder's fetch for the candidate names them (ADR 0499 clause 4)
}
@@ -345,13 +349,13 @@ func (ix *Indexer) candidateModule(ctx context.Context, m, dir string, pv delive
// candidatePackages is the version each package of a candidate's module is built against (novox/hq ADR 0573 §2, §4): the
// head of the candidate's pull request whose repository publishes it (members, by repository), else its latest published
// version, as the trunk's modules hold it. A package with neither is left out, and the fingerprint is then none.
func candidatePackages(packages []string, held map[string]delivery.Module, members map[string]string) delivery.PackageVersions {
func candidatePackages(packages []string, held map[string]meshcatalog.ModulesByModuleValue, members map[string]string) sdkstep.PackageVersions {
versions, _ := fingerprint.HeldVersionsOf(packages, held)
for _, p := range packages {
repository, _, _ := delivery.PackageRepository(p)
repository, _, _ := trunk.PackageRepository(p)
if head := members[repository]; head != "" {
if versions == nil {
versions = delivery.PackageVersions{}
versions = sdkstep.PackageVersions{}
}
versions[p] = head
}
@@ -7,8 +7,13 @@ import (
"strings"
"testing"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/internal/fingerprint"
)
@@ -27,7 +32,7 @@ func previewBed(t *testing.T, report bool) (*Indexer, *memStore, *memStore) {
if report {
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
}
if err := ix.ApplySource(context.Background(), 5, delivery.Source{Group: "g", Heads: map[string]string{catalogueRepo: trunkNow}}); err != nil {
if err := ix.ApplySource(context.Background(), 5, meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{catalogueRepo: trunkNow}}); err != nil {
t.Fatal(err)
}
return ix, mods, ix.Store.(*memStore)
@@ -46,18 +51,18 @@ func avahiPull(t *testing.T, head string) forgemsg.Forge {
return f
}
func avahiPreview(t *testing.T, trunk, head string) delivery.Preview {
return delivery.Preview{Repository: catalogueRepo, Head: head, Trunk: trunk, Merge: mergeSha, Modules: map[string]delivery.PreviewModule{
"modules/avahi": {Tree: "tree-avahi", Manifest: delivery.ManifestDigest([]byte(avahiOnTools(t))), Locks: map[string]string{"go.sum": avahiGoSum}}}}
func avahiPreview(t *testing.T, trunk, head string) meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue {
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: catalogueRepo, Head: head, Trunk: trunk, Merge: mergeSha, Modules: map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{
"modules/avahi": {Tree: "tree-avahi", Manifest: meshcatalogclient.ManifestDigest([]byte(avahiOnTools(t))), Locks: map[string]string{"go.sum": avahiGoSum}}}}
}
func applyPreview(t *testing.T, ix *Indexer, pv delivery.Preview) {
func applyPreview(t *testing.T, ix *Indexer, pv meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue) {
t.Helper()
key, err := delivery.PreviewKey(pv.Repository, pv.Head, pv.Trunk)
key, err := PreviewKey(pv.Repository, pv.Head, pv.Trunk)
if err != nil {
t.Fatal(err)
}
data, err := delivery.EncodePreview(pv)
data, err := EncodePreview(pv)
if err != nil {
t.Fatal(err)
}
@@ -66,7 +71,7 @@ func applyPreview(t *testing.T, ix *Indexer, pv delivery.Preview) {
}
}
func fpOf(t *testing.T, st *memStore) delivery.CandidateModule {
func fpOf(t *testing.T, st *memStore) meshcatalog.CandidatesByGroupValuePullsItemFpsValue {
t.Helper()
c := candidateOf(t, st)
if len(c.Pulls) != 1 {
@@ -78,7 +83,7 @@ func fpOf(t *testing.T, st *memStore) delivery.CandidateModule {
func TestACandidatesModuleGetsTheFingerprintATrunkModuleOfTheSameTreeHas(t *testing.T) { // 1445/AC2
ix, mods, cands := previewBed(t, true)
trunk, _ := moduleOf(t, mods, "avahi")
if trunk.FP2 == "" {
if trunk.Fp2 == "" {
t.Fatalf("the trunk's avahi has no fingerprint of the new form: %+v", trunk)
}
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
@@ -89,12 +94,12 @@ func TestACandidatesModuleGetsTheFingerprintATrunkModuleOfTheSameTreeHas(t *test
}
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
got := fpOf(t, cands)
if got.FP2 != trunk.FP2 || got.None2 != "" || got.Manifest != delivery.ManifestDigest([]byte(avahiOnTools(t))) {
t.Fatalf("candidate avahi = %+v, want the trunk's fingerprint %s", got, trunk.FP2)
if got.Fp2 != trunk.Fp2 || got.None2 != "" || got.Manifest != meshcatalogclient.ManifestDigest([]byte(avahiOnTools(t))) {
t.Fatalf("candidate avahi = %+v, want the trunk's fingerprint %s", got, trunk.Fp2)
}
c := candidateOf(t, cands)
if c.FPSet != delivery.FingerprintSet(c.Pulls) || c.FPSet == "" {
t.Errorf("fpset = %q", c.FPSet)
if c.Fpset != meshcatalogclient.FingerprintSet(c.Pulls) || c.Fpset == "" {
t.Errorf("fpset = %q", c.Fpset)
}
// The candidate names the source revision the preview was read onto (5, whose catalogue head is the preview's trunk):
// the builder merges the preview onto that head, never onto the module value's commit.
@@ -109,14 +114,14 @@ func TestACandidatesModuleGetsTheFingerprintATrunkModuleOfTheSameTreeHas(t *test
pv.Modules["modules/avahi"] = m
pv.Merge = "7777777777777777777777777777777777777777"
applyPreview(t, ix, pv)
if other := fpOf(t, cands); other.FP2 == "" || other.FP2 == trunk.FP2 {
if other := fpOf(t, cands); other.Fp2 == "" || other.Fp2 == trunk.Fp2 {
t.Errorf("a changed tree: %+v", other)
}
// Issue 1585 (ADR 0573 §2, §4): a candidate's module is built against the head of the candidate's pull request that
// publishes a package it uses, else against the package's latest published version; a preview naming a package of
// mesh-sdk that nothing published gives no fingerprint, and one of its own repository's comes with its tree.
const sdk, own = "git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-catalog/contract"
held := map[string]delivery.Module{"mesh-sdk": {Repository: "novox/mesh-sdk", Path: delivery.RootPath, Commit: trunkOld}}
held := map[string]meshcatalog.ModulesByModuleValue{"mesh-sdk": {Repository: "novox/mesh-sdk", Path: meshcatalogclient.RootPath, Commit: trunkOld}}
if got := candidatePackages([]string{sdk}, held, map[string]string{catalogueRepo: h1}); got[sdk] != trunkOld {
t.Errorf("outside a group: %v, want mesh-sdk at its trunk %s", got, trunkOld)
}
@@ -127,7 +132,7 @@ func TestACandidatesModuleGetsTheFingerprintATrunkModuleOfTheSameTreeHas(t *test
pv.Modules["modules/avahi"] = m
pv.Merge = "8888888888888888888888888888888888888888"
applyPreview(t, ix, pv)
if got := fpOf(t, cands); got.FP2 != "" || got.None2 != fingerprint.NoPackageVersion+sdk {
if got := fpOf(t, cands); got.Fp2 != "" || got.None2 != fingerprint.NoPackageVersion+sdk {
t.Errorf("a package nothing published: %+v", got)
}
}
@@ -138,8 +143,8 @@ func TestAnUnknownToolchainLeavesTheCandidatesModuleNamingIt(t *testing.T) { //
t.Fatal(err)
}
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
if got := fpOf(t, cands); got.FP2 != "" || !strings.Contains(got.None2, delivery.ToolchainNoReport) {
t.Errorf("candidate avahi = %+v, want none2 naming %q", got, delivery.ToolchainNoReport)
if got := fpOf(t, cands); got.Fp2 != "" || !strings.Contains(got.None2, sdkstep.ToolchainNoReport) {
t.Errorf("candidate avahi = %+v, want none2 naming %q", got, sdkstep.ToolchainNoReport)
}
}
@@ -149,15 +154,15 @@ func TestAPreviewMadeOntoAnOldTrunkIsOutOfDate(t *testing.T) { // 1445/AC3
t.Fatal(err)
}
applyPreview(t, ix, avahiPreview(t, trunkOld, h1))
if got := fpOf(t, cands); got.FP2 != "" || got.None2 != NonePreviewStale {
if got := fpOf(t, cands); got.Fp2 != "" || got.None2 != NonePreviewStale {
t.Errorf("candidate avahi = %+v, want it out of date", got)
}
// A preview onto the trunk head now makes it good again, and the trunk moving again makes it stale.
applyPreview(t, ix, avahiPreview(t, trunkNow, h1))
if got := fpOf(t, cands); got.FP2 == "" {
if got := fpOf(t, cands); got.Fp2 == "" {
t.Fatalf("after the right preview: %+v", got)
}
if err := ix.ApplySource(context.Background(), 6, delivery.Source{Group: "g", Heads: map[string]string{catalogueRepo: "8888888888888888888888888888888888888888"}}); err != nil {
if err := ix.ApplySource(context.Background(), 6, meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{catalogueRepo: "8888888888888888888888888888888888888888"}}); err != nil {
t.Fatal(err)
}
if got := fpOf(t, cands); got.None2 != NonePreviewStale {
@@ -172,14 +177,14 @@ func TestAPreviewHeardBeforeItsPullRequestGivesTheFingerprintWhenTheCandidateIsW
t.Fatal(err)
}
trunk, _ := moduleOf(t, mods, "avahi")
if got := fpOf(t, cands); got.FP2 != trunk.FP2 {
t.Errorf("candidate avahi = %+v, want %s", got, trunk.FP2)
if got := fpOf(t, cands); got.Fp2 != trunk.Fp2 {
t.Errorf("candidate avahi = %+v, want %s", got, trunk.Fp2)
}
// The same pull request at a new head has no preview of it yet.
if err := ix.Apply(context.Background(), avahiPull(t, h2)); err != nil {
t.Fatal(err)
}
if got := fpOf(t, cands); got.FP2 != "" || got.None2 != NoneNoPreview {
if got := fpOf(t, cands); got.Fp2 != "" || got.None2 != NoneNoPreview {
t.Errorf("a new head: %+v, want no preview", got)
}
}
@@ -191,7 +196,7 @@ func TestAPreviewNamingNoManifestOfTheModulesDirectoryGivesAReason(t *testing.T)
t.Fatal(err)
}
pv := avahiPreview(t, trunkNow, h1)
pv.Modules = map[string]delivery.PreviewModule{"modules/other": pv.Modules["modules/avahi"]}
pv.Modules = map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{"modules/other": pv.Modules["modules/avahi"]}
applyPreview(t, ix, pv)
if got := fpOf(t, cands); got.None2 != NonePreviewNoModule || !strings.Contains(NonePreviewNoModule, "manifest") {
t.Errorf("candidate avahi = %+v", got)
@@ -216,9 +221,9 @@ func TestAPreviewOfConflictsGivesTheCandidatesModuleTheReasonItDoesNotMerge(t *t
if err := ix.Apply(context.Background(), avahiPull(t, h1)); err != nil {
t.Fatal(err)
}
applyPreview(t, ix, delivery.Preview{Repository: catalogueRepo, Head: h1, Trunk: trunkNow, Conflicts: []string{"modules/avahi/main.go", "go.mod"}})
applyPreview(t, ix, meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: catalogueRepo, Head: h1, Trunk: trunkNow, Conflicts: []string{"modules/avahi/main.go", "go.mod"}})
got := fpOf(t, cands)
if got.FP2 != "" || !strings.HasPrefix(got.None2, "does not merge") || !strings.Contains(got.None2, "go.mod, modules/avahi/main.go") && !strings.Contains(got.None2, "modules/avahi/main.go, go.mod") {
if got.Fp2 != "" || !strings.HasPrefix(got.None2, "does not merge") || !strings.Contains(got.None2, "go.mod, modules/avahi/main.go") && !strings.Contains(got.None2, "modules/avahi/main.go, go.mod") {
t.Errorf("candidate avahi = %+v, want it not merging and naming the files", got)
}
}
@@ -227,8 +232,8 @@ func TestAPreviewOfConflictsGivesTheCandidatesModuleTheReasonItDoesNotMerge(t *t
// indexer writes it and the gate's source revision it names, kept in testdata/candidate-written.json so the builder is tested
// on what the indexer writes, never on a candidate made by hand. MESH_WRITE_TESTDATA=1 writes it again.
type writtenCandidate struct {
Source sourceRev `json:"source"`
Candidate delivery.Candidate `json:"candidate"`
Source sourceRev `json:"source"`
Candidate meshcatalog.CandidatesByGroupValue `json:"candidate"`
}
type sourceRev struct {
@@ -0,0 +1,75 @@
package index
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// A preview as the checker writes it into its state previews, read here (reads: mesh-delivery-checker.previews): the
// seat's type and the client's rules; the key is the form the seat names, `<repository>.<head>.<trunk>`, the repository
// as a key part (keys.EscapeName).
// PreviewKey is the key of a head merged onto a trunk commit.
func PreviewKey(repository, head, trunk string) (string, error) {
if err := meshdeliverycheckerclient.CheckRepository(repository); err != nil {
return "", err
}
if err := keys.CheckName("head", head); err != nil {
return "", err
}
if err := keys.CheckName("trunk commit", trunk); err != nil {
return "", err
}
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkKey(keys.EscapeName(repository), head, trunk), nil
}
// ParsePreviewKey is the inverse of PreviewKey.
func ParsePreviewKey(key string) (repository, head, trunk string, err error) {
parts := strings.Split(key, ".")
if len(parts) != 3 {
return "", "", "", fmt.Errorf("index: %q is not a preview key, <repository>.<head>.<trunk>", key)
}
repository, err = keys.UnescapeName(parts[0])
if err != nil {
return "", "", "", fmt.Errorf("index: %q is not a preview key: %w", key, err)
}
if _, err := PreviewKey(repository, parts[1], parts[2]); err != nil {
return "", "", "", fmt.Errorf("index: %q is not a preview key: %w", key, err)
}
return repository, parts[1], parts[2], nil
}
// DecodePreview reads a preview.
func DecodePreview(data []byte) (meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue, error) {
if err := keys.CheckVersion(data); err != nil {
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}, err
}
p, err := meshdeliverychecker.DecodePreviewsByRepositoryHeadTrunkValue(data)
if err != nil {
return p, fmt.Errorf("index: a preview does not fit its shape: %w", err)
}
if err := meshdeliverycheckerclient.CheckPreview(p); err != nil {
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}, err
}
return p, nil
}
// EncodePreview is p as JSON at the kept values' version (the checker writes previews; the indexer's tests do too).
func EncodePreview(p meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue) ([]byte, error) {
if err := meshdeliverycheckerclient.CheckPreview(p); err != nil {
return nil, err
}
p.V = keys.Version
return json.Marshal(p)
}
// SamePreview says whether two previews are the same value.
func SamePreview(a, b meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue) bool {
return reflect.DeepEqual(a, b)
}
@@ -7,8 +7,10 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// A module registered in the catalogue from its own repository (the operator registers it with the repository and the
@@ -57,7 +59,7 @@ func (ix *Indexer) indexAt(ctx context.Context, r Registration, force, reread bo
if ix.Modules == nil || ix.Read == nil {
return taken{why: "the indexer reads no forge or writes no modules"}, nil
}
key, err := delivery.ModulesKey(r.Module)
key, err := meshcatalogclient.ModulesKey(r.Module)
if err != nil {
ix.logf("%s: the registered module has no usable key, skipped: %v", r.Module, err)
return taken{why: fmt.Sprintf("the module has no usable key: %v", err)}, nil
@@ -68,17 +70,17 @@ func (ix *Indexer) indexAt(ctx context.Context, r Registration, force, reread bo
ix.markRead(r.Module)
return taken{held: true, why: "the modules state holds it already: a merge to its repository writes it from now on"}, nil
}
repository := delivery.RepositoryName(r.Repository)
repository := trunk.RepositoryName(r.Repository)
commit := r.Commit
dir := strings.Trim(r.Path, "/")
if dir == delivery.RootPath {
if dir == meshcatalogclient.RootPath {
dir = ""
}
// The head the gate's source names for the repository, when it names one, else the registration's commit. A module a
// merge removed is absent at the head and is said and left.
ix.mu.Lock()
for repo, head := range ix.source.heads {
if delivery.SameRepository(repo, r.Repository) && head != "" {
if trunk.SameRepository(repo, r.Repository) && head != "" {
commit = head
}
}
@@ -126,7 +128,7 @@ func (ix *Indexer) indexAt(ctx context.Context, r Registration, force, reread bo
t.why = fmt.Sprintf("the manifest at %q of %s at %s names the module %q", dir, repository, commit, name)
return t, nil
}
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repository, Merge: commit, Root: read.Root,
f := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: repository, Merge: commit, Root: read.Root,
Trees: map[string]string{dir: read.Trees[dir]},
Manifests: map[string]string{dir: raw}}
if l := read.Locks[dir]; len(l) > 0 {
@@ -234,7 +236,7 @@ func (ix *Indexer) TakeIn(ctx context.Context, module string) TakenIn {
}
regs := ix.Builds.RegistrationsOf(module)
if len(regs) == 0 {
if key, err := delivery.ModulesKey(module); err == nil && ix.Modules != nil {
if key, err := meshcatalogclient.ModulesKey(module); err == nil && ix.Modules != nil {
if _, _, found, err := ix.Modules.Get(ctx, key); err == nil && found {
out.Held, out.Answer = true, "the modules state holds it already: a merge to its repository writes it from now on"
return out
@@ -305,7 +307,7 @@ func (ix *Indexer) IndexRegisteredAll(ctx context.Context) error {
// apply: no such module held, or no head named, or the record at that head already.
func (ix *Indexer) takeInHeld(ctx context.Context, module string) (TakenIn, bool) {
out := TakenIn{Module: module}
key, err := delivery.ModulesKey(module)
key, err := meshcatalogclient.ModulesKey(module)
if err != nil || ix.Modules == nil {
return out, false
}
@@ -313,14 +315,14 @@ func (ix *Indexer) takeInHeld(ctx context.Context, module string) (TakenIn, bool
if err != nil || !found {
return out, false
}
cur, err := delivery.DecodeModule(value)
cur, err := meshcatalogclient.DecodeModule(value)
if err != nil || cur.Repository == "" || cur.Path == "" {
return out, false
}
head := ""
ix.mu.Lock()
for repo, h := range ix.source.heads {
if delivery.SameRepository(repo, cur.Repository) && h != "" {
if trunk.SameRepository(repo, cur.Repository) && h != "" {
head = h
}
}
@@ -373,7 +375,7 @@ func (ix *Indexer) TakeInRepository(ctx context.Context, repository string) Take
ix.mu.Lock()
head := ""
for repo, h := range ix.source.heads {
if delivery.SameRepository(repo, repository) && h != "" {
if trunk.SameRepository(repo, repository) && h != "" {
head, out.Repository = h, repo
}
}
@@ -397,7 +399,7 @@ func (ix *Indexer) TakeInRepository(ctx context.Context, repository string) Take
out.Answer = "the modules state cannot be read: " + err.Error()
return out
}
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: out.Repository, Merge: head}
f := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: out.Repository, Merge: head}
if _, err := ix.ApplyMessage(ctx, f); err != nil {
out.Answer = fmt.Sprintf("the merge of %s at %.12s could not be taken in: %v", repository, head, err)
return out
@@ -408,7 +410,7 @@ func (ix *Indexer) TakeInRepository(ctx context.Context, repository string) Take
return out
}
for _, key := range slices.Sorted(maps.Keys(before)) {
if !delivery.SameRepository(before[key].Repository, repository) {
if !trunk.SameRepository(before[key].Repository, repository) {
continue
}
if now, ok := after[key]; ok {
@@ -7,7 +7,10 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the grants part's gaps: modules registered from their own repositories had no manifest here
@@ -50,12 +53,12 @@ func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the gran
t.Run(tc.name, func(t *testing.T) {
ix, mods := modulesIndexer(t, rec(t, "build-1", "snake", forge, tc.path, tc.ref, "", "2026-10-19T09:00:00Z", ""))
if tc.held {
if err := mods.Create(context.Background(), "snake", mustModule(t, delivery.Module{Repository: "jschoubben/snake", Commit: h1, Path: ".", None2: "x"})); err != nil {
if err := mods.Create(context.Background(), "snake", mustModule(t, meshcatalog.ModulesByModuleValue{Repository: "jschoubben/snake", Commit: h1, Path: ".", None2: "x"})); err != nil {
t.Fatal(err)
}
}
if tc.other {
if err := mods.Create(context.Background(), "other", mustModule(t, delivery.Module{Repository: "jschoubben/snake", Commit: h1, Path: "modules/other", None2: "x"})); err != nil {
if err := mods.Create(context.Background(), "other", mustModule(t, meshcatalog.ModulesByModuleValue{Repository: "jschoubben/snake", Commit: h1, Path: "modules/other", None2: "x"})); err != nil {
t.Fatal(err)
}
}
@@ -172,9 +175,9 @@ func TestARegisteredModuleIsIndexedFromItsRepository(t *testing.T) { // the gran
}
}
func mustModule(t *testing.T, m delivery.Module) []byte {
func mustModule(t *testing.T, m meshcatalog.ModulesByModuleValue) []byte {
t.Helper()
b, err := delivery.EncodeModule(withV(m))
b, err := meshcatalogclient.EncodeModule(withV(m))
if err != nil {
t.Fatal(err)
}
@@ -258,7 +261,7 @@ func TestAForgeThatCannotSayLeavesTheModuleUnwrittenAndAsksAgain(t *testing.T) {
t.Fatal(err)
}
case "a new source revision":
if err := ix.ApplySource(ctx, 2, delivery.Source{Heads: map[string]string{"jschoubben/snake": h2, "novox/other": h1}}); err != nil {
if err := ix.ApplySource(ctx, 2, meshdelivery.SourceByMainValue{Heads: map[string]string{"jschoubben/snake": h2, "novox/other": h1}}); err != nil {
t.Fatal(err)
}
ix.retryWG.Wait() // read again in the background
@@ -278,7 +281,7 @@ func TestAForgeThatCannotSayLeavesTheModuleUnwrittenAndAsksAgain(t *testing.T) {
}
// Taken in, it is read no more: a further event asks the forge nothing.
n := len(asked)
if err := ix.ApplySource(ctx, 3, delivery.Source{Heads: map[string]string{"jschoubben/snake": h2}}); err != nil {
if err := ix.ApplySource(ctx, 3, meshdelivery.SourceByMainValue{Heads: map[string]string{"jschoubben/snake": h2}}); err != nil {
t.Fatal(err)
}
ix.retryWG.Wait()
@@ -317,7 +320,7 @@ func TestARecordOfAPullRequestDoesNotReplaceTheRegisteredRef(t *testing.T) { //
// An entry of another kind in the builder's log (the old reference form's close, design 51), of this form or another,
// is no build: passed over, never refused.
for _, other := range []string{
fmt.Sprintf(`{"v":%d,"event":"old-form-closed","module":"node-engine","at":"2026-10-04T09:00:00Z"}`, delivery.Version),
fmt.Sprintf(`{"v":%d,"event":"old-form-closed","module":"node-engine","at":"2026-10-04T09:00:00Z"}`, keys.Version),
`{"v":99,"event":"old-form-closed","at":"2026-10-04T09:00:00Z","nodes":["ace"]}`,
} {
if err := b.Put([]byte(other)); err != nil {
@@ -6,7 +6,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
)
func twoModules(t *testing.T) (*Indexer, *memStore) {
@@ -26,9 +27,9 @@ func twoModules(t *testing.T) (*Indexer, *memStore) {
func TestAMergeThatRemovesAModuleDeletesItsKey(t *testing.T) { // 1900/AC1
ix, mods := twoModules(t)
// DLV-I9 (design 25 § 12.2): the module leaving the trunk is said as an empty manifest-accepted, under an id naming it.
var said []delivery.ManifestAccepted
var said []meshcatalog.ManifestAcceptedEvent
var ids []string
ix.SayAccepted = func(_ context.Context, a delivery.ManifestAccepted, id string) error {
ix.SayAccepted = func(_ context.Context, a meshcatalog.ManifestAcceptedEvent, id string) error {
said, ids = append(said, a), append(ids, id)
return nil
}
@@ -37,9 +38,9 @@ func TestAMergeThatRemovesAModuleDeletesItsKey(t *testing.T) { // 1900/AC1
for i, a := range said {
if a.Digest == "" {
left = append(left, a.Module)
// Join item 3: the id is the event's own (delivery.ManifestAccepted.EventID), naming the module and the
// Join item 3: the id is the event's own (acceptedEventID), naming the module and the
// modules state's revision of the value that left, so two leaves of one module are two events.
if a.BusState == nil || len(a.BusState) != 0 || a.Revision == 0 || ids[i] != a.EventID() || !strings.Contains(ids[i], "-left-b-") {
if a.BusState == nil || len(a.BusState) != 0 || a.Revision == 0 || ids[i] != acceptedEventID(a) || !strings.Contains(ids[i], "-left-b-") {
t.Errorf("the empty manifest-accepted = %+v, id %s", a, ids[i])
}
}
@@ -138,7 +139,7 @@ func TestStartingDeletesNothingWhenTheTrunkCannotBeRead(t *testing.T) { // 1900/
t.Errorf("%s: %d keys held, want 2 untouched", name, len(mods.vals))
}
}
_ = delivery.RootPath
_ = meshcatalogclient.RootPath
}
func TestAKeyWrittenAheadOfTheSourceHeadStaysAndOneWithoutAPathIsSkipped(t *testing.T) { // 1900/AC3 (review): the source lags a merge already applied
@@ -148,7 +149,7 @@ func TestAKeyWrittenAheadOfTheSourceHeadStaysAndOneWithoutAPathIsSkipped(t *test
if err := ix.ApplySource(ctx, 3, sourceOf(catalogueRepo, older)); err != nil {
t.Fatal(err)
}
mods.put("nopath", mustEncode(t, delivery.Module{Repository: catalogueRepo, Commit: older, None2: "x", Manifest: strings.Repeat("a", 64)}))
mods.put("nopath", mustEncode(t, meshcatalog.ModulesByModuleValue{Repository: catalogueRepo, Commit: older, None2: "x", Manifest: strings.Repeat("a", 64)}))
ix.Trunk = func(context.Context, string, string) ([]string, error) { return []string{"modules/a"}, nil }
ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) { return commit == in, nil } // C is not in B
if err := ix.Reconcile(ctx); err != nil {
@@ -9,7 +9,8 @@ import (
"strconv"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The requirements state (DLV-I7, the lead's ruling on I7; REG S15, ADR 0606 §1): per mesh Go module, the highest
@@ -18,7 +19,7 @@ import (
// already requires.
const (
RequirementsState = "requirements"
RequirementsBucket = delivery.IndexerModule + "_" + RequirementsState
RequirementsBucket = Module + "_" + RequirementsState
RequirementsTool = "indexer_requirements"
)
@@ -47,10 +48,10 @@ func RequiredOf(goSum []byte) map[string]string {
out := map[string]string{}
for _, line := range strings.Split(string(goSum), "\n") {
f := strings.Fields(line)
if len(f) < 2 || !strings.HasPrefix(f[0], delivery.ForgeModulePrefix) {
if len(f) < 2 || !strings.HasPrefix(f[0], trunk.ForgeModulePrefix) {
continue
}
if _, _, ok := delivery.PackageRepository(f[0]); !ok {
if _, _, ok := trunk.PackageRepository(f[0]); !ok {
continue
}
v := strings.TrimSuffix(f[1], "/go.mod")
@@ -163,22 +164,22 @@ func (ix *Indexer) keepRequirements(ctx context.Context, repository, dir string,
ix.reqKept[consumer] = true
ix.reqMu.Unlock()
required := RequiredOf(goSum)
keys, err := ix.Requirements.Keys(ctx)
held, err := ix.Requirements.Keys(ctx)
if err != nil {
ix.logf("requirements: the state cannot be listed, so what %s requires is not kept: %v", consumer, err)
return
}
for _, pkg := range slices.Sorted(maps.Keys(required)) {
if !slices.Contains(keys, pkg) {
keys = append(keys, pkg)
if !slices.Contains(held, pkg) {
held = append(held, pkg)
}
}
for _, pkg := range keys {
for _, pkg := range held {
version, names := required[pkg]
err := write(ctx, ix.Requirements, pkg, func(value []byte, found bool) ([]byte, bool, error) {
r := Requirement{V: delivery.Version, Package: pkg, By: map[string]string{}}
r := Requirement{V: keys.Version, Package: pkg, By: map[string]string{}}
if found && json.Unmarshal(value, &r) != nil {
r = Requirement{V: delivery.Version, Package: pkg, By: map[string]string{}}
r = Requirement{V: keys.Version, Package: pkg, By: map[string]string{}}
}
if r.By == nil {
r.By = map[string]string{}
@@ -25,10 +25,12 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-sdk/go/views"
)
@@ -36,7 +38,7 @@ import (
const (
// SeatDefinitionsState is the indexer's state of the seat definitions (design 26), one key per seat and source.
SeatDefinitionsState = "seat-definitions"
SeatDefinitionsBucket = delivery.IndexerModule + "_" + SeatDefinitionsState
SeatDefinitionsBucket = Module + "_" + SeatDefinitionsState
// The seat mesh-catalog's verb and events (ADR 0636 §4): the indexer, which holds the seat, serves the verb and says
// the events on the seat's own subject, for the trunks' definitions only, changed when a digest moved and removed when
// a trunk's definition went.
@@ -109,12 +111,12 @@ func SeatDefinitionKey(seat, source, repository string, pull int) string {
}
func pullSuffix(repository string, pull int) string {
return SourcePull + "." + delivery.EscapeName(repository) + "." + strconv.Itoa(pull)
return SourcePull + "." + keys.EscapeName(repository) + "." + strconv.Itoa(pull)
}
// OutOfStepKey is the key of a pull request's out-of-step files.
func OutOfStepKey(repository string, pull int) string {
return outOfStepPrefix + delivery.EscapeName(repository) + "." + strconv.Itoa(pull)
return outOfStepPrefix + keys.EscapeName(repository) + "." + strconv.Itoa(pull)
}
// errForgeRead is a read the forge did not answer (never a file it does not hold): the message is left for redelivery.
@@ -138,7 +140,7 @@ func contractsModules(manifests map[string]string) []contractsModule {
continue
}
base := dir
if base == delivery.RootPath {
if base == meshcatalogclient.RootPath {
base = ""
}
out = append(out, contractsModule{module: m.Module, dir: dir, defs: path.Clean(path.Join(base, a.GeneratedFrom))})
@@ -323,7 +325,7 @@ func (ix *Indexer) SeatDefinitionsAgain(ctx context.Context, module string, pull
out.Answer = "the indexer holds no seat-definitions state or forge reader yet"
return out
}
key, err := delivery.ModulesKey(module)
key, err := meshcatalogclient.ModulesKey(module)
if err != nil {
out.Answer = err.Error()
return out
@@ -333,7 +335,7 @@ func (ix *Indexer) SeatDefinitionsAgain(ctx context.Context, module string, pull
out.Answer = fmt.Sprintf("the trunk holds no module %s (%v)", module, err)
return out
}
m, err := delivery.DecodeModule(raw)
m, err := meshcatalogclient.DecodeModule(raw)
if err != nil {
out.Answer = err.Error()
return out
@@ -345,13 +347,13 @@ func (ix *Indexer) SeatDefinitionsAgain(ctx context.Context, module string, pull
}
dir := m.Path
if dir == "" {
dir = delivery.RootPath
dir = meshcatalogclient.RootPath
}
whose := "its trunk's"
if pull > 0 {
// A pull request is read with its head's own manifest, so a change of generated-from is read as the head has it.
file := "module.json"
if dir != delivery.RootPath {
if dir != meshcatalogclient.RootPath {
file = path.Join(dir, file)
}
raw, err := ix.ReadFile(ctx, m.Repository, head, file)
@@ -572,7 +574,9 @@ func (ix *Indexer) sayChanged(ctx context.Context, c SeatDefinitionChanged) {
}
// findingID is the finding's subject id for a contracts module.
func findingID(what, module string) string { return delivery.IndexerModule + "." + what + "." + module }
func findingID(what, module string) string {
return Module + "." + what + "." + module
}
// unparsedFinding writes the contracts module's seat-definition-unparsed finding for its trunk's seats that did not
// parse (by seat), or clears it when every one parsed.
@@ -608,7 +612,7 @@ func (ix *Indexer) finding(ctx context.Context, id, kind string, raise bool, sum
at := ix.now().UTC()
data, err := health.EncodeFinding(health.Finding{Key: key, Kind: kind, Subject: health.Subject{Scope: health.ScopeModule, ID: id},
Severity: health.Warning, Summary: summary, Headline: headline, Explanation: explanation, Resolved: resolved,
Evidence: []health.Evidence{{At: at, Said: summary}}, Source: kind, Observer: delivery.IndexerModule, At: at})
Evidence: []health.Evidence{{At: at, Said: summary}}, Source: kind, Observer: Module, At: at})
return data, false, err
})
if err != nil {
@@ -629,7 +633,7 @@ func (ix *Indexer) ApplyOutOfStepBuild(ctx context.Context, value []byte) {
Commit string `json:"commit"`
OutOfStep []string `json:"out-of-step"`
}
if json.Unmarshal(value, &e) != nil || e.Event != delivery.BuildEndedEvent || e.Module == "" {
if json.Unmarshal(value, &e) != nil || e.Event != builds.BuildEndedEvent || e.Module == "" {
return
}
if !trunkLike[e.Ref] {
@@ -645,7 +649,7 @@ func (ix *Indexer) ApplyOutOfStepBuild(ctx context.Context, value []byte) {
"Its build's test step regenerated the types and found the committed files differ, missing or not written by the "+
"generator (a generator's change does this). A change regenerating them clears this once a build of "+e.Module+" ends clean.",
"A build of "+e.Module+" ended clean: its generated types match their definitions.")
case e.Outcome == delivery.BuildDone:
case e.Outcome == builds.BuildDone:
ix.finding(ctx, findingID("out-of-step", e.Module), OutOfStepKind, false, "", "", "", "")
}
}
@@ -762,7 +766,7 @@ func OutOfStepOf(defs, findings views.Source) ([]OutOfStepRow, error) {
if err != nil || f.Kind != OutOfStepKind {
continue
}
out = append(out, OutOfStepRow{Source: SourceTrunk, Module: strings.TrimPrefix(f.Subject.ID, delivery.IndexerModule+".out-of-step."), Summary: f.Summary})
out = append(out, OutOfStepRow{Source: SourceTrunk, Module: strings.TrimPrefix(f.Subject.ID, Module+".out-of-step."), Summary: f.Summary})
}
keys, err := defs.Keys()
if err != nil {
@@ -9,9 +9,12 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-delivery/sdk/packagename"
)
@@ -25,19 +28,33 @@ import (
// A series key always holds a dot (`mesh-sdk/go.0.1`), which EscapeName never leaves in a per-module key: a reader tells the
// two forms apart by IsSeriesKey.
// SeriesPackage is the value of a series key, and SeriesVersion one version of a series: mesh-sdk's
// delivery.SeriesPackage and SeriesVersion (J4), the contract every reader of the state decodes with
// delivery.DecodeSeriesPackage.
// SeriesPackage is the value of a series key, and SeriesVersion one version of a series: the seat mesh-catalog's
// types (the newest published version's; the numbered one's is the same fields, numberedOf), which every reader of the
// state decodes.
type (
SeriesPackage = delivery.SeriesPackage
SeriesVersion = delivery.SeriesVersion
SeriesPackage = meshcatalog.PackagesByPackageSeriesValue
SeriesVersion = meshcatalog.PackagesByPackageSeriesValueNewest
)
// SeriesKey is the key of a package's series in the packages state (delivery.SeriesPackageKey).
func SeriesKey(pkg, series string) string { return delivery.SeriesPackageKey(pkg, series) }
// numberedOf is a series version as the series' numbered one: the same fields in the contract's other type.
func numberedOf(v SeriesVersion) *meshcatalog.PackagesByPackageSeriesValueNumbered {
n := &meshcatalog.PackagesByPackageSeriesValueNumbered{Commit: v.Commit, Digest: v.Digest, Fingerprint: v.Fingerprint, Version: v.Version}
if c := v.Contents; c != nil {
p := publishedOfContents(*c)
nc := &meshcatalog.PackagesByPackageSeriesValueNumberedContents{At: p.At, Commit: p.Commit, Dir: p.Dir, Module: p.Module, Repository: p.Repository, Requires: p.Requires, V: p.V}
for _, d := range p.Packages {
nc.Packages = append(nc.Packages, meshcatalog.PackagesByPackageSeriesValueNumberedContentsPackagesItem{Package: d.Package, Digest: d.Digest})
}
n.Contents = nc
}
return n
}
// IsSeriesKey says whether a key of the packages state is a series key, not a per-module one (delivery.IsSeriesPackageKey).
func IsSeriesKey(key string) bool { return delivery.IsSeriesPackageKey(key) }
// SeriesKey is the key of a package's series in the packages state (SeriesPackageKey).
func SeriesKey(pkg, series string) string { return SeriesPackageKey(pkg, series) }
// IsSeriesKey says whether a key of the packages state is a series key, not a per-module one (IsSeriesPackageKey).
func IsSeriesKey(key string) bool { return IsSeriesPackageKey(key) }
// SeriesOf is the series of a trunk version (`0.1.15` is of `0.1`); false for a version that is no `<major>.<minor>.<patch>`
// or that carries a tag (a pre-release).
@@ -66,11 +83,11 @@ func (ix *Indexer) HearVersion(ctx context.Context, h HeardVersion) (bool, error
return false, nil
}
v := &SeriesVersion{Version: strings.TrimPrefix(h.Version, "v"), Commit: h.Commit, Digest: h.Digest, Fingerprint: h.Fingerprint}
numbered := *v
numbered := numberedOf(*v)
if h.Published && h.GoModule != "" && h.Commit != "" && ix.Files != nil {
// The digests of the published version's commit, read before the write (DLV-I6): a user accepting the series is
// fingerprinted against what this version holds, not against the latest merge.
repo, _, _ := delivery.PackageRepository(h.GoModule)
repo, _, _ := trunk.PackageRepository(h.GoModule)
got, err := ix.packagesAt(ctx, repo, h.Commit, []string{h.GoModule}, nil)
switch p, ok := got[h.GoModule]; {
case err != nil:
@@ -78,32 +95,30 @@ func (ix *Indexer) HearVersion(ctx context.Context, h HeardVersion) (bool, error
case !ok:
ix.logf("%s %s: the forge's listing at %s holds no Go module %s, so its users keep the commit form", h.Package, h.Version, h.Commit, h.GoModule)
default:
// When the contents were read: delivery.SeriesVersion.Contents is a PublishedPackages, which names its time
// When the contents were read: a series version's Contents is a published module, which names its time
// (DecodeSeriesPackage refuses one with none).
p.At = ix.now().UTC()
v.Contents = &p
p.At = ptr(ix.now().UTC())
v.Contents = contentsOfPublished(p)
}
}
newer := func(held *SeriesVersion) bool {
return held == nil || CompareVersions("v"+held.Version, "v"+v.Version) < 0
}
newer := func(held string, _ bool) bool { return CompareVersions("v"+held, "v"+v.Version) < 0 }
kept := false
err := write(ctx, ix.Packages, SeriesKey(h.Package, series), func(value []byte, found bool) ([]byte, bool, error) {
s := SeriesPackage{V: delivery.Version, Name: h.Package, Series: series}
s := SeriesPackage{V: keys.Version, Name: h.Package, Series: series}
if found {
if err := json.Unmarshal(value, &s); err != nil {
ix.logf("packages.%s holds what is not a series, written anew: %v", SeriesKey(h.Package, series), err)
s = SeriesPackage{V: delivery.Version, Name: h.Package, Series: series}
s = SeriesPackage{V: keys.Version, Name: h.Package, Series: series}
}
}
kept = false
if s.Name == "" {
s.Name, kept = h.Package, true
}
if newer(s.Numbered) {
s.Numbered, kept = &numbered, true
if s.Numbered == nil || newer(s.Numbered.Version, true) {
s.Numbered, kept = numbered, true
}
if h.Published && newer(s.Newest) {
if h.Published && (s.Newest == nil || newer(s.Newest.Version, true)) {
s.Newest, kept = v, true
}
if h.Seat != "" && s.Seat != h.Seat {
@@ -115,7 +130,7 @@ func (ix *Indexer) HearVersion(ctx context.Context, h HeardVersion) (bool, error
if !kept {
return nil, false, nil
}
s.At = ix.now()
s.At = ptr(ix.now())
next, err := json.Marshal(s)
return next, false, err
})
@@ -141,7 +156,7 @@ func (ix *Indexer) SeriesAll(ctx context.Context) map[string]SeriesPackage {
if err != nil || !found {
continue
}
s, err := delivery.DecodeSeriesPackage(value)
s, err := DecodeSeriesPackage(value)
if err != nil {
ix.logf("packages.%s holds what is not a series, left out: %v", key, err)
continue
@@ -158,21 +173,21 @@ func (ix *Indexer) SeriesAll(ctx context.Context) map[string]SeriesPackage {
// `v0.1.15`). A series with no published version yet, or whose contents the forge did not list, leaves the module's
// latest published commit (the commit form, milestone § 2 item 4). A module of the same repository takes its sibling by
// path, never by a series. With nothing to pin, published is answered as it is.
func acceptedSeries(series map[string]SeriesPackage, m catalogue.Manifest, repository string, published map[string]delivery.PublishedPackages) (map[string]delivery.PublishedPackages, delivery.PackageVersions) {
var out map[string]delivery.PublishedPackages
var pins delivery.PackageVersions
func acceptedSeries(series map[string]SeriesPackage, m catalogue.Manifest, repository string, published map[string]meshcatalog.PackagesByModuleValue) (map[string]meshcatalog.PackagesByModuleValue, sdkstep.PackageVersions) {
var out map[string]meshcatalog.PackagesByModuleValue
var pins sdkstep.PackageVersions
for _, key := range slices.Sorted(maps.Keys(series)) {
s := series[key]
if s.GoModule == "" || s.Newest == nil || s.Newest.Contents == nil || m.AcceptedSeries(s.Name) != s.Series {
continue
}
if repo, _, ok := delivery.PackageRepository(s.GoModule); !ok || delivery.SameRepository(repo, repository) {
if repo, _, ok := trunk.PackageRepository(s.GoModule); !ok || trunk.SameRepository(repo, repository) {
continue
}
if out == nil {
out, pins = maps.Clone(published), delivery.PackageVersions{}
out, pins = maps.Clone(published), sdkstep.PackageVersions{}
}
p := *s.Newest.Contents
p := publishedOfContents(*s.Newest.Contents)
version := "v" + s.Newest.Version
p.Commit = version
out[s.GoModule], pins[s.GoModule] = p, version
@@ -209,9 +224,9 @@ func (ix *Indexer) KeepPackageNames(ctx context.Context, f forgemsg.Forge) error
}
series, built := a.SeriesOf(), m.Module+"/"+a.Name
errs = append(errs, write(ctx, ix.Packages, SeriesKey(name, series), func(value []byte, found bool) ([]byte, bool, error) {
s := SeriesPackage{V: delivery.Version, Series: series}
s := SeriesPackage{V: keys.Version, Series: series}
if found && json.Unmarshal(value, &s) != nil {
s = SeriesPackage{V: delivery.Version, Series: series}
s = SeriesPackage{V: keys.Version, Series: series}
}
if s.Name == name && s.Package == built && (a.Language != "go" || s.GoModule == name) {
return nil, false, nil
@@ -220,7 +235,7 @@ func (ix *Indexer) KeepPackageNames(ctx context.Context, f forgemsg.Forge) error
if a.Language == "go" {
s.GoModule = name
}
s.At = ix.now()
s.At = ptr(ix.now())
next, err := json.Marshal(s)
return next, false, err
}))
@@ -6,7 +6,9 @@ import (
"maps"
"slices"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// The gate's source revision (novox/hq ADR 0437 §5, ADR 0450 §1): source.main names every repository's merged head, and its
@@ -36,7 +38,7 @@ func (ix *Indexer) srcOf(repository, merge string) uint64 {
// that carries no Src yet, is written again with Src the revision (once: a later revision leaves it); a module computed at another commit is left (a merge heard later is stamped
// when it is written, applyMerged), and a repository none of the indexer's modules is built from writes nothing. The
// modules of a repository are those the modules bucket holds of it (ADR 0501 clause 6).
func (ix *Indexer) ApplySource(ctx context.Context, rev uint64, s delivery.Source) error {
func (ix *Indexer) ApplySource(ctx context.Context, rev uint64, s meshdelivery.SourceByMainValue) error {
ix.kickJudge(ctx)
ix.mu.Lock()
if rev < ix.source.rev {
@@ -59,7 +61,7 @@ func (ix *Indexer) ApplySource(ctx context.Context, rev uint64, s delivery.Sourc
for _, key := range slices.Sorted(maps.Keys(held)) {
repository := ""
for r := range s.Heads {
if delivery.SameRepository(r, held[key].Repository) {
if trunk.SameRepository(r, held[key].Repository) {
repository = r
}
}
@@ -71,7 +73,7 @@ func (ix *Indexer) ApplySource(ctx context.Context, rev uint64, s delivery.Sourc
if !found {
return nil, false, nil
}
cur, err := delivery.DecodeModule(value)
cur, err := meshcatalogclient.DecodeModule(value)
if err != nil {
ix.logf("modules.%s holds what is not a module, left: %v", key, err)
return nil, false, nil
@@ -81,11 +83,11 @@ func (ix *Indexer) ApplySource(ctx context.Context, rev uint64, s delivery.Sourc
if cur.Repository != repository || cur.Commit != head || cur.Src != 0 {
return nil, false, nil
}
cur.Src = rev
if cur.FPSrc == 0 {
cur.FPSrc = rev // its fingerprint moved at this merge, whose revision is heard only now (ADR 0601 §1)
cur.Src = int64(rev)
if cur.FpSrc == 0 {
cur.FpSrc = int64(rev) // its fingerprint moved at this merge, whose revision is heard only now (ADR 0601 §1)
}
data, err := delivery.EncodeModule(cur)
data, err := meshcatalogclient.EncodeModule(cur)
return data, false, err
})
if err != nil {
@@ -5,13 +5,13 @@ import (
"reflect"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
const otherMerge = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
func sourceOf(repo, head string) delivery.Source {
return delivery.Source{Group: "g", Heads: map[string]string{repo: head}}
func sourceOf(repo, head string) meshdelivery.SourceByMainValue {
return meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{repo: head}}
}
func TestASourceRevisionStampsTheModulesOfTheRepositoriesHeadItNames(t *testing.T) { // 1344/AC1
@@ -25,13 +25,13 @@ func TestASourceRevisionStampsTheModulesOfTheRepositoriesHeadItNames(t *testing.
t.Fatalf("module before the source = %+v, want Src 0", before)
}
// The source names the head merged for the delivery repository, and another repository's head no module of ours is built from.
s := delivery.Source{Group: "g", Heads: map[string]string{"novox/mesh-delivery": merge, "novox/mesh-catalog": otherMerge}}
s := meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{"novox/mesh-delivery": merge, "novox/mesh-catalog": otherMerge}}
if err := ix.ApplySource(ctx, 7, s); err != nil {
t.Fatal(err)
}
m, ok := moduleOf(t, mods, "a")
want := before
want.Src, want.FPSrc = 7, 7 // its fingerprint moved at that merge (ADR 0601 §1)
want.Src, want.FpSrc = 7, 7 // its fingerprint moved at that merge (ADR 0601 §1)
if !ok || !reflect.DeepEqual(m, want) {
t.Errorf("module a = %+v (%v), want %+v: the fingerprint and the commit as the merge wrote them, with Src 7", m, ok, want)
}
@@ -20,7 +20,7 @@ func unreadableAvahi(t *testing.T) string {
func TestAMergeOfAnUnreadableManifestKeepsTheInputsBesideTheReason(t *testing.T) { // 1923/AC1
_, mods := newFormIndexer(t, mod{"avahi", unreadableAvahi(t)})
m, ok := moduleOf(t, mods, "avahi")
if !ok || !strings.HasPrefix(m.None2, "the manifest cannot be read: ") || m.FP2 != "" {
if !ok || !strings.HasPrefix(m.None2, "the manifest cannot be read: ") || m.Fp2 != "" {
t.Fatalf("module = %+v, want the reason 'the manifest cannot be read' and no fingerprint", m)
}
if m.Inputs2 == nil || m.Inputs2.Tree != "tree-avahi" || m.Inputs2.Locks["go.sum"] != avahiGoSum {
@@ -35,7 +35,7 @@ func TestAStaleUnreadableVerdictIsJudgedAgainAtStartAndAnotherReasonIsLeft(t *te
reportToolchain(t, ixGood, "mesh-build-go", "novox", "go", "go1.27.1")
reportToolchain(t, ixGood, "mesh-build-go", "ace", "go", "go1.27.1")
want, _ := moduleOf(t, good, "avahi")
if want.FP2 == "" {
if want.Fp2 == "" {
t.Fatalf("the readable manifest gives no fingerprint: %+v", want)
}
@@ -51,8 +51,8 @@ func TestAStaleUnreadableVerdictIsJudgedAgainAtStartAndAnotherReasonIsLeft(t *te
t.Fatal(err)
}
got, _ := moduleOf(t, mods, "avahi")
if got.FP2 != want.FP2 || got.None2 != "" {
t.Errorf("after the start: fp2 %q none2 %q, want %q and no reason", got.FP2, got.None2, want.FP2)
if got.Fp2 != want.Fp2 || got.None2 != "" {
t.Errorf("after the start: fp2 %q none2 %q, want %q and no reason", got.Fp2, got.None2, want.Fp2)
}
// A manifest still unreadable keeps its value as it was.
@@ -8,7 +8,8 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// The indexer's doing tool (novox/hq issue 1945; the instruction "tools are how a module is spoken to"): indexer_take_in
@@ -20,7 +21,7 @@ const (
TakeInTool = "indexer_take_in"
// TakeInsState is the indexer's state of take-in asks: the bucket mesh-delivery-indexer_takeins, keyed as the modules state.
TakeInsState = "takeins"
TakeInsBucket = delivery.IndexerModule + "_" + TakeInsState
TakeInsBucket = Module + "_" + TakeInsState
)
// TakeInAsk is a value of the takeins state: which module a person asked to be taken in, why, who and when, and, once the
@@ -84,7 +85,7 @@ func NewTakeIn(module, why, by string, at time.Time) (string, []byte, error) {
if module == "" {
return "", nil, errors.New("name the module to take in (module)")
}
key, err := delivery.ModulesKey(module)
key, err := meshcatalogclient.ModulesKey(module)
if err != nil {
return "", nil, err
}
@@ -95,7 +96,7 @@ func NewTakeIn(module, why, by string, at time.Time) (string, []byte, error) {
// NewTakeInRepository is the value indexer_take_in writes for a repository: its key and the value.
func NewTakeInRepository(repository, why, by string, at time.Time) (string, []byte, error) {
repository = strings.TrimSpace(repository)
key, err := delivery.RepositoryKey(repository)
key, err := trunk.RepositoryKey(repository)
if err != nil {
return "", nil, err
}
@@ -8,8 +8,10 @@ import (
"slices"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -30,14 +32,14 @@ func (ix *Indexer) writeTakenKnown(ctx context.Context, f forgemsg.Forge, notOld
if ix.Taken == nil || f.Merge == "" {
return nil
}
key, err := delivery.RepositoryKey(f.Repository)
key, err := trunk.RepositoryKey(f.Repository)
if err != nil {
ix.logf("%s at %s: no taken key is written: %v", f.Repository, f.Merge, err)
return nil
}
err = write(ctx, ix.Taken, key, func(value []byte, found bool) ([]byte, bool, error) {
if found {
if cur, err := delivery.DecodeTaken(value); err == nil {
if cur, err := takein.DecodeTaken(value); err == nil {
if cur.Merge == f.Merge {
return nil, false, nil
}
@@ -57,7 +59,7 @@ func (ix *Indexer) writeTakenKnown(ctx context.Context, f forgemsg.Forge, notOld
if ix.Now != nil {
now = ix.Now
}
data, err := delivery.EncodeTaken(delivery.Taken{Repository: delivery.RepositoryName(f.Repository), Merge: f.Merge, At: now().UTC()})
data, err := takein.EncodeTaken(takein.Taken{Repository: trunk.RepositoryName(f.Repository), Merge: f.Merge, At: now().UTC()})
return data, false, err
})
if errors.Is(err, process.ErrNotYet) {
@@ -6,26 +6,26 @@
}
},
"candidate": {
"v": 1,
"group": "feat/x",
"base": 5,
"fpset": "01c840ec276b",
"group": "feat/x",
"heads": "dfe9fb8f79e8",
"pulls": [
{
"repository": "novox/mesh-catalog",
"number": 301,
"head": "0123456789abcdef0123456789abcdef01234567",
"modules": [
"avahi"
],
"fps": {
"avahi": {
"fp2": "src2:e8d595aea1b13f1f5578ef1f245dab0d28a351c28079ecdcd2276c63a932d7be",
"manifest": "8a3856b6c0566d76f2556fc4f46c5251dc7935517bb6bd15809ded7e5546a0d8"
}
}
},
"head": "0123456789abcdef0123456789abcdef01234567",
"modules": [
"avahi"
],
"number": 301,
"repository": "novox/mesh-catalog"
}
],
"fpset": "01c840ec276b"
"v": 1
}
}
@@ -8,8 +8,12 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/judge"
)
@@ -23,7 +27,7 @@ import (
// records builds and makes no artifact's bytes (ADR 0498 §1), so its Builder input is empty for every module, the builder's
// own included; a change of how an artifact is built is a change of its build module's recipe (ADR 0530 §2).
// ToolchainModules are the build module of each language, whose state toolchains (delivery.ToolchainsBucket) the
// ToolchainModules are the build module of each language, whose state toolchains (ToolchainsBucket) the
// indexer reads: the language's agreed toolchain is the one every node's entry in it agrees on.
var ToolchainModules = map[string]string{"go": "mesh-build-go", "typescript": "mesh-build-node"}
@@ -33,7 +37,7 @@ var ToolchainModules = map[string]string{"go": "mesh-build-go", "typescript": "m
var ToolchainStreams = map[string]string{"mesh-build-go": "TOOLCHAINS_GO", "mesh-build-node": "TOOLCHAINS_NODE"}
// PutToolchain takes one change of a build module's toolchains bucket in, without writing any module: a put of a node's
// toolchain of a language (key <node>.<language>, delivery.ToolchainKey), or its delete. It answers whether what the
// toolchain of a language (key <node>.<language>, ToolchainKey), or its delete. It answers whether what the
// language's toolchain is of (version and recipe) changed. A key, or a value, that is not a toolchain of the module's
// language is said in the log and left out.
func (ix *Indexer) PutToolchain(module, key string, value []byte, deleted bool) bool {
@@ -47,7 +51,7 @@ func (ix *Indexer) PutToolchain(module, key string, value []byte, deleted bool)
ix.logf("toolchains of %q are not read: it is no build module the indexer knows", module)
return false
}
node, keyLanguage, err := delivery.ParseToolchainKey(key)
node, keyLanguage, err := ParseToolchainKey(key)
if err != nil || keyLanguage != language {
ix.logf("%s_toolchains.%s is not a %s toolchain's key, left out: %v", module, key, language, err)
return false
@@ -61,16 +65,16 @@ func (ix *Indexer) PutToolchain(module, key string, value []byte, deleted bool)
delete(ix.toolchains[language], node)
return true
}
tc, err := delivery.DecodeToolchain(value)
tc, err := DecodeToolchain(value)
if err != nil || tc.Language != language {
ix.logf("%s_toolchains.%s is not a %s toolchain, left out: %v", module, key, language, err)
return false
}
if ix.toolchains == nil {
ix.toolchains = map[string]map[string]delivery.Toolchain{}
ix.toolchains = map[string]map[string]nodebuildgo.Toolchain{}
}
if ix.toolchains[language] == nil {
ix.toolchains[language] = map[string]delivery.Toolchain{}
ix.toolchains[language] = map[string]nodebuildgo.Toolchain{}
}
cur, had := ix.toolchains[language][node]
ix.toolchains[language][node] = tc
@@ -102,32 +106,33 @@ func (ix *Indexer) agreed() map[string]toolchainOrNone {
defer ix.mu.Unlock()
out := map[string]toolchainOrNone{}
for language := range ToolchainModules {
var entries []delivery.NodeToolchain
var entries []sdkstep.NodeToolchain
for _, node := range slices.Sorted(maps.Keys(ix.toolchains[language])) {
entries = append(entries, delivery.NodeToolchain{Node: node, Toolchain: ix.toolchains[language][node]})
t := ix.toolchains[language][node]
entries = append(entries, sdkstep.NodeToolchain{Node: node, Toolchain: sdkstep.Toolchain{Language: t.Language, Version: t.Version, Recipe: t.Recipe}})
}
tc, none := delivery.AgreedToolchain(entries)
out[language] = toolchainOrNone{tc, none}
tc, none := sdkstep.AgreedToolchain(entries)
out[language] = toolchainOrNone{meshcatalogclient.Toolchain2{Language: tc.Language, Version: tc.Version, Recipe: tc.Recipe}, none}
}
return out
}
type toolchainOrNone struct {
tc delivery.Toolchain2
tc meshcatalogclient.Toolchain2
none string
}
// known is a module the indexer holds the inputs of: its record as read, the manifest's shape and the key it is written at.
type known struct {
key string
cur delivery.Module
cur meshcatalog.ModulesByModuleValue
manifest catalogue.Manifest
languages map[string]string
bases []string
contexts bool
// held is the fingerprint the bucket holds for the module and the toolchains it was computed with (for a merge, the
// value before it): what stillComputed keeps while the builders agree on no toolchain.
held delivery.Module
held meshcatalog.ModulesByModuleValue
}
// Recompute writes again every module whose new-form fingerprint, or reason for none, is no longer what it holds: from
@@ -142,7 +147,7 @@ func (ix *Indexer) Recompute(ctx context.Context) error { return ix.recomputeWit
// of what the bucket holds, are computed with the others, and are written once, with their final fingerprint or reason
// (issue 1444: a merge writes each module once). A record of the overlay that has no kept inputs is written as it is. w is
// the merge's write (order.go), nil for none; it is told each module written.
func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]delivery.Module, w *mergeWrite) error {
func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]meshcatalog.ModulesByModuleValue, w *mergeWrite) error {
if err := ix.fillPaths(ctx); err != nil {
return err
}
@@ -153,7 +158,7 @@ func (ix *Indexer) recomputeWith(ctx context.Context, overlay map[string]deliver
return ix.RecomputeCandidates(ctx)
}
func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]delivery.Module, w *mergeWrite) error {
func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]meshcatalog.ModulesByModuleValue, w *mergeWrite) error {
if ix.Modules == nil {
return nil
}
@@ -166,7 +171,7 @@ func (ix *Indexer) recomputeModules(ctx context.Context, overlay map[string]deli
}
// recomputeLocked is recomputeModules with ix.recompute held by the caller (a hold that moves, holds.go).
func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]delivery.Module, w *mergeWrite) error {
func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]meshcatalog.ModulesByModuleValue, w *mergeWrite) error {
if ix.Manifests == nil {
return ix.writeOverlay(ctx, overlay, nil, w)
}
@@ -180,18 +185,18 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
}
}
slices.Sort(keys)
held := map[string]delivery.Module{}
held := map[string]meshcatalog.ModulesByModuleValue{}
stale := map[string]bool{} // values an SDK that could not read their manifest wrote, judged again (issue 1923)
all := map[string]*known{}
for _, key := range keys {
cur, merged := overlay[key]
var before delivery.Module
var before meshcatalog.ModulesByModuleValue
value, _, found, err := ix.Modules.Get(ctx, key)
if err != nil {
return fmt.Errorf("modules.%s: %w", key, err)
}
if found {
if before, err = delivery.DecodeModule(value); err != nil && !merged {
if before, err = meshcatalogclient.DecodeModule(value); err != nil && !merged {
ix.logf("modules.%s holds what is not a module, left: %v", key, err)
continue
}
@@ -222,8 +227,8 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
series := ix.SeriesAll(ctx) // each package's series, the newest a user accepting it takes (DLV-I6)
type result struct {
fp, none string
tcs map[string]delivery.ToolchainUsed // by language, when there is a fingerprint
pkgs delivery.PackageVersions // the package versions, when there is a fingerprint
tcs map[string]meshcatalog.ToolchainUsed // by language, when there is a fingerprint
pkgs sdkstep.PackageVersions // the package versions, when there is a fingerprint
}
done := map[string]result{}
visiting := map[string]bool{}
@@ -238,8 +243,8 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
switch {
case !has:
return result{fp: "", none: "the indexer holds no record of it"}
case cur.FP2 != "":
return result{fp: cur.FP2, none: ""}
case cur.Fp2 != "":
return result{fp: cur.Fp2, none: ""}
case cur.None2 != "":
return result{fp: "", none: cur.None2}
}
@@ -263,7 +268,7 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
}
slices.Sort(kept.Bases)
slices.Sort(missing)
toolchains := map[string]delivery.Toolchain2{}
toolchains := map[string]meshcatalogclient.Toolchain2{}
var noToolchain string
for _, artifact := range slices.Sorted(maps.Keys(k.languages)) {
t := tools[k.languages[artifact]]
@@ -298,7 +303,7 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
k.cur.Inputs2 = &kept
return r
}
finals := map[string]delivery.Module{}
finals := map[string]meshcatalog.ModulesByModuleValue{}
edges := map[string]graphEdges{} // the module graph's nodes of this recompute (graph.go)
for _, key := range slices.Sorted(maps.Keys(all)) {
k := all[key]
@@ -306,11 +311,11 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
edges[key] = graphEdges{module: key, cur: k.cur, manifest: k.manifest, fp: r.fp, none: r.none, tcs: r.tcs, pkgs: r.pkgs}
if _, merged := overlay[key]; merged {
final := overlay[key]
final.FP2, final.None2, final.Inputs2, final.Toolchains, final.Packages = r.fp, r.none, k.cur.Inputs2, r.tcs, r.pkgs
final.Fp2, final.None2, final.Inputs2, final.Toolchains, final.Packages = r.fp, r.none, k.cur.Inputs2, r.tcs, r.pkgs
// What the bases gave is kept as it was while the value it gave does not change, so a merge
// that moves nothing for the module writes nothing (as Recompute does).
if value, _, found, err := ix.Modules.Get(ctx, key); err == nil && found {
if old, err := delivery.DecodeModule(value); err == nil && old.Inputs2 != nil && old.FP2 == r.fp && old.None2 == r.none {
if old, err := meshcatalogclient.DecodeModule(value); err == nil && old.Inputs2 != nil && old.Fp2 == r.fp && old.None2 == r.none {
in := *final.Inputs2
in.Bases, in.Builder = old.Inputs2.Bases, old.Inputs2.Builder
final.Inputs2 = &in
@@ -319,32 +324,32 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
finals[key] = final
continue
}
if !stale[key] && k.cur.FP2 == r.fp && k.cur.None2 == r.none && reflect.DeepEqual(k.cur.Toolchains, r.tcs) && reflect.DeepEqual(k.cur.Packages, r.pkgs) {
if !stale[key] && k.cur.Fp2 == r.fp && k.cur.None2 == r.none && reflect.DeepEqual(k.cur.Toolchains, r.tcs) && reflect.DeepEqual(k.cur.Packages, r.pkgs) {
continue
}
digest, inputs, paths := k.cur.Manifest, k.cur.Inputs2, k.cur.ArtifactPaths
digest, inputs, paths := k.cur.Manifest, k.cur.Inputs2, k.cur.Paths
wrote := false
err := write(ctx, ix.Modules, key, func(value []byte, found bool) ([]byte, bool, error) {
if !found {
return nil, false, nil
}
cur, err := delivery.DecodeModule(value)
cur, err := meshcatalogclient.DecodeModule(value)
if err != nil || cur.Manifest != digest || cur.Inputs2 == nil {
return nil, false, nil // a merge changed it meanwhile; its own recompute follows
}
if cur.FP2 == r.fp && cur.None2 == r.none && reflect.DeepEqual(cur.Toolchains, r.tcs) && reflect.DeepEqual(cur.Packages, r.pkgs) {
if cur.Fp2 == r.fp && cur.None2 == r.none && reflect.DeepEqual(cur.Toolchains, r.tcs) && reflect.DeepEqual(cur.Packages, r.pkgs) {
return nil, false, nil
}
if cur.FP2 != r.fp {
if cur.Fp2 != r.fp {
// Moved with no merge (a toolchain, a package, a base): no release carries this build, and the source revision
// moves only at a merge, so it is not known (0) and the move is asked of a person; never the revision of now,
// which may be the very release that last merged the module (ADR 0601 §1). The source stamp leaves it, since
// Src is set.
cur.FPSrc = 0
cur.FpSrc = 0
}
cur.FP2, cur.None2, cur.Toolchains, cur.Packages = r.fp, r.none, r.tcs, r.pkgs
cur.Inputs2, cur.ArtifactPaths = inputs, paths
data, err := delivery.EncodeModule(cur)
cur.Fp2, cur.None2, cur.Toolchains, cur.Packages = r.fp, r.none, r.tcs, r.pkgs
cur.Inputs2, cur.Paths = inputs, paths
data, err := meshcatalogclient.EncodeModule(cur)
wrote = err == nil
return data, false, err
})
@@ -371,7 +376,7 @@ func (ix *Indexer) recomputeLocked(ctx context.Context, overlay map[string]deliv
// the repository, and the controller's take-in hears only that one. The repository's trunk head is the gate's source.main.
//
// A merged message older than the repository's taken merge never reaches here (order.go): it is asked before any write.
func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]delivery.Module, w *mergeWrite) error {
func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]meshcatalog.ModulesByModuleValue, w *mergeWrite) error {
for _, key := range slices.Sorted(maps.Keys(overlay)) {
next := overlay[key]
if f, ok := finals[key]; ok {
@@ -381,7 +386,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
err := write(ctx, ix.Modules, key, func(value []byte, found bool) ([]byte, bool, error) {
wrote = false
next := next
cur, err := delivery.DecodeModule(value)
cur, err := meshcatalogclient.DecodeModule(value)
held := found && err == nil
if held && cur.Manifest == next.Manifest {
// The judge's verdict stands while the digest does (ADR 0578 §3, ADR 0579 §1): a recompute never writes a
@@ -391,10 +396,10 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
}
// The source revision at which FP2 last changed (ADR 0601 §1): this merge's when it moved it (0 until the
// source revision naming the merge is heard, which stamps it), else the one held.
if held && cur.FP2 == next.FP2 {
next.FPSrc = cur.FPSrc
if held && cur.Fp2 == next.Fp2 {
next.FpSrc = cur.FpSrc
} else {
next.FPSrc = next.Src
next.FpSrc = next.Src
}
if held && sameButWhere(cur, next) {
return nil, false, nil
@@ -402,7 +407,7 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
if next.Manifest != "" && (!held || cur.Manifest != next.Manifest) {
w.taken(next.Manifest) // a new module, or a changed manifest: fresh, judged in full (ADR 0579 §1)
}
data, err := delivery.EncodeModule(next)
data, err := meshcatalogclient.EncodeModule(next)
wrote = err == nil
return data, false, err
})
@@ -418,9 +423,9 @@ func (ix *Indexer) writeOverlay(ctx context.Context, overlay, finals map[string]
// sameButWhere says the held value cur is next but for where it was computed (Commit and Src) and the judge's verdict,
// which only the judge writes: the module is unchanged.
func sameButWhere(cur, next delivery.Module) bool {
func sameButWhere(cur, next meshcatalog.ModulesByModuleValue) bool {
next = withV(next)
next.Commit, next.Src, next.FPSrc = cur.Commit, cur.Src, cur.FPSrc
next.Commit, next.Src, next.FpSrc = cur.Commit, cur.Src, cur.FpSrc
next.Verdict, next.Problems, next.Unknown, next.Warnings = cur.Verdict, cur.Problems, cur.Unknown, cur.Warnings
next.Needs = cur.Needs
return reflect.DeepEqual(cur, next)
@@ -428,7 +433,7 @@ func sameButWhere(cur, next delivery.Module) bool {
// manifestRaw is manifests.<digest>: the manifest as stored and parsed by the running SDK.
func (ix *Indexer) manifestRaw(ctx context.Context, digest string) (string, catalogue.Manifest, error) {
key, err := delivery.ManifestsKey(digest)
key, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
return "", catalogue.Manifest{}, err
}
@@ -439,20 +444,20 @@ func (ix *Indexer) manifestRaw(ctx context.Context, digest string) (string, cata
if !found {
return "", catalogue.Manifest{}, fmt.Errorf("manifests.%s is not there", key)
}
kept, err := delivery.DecodeManifest(value)
kept, err := meshcatalogclient.DecodeManifest(value)
if err != nil {
return "", catalogue.Manifest{}, err
}
m, err := judge.ForInputs([]byte(kept.JSON))
return kept.JSON, m, err
m, err := judge.ForInputs([]byte(kept.Manifest))
return kept.Manifest, m, err
}
// rejudged is a value an SDK that could not read its manifest wrote, as the running SDK judges it: its kept inputs (the
// tree, the lock hashes by file name, the named paths' objects) taken through the manifest as a merge takes them, and the
// reason that gives. It answers false for any other value, and for one whose manifest the running SDK still cannot make
// inputs of (issue 1923).
func (ix *Indexer) rejudged(ctx context.Context, m catalogue.Manifest, cur delivery.Module, raw string) (delivery.Module, bool) {
if cur.FP2 != "" || cur.Inputs2 == nil || !strings.HasPrefix(cur.None2, None2Unreadable) {
func (ix *Indexer) rejudged(ctx context.Context, m catalogue.Manifest, cur meshcatalog.ModulesByModuleValue, raw string) (meshcatalog.ModulesByModuleValue, bool) {
if cur.Fp2 != "" || cur.Inputs2 == nil || !strings.HasPrefix(cur.None2, None2Unreadable) {
return cur, false
}
in := inputsOfNamed(m.Module, raw, cur.Inputs2.Tree, cur.Inputs2.Locks, cur.Inputs2.Paths, cur.Inputs2.Packages, ix.baseFP(ctx))
@@ -460,7 +465,7 @@ func (ix *Indexer) rejudged(ctx context.Context, m catalogue.Manifest, cur deliv
return cur, false
}
in.Kept.Imports, in.Kept.ImportForm = cur.Inputs2.Imports, cur.Inputs2.ImportForm
cur.Inputs2, cur.None2, cur.ArtifactPaths = in.Kept, in.None2, in.ArtifactPaths
cur.Inputs2, cur.None2, cur.Paths = in.Kept, in.None2, in.ArtifactPaths
return cur, true
}
@@ -484,11 +489,11 @@ func hasContext(m catalogue.Manifest) bool {
// with the toolchains it was computed with in place of the languages agreed on none, give it again, in its form. Any
// other input that moved (a merge, a base, an agreed toolchain, an imported package) gives another fingerprint, and the
// module has none until they agree.
func stillComputed(module string, kept delivery.KeptInputs2, k *known, agreed map[string]delivery.Toolchain2, held map[string]delivery.Module, published map[string]delivery.PublishedPackages, pins delivery.PackageVersions) (string, delivery.PackageVersions, bool) {
if k.held.FP2 == "" {
func stillComputed(module string, kept meshcatalog.ModulesByModuleValueInputs2, k *known, agreed map[string]meshcatalogclient.Toolchain2, held map[string]meshcatalog.ModulesByModuleValue, published map[string]meshcatalog.PackagesByModuleValue, pins sdkstep.PackageVersions) (string, sdkstep.PackageVersions, bool) {
if k.held.Fp2 == "" {
return "", nil, false
}
toolchains := map[string]delivery.Toolchain2{}
toolchains := map[string]meshcatalogclient.Toolchain2{}
for artifact, language := range k.languages {
if tc, ok := agreed[artifact]; ok {
toolchains[artifact] = tc
@@ -498,20 +503,20 @@ func stillComputed(module string, kept delivery.KeptInputs2, k *known, agreed ma
if !ok {
return "", nil, false
}
toolchains[artifact] = delivery.Toolchain2{Language: language, Version: used.Version, Recipe: used.Recipe}
toolchains[artifact] = meshcatalogclient.Toolchain2{Language: language, Version: used.Version, Recipe: used.Recipe}
}
f := formOf(module, k.held.Repository, &kept, toolchains, k.held, held, published, pins, nil)
return f.fp, f.versions, f.none == "" && f.fp == k.held.FP2
return f.fp, f.versions, f.none == "" && f.fp == k.held.Fp2
}
// toolchainsUsed are the toolchains a fingerprint was computed with, by language (an artifact's toolchain is its language's).
func toolchainsUsed(by map[string]delivery.Toolchain2) map[string]delivery.ToolchainUsed {
func toolchainsUsed(by map[string]meshcatalogclient.Toolchain2) map[string]meshcatalog.ToolchainUsed {
if len(by) == 0 {
return nil
}
out := map[string]delivery.ToolchainUsed{}
out := map[string]meshcatalog.ToolchainUsed{}
for _, t := range by {
out[t.Language] = delivery.ToolchainUsed{Version: t.Version, Recipe: t.Recipe}
out[t.Language] = meshcatalog.ToolchainUsed{Version: t.Version, Recipe: t.Recipe}
}
return out
}
@@ -554,12 +559,12 @@ func (ix *Indexer) fillPaths(ctx context.Context) error {
if !found {
return nil, false, nil
}
cur, err := delivery.DecodeModule(value)
cur, err := meshcatalogclient.DecodeModule(value)
if err != nil || cur.Path != "" {
return nil, false, nil
}
cur.Path = dir
data, err := delivery.EncodeModule(cur)
data, err := meshcatalogclient.EncodeModule(cur)
return data, false, err
})
if err != nil {
@@ -10,8 +10,15 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -56,8 +63,8 @@ const (
func agentBuild(id, src, at string) [2]any {
when, _ := time.Parse(time.RFC3339, at)
raw, _ := delivery.EncodeBuildLogEntry(delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: "build-agent", FP: src, Repository: agentCloneURL,
Directory: "modules/build-agent", Commit: "cada7e3c", Node: "n1", At: when, Outcome: delivery.BuildDone})
raw, _ := builds.EncodeBuildLogEntry(builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "build-agent", FP: src, Repository: agentCloneURL,
Directory: "modules/build-agent", Commit: "cada7e3c", Node: "n1", At: when, Outcome: builds.BuildDone})
return [2]any{"build-agent", raw}
}
@@ -111,7 +118,7 @@ func newFormIndexerUnmerged(t *testing.T, mods ...mod) (*Indexer, *memStore, for
// toolchainValue is a toolchains value as the build module's writer encodes it (mesh-sdk go/delivery EncodeToolchain).
func toolchainValue(t *testing.T, language, version, recipe string) []byte {
t.Helper()
b, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now()})
b, err := EncodeToolchain(nodebuildgo.Toolchain{Language: language, Version: version, Recipe: recipe, At: ptr(time.Now())})
if err != nil {
t.Fatal(err)
}
@@ -125,7 +132,7 @@ func reportToolchain(t *testing.T, ix *Indexer, module, node, language, version
func reportToolchainRecipe(t *testing.T, ix *Indexer, module, node, language, version, recipe string) {
t.Helper()
key, err := delivery.ToolchainKey(node, language)
key, err := ToolchainKey(node, language)
if err != nil {
t.Fatal(err)
}
@@ -136,27 +143,27 @@ func reportToolchainRecipe(t *testing.T, ix *Indexer, module, node, language, ve
func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 1444/AC5
ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")})
if m, _ := moduleOf(t, mods, "avahi"); m.None2 != delivery.ToolchainNoReport {
if m, _ := moduleOf(t, mods, "avahi"); m.None2 != sdkstep.ToolchainNoReport {
t.Run("a recipe change builds a Go module at once and again once the agreement moves", aRecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves)
t.Fatalf("before: none2 = %q", m.None2)
}
// The release that last merged avahi: its source revision names avahi's commit, so Src (and FPSrc) are 7.
merged, _ := moduleOf(t, mods, "avahi")
src := delivery.Source{Group: "g", Heads: map[string]string{merged.Repository: merged.Commit}}
src := meshdelivery.SourceByMainValue{Group: "g", Heads: map[string]string{merged.Repository: merged.Commit}}
if err := ix.ApplySource(context.Background(), 7, src); err != nil {
t.Fatal(err)
}
if m, _ := moduleOf(t, mods, "avahi"); m.Src != 7 || m.FPSrc != 7 {
t.Fatalf("before: src %d fp-src %d, want 7 and 7", m.Src, m.FPSrc)
if m, _ := moduleOf(t, mods, "avahi"); m.Src != 7 || m.FpSrc != 7 {
t.Fatalf("before: src %d fp-src %d, want 7 and 7", m.Src, m.FpSrc)
}
if g := graphOf(t, ix, "avahi"); g.None != delivery.ToolchainNoReport || g.Toolchains != nil {
if g := graphOf(t, ix, "avahi"); g.None != sdkstep.ToolchainNoReport || g.Toolchains != nil {
t.Errorf("before: graph.avahi = %+v, want no toolchain edge and the reason", g)
}
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.1")
m, _ := moduleOf(t, mods, "avahi")
// L3 (ADR 0603 §2, §3): the agreement moving rewrites the module's toolchain edge, and again when it moves on.
if g := graphOf(t, ix, "avahi"); g.FP != m.FP2 || g.Toolchains["go"].Version != "go1.27.1" || g.Toolchains["go"].Module != "mesh-build-go" {
if g := graphOf(t, ix, "avahi"); g.FP != m.Fp2 || g.Toolchains["go"].Version != "go1.27.1" || g.Toolchains["go"].Module != "mesh-build-go" {
t.Errorf("after: graph.avahi = %+v, want the toolchain edge of go1.27.1", g)
}
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.28.0")
@@ -167,19 +174,19 @@ func TestAgreedToolchainChangesAGoModulesReasonWithNoMerge(t *testing.T) { // 14
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.1")
m, _ = moduleOf(t, mods, "avahi")
if m.None2 != "" || m.FP2 == "" {
t.Errorf("none2 = %q fp2 = %q, want a fingerprint: no builder input is waited for (ADR 0530 §1)", m.None2, m.FP2)
if m.None2 != "" || m.Fp2 == "" {
t.Errorf("none2 = %q fp2 = %q, want a fingerprint: no builder input is waited for (ADR 0530 §1)", m.None2, m.Fp2)
}
// ADR 0601 §1 (review round 2): a fingerprint moved with no merge has no release that carries it: fp-src 0, not known,
// though the source revision is still the release's at the module's own Src; a source revision heard again stamps nothing.
if m.FPSrc != 0 {
t.Errorf("a fingerprint moved by a recompute: fp-src %d, want 0", m.FPSrc)
if m.FpSrc != 0 {
t.Errorf("a fingerprint moved by a recompute: fp-src %d, want 0", m.FpSrc)
}
if err := ix.ApplySource(context.Background(), 8, src); err != nil {
t.Fatal(err)
}
if again, _ := moduleOf(t, mods, "avahi"); again.FPSrc != 0 {
t.Errorf("a source revision stamped the recomputed fingerprint: fp-src %d", again.FPSrc)
if again, _ := moduleOf(t, mods, "avahi"); again.FpSrc != 0 {
t.Errorf("a source revision stamped the recomputed fingerprint: fp-src %d", again.FpSrc)
}
}
@@ -192,19 +199,19 @@ func aRecipeChangeBuildsAGoModuleAtOnceAndAgainOnceTheAgreementMoves(t *testing.
reportToolchain(t, ix, "mesh-build-go", node, "go", "go1.27.1")
}
first, _ := moduleOf(t, mods, "avahi")
if first.FP2 == "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "1" {
t.Fatalf("with the running recipe: fp2 %q, graph %+v", first.FP2, graphOf(t, ix, "avahi"))
if first.Fp2 == "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "1" {
t.Fatalf("with the running recipe: fp2 %q, graph %+v", first.Fp2, graphOf(t, ix, "avahi"))
}
// The release carrying the new recipe is merged, but the nodes still run the old one: one node reporting the new recipe is
// no strict majority, so the fingerprint stays, nothing waits.
reportToolchainRecipe(t, ix, "mesh-build-go", "novox", "go", "go1.27.1", "2")
if held, _ := moduleOf(t, mods, "avahi"); held.FP2 != first.FP2 || held.None2 != "" {
t.Errorf("one node on the new recipe: fp2 %q none2 %q, want the fingerprint of the running recipe, %q", held.FP2, held.None2, first.FP2)
if held, _ := moduleOf(t, mods, "avahi"); held.Fp2 != first.Fp2 || held.None2 != "" {
t.Errorf("one node on the new recipe: fp2 %q none2 %q, want the fingerprint of the running recipe, %q", held.Fp2, held.None2, first.Fp2)
}
reportToolchainRecipe(t, ix, "mesh-build-go", "ace", "go", "go1.27.1", "2")
again, _ := moduleOf(t, mods, "avahi")
if again.FP2 == "" || again.FP2 == first.FP2 || again.None2 != "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "2" {
t.Errorf("the agreement moved: fp2 %q (was %q) none2 %q graph %+v, want a second fingerprint at recipe 2", again.FP2, first.FP2, again.None2, graphOf(t, ix, "avahi"))
if again.Fp2 == "" || again.Fp2 == first.Fp2 || again.None2 != "" || graphOf(t, ix, "avahi").Toolchains["go"].Recipe != "2" {
t.Errorf("the agreement moved: fp2 %q (was %q) none2 %q graph %+v, want a second fingerprint at recipe 2", again.Fp2, first.Fp2, again.None2, graphOf(t, ix, "avahi"))
}
}
@@ -213,7 +220,7 @@ func TestNodesReportingDifferentVersionsLeaveTheReasonNamingBoth(t *testing.T) {
// disagree is kept: TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith).
ix, mods := newFormIndexer(t, mod{"avahi", catalogueManifest(t, "avahi")})
for node, version := range map[string]string{"novox": "go1.27.1", "ace": "go1.26.0"} {
key, _ := delivery.ToolchainKey(node, "go")
key, _ := ToolchainKey(node, "go")
ix.PutToolchain("mesh-build-go", key, toolchainValue(t, "go", version, "1"), false)
}
if err := ix.Recompute(context.Background()); err != nil {
@@ -241,7 +248,7 @@ func goInputs(t *testing.T, ix *Indexer, mods *memStore, module, builder string,
t.Helper()
return fingerprint.Inputs{Module: module, Tree: "tree-" + module, Builder: builder, Bases: bases,
Locks: map[string]string{"tools": avahiGoSum},
Toolchains: map[string]delivery.Toolchain2{"tools": {Language: "go", Version: "go1.27.1", Recipe: "1"}}}
Toolchains: map[string]meshcatalogclient.Toolchain2{"tools": {Language: "go", Version: "go1.27.1", Recipe: "1"}}}
}
func fp2(t *testing.T, in fingerprint.Inputs) string {
@@ -262,12 +269,12 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
toolsFP := fp2(t, goInputs(t, ix, mods, "mesh-tools", ""))
want := map[string]string{"mesh-delivery-builder": builderFP, "mesh-tools": toolsFP, "avahi": fp2(t, goInputs(t, ix, mods, "avahi", "", toolsFP))}
for name, fp := range want {
if m, _ := moduleOf(t, mods, name); m.FP2 != fp || m.None2 != "" || m.Inputs2 == nil || m.Inputs2.Builder != "" {
t.Errorf("%s: fp2 = %q none2 = %q inputs %+v, want %s with no builder input", name, m.FP2, m.None2, m.Inputs2, fp)
if m, _ := moduleOf(t, mods, name); m.Fp2 != fp || m.None2 != "" || m.Inputs2 == nil || m.Inputs2.Builder != "" {
t.Errorf("%s: fp2 = %q none2 = %q inputs %+v, want %s with no builder input", name, m.Fp2, m.None2, m.Inputs2, fp)
}
}
if m, _ := moduleOf(t, mods, "other"); m.FP2 != "" || !strings.Contains(m.None2, "ts-base") || !strings.Contains(m.None2, "base") {
t.Errorf("other: fp2 = %q none2 = %q, want none2 naming the base ts-base", m.FP2, m.None2)
if m, _ := moduleOf(t, mods, "other"); m.Fp2 != "" || !strings.Contains(m.None2, "ts-base") || !strings.Contains(m.None2, "base") {
t.Errorf("other: fp2 = %q none2 = %q, want none2 naming the base ts-base", m.Fp2, m.None2)
}
// L3 (ADR 0603 §2): the module graph holds each module's edges as this recompute computed them.
goTool := map[string]graph.Toolchain{"go": {Module: "mesh-build-go", Version: "go1.27.1", Recipe: "1"}}
@@ -328,8 +335,8 @@ func TestBasesAreComputedFirstAndNoFingerprintTakesTheBuilder(t *testing.T) { //
t.Fatal(err)
}
for name, fp := range want {
if m, _ := moduleOf(t, mods, name); m.FP2 != fp {
t.Errorf("%s moved with a build of build-agent: fp2 = %q, want %s", name, m.FP2, fp)
if m, _ := moduleOf(t, mods, name); m.Fp2 != fp {
t.Errorf("%s moved with a build of build-agent: fp2 = %q, want %s", name, m.Fp2, fp)
}
}
// ADR 0614 §2, §3: the graph names what each module invokes and serves, and leaves out, over the whole graph, the
@@ -356,7 +363,7 @@ func TestABuildHeardBeforeAnyToolchainIsReadDropsNoFingerprint(t *testing.T) { /
ix, mods := newFormIndexer(t, builderModules(t)...)
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
before, _ := moduleOf(t, mods, "mesh-tools")
if before.FP2 == "" {
if before.Fp2 == "" {
t.Fatalf("mesh-tools has no fp2 to begin with: %+v", before)
}
// a restarted indexer has read no toolchains bucket yet
@@ -366,8 +373,8 @@ func TestABuildHeardBeforeAnyToolchainIsReadDropsNoFingerprint(t *testing.T) { /
if err := ix.RecomputeAtStart(context.Background()); err != nil {
t.Fatal(err)
}
if m, _ := moduleOf(t, mods, "mesh-tools"); m.FP2 != before.FP2 || m.None2 != "" {
t.Errorf("mesh-tools: fp2 = %q none2 = %q, want fp2 %q kept", m.FP2, m.None2, before.FP2)
if m, _ := moduleOf(t, mods, "mesh-tools"); m.Fp2 != before.Fp2 || m.None2 != "" {
t.Errorf("mesh-tools: fp2 = %q none2 = %q, want fp2 %q kept", m.Fp2, m.None2, before.Fp2)
}
}
@@ -388,8 +395,8 @@ func TestModulesThatStandOnEachOtherAreNamedAsACycle(t *testing.T) { // 1444 (th
mod{"bb", standingOn(t, renamed(t, "avahi", "bb"), "aa")})
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
for _, name := range []string{"aa", "bb"} {
if m, _ := moduleOf(t, mods, name); m.FP2 != "" || !strings.Contains(m.None2, "cycle") {
t.Errorf("%s: fp2 = %q none2 = %q, want a reason naming the cycle", name, m.FP2, m.None2)
if m, _ := moduleOf(t, mods, name); m.Fp2 != "" || !strings.Contains(m.None2, "cycle") {
t.Errorf("%s: fp2 = %q none2 = %q, want a reason naming the cycle", name, m.Fp2, m.None2)
}
}
}
@@ -402,8 +409,8 @@ func TestAMergeWritesEachModuleOnceWithItsFinalValueAndAReplayWritesNothing(t *t
t.Fatal(err)
}
m, _ := moduleOf(t, mods, "avahi")
if mods.writes != 1 || m.FP2 == "" {
t.Errorf("writes = %d fp2 = %q after one merge, want one write with its fingerprint", mods.writes, m.FP2)
if mods.writes != 1 || m.Fp2 == "" {
t.Errorf("writes = %d fp2 = %q after one merge, want one write with its fingerprint", mods.writes, m.Fp2)
}
if err := ix.Apply(ctx, f); err != nil {
t.Fatal(err)
@@ -418,7 +425,7 @@ func TestAMergeKeepsAFingerprintTheModuleStillHas(t *testing.T) { // 1444 (revie
ix, mods := newFormIndexer(t, builderModules(t)...)
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
before, _ := moduleOf(t, mods, "mesh-tools")
if before.FP2 == "" {
if before.Fp2 == "" {
t.Fatalf("mesh-tools has no fp2: %+v", before)
}
w := mods.writes
@@ -429,8 +436,8 @@ func TestAMergeKeepsAFingerprintTheModuleStillHas(t *testing.T) { // 1444 (revie
t.Fatal(err)
}
// and the commit stays the one the fingerprint first appeared at (the operator, 2026-10-25)
if after, _ := moduleOf(t, mods, "mesh-tools"); after.FP2 != before.FP2 || after.Commit != before.Commit || mods.writes != w {
t.Errorf("fp2 = %q at %.8s (was %q at %.8s), writes %d more", after.FP2, after.Commit, before.FP2, before.Commit, mods.writes-w)
if after, _ := moduleOf(t, mods, "mesh-tools"); after.Fp2 != before.Fp2 || after.Commit != before.Commit || mods.writes != w {
t.Errorf("fp2 = %q at %.8s (was %q at %.8s), writes %d more", after.Fp2, after.Commit, before.Fp2, before.Commit, mods.writes-w)
}
}
@@ -441,18 +448,18 @@ func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) {
}
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.1")
m, _ := moduleOf(t, mods, "avahi")
want := map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}}
if m.FP2 == "" || !reflect.DeepEqual(m.Toolchains, want) {
t.Fatalf("avahi: fp2 %q toolchains %+v, want %+v", m.FP2, m.Toolchains, want)
want := map[string]meshcatalog.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}}
if m.Fp2 == "" || !reflect.DeepEqual(m.Toolchains, want) {
t.Fatalf("avahi: fp2 %q toolchains %+v, want %+v", m.Fp2, m.Toolchains, want)
}
if o, _ := moduleOf(t, mods, "other"); o.FP2 != "" || o.Toolchains != nil {
if o, _ := moduleOf(t, mods, "other"); o.Fp2 != "" || o.Toolchains != nil {
t.Errorf("a module with no fingerprint carries toolchains %+v", o.Toolchains)
}
// An upgrade moves the fingerprint and the toolchains together, with no merge.
reportToolchain(t, ix, "mesh-build-go", "novox", "go", "go1.27.2")
n, _ := moduleOf(t, mods, "avahi")
if n.FP2 == m.FP2 || n.Toolchains["go"].Version != "go1.27.2" {
t.Errorf("after the upgrade: fp2 %q (was %q) toolchains %+v, want the new version with the new fingerprint", n.FP2, m.FP2, n.Toolchains)
if n.Fp2 == m.Fp2 || n.Toolchains["go"].Version != "go1.27.2" {
t.Errorf("after the upgrade: fp2 %q (was %q) toolchains %+v, want the new version with the new fingerprint", n.Fp2, m.Fp2, n.Toolchains)
}
// Issue 1585 (ADR 0573 §2): a module whose go.sum names mesh-sdk takes mesh-sdk's latest published version into its
// fingerprint and keeps it beside its toolchains; a mesh-sdk trunk merge moves it with no merge of its own, and a module
@@ -463,50 +470,50 @@ func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) {
if err := ix.Apply(context.Background(), uses); err != nil {
t.Fatal(err)
}
if u, _ := moduleOf(t, mods, "avahi"); u.FP2 != "" || u.None2 != fingerprint.NoPackageVersion+sdk {
t.Errorf("no published mesh-sdk: fp2 %q none2 %q", u.FP2, u.None2)
if u, _ := moduleOf(t, mods, "avahi"); u.Fp2 != "" || u.None2 != fingerprint.NoPackageVersion+sdk {
t.Errorf("no published mesh-sdk: fp2 %q none2 %q", u.Fp2, u.None2)
}
tools, _ := moduleOf(t, mods, "mesh-tools")
sdkMerge := func(commit, tree string) delivery.Module {
sdkMerge := func(commit, tree string) meshcatalog.ModulesByModuleValue {
t.Helper()
f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1,
f := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1,
Merge: commit, Root: tree, Trees: map[string]string{"": tree}, Manifests: map[string]string{"": `{"module":"mesh-sdk","version":"1"}`}}
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
a, _ := moduleOf(t, mods, "avahi")
if a.FP2 == "" || !reflect.DeepEqual(a.Packages, delivery.PackageVersions{sdk: commit}) {
t.Fatalf("after mesh-sdk's merge %.8s: fp2 %q none2 %q packages %v", commit, a.FP2, a.None2, a.Packages)
if a.Fp2 == "" || !reflect.DeepEqual(a.Packages, map[string]string{sdk: commit}) {
t.Fatalf("after mesh-sdk's merge %.8s: fp2 %q none2 %q packages %v", commit, a.Fp2, a.None2, a.Packages)
}
if o, _ := moduleOf(t, mods, "mesh-tools"); o.FP2 != tools.FP2 || o.Packages != nil {
t.Errorf("mesh-tools uses no package and moved: fp2 %q (was %q) packages %v", o.FP2, tools.FP2, o.Packages)
if o, _ := moduleOf(t, mods, "mesh-tools"); o.Fp2 != tools.Fp2 || o.Packages != nil {
t.Errorf("mesh-tools uses no package and moved: fp2 %q (was %q) packages %v", o.Fp2, tools.Fp2, o.Packages)
}
return a
}
first := sdkMerge(strings.Repeat("1", 40), "tree-sdk-1")
second := sdkMerge(strings.Repeat("2", 40), "tree-sdk-2")
if second.FP2 == first.FP2 {
t.Errorf("a mesh-sdk trunk merge kept avahi's fingerprint %q", first.FP2)
if second.Fp2 == first.Fp2 {
t.Errorf("a mesh-sdk trunk merge kept avahi's fingerprint %q", first.Fp2)
}
n = second
// Nodes that disagree keep the fingerprint already computed, with its toolchains (issue 1940, 2026-10-25 20:56: a
// stale report blanked every module and the controller's take-in refused modules already built); only a new
// fingerprint needs the agreement: a merge that moves the module's tree gives it none, naming the disagreement.
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.26.0")
if d, _ := moduleOf(t, mods, "avahi"); d.FP2 != n.FP2 || !reflect.DeepEqual(d.Toolchains, n.Toolchains) || d.None2 != "" {
t.Errorf("disagreeing nodes: fp2 %q toolchains %+v none2 %q, want the fingerprint kept (%q) with %+v", d.FP2, d.Toolchains, d.None2, n.FP2, n.Toolchains)
if d, _ := moduleOf(t, mods, "avahi"); d.Fp2 != n.Fp2 || !reflect.DeepEqual(d.Toolchains, n.Toolchains) || d.None2 != "" {
t.Errorf("disagreeing nodes: fp2 %q toolchains %+v none2 %q, want the fingerprint kept (%q) with %+v", d.Fp2, d.Toolchains, d.None2, n.Fp2, n.Toolchains)
}
_, _, f := newFormIndexerUnmerged(t, builderModules(t)...)
f.Trees["modules/avahi"] = "tree-avahi-moved"
if err := ix.Apply(context.Background(), f); err != nil {
t.Fatal(err)
}
if d, _ := moduleOf(t, mods, "avahi"); d.FP2 != "" || d.Toolchains != nil || !strings.HasPrefix(d.None2, "builders disagree") {
t.Errorf("a new tree while nodes disagree: fp2 %q toolchains %+v none2 %q, want no fingerprint and the disagreement", d.FP2, d.Toolchains, d.None2)
if d, _ := moduleOf(t, mods, "avahi"); d.Fp2 != "" || d.Toolchains != nil || !strings.HasPrefix(d.None2, "builders disagree") {
t.Errorf("a new tree while nodes disagree: fp2 %q toolchains %+v none2 %q, want no fingerprint and the disagreement", d.Fp2, d.Toolchains, d.None2)
}
// They agree again: the new tree gets its fingerprint.
reportToolchain(t, ix, "mesh-build-go", "ace", "go", "go1.27.2")
if a, _ := moduleOf(t, mods, "avahi"); a.FP2 == "" || a.FP2 == n.FP2 {
t.Errorf("agreed again: fp2 %q (was %q), want a new fingerprint", a.FP2, n.FP2)
if a, _ := moduleOf(t, mods, "avahi"); a.Fp2 == "" || a.Fp2 == n.Fp2 {
t.Errorf("agreed again: fp2 %q (was %q), want a new fingerprint", a.Fp2, n.Fp2)
}
}
@@ -0,0 +1,81 @@
package index
import (
"encoding/json"
"fmt"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// The toolchains the build modules keep in their state toolchains, one key per node and language, `<node>.<language>`
// (reads: mesh-build-go.toolchains, mesh-build-node.toolchains). Both build modules state it in the one form of a
// node-build seat; the indexer reads it as nodebuildgo.Toolchain and takes the agreed one with the delivery sdk's rule
// (buildstep.AgreedToolchain, novox/hq ADR 0584).
func checkToolchainLanguage(language string) error {
switch language {
case "go", "typescript":
return nil
}
return fmt.Errorf("index: the toolchain language %q is not go or typescript", language)
}
// ToolchainKey is the key of a node's toolchain of one language. It refuses a node that is not a name and a language
// other than go or typescript.
func ToolchainKey(node, language string) (string, error) {
if err := keys.CheckName("node", node); err != nil {
return "", err
}
if err := checkToolchainLanguage(language); err != nil {
return "", err
}
return nodebuildgo.ToolchainsByNodeLanguageKey(node, language), nil
}
// ParseToolchainKey is the inverse of ToolchainKey.
func ParseToolchainKey(key string) (node, language string, err error) {
node, language, ok := strings.Cut(key, ".")
if !ok {
return "", "", fmt.Errorf("index: %q is not a toolchain key, <node>.<language>", key)
}
if _, err := ToolchainKey(node, language); err != nil {
return "", "", fmt.Errorf("index: %q is not a toolchain key: %w", key, err)
}
return node, language, nil
}
func checkToolchain(t nodebuildgo.Toolchain) error {
if err := checkToolchainLanguage(t.Language); err != nil {
return err
}
if t.Version == "" || t.Recipe == "" {
return fmt.Errorf("index: a %s toolchain needs its version and its recipe", t.Language)
}
return nil
}
// EncodeToolchain is t as JSON at the kept values' version (the build modules write it; the indexer's tests do too).
func EncodeToolchain(t nodebuildgo.Toolchain) ([]byte, error) {
if err := checkToolchain(t); err != nil {
return nil, err
}
t.V = keys.Version
return json.Marshal(t)
}
// DecodeToolchain reads a toolchains value; it refuses an unknown version and what EncodeToolchain refuses.
func DecodeToolchain(data []byte) (nodebuildgo.Toolchain, error) {
if err := keys.CheckVersion(data); err != nil {
return nodebuildgo.Toolchain{}, err
}
t, err := nodebuildgo.DecodeToolchainsByNodeLanguageValue(data)
if err != nil {
return nodebuildgo.Toolchain{}, fmt.Errorf("index: a toolchain does not fit its shape: %w", err)
}
if err := checkToolchain(t); err != nil {
return nodebuildgo.Toolchain{}, err
}
return t, nil
}
@@ -10,22 +10,23 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-indexer/judge"
)
// The indexer judges every manifest it holds, once, with its own judge (novox/hq ADR 0578 §1): it writes each verdict in
// its state verdicts (delivery.VerdictsBucket), one key per manifest digest, only when the verdict differs from the one
// its state verdicts (VerdictsBucket), one key per manifest digest, only when the verdict differs from the one
// held; a module's value in the modules state carries a refusal (Verdict, Problems), so a reader of modules takes it
// without a second read; a new refusal is said as the event manifest-refused; a manifest with a field the judge does not know waits instead (ownBuildIsNewest). It judges at its start, so a judge that
// changed (a new indexer) judges every manifest again, and after every merge it takes in, since a rule between manifests
// can turn on any of them. Nothing polls.
// VerdictsBucket is the indexer's state of verdicts; VerdictsState is its local name, as the tools bundle reads it.
var VerdictsBucket = delivery.VerdictsBucket
const VerdictsBucket = Module + "_" + VerdictsState
const VerdictsState = "verdicts"
@@ -113,12 +114,12 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, overMod
ix.logf("judge: the manifest of %s (digest %s) is not in the manifests state, so it is not judged", name, digest)
continue
}
kept, err := delivery.DecodeManifest(value)
kept, err := meshcatalogclient.DecodeManifest(value)
if err != nil {
ix.logf("judge: manifests.%s of %s cannot be read, so it is not judged: %v", digest, name, err)
continue
}
raws[name] = []byte(kept.JSON)
raws[name] = []byte(kept.Manifest)
digests[name] = digest
}
predefined := ix.predefinedList(ctx)
@@ -134,13 +135,13 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, overMod
continue
}
value, _, found, err := ix.Manifests.Get(ctx, digest)
kept, derr := delivery.DecodeManifest(value)
kept, derr := meshcatalogclient.DecodeManifest(value)
if err != nil || !found || derr != nil {
ix.logf("judge: the trunk's manifest %s of %s cannot be read, so its renames are not judged against it", digest, module)
continue
}
head, herr := catalogue.Decode(raws[module])
trunk, terr := catalogue.Decode([]byte(kept.JSON))
trunk, terr := catalogue.Decode([]byte(kept.Manifest))
if herr != nil || terr != nil {
continue // the manifest's own problems say why
}
@@ -163,11 +164,11 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, overMod
continue
}
out.Judged++
v := delivery.ManifestVerdict{Module: name, State: delivery.VerdictAccepted, Judge: id, At: ix.now()}
v := meshcatalog.VerdictsByDigestValue{Module: name, State: meshcatalogclient.VerdictAccepted, Judge: id, At: ptr(ix.now())}
if fields := waits[name]; len(fields) > 0 {
accepted := ix.acceptedBefore(ctx, digests[name], fresh)
if !newest && !accepted {
v.State, v.Unknown = delivery.VerdictManifestWaits, fields
v.State, v.Unknown = meshcatalogclient.VerdictManifestWaits, fields
out.Waiting = append(out.Waiting, name)
} else {
_, problems := judge.Manifest(raws[name], ix.predefinedList(ctx))
@@ -175,7 +176,7 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, overMod
v.Warnings = problems
out.Outgrown = append(out.Outgrown, name)
} else {
v.State, v.Problems = delivery.VerdictRefused, problems
v.State, v.Problems = meshcatalogclient.VerdictRefused, problems
}
}
} else if problems := refused[name]; len(problems) > 0 {
@@ -183,10 +184,10 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, overMod
v.Warnings = problems
out.Outgrown = append(out.Outgrown, name)
} else {
v.State, v.Problems = delivery.VerdictRefused, problems
v.State, v.Problems = meshcatalogclient.VerdictRefused, problems
}
}
if !v.Refused() {
if !meshcatalogclient.VerdictRefuses(v) {
if m, err := catalogue.Decode(raws[name]); err == nil {
m.Module = name
accepted = append(accepted, m)
@@ -205,21 +206,21 @@ func (ix *Indexer) judgeOver(ctx context.Context, fresh map[string]bool, overMod
ix.logf("judge: %s judged again by %s: %s", digests[name], id, v.State)
} else if wrote {
out.Written++
if v.Waits() {
ix.logf("judge: %s", v.Why())
if meshcatalogclient.VerdictWaits(v) {
ix.logf("judge: %s", meshcatalogclient.VerdictWhy(v))
}
if v.Refused() {
ix.logf("judge: %s", v.Why())
if meshcatalogclient.VerdictRefuses(v) {
ix.logf("judge: %s", meshcatalogclient.VerdictWhy(v))
if ix.SayRefused != nil {
r := delivery.RefusedOf(digests[name], v)
r.Revision = revision
r := refusedOf(digests[name], v)
r.Revision = int64(revision)
ix.SayRefused(ctx, r)
}
} else if !v.Waits() {
} else if !meshcatalogclient.VerdictWaits(v) {
// Every manifest newly accepted is said whole to the bus module, its entries empty when it asks for none
// (issue 1966 D3, ADR 0595 §4, design 25 § 12.2); a manifest set aside to wait gets none (DLV-I9).
a := acceptedOf(digests[name], v, revision, raws[name])
_ = ix.sayAccepted(ctx, a, a.EventID())
_ = ix.sayAccepted(ctx, a, acceptedEventID(a))
}
}
// The reader capabilities the manifest's own words, forms and seat names need, named with the verdict (ADR 0614 §4).
@@ -270,8 +271,8 @@ func (ix *Indexer) staleVerdict(ctx context.Context, digest string) bool {
if !found {
return true
}
v, err := delivery.DecodeManifestVerdict(value)
return err != nil || (v.Held() && v.Judge != ix.judgeID())
v, err := meshcatalogclient.DecodeVerdict(value)
return err != nil || (meshcatalogclient.VerdictHeld(v) && v.Judge != ix.judgeID())
}
// judgeDigests judges each digest of over (module to digest) whose verdict is stale (staleVerdict), one at a time in the
@@ -307,7 +308,7 @@ func (ix *Indexer) judgeStale(ctx context.Context) {
if err != nil || !found {
continue
}
if v, err := delivery.DecodeManifestVerdict(value); err == nil && v.Held() && v.Judge != ix.judgeID() {
if v, err := meshcatalogclient.DecodeVerdict(value); err == nil && meshcatalogclient.VerdictHeld(v) && v.Judge != ix.judgeID() {
byModule[v.Module] = append(byModule[v.Module], digest)
}
}
@@ -351,7 +352,7 @@ func (ix *Indexer) knownPackages(ctx context.Context) judge.KnownPackages {
ix.logf("judge: packages.%s cannot be read, so the names of packages are not judged: %v", key, err)
return nil
}
if s, err := delivery.DecodeSeriesPackage(value); found && err == nil {
if s, err := DecodeSeriesPackage(value); found && err == nil {
known[s.Name] = true
}
}
@@ -380,35 +381,35 @@ func (ix *Indexer) acceptedBefore(ctx context.Context, digest string, fresh map[
if !found {
return !fresh[digest]
}
prev, err := delivery.DecodeManifestVerdict(value)
return err == nil && prev.State == delivery.VerdictAccepted
prev, err := meshcatalogclient.DecodeVerdict(value)
return err == nil && prev.State == meshcatalogclient.VerdictAccepted
}
// writeVerdict writes v under the digest's key unless the verdict held there says the same (ManifestVerdict.Same); wrote
// says it was written, and revision is the verdicts state's revision of that write, which the event saying it carries
// (novox/hq ADR 0622 §2). It is read back after the write: when another write came between, the revision read holds
// another verdict and revision is 0, so a reader takes the later write's own event and never pairs v with its revision.
func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v delivery.ManifestVerdict) (wrote bool, revision uint64, err error) {
key, err := delivery.VerdictsKey(digest)
func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v meshcatalog.VerdictsByDigestValue) (wrote bool, revision uint64, err error) {
key, err := meshcatalogclient.VerdictsKey(digest)
if err != nil {
return false, 0, err
}
err = write(ctx, ix.Verdicts, key, func(value []byte, found bool) ([]byte, bool, error) {
wrote = false
if found {
if cur, err := delivery.DecodeManifestVerdict(value); err == nil && cur.Same(v) {
if cur, err := meshcatalogclient.DecodeVerdict(value); err == nil && meshcatalogclient.SameVerdict(cur, v) {
return nil, false, nil
}
}
wrote = true
next, err := delivery.EncodeManifestVerdict(v)
next, err := meshcatalogclient.EncodeVerdict(v)
return next, false, err
})
if err != nil || !wrote {
return wrote, 0, err
}
if value, rev, found, err := ix.Verdicts.Get(ctx, key); err == nil && found {
if cur, err := delivery.DecodeManifestVerdict(value); err == nil && cur.Same(v) {
if cur, err := meshcatalogclient.DecodeVerdict(value); err == nil && meshcatalogclient.SameVerdict(cur, v) {
revision = rev
}
}
@@ -435,8 +436,8 @@ func (ix *Indexer) learnOwnBuild(ctx context.Context) {
// ownBuildIsNewest says whether the build of the indexer this program runs (ownFP, from the node-engine) is the newest the
// trunk holds: the indexer's own modules value names it. A newer build merged and not yet delivered ends it at the trunk's
// merge, as one that pins a changed mesh-sdk always is; false when either is unknown.
func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool {
fp := held[delivery.IndexerModule].FP2
func (ix *Indexer) ownBuildIsNewest(held map[string]meshcatalog.ModulesByModuleValue) bool {
fp := held[Module].Fp2
if fp == "" {
return false
}
@@ -448,9 +449,9 @@ func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool {
// carryVerdict puts a refusal into the module's value, and takes one out once the manifest is accepted, while the value
// still names the digest judged, and the reader capabilities the manifest needs beside it (needs, ADR 0614 §4); an
// accepted module's value carries no verdict, so judging writes no value of a module the
// judge accepts (a reader takes a value without a verdict as accepted, delivery.TakeManifestVerdict).
func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v delivery.ManifestVerdict, needs []string) error {
key, err := delivery.ModulesKey(name)
// judge accepts (a reader takes a value without a verdict as accepted, meshcatalogclient.TakeVerdict).
func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v meshcatalog.VerdictsByDigestValue, needs []string) error {
key, err := meshcatalogclient.ModulesKey(name)
if err != nil {
return err
}
@@ -458,17 +459,17 @@ func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v deli
if !found {
return nil, false, nil
}
cur, err := delivery.DecodeModule(value)
cur, err := meshcatalogclient.DecodeModule(value)
if err != nil || cur.Manifest != digest {
return nil, false, nil // gone, unreadable, or moved to another manifest since: the next pass judges it
}
// A wait is written as a refusal naming the fields, so a reader pinned before the verdict waits holds it as refused.
state, problems := "", v.Problems
switch {
case v.Waits():
state, problems = delivery.VerdictRefused, []string{v.WaitsOn()}
case v.Refused():
state = delivery.VerdictRefused
case meshcatalogclient.VerdictWaits(v):
state, problems = meshcatalogclient.VerdictRefused, []string{meshcatalogclient.WaitsOn(v)}
case meshcatalogclient.VerdictRefuses(v):
state = meshcatalogclient.VerdictRefused
}
// The judge reads the manifest alone; the forms its templates' text writes came with its merge (ADR 0632 §2) and
// stand, cur being of the same manifest.
@@ -481,7 +482,7 @@ func (ix *Indexer) carryVerdict(ctx context.Context, name, digest string, v deli
if state == "" {
cur.Problems = nil
}
next, err := delivery.EncodeModule(cur)
next, err := meshcatalogclient.EncodeModule(cur)
return next, false, err
})
}
@@ -492,11 +493,13 @@ func (ix *Indexer) predefinedList(ctx context.Context) catalogue.PredefinedList
if ix.Predefined == nil {
return nil
}
value, _, found, err := ix.Predefined.Get(ctx, delivery.PredefinedListKey)
value, _, found, err := ix.Predefined.Get(ctx, PredefinedListKey)
if err != nil || !found {
return nil
}
var kept delivery.PredefinedKept
var kept struct {
List json.RawMessage `json:"list"`
}
if json.Unmarshal(value, &kept) != nil {
return nil
}
@@ -542,16 +545,16 @@ func ListVerdicts(keys []string, get func(key string) ([]byte, error), module st
}
continue
}
v, err := delivery.DecodeManifestVerdict(value)
v, err := meshcatalogclient.DecodeVerdict(value)
if err != nil {
unread[key] = err.Error()
continue
}
if (module != "" && v.Module != module) || (!all && !v.Held()) {
if (module != "" && v.Module != module) || (!all && !meshcatalogclient.VerdictHeld(v)) {
continue
}
rows = append(rows, VerdictRow{Module: v.Module, Digest: key, State: v.State, Words: v.Why(), Unknown: v.Unknown,
Problems: v.Problems, Warnings: v.Warnings, Judge: v.Judge, At: v.At})
rows = append(rows, VerdictRow{Module: v.Module, Digest: key, State: v.State, Words: meshcatalogclient.VerdictWhy(v), Unknown: v.Unknown,
Problems: v.Problems, Warnings: v.Warnings, Judge: v.Judge, At: timeOf(v.At)})
}
slices.SortFunc(rows, func(a, b VerdictRow) int {
if c := strings.Compare(a.Module, b.Module); c != 0 {
@@ -578,7 +581,7 @@ func (ix *Indexer) JudgeAgain(ctx context.Context, module, digest string) TakenI
if digest != "" {
// One digest (a candidate's, or one the trunk no longer names), judged by this judge in the catalogue the trunk holds.
value, _, found, err := ix.Manifests.Get(ctx, digest)
kept, derr := delivery.DecodeManifest(value)
kept, derr := meshcatalogclient.DecodeManifest(value)
switch {
case err != nil || !found:
out.Answer = "the indexer keeps no manifest of digest " + digest
@@ -617,15 +620,15 @@ func (ix *Indexer) answerVerdict(ctx context.Context, out TakenIn, digest string
out.Answer = "judged, but the verdict on " + module + " cannot be read"
return out
}
v, err := delivery.DecodeManifestVerdict(value)
v, err := meshcatalogclient.DecodeVerdict(value)
if err != nil {
out.Answer = "judged, but the verdict on " + module + " cannot be read: " + err.Error()
return out
}
out.Stored = true
out.Answer = "judged again by " + v.Judge + ": " + v.State
if v.Held() {
out.Answer += " (" + v.Why() + ")"
if meshcatalogclient.VerdictHeld(v) {
out.Answer += " (" + meshcatalogclient.VerdictWhy(v) + ")"
}
return out
}
@@ -634,7 +637,7 @@ func (ix *Indexer) answerVerdict(ctx context.Context, out TakenIn, digest string
// the takeins state and the value.
func NewJudgeAgain(module, digest, why, by string, at time.Time) (string, []byte, error) {
if digest != "" {
if _, err := delivery.ManifestsKey(digest); err != nil {
if _, err := meshcatalogclient.ManifestsKey(digest); err != nil {
return "", nil, err
}
}
@@ -663,3 +666,11 @@ func withTemplateForms(needs, had []string) []string {
slices.Sort(out)
return out
}
// timeOf is the time a pointer holds, the zero time for none.
func timeOf(t *time.Time) time.Time {
if t == nil {
return time.Time{}
}
return *t
}
@@ -9,9 +9,10 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-indexer/judge"
)
@@ -23,14 +24,14 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
ctx := context.Background()
ix := &Indexer{Modules: newMem(), Manifests: newMem(), Verdicts: newMem(), JudgeID: "judge-1",
Now: func() time.Time { return time.Date(2026, 11, 1, 3, 0, 0, 0, time.UTC) }}
var said []delivery.ManifestRefused
ix.SayRefused = func(_ context.Context, r delivery.ManifestRefused) { said = append(said, r) }
var said []meshcatalog.ManifestRefusedEvent
ix.SayRefused = func(_ context.Context, r meshcatalog.ManifestRefusedEvent) { said = append(said, r) }
hold := func(module, raw string) string {
digest, err := ix.keepManifest(ctx, module, raw)
if err != nil {
t.Fatal(err)
}
value, err := delivery.EncodeModule(delivery.Module{FP2: "src2:" + strings.Repeat("a", 64), Manifest: digest,
value, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("a", 64), Manifest: digest,
Repository: "novox/mesh-catalog", Commit: "c"})
if err != nil {
t.Fatal(err)
@@ -45,29 +46,29 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if err != nil || j.Judged != 2 || j.Written != 2 || len(j.Refused) != 1 {
t.Fatalf("the first pass: %+v %v", j, err)
}
verdict := func(digest string) delivery.ManifestVerdict {
verdict := func(digest string) meshcatalog.VerdictsByDigestValue {
value, _, found, _ := ix.Verdicts.Get(ctx, digest)
v, err := delivery.DecodeManifestVerdict(value)
v, err := meshcatalogclient.DecodeVerdict(value)
if !found || err != nil {
t.Fatalf("no verdict under %s: %v", digest, err)
}
return v
}
if v := verdict(good); v.Refused() || v.Judge != "judge-1" {
if v := verdict(good); meshcatalogclient.VerdictRefuses(v) || v.Judge != "judge-1" {
t.Fatalf("good: %+v", v)
}
if v := verdict(bad); !v.Refused() || !strings.Contains(v.Problems[0], `requires "amqp"`) {
if v := verdict(bad); !meshcatalogclient.VerdictRefuses(v) || !strings.Contains(v.Problems[0], `requires "amqp"`) {
t.Fatalf("bad: %+v", v)
}
module := func(name string) delivery.Module {
module := func(name string) meshcatalog.ModulesByModuleValue {
value, _, _, _ := ix.Modules.Get(ctx, name)
m, err := delivery.DecodeModule(value)
m, err := meshcatalogclient.DecodeModule(value)
if err != nil {
t.Fatal(err)
}
return m
}
if m := module("bad"); m.Verdict != delivery.VerdictRefused || len(m.Problems) != 1 {
if m := module("bad"); m.Verdict != meshcatalogclient.VerdictRefused || len(m.Problems) != 1 {
t.Fatalf("modules.bad carries %+v", m)
}
if m := module("good"); m.Verdict != "" {
@@ -75,7 +76,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
// ADR 0622 §2: the event carries the verdicts state's revision of the verdict it says.
if _, rev, _, _ := ix.Verdicts.Get(ctx, bad); len(said) != 1 || said[0].Module != "bad" || said[0].Digest != bad ||
said[0].Judge != "judge-1" || said[0].Revision == 0 || said[0].Revision != rev {
said[0].Judge != "judge-1" || said[0].Revision == 0 || said[0].Revision != int64(rev) {
t.Fatalf("said %+v (the verdict's revision %d)", said, rev)
}
// A new judge that judges alike writes nothing and says nothing again.
@@ -103,7 +104,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Outgrown) != 1 || j.Outgrown[0] != "old" {
t.Fatalf("an outgrown manifest: %+v %v", j, err)
}
if v := verdict(old); v.Refused() || len(v.Warnings) != 1 || !strings.Contains(v.Warnings[0], `requires "amqp"`) {
if v := verdict(old); meshcatalogclient.VerdictRefuses(v) || len(v.Warnings) != 1 || !strings.Contains(v.Warnings[0], `requires "amqp"`) {
t.Fatalf("old: %+v", v)
}
if m := module("old"); m.Verdict != "" || len(m.Warnings) != 1 || len(said) != 1 {
@@ -114,18 +115,18 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if j, err := ix.JudgeAll(ctx, map[string]bool{changed: true, fresh: true}); err != nil || len(j.Outgrown) != 0 {
t.Fatalf("a changed and a new manifest: %+v %v", j, err)
}
if !verdict(changed).Refused() || !verdict(fresh).Refused() || module("old").Verdict != delivery.VerdictRefused || len(module("old").Warnings) != 0 {
if !meshcatalogclient.VerdictRefuses(verdict(changed)) || !meshcatalogclient.VerdictRefuses(verdict(fresh)) || module("old").Verdict != meshcatalogclient.VerdictRefused || len(module("old").Warnings) != 0 {
t.Fatalf("the changed and the new manifest are not refused: %+v %+v", verdict(changed), verdict(fresh))
}
// A merge's write of a module whose digest it leaves as it is carries the verdict over, and nothing is fresh; a changed
// digest is fresh, judged in full (review R1, R2).
w := &mergeWrite{}
oldNeeds := module("old").Needs
same := delivery.Module{FP2: "src2:" + strings.Repeat("c", 64), Manifest: changed, Repository: "novox/mesh-catalog", Commit: "c2"}
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil {
same := meshcatalog.ModulesByModuleValue{Fp2: "src2:" + strings.Repeat("c", 64), Manifest: changed, Repository: "novox/mesh-catalog", Commit: "c2"}
if err := ix.writeOverlay(ctx, map[string]meshcatalog.ModulesByModuleValue{"old": same}, nil, w); err != nil {
t.Fatal(err)
}
if m := module("old"); m.Verdict != delivery.VerdictRefused || len(m.Problems) == 0 || len(w.fresh) != 0 {
if m := module("old"); m.Verdict != meshcatalogclient.VerdictRefused || len(m.Problems) == 0 || len(w.fresh) != 0 {
t.Fatalf("a merge rewrote a refused module as %+v, fresh %v", m, w.fresh)
}
if got := module("old").Needs; len(oldNeeds) == 0 || !slices.Equal(got, oldNeeds) {
@@ -133,15 +134,15 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
}
other := strings.Repeat("d", 64)
same.Manifest = other
if err := ix.writeOverlay(ctx, map[string]delivery.Module{"old": same}, nil, w); err != nil || !w.fresh[other] {
if err := ix.writeOverlay(ctx, map[string]meshcatalog.ModulesByModuleValue{"old": same}, nil, w); err != nil || !w.fresh[other] {
t.Fatalf("a changed digest is not fresh: %v %v", w.fresh, err)
}
// 1966/AC10: a newly accepted manifest asking for bus state is said once as manifest-accepted, carrying both entries
// whole; a verdict already held says nothing.
var accepted []delivery.ManifestAccepted
var accepted []meshcatalog.ManifestAcceptedEvent
var ids []string
var failing int // publishes still to fail
ix.SayAccepted = func(_ context.Context, a delivery.ManifestAccepted, id string) error {
ix.SayAccepted = func(_ context.Context, a meshcatalog.ManifestAcceptedEvent, id string) error {
if failing > 0 {
failing--
return errors.New("no stream answered")
@@ -166,12 +167,12 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if needs := module("good").Needs; !slices.Contains(needs, "word:module") || len(catalogue.MissingManifestReads(needs, nil)) != 0 {
t.Fatalf("modules.good needs %v", needs)
}
if _, rev, _, _ := ix.Verdicts.Get(ctx, moved); len(accepted) != 1 || accepted[0].Revision == 0 || accepted[0].Revision != rev {
if _, rev, _, _ := ix.Verdicts.Get(ctx, moved); len(accepted) != 1 || accepted[0].Revision == 0 || accepted[0].Revision != int64(rev) {
t.Fatalf("said accepted %+v, want the verdict's revision %d (ADR 0622 §2)", accepted, rev)
}
if len(accepted) != 1 || accepted[0].Module != "moved" || accepted[0].Digest != moved || accepted[0].Judge != "judge-2" ||
len(accepted[0].BusState) != 2 || accepted[0].BusState[0].Name != "cache" || accepted[0].BusState[1].Template != "log" ||
string(accepted[0].BusState[0].Options["history"]) != "8" {
fmt.Sprint(accepted[0].BusState[0].Options["history"]) != "8" {
t.Fatalf("said accepted %+v", accepted)
}
// DLV-I9 (design 25 § 12.2): a manifest asking for none, newly accepted, is said too, its entries an empty list.
@@ -237,7 +238,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatal(err)
}
ix.JudgeAgain(ctx, "renamer", head)
if v := verdict(head); (want == "") != !v.Refused() || (want != "" && !strings.Contains(strings.Join(v.Problems, " "), want)) {
if v := verdict(head); (want == "") != !meshcatalogclient.VerdictRefuses(v) || (want != "" && !strings.Contains(strings.Join(v.Problems, " "), want)) {
t.Errorf("the change %s: %+v, want refused naming %q", raw, v, want)
}
}
@@ -246,7 +247,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// takes in is, though its candidate was accepted; a name the state knows is accepted at both.
ix.Packages = newMem()
sdkGo := "git.novox.be/novox/mesh-sdk/go"
if value, err := delivery.EncodeSeriesPackage(delivery.SeriesPackage{V: 1, Name: sdkGo, Package: "mesh-sdk/go", Series: "0.1"}); err != nil {
if value, err := EncodeSeriesPackage(SeriesPackage{V: 1, Name: sdkGo, Package: "mesh-sdk/go", Series: "0.1"}); err != nil {
t.Fatal(err)
} else {
ix.Packages.(*memStore).put(SeriesKey(sdkGo, "0.1"), value)
@@ -260,14 +261,14 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatal(err)
}
ix.judgeDigests(ctx, map[string]string{"pkguser": head}, "novox/mesh-catalog#1")
if v := verdict(head); v.Refused() {
if v := verdict(head); meshcatalogclient.VerdictRefuses(v) {
t.Errorf("the candidate %s is refused: %+v", raw, v)
}
merged := hold("pkguser", raw)
if _, err := ix.JudgeAll(ctx, map[string]bool{merged: true}); err != nil {
t.Fatal(err)
}
if v := verdict(merged); v.Refused() != refusedAtMerge || refusedAtMerge && !strings.Contains(strings.Join(v.Problems, " "), `name "@novox/ui", which no package`) {
if v := verdict(merged); meshcatalogclient.VerdictRefuses(v) != refusedAtMerge || refusedAtMerge && !strings.Contains(strings.Join(v.Problems, " "), `name "@novox/ui", which no package`) {
t.Errorf("the merge of %s: %+v, want refused %v for the name", raw, v, refusedAtMerge)
}
}
@@ -310,7 +311,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if _, err := ix.JudgeAll(ctx, map[string]bool{cfg: true}); err != nil {
t.Fatal(err)
}
if v := verdict(cfg); !v.Refused() || !strings.Contains(strings.Join(v.Problems, " "), "part of the foundation") {
if v := verdict(cfg); !meshcatalogclient.VerdictRefuses(v) || !strings.Contains(strings.Join(v.Problems, " "), "part of the foundation") {
t.Fatalf("mesh-config asking new-form bus state: %+v", v)
}
if !slices.Equal(judge.Foundation, []string{"nats", "mesh-grants", "mesh-delivery-indexer", "mesh-delivery-assigner", "node-tools",
@@ -322,10 +323,10 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if _, err := ix.JudgeAll(ctx, map[string]bool{late: true, grants: true}); err != nil {
t.Fatal(err)
}
if v := verdict(late); !v.Refused() || !strings.Contains(v.Problems[0], "late uses the old section logs, which no other module uses any more") {
if v := verdict(late); !meshcatalogclient.VerdictRefuses(v) || !strings.Contains(v.Problems[0], "late uses the old section logs, which no other module uses any more") {
t.Fatalf("late: %+v", v)
}
if v := verdict(grants); v.Refused() {
if v := verdict(grants); meshcatalogclient.VerdictRefuses(v) {
t.Fatalf("the foundation's manifest was refused: %+v", v)
}
// 1966/AC11: the sole user of an old section changes its manifest and keeps the section: accepted, since it was accepted
@@ -338,7 +339,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if _, err := ix.JudgeAll(ctx, map[string]bool{solo2: true}); err != nil {
t.Fatal(err)
}
if v := verdict(solo2); v.Refused() {
if v := verdict(solo2); meshcatalogclient.VerdictRefuses(v) {
t.Fatalf("the sole user of objects, changing its manifest and keeping objects, was refused: %+v", v)
}
// A count that cannot be read is never taken as 0: the zero rule is not applied in that pass and the log says so.
@@ -350,7 +351,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if _, err := ix.JudgeAll(ctx, map[string]bool{retry: true}); err != nil {
t.Fatal(err)
}
if v := verdict(retry); v.Refused() || !strings.Contains(strings.Join(logged, "\n"), "the zero rule is not applied") {
if v := verdict(retry); meshcatalogclient.VerdictRefuses(v) || !strings.Contains(strings.Join(logged, "\n"), "the zero rule is not applied") {
t.Fatalf("a count that could not be read: %+v, logged %q", v, logged)
}
ix.OldSections = held
@@ -358,7 +359,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// event) while a newer build of the indexer is on the trunk, and is judged again at the next judge; once this program's
// build is the trunk's newest it is refused; a wait is written into the module's value as a refusal naming the fields.
ownFP := "src2:" + strings.Repeat("e", 64)
hold(delivery.IndexerModule, `{"module":"mesh-delivery-indexer","version":"1"}`)
hold(Module, `{"module":"mesh-delivery-indexer","version":"1"}`)
ix.ownFP = ownFP // this program started when the trunk held another build of the indexer than the one it holds now
later := hold("later", `{"module":"later","version":"1","a-field-from-later":1,"another":{"x":1}}`)
said = nil
@@ -372,10 +373,10 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
t.Fatalf("a manifest set aside to wait was said accepted: %+v (DLV-I9)", a)
}
}
if v := verdict(later); !v.Waits() || v.Refused() || strings.Join(v.Unknown, ",") != "a-field-from-later,another" {
if v := verdict(later); !meshcatalogclient.VerdictWaits(v) || meshcatalogclient.VerdictRefuses(v) || strings.Join(v.Unknown, ",") != "a-field-from-later,another" {
t.Fatalf("later: %+v", v)
}
if m := module("later"); m.Verdict != delivery.VerdictRefused || len(m.Unknown) != 2 || !m.VerdictOf("later").Waits() ||
if m := module("later"); m.Verdict != meshcatalogclient.VerdictRefused || len(m.Unknown) != 2 || !meshcatalogclient.VerdictWaits(*meshcatalogclient.ModuleVerdict("later", m)) ||
!strings.Contains(m.Problems[0], "does not know yet") {
t.Fatalf("modules.later carries %+v", m)
}
@@ -383,33 +384,33 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
// trunk's value of the indexer names a newer build than the one the engine says runs, so the field still waits.
ix.ownFP = ""
ix.OwnBuild = func(context.Context) (string, error) { return ownFP, nil }
if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() {
if _, err := ix.JudgeAtStart(ctx); err != nil || !meshcatalogclient.VerdictWaits(verdict(later)) {
t.Fatalf("the trunk moved before the start pass: %v %+v", err, verdict(later))
}
ix.OwnBuild = func(context.Context) (string, error) { return "", errors.New("the engine does not answer") }
ix.ownFP = ""
if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() {
if _, err := ix.JudgeAtStart(ctx); err != nil || !meshcatalogclient.VerdictWaits(verdict(later)) {
t.Fatalf("an unknown build refused: %v %+v", err, verdict(later))
}
ix.ownFP = ownFP
// A renamed field is a known mistake: refused at once.
renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`)
if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !verdict(renamed).Refused() {
if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !meshcatalogclient.VerdictRefuses(verdict(renamed)) {
t.Fatalf("a renamed field: %+v %v", j, err)
}
// The trunk's newest build of the indexer is this program's: the field is one no judge to come knows.
ix.ownFP = "src2:" + strings.Repeat("a", 64)
if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Waiting) != 0 || !verdict(later).Refused() || !strings.Contains(verdict(later).Problems[0], "a-field-from-later") {
if j, err := ix.JudgeAll(ctx, nil); err != nil || len(j.Waiting) != 0 || !meshcatalogclient.VerdictRefuses(verdict(later)) || !strings.Contains(verdict(later).Problems[0], "a-field-from-later") {
t.Fatalf("the newest build refuses: %+v %v %+v", j, err, verdict(later))
}
if m := module("later"); m.Verdict != delivery.VerdictRefused || m.VerdictOf("later").Waits() || len(m.Unknown) != 0 {
if m := module("later"); m.Verdict != meshcatalogclient.VerdictRefused || meshcatalogclient.VerdictWaits(*meshcatalogclient.ModuleVerdict("later", m)) || len(m.Unknown) != 0 {
t.Fatalf("modules.later after the refusal carries %+v", m)
}
// The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge
// judges now and says how the module's verdict stands.
keys, _ := ix.Verdicts.Keys(ctx)
get := func(key string) ([]byte, error) { v, _, _, err := ix.Verdicts.Get(ctx, key); return v, err }
if rows, unread := ListVerdicts(keys, get, "later", false); len(rows) != 1 || rows[0].State != delivery.VerdictRefused || len(unread) != 0 {
if rows, unread := ListVerdicts(keys, get, "later", false); len(rows) != 1 || rows[0].State != meshcatalogclient.VerdictRefused || len(unread) != 0 {
t.Fatalf("indexer_verdicts of later: %+v %v", rows, unread)
}
if rows, _ := ListVerdicts(keys, get, "", true); len(rows) <= len(keys)-3 || rows[0].Module > rows[len(rows)-1].Module {
@@ -446,32 +447,32 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if _, _, err := ix.writeVerdict(ctx, digest, delivery.ManifestVerdict{Module: module, State: delivery.VerdictRefused,
Problems: []string{module + " says tool-runner, which this judge does not know"}, Judge: "judge-0", At: time.Now()}); err != nil {
if _, _, err := ix.writeVerdict(ctx, digest, meshcatalog.VerdictsByDigestValue{Module: module, State: meshcatalogclient.VerdictRefused,
Problems: []string{module + " says tool-runner, which this judge does not know"}, Judge: "judge-0", At: ptr(time.Now())}); err != nil {
t.Fatal(err)
}
return digest
}
atStart := stale("tools", `{"module":"tools","version":"7"}`)
ownRefusal := stale("client", `{"module":"client","version":"7"}`)
if _, _, err := ix.writeVerdict(ctx, ownRefusal, delivery.ManifestVerdict{Module: "client", State: delivery.VerdictRefused,
Problems: []string{"x"}, Judge: ix.judgeID(), At: time.Now()}); err != nil {
if _, _, err := ix.writeVerdict(ctx, ownRefusal, meshcatalog.VerdictsByDigestValue{Module: "client", State: meshcatalogclient.VerdictRefused,
Problems: []string{"x"}, Judge: ix.judgeID(), At: ptr(time.Now())}); err != nil {
t.Fatal(err)
}
if _, err := ix.JudgeAtStart(ctx); err != nil || verdict(atStart).Refused() || verdict(atStart).Judge != ix.judgeID() || !verdict(ownRefusal).Refused() {
if _, err := ix.JudgeAtStart(ctx); err != nil || meshcatalogclient.VerdictRefuses(verdict(atStart)) || verdict(atStart).Judge != ix.judgeID() || !meshcatalogclient.VerdictRefuses(verdict(ownRefusal)) {
t.Fatalf("the start pass: %+v, this judge's own refusal %+v (%v)", verdict(atStart), verdict(ownRefusal), err)
}
byPull := stale("tools", `{"module":"tools","version":"8"}`)
pull := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main",
Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8"}`}}
if _, err := ix.ApplyMessage(ctx, pull); err != nil || verdict(byPull).Refused() || verdict(byPull).Judge != ix.judgeID() {
if _, err := ix.ApplyMessage(ctx, pull); err != nil || meshcatalogclient.VerdictRefuses(verdict(byPull)) || verdict(byPull).Judge != ix.judgeID() {
t.Fatalf("a pull carrying a digest an older judge refused: %+v %v", verdict(byPull), err)
}
byTool := stale("tools", `{"module":"tools","version":"9"}`)
if got := ix.JudgeAgain(ctx, "client", byTool); got.Stored || !strings.Contains(got.Answer, "tools's, not client's") || !verdict(byTool).Refused() {
if got := ix.JudgeAgain(ctx, "client", byTool); got.Stored || !strings.Contains(got.Answer, "tools's, not client's") || !meshcatalogclient.VerdictRefuses(verdict(byTool)) {
t.Fatalf("indexer_judge with another module's digest: %+v", got)
}
if got := ix.JudgeAgain(ctx, "tools", byTool); !got.Stored || !strings.Contains(got.Answer, "accepted") || verdict(byTool).Refused() {
if got := ix.JudgeAgain(ctx, "tools", byTool); !got.Stored || !strings.Contains(got.Answer, "accepted") || meshcatalogclient.VerdictRefuses(verdict(byTool)) {
t.Fatalf("indexer_judge with a digest: %+v %+v", got, verdict(byTool))
}
if key, value, err := NewJudgeAgain("tools", byTool, "why", "me", time.Now()); err != nil || key != "judge.tools" {
@@ -1,7 +1,6 @@
package index
import (
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/health"
"git.novox.be/novox/mesh-delivery/sdk/graph"
@@ -9,7 +8,7 @@ import (
)
// MarksBucket is the indexer's state of what its views family said each state up to.
const MarksBucket = delivery.IndexerModule + "_" + views.MarksState
const MarksBucket = Module + "_" + views.MarksState
// FirstViewTools are the indexer's first-view tools (ADR 0621 §1, ADR 0622 §1, ADR 0631 §1): the tool, the state it answers, and
// the words of what that state holds. Each answers only its callers list (CallersEnv), which the manifest writes.
@@ -24,10 +23,10 @@ var FirstViewTools = []ViewedTool{
var ReadTools = []ViewedTool{
{Tool: "indexer_read_manifests", State: "manifests", Words: "every manifest the indexer keeps, by its digest."},
{Tool: "indexer_read_modules", State: "modules", Words: "every module the indexer holds, its fingerprint, inputs and manifest digest."},
{Tool: "indexer_read_predefined", State: delivery.IndexerPredefinedState, Words: "the predefined config values of each module on the trunk."},
{Tool: "indexer_read_config_candidates", State: delivery.IndexerConfigCandidatesState, Words: "the config declarations of each open pull request."},
{Tool: "indexer_read_predefined", State: PredefinedState, Words: "the predefined config values of each module on the trunk."},
{Tool: "indexer_read_config_candidates", State: ConfigCandidatesState, Words: "the config declarations of each open pull request."},
{Tool: "indexer_read_candidates", State: "candidates", Words: "the candidate groups: the pull requests sharing a branch, their heads and the modules each touches."},
{Tool: "indexer_read_packages", State: delivery.PackagesState, Words: "the published packages per series."},
{Tool: "indexer_read_packages", State: PackagesState, Words: "the published packages per series."},
{Tool: "indexer_read_graph", State: graph.State, Words: "the module graph: each module's build and provision edges."},
{Tool: "indexer_read_heard", State: HeardState, Words: "the forge messages the indexer heard and what it did with each."},
{Tool: "indexer_read_taken", State: "taken", Words: "each repository's merge the indexer took in last."},
@@ -42,16 +41,16 @@ type ViewedTool struct {
// ViewedStates are the states the indexer's views family says, each by its bucket: the ones its readers read.
var ViewedStates = map[string]string{
"manifests": delivery.ManifestsBucket,
"modules": delivery.ModulesBucket,
VerdictsState: VerdictsBucket,
delivery.IndexerPredefinedState: PredefinedBucket,
delivery.IndexerConfigCandidatesState: ConfigCandidatesBucket,
"candidates": delivery.CandidatesBucket,
delivery.PackagesState: delivery.PackagesBucket,
graph.State: graph.Bucket,
HeardState: HeardBucket,
"taken": delivery.TakenBucket,
health.FindingsState: health.FindingsBucket(delivery.IndexerModule),
SeatDefinitionsState: SeatDefinitionsBucket,
"manifests": ManifestsBucket,
"modules": ModulesBucket,
VerdictsState: VerdictsBucket,
PredefinedState: PredefinedBucket,
ConfigCandidatesState: ConfigCandidatesBucket,
"candidates": CandidatesBucket,
PackagesState: PackagesBucket,
graph.State: graph.Bucket,
HeardState: HeardBucket,
"taken": TakenBucket,
health.FindingsState: health.FindingsBucket(Module),
SeatDefinitionsState: SeatDefinitionsBucket,
}
+3 -2
View File
@@ -1,6 +1,6 @@
// Package judge is the mesh's one judge of module manifests (novox/hq ADR 0578 §1, §2): strict decoding, the seat and
// namespace rules, the bus and stream limits, and the rules between the manifests of one catalogue. Only the indexer,
// which judges every manifest it takes in and writes the verdicts (delivery.VerdictsBucket), and the checker, which
// which judges every manifest it takes in and writes the verdicts (index.VerdictsBucket), and the checker, which
// judges every manifest a change touches by running this judge built at the trunk or a group's head (cmd/mesh-judge,
// as the composer is run), use it. Every other part decodes a manifest leniently (catalogue.Decode) and takes the
// verdict; it never judges (ADR 0578 §3).
@@ -15,7 +15,6 @@ import (
"encoding/hex"
json "encoding/json"
"fmt"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"io"
"maps"
"os"
@@ -26,6 +25,8 @@ import (
"strings"
"sync"
"git.novox.be/novox/mesh-sdk/go/seatdef"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
@@ -11,7 +11,7 @@ import (
// KnownPackages are the names of the packages the mesh knows, each by its name in its own ecosystem (design 32: a Go
// package's go.mod module path, a TypeScript package's package.json name): the names the indexer's packages state holds
// (its series keys, delivery.SeriesPackage's Name), read at each trunk merge, and, at the merge check, the names a member
// (its series keys, index.SeriesPackage's Name), read at each trunk merge, and, at the merge check, the names a member
// of the change's delivery group names at its head for a package new in the group. Both read a name with mesh-delivery's
// sdk/packagename. Nil is no list at hand: the names are then not judged, only their series.
type KnownPackages map[string]bool
+18 -2
View File
@@ -14,7 +14,6 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"git.novox.be/novox/mesh-sdk/go/health"
@@ -114,12 +113,29 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
t.Errorf("emits = %v, want the two take-in events (the verdict events are the seat mesh-catalog's)", m.Emits)
}
// 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard.
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory},
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: index.PackagesState, History: index.PackagesHistory},
{Name: graph.State, History: graph.History}, {Name: views.MarksState, History: 1}, {Name: health.FindingsState, History: 5}, {Name: views.StatusState, History: 1},
{Name: index.RequirementsState, History: 5}, {Name: index.SeatDefinitionsState, History: 5},
{Name: index.HoldsState, History: 5}, {Name: index.EngineReadsState, History: 1}, {Name: index.HoldBoundsState, History: 1, KeyTTL: true}, {Name: index.PendingState, History: 1}}; !reflect.DeepEqual(m.State, want) { // DLV-I7, DLV-I17, ADR 0632 §2
t.Errorf("state = %+v, want %+v", m.State, want)
}
// D-11 (ADR 0634 §1): the holders and buckets the indexer names (internal/index/names.go) are its manifest's: its own
// name and states with their revisions, and the states it reads, each as `<holder>.<state>`.
if index.Module != m.Module {
t.Errorf("the indexer names itself %q, its manifest's module is %q", index.Module, m.Module)
}
for bucket, history := range map[string]int{index.CandidatesBucket: index.CandidatesHistory, index.ModulesBucket: index.ModulesHistory, index.ManifestsBucket: index.ManifestsHistory,
index.VerdictsBucket: index.VerdictsHistory, index.PackagesBucket: index.PackagesHistory, index.TakenBucket: 1, index.PredefinedBucket: 5, index.ConfigCandidatesBucket: 5} {
if !slices.ContainsFunc(m.State, func(st state) bool { return m.Module+"_"+st.Name == bucket && st.History == history }) {
t.Errorf("the indexer names its bucket %s with %d revisions, its manifest's states are %+v", bucket, history, m.State)
}
}
for bucket, read := range map[string]string{index.SourceBucket: "mesh-delivery-gate.source", index.PreviewsBucket: "mesh-delivery-checker.previews",
index.ToolchainsBucket(index.BuildGoModule): "mesh-build-go.toolchains", index.ToolchainsBucket(index.BuildNodeModule): "mesh-build-node.toolchains"} {
if !slices.Contains(m.Reads, read) || strings.Replace(read, ".", "_", 1) != bucket {
t.Errorf("the indexer reads the bucket %s, its manifest's reads are %v (want %s)", bucket, m.Reads, read)
}
}
// DLV-F1r (ADR 0615 §1): the assigner's placement is read through its first view and views family, never its state.
if want := []string{"mesh-build-go.toolchains", "mesh-build-node.toolchains", "mesh-delivery-builder.build-log", "mesh-delivery-checker.previews", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) {
t.Errorf("reads = %v, want %v", m.Reads, want)
+30 -29
View File
@@ -11,7 +11,8 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// Merge is one merge a take-in event or a wait holds: the repository as `<owner>/<name>`, its pull request and the
@@ -31,7 +32,7 @@ func checkRepository(repository string) error {
return nil
}
// decode reads a value of delivery.Version, refusing another or none with delivery.ErrUnknownVersion, as mesh-sdk
// decode reads a value of keys.Version, refusing another or none with keys.ErrUnknownVersion, as mesh-sdk
// delivery's values are read.
func decode(data []byte, into any) error {
var probe struct {
@@ -40,8 +41,8 @@ func decode(data []byte, into any) error {
if err := json.Unmarshal(data, &probe); err != nil {
return fmt.Errorf("delivery: value is not JSON: %w", err)
}
if probe.V == nil || *probe.V != delivery.Version {
return fmt.Errorf("%w: this reader reads %d", delivery.ErrUnknownVersion, delivery.Version)
if probe.V == nil || *probe.V != keys.Version {
return fmt.Errorf("%w: this reader reads %d", keys.ErrUnknownVersion, keys.Version)
}
if err := json.Unmarshal(data, into); err != nil {
return fmt.Errorf("delivery: value does not fit its shape: %w", err)
@@ -121,18 +122,18 @@ func checkTakeInKey(key string) error {
// it holds, oldest first; Since is when the wait began. A `taken` event names the same once the wait ended; By and Why
// name a hand act (a hand take-in, or a drop, Dropped), and are carried by nothing else.
type TakeIn struct {
V int `json:"v"`
Kind string `json:"kind"`
Key string `json:"key"`
What string `json:"what"`
Merges []Merge `json:"merges"`
Group string `json:"group,omitempty"`
Members []delivery.Head `json:"members,omitempty"`
WaitsFor string `json:"waits-for,omitempty"`
Since time.Time `json:"since"`
By string `json:"by,omitempty"`
Why string `json:"why,omitempty"`
Dropped bool `json:"dropped,omitempty"`
V int `json:"v"`
Kind string `json:"kind"`
Key string `json:"key"`
What string `json:"what"`
Merges []Merge `json:"merges"`
Group string `json:"group,omitempty"`
Members []meshdeliveryclient.Head `json:"members,omitempty"`
WaitsFor string `json:"waits-for,omitempty"`
Since time.Time `json:"since"`
By string `json:"by,omitempty"`
Why string `json:"why,omitempty"`
Dropped bool `json:"dropped,omitempty"`
// Lost are the keys of merges whose message the forge bridge's stream no longer holds: they are not taken in, and the
// revision waits until indexer_drop_pending drops them (issue 1585).
Lost []string `json:"lost,omitempty"`
@@ -194,7 +195,7 @@ func EncodeTakeIn(t TakeIn) ([]byte, error) {
if err := t.check(); err != nil {
return nil, err
}
t.V = delivery.Version
t.V = keys.Version
return json.Marshal(t)
}
@@ -221,18 +222,18 @@ func SameTakeIn(a, b TakeIn) bool { return reflect.DeepEqual(a, b) }
// group being gathered names that group's key in Behind. Keys are the pending keys of its merges, in Merges' order (indexer_drop_pending drops one). Holds are the modules it holds back: every module of its
// merges' trees. Said is the last take-in event said of it (`take-in.<kind>`, of its Key) and SaidAt when.
type PendingWait struct {
Key string `json:"key"`
What string `json:"what"`
Merges []Merge `json:"merges"`
Keys []string `json:"keys"`
Group string `json:"group,omitempty"`
Members []delivery.Head `json:"members,omitempty"`
Behind string `json:"behind,omitempty"`
WaitsFor string `json:"waits-for,omitempty"`
Since time.Time `json:"since"`
Holds []string `json:"holds"`
Said string `json:"said,omitempty"`
SaidAt time.Time `json:"said-at,omitzero"`
Key string `json:"key"`
What string `json:"what"`
Merges []Merge `json:"merges"`
Keys []string `json:"keys"`
Group string `json:"group,omitempty"`
Members []meshdeliveryclient.Head `json:"members,omitempty"`
Behind string `json:"behind,omitempty"`
WaitsFor string `json:"waits-for,omitempty"`
Since time.Time `json:"since"`
Holds []string `json:"holds"`
Said string `json:"said,omitempty"`
SaidAt time.Time `json:"said-at,omitzero"`
// Lost are the keys of its merges whose message the forge bridge's stream no longer holds (TakeIn.Lost).
Lost []string `json:"lost,omitempty"`
}
@@ -8,7 +8,8 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// sampleTakeIns are a pending revision, a group being gathered and a hand take-in's taken (1585/T2).
@@ -17,7 +18,7 @@ func sampleTakeIns(since time.Time) []TakeIn {
return []TakeIn{
{Kind: TakeInPending, Key: PendingKey(12), What: TakeInRevision, Merges: m, WaitsFor: "src2:abc", Since: since},
{Kind: TakeInPending, Key: GroupTakeInKey("feat/take-in"), What: TakeInGroup, Merges: m, Group: "feat/take-in",
Members: []delivery.Head{{Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}, {Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}}, Since: since},
Members: []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}, {Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}}, Since: since},
{Kind: TakeInTaken, Key: PendingKey(12), What: TakeInRevision, Merges: m, WaitsFor: "src2:abc", Since: since,
By: "lead-47@shanks", Why: "the named indexer can never run"},
}
@@ -50,14 +51,14 @@ func TestTakeInEvents(t *testing.T) {
if err != nil {
t.Fatalf("take-in %s %s: %v", ti.Kind, ti.What, err)
}
ti.V = delivery.Version
ti.V = keys.Version
if got, err := DecodeTakeIn(data); err != nil || !SameTakeIn(got, ti) {
t.Errorf("take-in %s round trip: %+v %v", ti.Key, got, err)
}
}
for _, in := range []string{`{"key":"m.1"}`, `{"v":2,"key":"m.1"}`} {
if _, err := DecodeTakeIn([]byte(in)); !errors.Is(err, delivery.ErrUnknownVersion) {
t.Errorf("decode of %s = %v, want delivery.ErrUnknownVersion", in, err)
if _, err := DecodeTakeIn([]byte(in)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("decode of %s = %v, want keys.ErrUnknownVersion", in, err)
}
}
// A body that does not hold is refused, naming why.