From 8448315b7263d2e0ef4996d9c4bb5ca52083bc02 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:11:41 +0100 Subject: [PATCH 01/11] =?UTF-8?q?Delivery=20SDK:=20the=20toolchain=20a=20l?= =?UTF-8?q?anguage's=20builders=20agree=20on=20(entries,=20majority=20rule?= =?UTF-8?q?)=20over=20a=20seat-neutral=20form,=20so=20the=20builder,=20the?= =?UTF-8?q?=20checker=20and=20the=20indexer=20read=20a=20build=20seat's=20?= =?UTF-8?q?toolchains=20state=20with=20one=20rule=20(ADR=200634=20=C2=A71,?= =?UTF-8?q?=20=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- sdk/buildstep/toolchains.go | 85 ++++++++++++++++++++++++++++++++ sdk/buildstep/toolchains_test.go | 58 ++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 sdk/buildstep/toolchains.go create mode 100644 sdk/buildstep/toolchains_test.go diff --git a/sdk/buildstep/toolchains.go b/sdk/buildstep/toolchains.go new file mode 100644 index 00000000..6dc878f7 --- /dev/null +++ b/sdk/buildstep/toolchains.go @@ -0,0 +1,85 @@ +package buildstep + +import ( + "fmt" + "sort" + "strings" +) + +// **The toolchain a language's builders agree on** (novox/hq ADR 0584): the nodes report their toolchain in the state +// `toolchains` of a node-build seat (the seat's generated type holds the entry); this file holds the one rule over the +// entries every reader of that state shares, over a seat-neutral form, so no reader copies it. + +// ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain. +const ToolchainNoReport = "toolchain not yet reported" + +// Toolchain is what a toolchain is identified by: the language, the installed version and the build module's recipe +// version. It has the fields of the mesh-catalog client's Toolchain2, which a caller converts it to. +type Toolchain struct { + Language string + Version string + Recipe string +} + +// NodeToolchain is one node's entry of a language, as read from a build seat's state toolchains. +type NodeToolchain struct { + Node string + Toolchain +} + +// SittingOut is a node whose toolchain of a language is not the agreed one: it builds nothing of that language until +// it reports the agreed version (novox/hq ADR 0584 clause 2). +type SittingOut struct { + Node string + Version string + Recipe string +} + +// Agreement is what the nodes' entries of one language come to. Toolchain is the agreed one, set when Wait is empty. +// Wait is why nothing is agreed: "toolchain not yet reported" with no entries, or "builders disagree" naming each version +// and recipe with the nodes reporting it, when no version has a strict majority. SittingOut lists, sorted by node, the +// nodes that report another version than the agreed one. +type Agreement struct { + Toolchain Toolchain + SittingOut []SittingOut + Wait string +} + +// ToolchainAgreement is the agreed toolchain of a language: the version and recipe a strict majority of the reporting +// nodes report (novox/hq ADR 0584 clause 1). With no strict majority (two against two) or no entry it gives no toolchain +// and says why in Wait. The answer does not depend on the entries' order. The entries must all be of one language. +func ToolchainAgreement(entries []NodeToolchain) Agreement { + if len(entries) == 0 { + return Agreement{Wait: ToolchainNoReport} + } + groups := map[Toolchain][]string{} + for _, e := range entries { + k := Toolchain{Language: e.Language, Version: e.Version, Recipe: e.Recipe} + groups[k] = append(groups[k], e.Node) + } + for k, nodes := range groups { + if 2*len(nodes) > len(entries) { + a := Agreement{Toolchain: k} + for _, e := range entries { + if e.Version != k.Version || e.Recipe != k.Recipe { + a.SittingOut = append(a.SittingOut, SittingOut{Node: e.Node, Version: e.Version, Recipe: e.Recipe}) + } + } + sort.Slice(a.SittingOut, func(i, j int) bool { return a.SittingOut[i].Node < a.SittingOut[j].Node }) + return a + } + } + parts := make([]string, 0, len(groups)) + for k, nodes := range groups { + sort.Strings(nodes) + parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", "))) + } + sort.Strings(parts) + return Agreement{Wait: "builders disagree: " + strings.Join(parts, "; ")} +} + +// AgreedToolchain is ToolchainAgreement's toolchain, or none and why (its Wait). +func AgreedToolchain(entries []NodeToolchain) (tc Toolchain, none string) { + a := ToolchainAgreement(entries) + return a.Toolchain, a.Wait +} diff --git a/sdk/buildstep/toolchains_test.go b/sdk/buildstep/toolchains_test.go new file mode 100644 index 00000000..a1f376ef --- /dev/null +++ b/sdk/buildstep/toolchains_test.go @@ -0,0 +1,58 @@ +package buildstep + +import ( + "strings" + "testing" +) + +func tcEntry(node, lang, version, recipe string) NodeToolchain { + return NodeToolchain{Node: node, Toolchain: Toolchain{Language: lang, Version: version, Recipe: recipe}} +} + +// 1443/AC2-4, ADR 0584: the agreed toolchain is the strict majority's; a minority node sits out; a tie waits. +func TestToolchainAgreement(t *testing.T) { + e := tcEntry + for _, c := range []struct { + name string + in []NodeToolchain + version string + out []SittingOut + wait []string // "" wait means agreed + }{ + {"all agree", []NodeToolchain{e("ace", "go", "1.26.1", "3"), e("novox", "go", "1.26.1", "3")}, "1.26.1", nil, nil}, + {"three against one", []NodeToolchain{e("ace", "typescript", "26.12.1", "3"), e("g14", "typescript", "26.13.0", "3"), e("novox", "typescript", "26.13.0", "3"), e("shanks", "typescript", "26.13.0", "3")}, + "26.13.0", []SittingOut{{"ace", "26.12.1", "3"}}, nil}, + {"two against two", []NodeToolchain{e("ace", "go", "1.26.1", "3"), e("novox", "go", "1.26.2", "3"), e("g14", "go", "1.26.1", "3"), e("shanks", "go", "1.26.2", "3")}, + "", nil, []string{"builders disagree", "1.26.1 recipe 3 on ace, g14", "1.26.2 recipe 3 on novox, shanks"}}, + {"one node alone", []NodeToolchain{e("ace", "go", "1.26.1", "3")}, "1.26.1", nil, nil}, + {"no report", nil, "", nil, []string{ToolchainNoReport}}, + } { + a := ToolchainAgreement(c.in) + rev := make([]NodeToolchain, len(c.in)) + for i, x := range c.in { + rev[len(c.in)-1-i] = x + } + if b := ToolchainAgreement(rev); b.Wait != a.Wait || b.Toolchain != a.Toolchain { + t.Errorf("%s: order changed the answer: %+v vs %+v", c.name, a, b) + } + if a.Toolchain.Version != c.version || len(a.SittingOut) != len(c.out) { + t.Errorf("%s: %+v", c.name, a) + } + for i := range c.out { + if a.SittingOut[i] != c.out[i] { + t.Errorf("%s: sitting out %+v, want %+v", c.name, a.SittingOut, c.out) + } + } + if (len(c.wait) == 0) != (a.Wait == "") { + t.Errorf("%s: wait %q", c.name, a.Wait) + } + for _, w := range c.wait { + if !strings.Contains(a.Wait, w) { + t.Errorf("%s: %q does not name %q", c.name, a.Wait, w) + } + } + if tc, none := AgreedToolchain(c.in); tc != a.Toolchain || none != a.Wait { + t.Errorf("%s: AgreedToolchain %+v %q", c.name, tc, none) + } + } +} From 6b610e16576cfafa2f95f3a4314f246ce9852cae Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:14:18 +0100 Subject: [PATCH 02/11] =?UTF-8?q?Builder=20runner:=20the=20steps,=20asks?= =?UTF-8?q?=20and=20toolchain=20agreement=20are=20the=20Delivery=20SDK's,?= =?UTF-8?q?=20the=20toolchains=20the=20build=20seat's=20and=20the=20presen?= =?UTF-8?q?ce=20the=20watchdogs=20seat's=20types,=20the=20haves=20and=20pl?= =?UTF-8?q?acements=20their=20clients';=20the=20buckets=20it=20reads=20are?= =?UTF-8?q?=20its=20own=20constants=20(D-11;=20ADR=200634=20=C2=A71,=20?= =?UTF-8?q?=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../mesh-delivery-builder/buildrun/asks.go | 84 +++++++++---------- .../buildrun/asks_integration_test.go | 10 +-- .../buildrun/asks_test.go | 64 +++++++------- .../mesh-delivery-builder/buildrun/choose.go | 12 +-- .../buildrun/choose_test.go | 12 +-- modules/mesh-delivery-builder/buildrun/go.mod | 6 ++ .../mesh-delivery-builder/buildrun/have.go | 8 +- .../buildrun/heardends.go | 32 +++---- .../mesh-delivery-builder/buildrun/lostask.go | 20 +++-- .../mesh-delivery-builder/buildrun/names.go | 15 ++++ .../buildrun/presence.go | 11 ++- .../buildrun/presence_test.go | 5 +- .../buildrun/standing.go | 33 ++++---- .../buildrun/standing_test.go | 37 ++++---- .../mesh-delivery-builder/buildrun/steps.go | 8 +- .../buildrun/steps_test.go | 18 ++-- .../buildrun/toolchains.go | 21 ++--- .../buildrun/toolchains_test.go | 6 +- 18 files changed, 217 insertions(+), 185 deletions(-) create mode 100644 modules/mesh-delivery-builder/buildrun/names.go diff --git a/modules/mesh-delivery-builder/buildrun/asks.go b/modules/mesh-delivery-builder/buildrun/asks.go index fdaf3057..1af73b84 100644 --- a/modules/mesh-delivery-builder/buildrun/asks.go +++ b/modules/mesh-delivery-builder/buildrun/asks.go @@ -10,7 +10,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // **An asker puts an ask on its build seat's stream and waits for its step** (novox/hq ADR 0525 §1, §6; issue 1887 A4, @@ -25,7 +25,7 @@ type AskPublisher interface { // AskWaiter watches an ask's steps: every value a key `*.` of the build module's steps state takes, running or // ended, on the channel it answers, until ctx ends. A watch, not a poll. type AskWaiter interface { - WatchAsk(ctx context.Context, module, askID string) (<-chan delivery.Step, error) + WatchAsk(ctx context.Context, module, askID string) (<-chan buildstep.Step, error) } // Asker puts asks and waits for their steps. @@ -39,12 +39,12 @@ type Asker struct { } // Put puts a on its stream: the seat's accept subject of its language and kind. -func (k Asker) Put(ctx context.Context, a delivery.StepAsk) error { - raw, err := delivery.EncodeStepAsk(a) +func (k Asker) Put(ctx context.Context, a buildstep.StepAsk) error { + raw, err := buildstep.EncodeStepAsk(a) if err != nil { return err } - subject, err := delivery.AskSubject(a.Language, a.Kind) + subject, err := buildstep.AskSubject(a.Language, a.Kind) if err != nil { return err } @@ -57,16 +57,16 @@ func (k Asker) Put(ctx context.Context, a delivery.StepAsk) error { // Wait waits for the step of an ask already put (a builder that started again takes over its own asks with it, never // putting them twice) and answers it when it ended, whatever its state. The bound runs from the step's start on the node // that took the ask, read from its running key, so an ask that waits in the stream for a free slot waits as long as ctx. -func (k Asker) Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, error) { - module, err := delivery.BuildModuleOf(a.Language) +func (k Asker) Wait(ctx context.Context, a buildstep.StepAsk) (buildstep.Step, error) { + module, err := buildstep.BuildModuleOf(a.Language) if err != nil { - return delivery.Step{}, err + return buildstep.Step{}, err } watch, cancel := context.WithCancel(ctx) defer cancel() steps, err := k.Waiter.WatchAsk(watch, module, a.ID) if err != nil { - return delivery.Step{}, err + return buildstep.Step{}, err } var bound, outerCap <-chan time.Time var timer, capTimer *time.Timer @@ -89,20 +89,20 @@ func (k Asker) Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, err for { select { case <-ctx.Done(): - return delivery.Step{}, ctx.Err() + return buildstep.Step{}, ctx.Err() case <-bound: - return delivery.Step{}, standStill() + return buildstep.Step{}, standStill() case <-outerCap: - return delivery.Step{}, fmt.Errorf("the ask %s's step on %s did not end within %s of its start: it kept moving (last phase %q at %s) but never ended", + return buildstep.Step{}, fmt.Errorf("the ask %s's step on %s did not end within %s of its start: it kept moving (last phase %q at %s) but never ended", a.ID, running, 3*k.Bound, phase, phaseAt.UTC().Format(time.RFC3339)) case step, ok := <-steps: if !ok { - return delivery.Step{}, errors.New("the watch of the ask's step ended") + return buildstep.Step{}, errors.New("the watch of the ask's step ended") } if seen := stepSeenOf(ctx); seen != nil { seen(step) } - if step.State != delivery.StepRunning { + if step.State != buildstep.StepRunning { return step, nil } if k.Bound <= 0 { @@ -148,31 +148,31 @@ func (k Asker) Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, err type stepSeenKey struct{} // WithStepSeen is ctx carrying seen, which Asker.Wait hands every value the ask's step key takes while it waits: the node -// that took it, each phase it says while running (delivery.Step.Phase), and its end. A try names its asks' progress so +// that took it, each phase it says while running (buildstep.Step.Phase), and its end. A try names its asks' progress so // (issue 1890). seen runs in the waiting goroutine and must not block. -func WithStepSeen(ctx context.Context, seen func(delivery.Step)) context.Context { +func WithStepSeen(ctx context.Context, seen func(buildstep.Step)) context.Context { return context.WithValue(ctx, stepSeenKey{}, seen) } -func stepSeenOf(ctx context.Context) func(delivery.Step) { - seen, _ := ctx.Value(stepSeenKey{}).(func(delivery.Step)) +func stepSeenOf(ctx context.Context) func(buildstep.Step) { + seen, _ := ctx.Value(stepSeenKey{}).(func(buildstep.Step)) return seen } // Ask puts a and waits for its step; a step that did not pass is an error naming its node, its state and its reason. -func (k Asker) Ask(ctx context.Context, a delivery.StepAsk) (delivery.Step, error) { +func (k Asker) Ask(ctx context.Context, a buildstep.StepAsk) (buildstep.Step, error) { if err := k.Put(ctx, a); err != nil { - return delivery.Step{}, err + return buildstep.Step{}, err } return k.Passed(k.Wait(ctx, a)) } // Passed is step when it passed, else an error naming where it ended and why. -func (k Asker) Passed(step delivery.Step, err error) (delivery.Step, error) { +func (k Asker) Passed(step buildstep.Step, err error) (buildstep.Step, error) { if err != nil { return step, err } - if step.State != delivery.StepPassed { + if step.State != buildstep.StepPassed { return step, fmt.Errorf("the ask %s on %s %s: %s", step.ID, step.Node, step.State, step.Reason) } return step, nil @@ -194,20 +194,20 @@ func (s JetStreamAsks) PublishAsk(ctx context.Context, subject, id string, data // WatchAsk is AskWaiter's: the watch hands over what the keys hold now, so a step that ended before it began is read at // once, and every change after. -func (s JetStreamAsks) WatchAsk(ctx context.Context, module, askID string) (<-chan delivery.Step, error) { - pattern, err := delivery.AskStepWatch(askID) +func (s JetStreamAsks) WatchAsk(ctx context.Context, module, askID string) (<-chan buildstep.Step, error) { + pattern, err := buildstep.AskStepWatch(askID) if err != nil { return nil, err } - kv, err := s.JS.KeyValue(ctx, delivery.StepsBucket(module)) + kv, err := s.JS.KeyValue(ctx, StepsBucket(module)) if err != nil { - return nil, fmt.Errorf("the bucket %s: %w", delivery.StepsBucket(module), err) + return nil, fmt.Errorf("the bucket %s: %w", StepsBucket(module), err) } w, err := kv.Watch(ctx, pattern) if err != nil { return nil, err } - out := make(chan delivery.Step) + out := make(chan buildstep.Step) go func() { defer close(out) defer w.Stop() @@ -222,7 +222,7 @@ func (s JetStreamAsks) WatchAsk(ctx context.Context, module, askID string) (<-ch if e == nil || e.Operation() != jetstream.KeyValuePut { continue } - step, err := delivery.DecodeStep(e.Value()) + step, err := buildstep.DecodeStep(e.Value()) if err != nil { continue // not a step (a node's pause key never matches an ask id; anything else is not this ask's) } @@ -241,17 +241,17 @@ func (s JetStreamAsks) WatchAsk(ctx context.Context, module, askID string) (<-ch // written at once with what Answer says a build node wrote (running or ended). Write writes a step later. Safe for // concurrent use. type FakeAsks struct { - Answer func(a delivery.StepAsk) delivery.Step + Answer func(a buildstep.StepAsk) buildstep.Step mu sync.Mutex - asks []delivery.StepAsk - steps map[string][]delivery.Step - watchers map[string][]chan delivery.Step + asks []buildstep.StepAsk + steps map[string][]buildstep.Step + watchers map[string][]chan buildstep.Step } // PublishAsk is AskPublisher's. func (f *FakeAsks) PublishAsk(_ context.Context, _, id string, data []byte) error { - a, err := delivery.DecodeStepAsk(data) + a, err := buildstep.DecodeStepAsk(data) if err != nil { return err } @@ -265,13 +265,13 @@ func (f *FakeAsks) PublishAsk(_ context.Context, _, id string, data []byte) erro } // Write writes an ask's step, as a build node does. -func (f *FakeAsks) Write(step delivery.Step) { +func (f *FakeAsks) Write(step buildstep.Step) { f.mu.Lock() if f.steps == nil { - f.steps, f.watchers = map[string][]delivery.Step{}, map[string][]chan delivery.Step{} + f.steps, f.watchers = map[string][]buildstep.Step{}, map[string][]chan buildstep.Step{} } f.steps[step.ID] = append(f.steps[step.ID], step) - ws := append([]chan delivery.Step(nil), f.watchers[step.ID]...) + ws := append([]chan buildstep.Step(nil), f.watchers[step.ID]...) f.mu.Unlock() for _, w := range ws { w <- step @@ -279,14 +279,14 @@ func (f *FakeAsks) Write(step delivery.Step) { } // End is Write: kept for the tests that end a step. -func (f *FakeAsks) End(step delivery.Step) { f.Write(step) } +func (f *FakeAsks) End(step buildstep.Step) { f.Write(step) } // WatchAsk is AskWaiter's: what was written so far, then every write. -func (f *FakeAsks) WatchAsk(ctx context.Context, _ string, askID string) (<-chan delivery.Step, error) { - out := make(chan delivery.Step, 64) +func (f *FakeAsks) WatchAsk(ctx context.Context, _ string, askID string) (<-chan buildstep.Step, error) { + out := make(chan buildstep.Step, 64) f.mu.Lock() if f.steps == nil { - f.steps, f.watchers = map[string][]delivery.Step{}, map[string][]chan delivery.Step{} + f.steps, f.watchers = map[string][]buildstep.Step{}, map[string][]chan buildstep.Step{} } for _, s := range f.steps[askID] { out <- s @@ -309,8 +309,8 @@ func (f *FakeAsks) WatchAsk(ctx context.Context, _ string, askID string) (<-chan } // Asks are the asks put so far. -func (f *FakeAsks) Asks() []delivery.StepAsk { +func (f *FakeAsks) Asks() []buildstep.StepAsk { f.mu.Lock() defer f.mu.Unlock() - return append([]delivery.StepAsk(nil), f.asks...) + return append([]buildstep.StepAsk(nil), f.asks...) } diff --git a/modules/mesh-delivery-builder/buildrun/asks_integration_test.go b/modules/mesh-delivery-builder/buildrun/asks_integration_test.go index 6c6961c0..31363fad 100644 --- a/modules/mesh-delivery-builder/buildrun/asks_integration_test.go +++ b/modules/mesh-delivery-builder/buildrun/asks_integration_test.go @@ -11,7 +11,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/testbus" ) @@ -29,7 +29,7 @@ func TestAnAskIsPutOnceAndItsStepIsRead(t *testing.T) { Retention: jetstream.WorkQueuePolicy}); err != nil { t.Fatal(err) } - kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.StepsBucket("mesh-build-go")}) + kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: StepsBucket("mesh-build-go")}) if err != nil { t.Fatal(err) } @@ -47,10 +47,10 @@ func TestAnAskIsPutOnceAndItsStepIsRead(t *testing.T) { } go func() { time.Sleep(100 * time.Millisecond) - key, _ := delivery.AskStepKey("ace", a.ID) - running, _ := delivery.EncodeStep(delivery.Step{ID: a.ID, Tool: "delivery ask", Node: "ace", State: delivery.StepRunning, Started: time.Now()}) + key, _ := buildstep.AskStepKey("ace", a.ID) + running, _ := buildstep.EncodeStep(buildstep.Step{ID: a.ID, Tool: "delivery ask", Node: "ace", State: buildstep.StepRunning, Started: time.Now()}) _, _ = kv.Put(ctx, key, running) - passed, _ := delivery.EncodeStep(delivery.Step{ID: a.ID, Tool: "delivery ask", Node: "ace", State: delivery.StepPassed, Started: time.Now(), + passed, _ := buildstep.EncodeStep(buildstep.Step{ID: a.ID, Tool: "delivery ask", Node: "ace", State: buildstep.StepPassed, Started: time.Now(), Result: json.RawMessage(`{"artifacts":[]}`)}) _, _ = kv.Put(ctx, key, passed) }() diff --git a/modules/mesh-delivery-builder/buildrun/asks_test.go b/modules/mesh-delivery-builder/buildrun/asks_test.go index ec6b94f9..357ade87 100644 --- a/modules/mesh-delivery-builder/buildrun/asks_test.go +++ b/modules/mesh-delivery-builder/buildrun/asks_test.go @@ -9,30 +9,30 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/health" ) -func goAsk(id string) delivery.StepAsk { - return delivery.StepAsk{ID: id, Kind: delivery.AskDelivery, Language: delivery.AskGo, Asker: "mesh-delivery-builder", At: time.Now(), - Build: &delivery.BuildJob{Module: "m", Path: ".", Fetch: map[string]any{"repository": "novox/x", "commit": strings.Repeat("a", 40)}, - Artifacts: []delivery.AskArtifact{{Artifact: "program"}}}} +func goAsk(id string) buildstep.StepAsk { + return buildstep.StepAsk{ID: id, Kind: buildstep.AskDelivery, Language: buildstep.AskGo, Asker: "mesh-delivery-builder", At: time.Now(), + Build: &buildstep.BuildJob{Module: "m", Path: ".", Fetch: &buildstep.Fetch{Repository: "novox/x", Commit: strings.Repeat("a", 40)}, + Artifacts: []buildstep.AskArtifact{{Artifact: "program"}}}} } // 1887/A4a: an asker puts the ask once and answers the step a build node wrote for it, naming the node; one that did not // pass is an error naming the node and why; a wait of an ask already put puts nothing. func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { - f := &FakeAsks{Answer: func(a delivery.StepAsk) delivery.Step { - return delivery.Step{ID: a.ID, Node: "ace", State: delivery.StepPassed, Result: []byte(`{"artifacts":[]}`)} + f := &FakeAsks{Answer: func(a buildstep.StepAsk) buildstep.Step { + return buildstep.Step{ID: a.ID, Node: "ace", State: buildstep.StepPassed, Result: []byte(`{"artifacts":[]}`)} }} k := Asker{Publisher: f, Waiter: f} - var seen []delivery.Step // issue 1890: a waiter that asks to see each value of the step sees it - step, err := k.Ask(WithStepSeen(context.Background(), func(s delivery.Step) { seen = append(seen, s) }), goAsk("mvl0frv18d1957")) + var seen []buildstep.Step // issue 1890: a waiter that asks to see each value of the step sees it + step, err := k.Ask(WithStepSeen(context.Background(), func(s buildstep.Step) { seen = append(seen, s) }), goAsk("mvl0frv18d1957")) if err != nil || step.Node != "ace" || len(f.Asks()) != 1 || len(seen) != 1 || seen[0].Node != "ace" { t.Fatalf("step %+v, %v, asks %d, seen %+v", step, err, len(f.Asks()), seen) } - f.Answer = func(a delivery.StepAsk) delivery.Step { - return delivery.Step{ID: a.ID, Node: "shanks", State: delivery.StepRefused, Reason: "go 1.27 installed"} + f.Answer = func(a buildstep.StepAsk) buildstep.Step { + return buildstep.Step{ID: a.ID, Node: "shanks", State: buildstep.StepRefused, Reason: "go 1.27 installed"} } if _, err := k.Ask(context.Background(), goAsk("mvl0frv18d1958")); err == nil || !strings.Contains(err.Error(), "shanks refused: go 1.27 installed") { t.Fatalf("a refused step: %v", err) @@ -46,7 +46,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { if _, err := k.Wait(waiting, goAsk("mvl0frv18d1959")); err != context.DeadlineExceeded { t.Fatalf("an ask no node took: %v, want it still waiting when ctx ended", err) } - g.Write(delivery.Step{ID: "mvl0frv18d1961", Node: "ace", State: delivery.StepRunning, Started: time.Now()}) + g.Write(buildstep.Step{ID: "mvl0frv18d1961", Node: "ace", State: buildstep.StepRunning, Started: time.Now()}) if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1961")); err == nil || !strings.Contains(err.Error(), "on ace did not end within") { t.Fatalf("a step that never ends: %v", err) } @@ -60,16 +60,16 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { // of 60 ms). k = Asker{Publisher: g, Waiter: g, Bound: time.Hour} now := time.Now() - g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepRunning, Started: now.Add(-90 * time.Minute), Phase: "pb", PhaseAt: now}) - g.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "ace", State: delivery.StepPassed}) - if step, err := k.Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || step.State != delivery.StepPassed { + g.Write(buildstep.Step{ID: "mvl0frv18d1980", Node: "ace", State: buildstep.StepRunning, Started: now.Add(-90 * time.Minute), Phase: "pb", PhaseAt: now}) + g.Write(buildstep.Step{ID: "mvl0frv18d1980", Node: "ace", State: buildstep.StepPassed}) + if step, err := k.Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || step.State != buildstep.StepPassed { t.Fatalf("a step that kept moving past its bound: %+v, %v", step, err) } - g.Write(delivery.Step{ID: "mvl0frv18d1981", Node: "ace", State: delivery.StepRunning, Started: now.Add(-time.Hour), Phase: "integration: tests", PhaseAt: now.Add(-time.Hour)}) + g.Write(buildstep.Step{ID: "mvl0frv18d1981", Node: "ace", State: buildstep.StepRunning, Started: now.Add(-time.Hour), Phase: "integration: tests", PhaseAt: now.Add(-time.Hour)}) if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1981")); err == nil || !strings.Contains(err.Error(), `stood still in "integration: tests"`) { t.Fatalf("a step that stood still in a phase: %v", err) } - g.Write(delivery.Step{ID: "mvl0frv18d1982", Node: "ace", State: delivery.StepRunning, Started: now.Add(-3 * time.Hour), Phase: "qz", PhaseAt: now}) + g.Write(buildstep.Step{ID: "mvl0frv18d1982", Node: "ace", State: buildstep.StepRunning, Started: now.Add(-3 * time.Hour), Phase: "qz", PhaseAt: now}) if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1982")); err == nil || !strings.Contains(err.Error(), "kept moving") { t.Fatalf("a step that never stops moving: %v", err) } @@ -79,7 +79,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { dead := &DeadLetters{} lw := LostAskWaiter{Inner: g, Dead: dead} k = Asker{Publisher: g, Waiter: lw, Bound: time.Minute} - g.Write(delivery.Step{ID: "mvl0frv18d1970", Node: "ace", State: delivery.StepRunning, Started: time.Now()}) + g.Write(buildstep.Step{ID: "mvl0frv18d1970", Node: "ace", State: buildstep.StepRunning, Started: time.Now()}) given, ok := GivenUpOf(map[string][]string{"Mesh-Dead-Msg-Id": {"mvl0frv18d1970"}, "Mesh-Dead-Deliveries": {"5"}, "Mesh-Dead-Stream": {"SEAT_NODE_BUILD_GO"}, "Mesh-Dead-Gave-Up": {"2026-10-26T12:26:40Z"}}) if !ok { @@ -87,7 +87,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { } go dead.Heard(given) step, err = k.Wait(context.Background(), goAsk("mvl0frv18d1970")) - if err != nil || step.State != delivery.StepFailed || !IsLost(step) || + if err != nil || step.State != buildstep.StepFailed || !IsLost(step) || !strings.Contains(step.Reason, LostWords+" after 5 deliveries at 2026-10-26T12:26:40Z") { t.Fatalf("a given-up ask: %+v, %v", step, err) } @@ -98,7 +98,7 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { if err != nil || !IsNotTaken(step) || IsLost(step) { t.Fatalf("an ask no node took: %+v, %v", step, err) } - g.Write(delivery.Step{ID: "mvl0frv18d1972", Node: "g14", State: delivery.StepPassed}) + g.Write(buildstep.Step{ID: "mvl0frv18d1972", Node: "g14", State: buildstep.StepPassed}) if step, err = k.Wait(NotTakenBound(context.Background(), time.Minute), goAsk("mvl0frv18d1972")); err != nil || step.Node != "g14" { t.Fatalf("a step written: %+v, %v", step, err) } @@ -107,14 +107,14 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { fol := &refusingFollower{refuse: 1} var mu sync.Mutex var told []Hearing - hearing := NewDeadLetterHearing(context.Background(), fol, []string{delivery.AskGo}, dead, nil, + hearing := NewDeadLetterHearing(context.Background(), fol, []string{buildstep.AskGo}, dead, nil, func(h Hearing) { mu.Lock(); told = append(told, h); mu.Unlock() }) hearing.Wait() if why := hearing.Deaf(); !strings.Contains(why, "SEAT_NODE_BUILD_GO: permissions violation") { t.Fatalf("a refused hearing says %q", why) } k = Asker{Publisher: g, Waiter: LostAskWaiter{Inner: g, Dead: dead, Hearing: hearing}, Bound: time.Minute} - g.Write(delivery.Step{ID: "mvl0frv18d1973", Node: "g14", State: delivery.StepPassed}) + g.Write(buildstep.Step{ID: "mvl0frv18d1973", Node: "g14", State: buildstep.StepPassed}) if _, err := k.Wait(context.Background(), goAsk("mvl0frv18d1973")); err != nil { t.Fatal(err) } @@ -134,8 +134,8 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { d := &FakeAsks{} heard := &HeardEnds{Inner: deafWatch{}, Read: d} k = Asker{Publisher: d, Waiter: heard, Bound: time.Minute} - d.Write(delivery.Step{ID: "mvl0frv18d1980", Node: "shanks", State: delivery.StepPassed}) - got := make(chan delivery.Step, 1) + d.Write(buildstep.Step{ID: "mvl0frv18d1980", Node: "shanks", State: buildstep.StepPassed}) + got := make(chan buildstep.Step, 1) go func() { s, _ := k.Wait(context.Background(), goAsk("mvl0frv18d1980")); got <- s }() for heard.Waiting() == 0 { time.Sleep(time.Millisecond) @@ -147,11 +147,11 @@ func TestAnAskerPutsOnceAndReadsTheStep(t *testing.T) { case <-time.After(20 * time.Millisecond): } heard.Nudge(GoBuildModule) - if s := <-got; s.State != delivery.StepPassed || s.Node != "shanks" { + if s := <-got; s.State != buildstep.StepPassed || s.Node != "shanks" { t.Fatalf("a step its watch never handed over: %+v", s) } closing := &HeardEnds{Inner: &closingWatch{inner: d}, Read: d} - if s, err := (Asker{Publisher: d, Waiter: closing}).Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || s.State != delivery.StepPassed { + if s, err := (Asker{Publisher: d, Waiter: closing}).Wait(context.Background(), goAsk("mvl0frv18d1980")); err != nil || s.State != buildstep.StepPassed { t.Fatalf("a watch that closed once: %+v, %v", s, err) } for _, row := range []struct { @@ -178,8 +178,8 @@ func TestARedeliveryToTheSameNodeRestartsTheBound(t *testing.T) { k := Asker{Publisher: f, Waiter: f, Bound: time.Hour} const id = "mvl0frv18d1962" now := time.Now() - f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: now}) - f.Write(delivery.Step{ID: id, Node: "ace", State: delivery.StepRunning, Started: now.Add(-time.Hour)}) + f.Write(buildstep.Step{ID: id, Node: "ace", State: buildstep.StepRunning, Started: now}) + f.Write(buildstep.Step{ID: id, Node: "ace", State: buildstep.StepRunning, Started: now.Add(-time.Hour)}) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if _, err := k.Wait(ctx, goAsk(id)); err == nil || !strings.Contains(err.Error(), "on ace did not end within 1h0m0s of its start") { @@ -213,8 +213,8 @@ func (f *refusingFollower) heard(g GivenUp) { f.mu.Lock(); h := f.hand; f.mu.Unl // deafWatch is a watch of an ask's step that stays open and hears nothing. type deafWatch struct{} -func (deafWatch) WatchAsk(context.Context, string, string) (<-chan delivery.Step, error) { - return make(chan delivery.Step), nil +func (deafWatch) WatchAsk(context.Context, string, string) (<-chan buildstep.Step, error) { + return make(chan buildstep.Step), nil } // closingWatch closes its first watch at once, and hands the later ones to inner. @@ -223,10 +223,10 @@ type closingWatch struct { closed bool } -func (c *closingWatch) WatchAsk(ctx context.Context, module, askID string) (<-chan delivery.Step, error) { +func (c *closingWatch) WatchAsk(ctx context.Context, module, askID string) (<-chan buildstep.Step, error) { if !c.closed { c.closed = true - ch := make(chan delivery.Step) + ch := make(chan buildstep.Step) close(ch) return ch, nil } diff --git a/modules/mesh-delivery-builder/buildrun/choose.go b/modules/mesh-delivery-builder/buildrun/choose.go index 06aea831..b8997280 100644 --- a/modules/mesh-delivery-builder/buildrun/choose.go +++ b/modules/mesh-delivery-builder/buildrun/choose.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // ChooseNodes picks up to want build nodes for a language (ADR 0478 clause 3): the nodes whose toolchains key reports the @@ -18,10 +18,10 @@ import ( // agree on, qualifying or not, as the indexer computes it. When nothing is reported, or the nodes report different // versions (so the indexer agreed none), it fails naming the language and why; when no reporting node qualifies it fails // naming the nodes. A node whose build module refuses on its toolchain refuses the step, and the build fails naming it. -func ChooseNodes(language string, entries []delivery.NodeToolchain, places *Places, qualifies func(node string) bool, want int) ([]string, delivery.Toolchain2, error) { - tc, none := delivery.AgreedToolchain(entries) +func ChooseNodes(language string, entries []buildstep.NodeToolchain, places *Places, qualifies func(node string) bool, want int) ([]string, buildstep.Toolchain, error) { + tc, none := buildstep.AgreedToolchain(entries) if none != "" { - return nil, delivery.Toolchain2{}, fmt.Errorf("no %s build node qualifies: %s", language, none) + return nil, buildstep.Toolchain{}, fmt.Errorf("no %s build node qualifies: %s", language, none) } nodes, err := choose(language, entries, places, qualifies, want) return nodes, tc, err @@ -29,11 +29,11 @@ func ChooseNodes(language string, entries []delivery.NodeToolchain, places *Plac // ChooseAnyNodes is ChooseNodes for artifacts that need no toolchain, such as an archive (ADR 0478 clause 1): the // build module's nodes agree on nothing, so any node of the build module that qualifies is chosen, by capacity and load only. -func ChooseAnyNodes(language string, entries []delivery.NodeToolchain, places *Places, qualifies func(node string) bool, want int) ([]string, error) { +func ChooseAnyNodes(language string, entries []buildstep.NodeToolchain, places *Places, qualifies func(node string) bool, want int) ([]string, error) { return choose(language, entries, places, qualifies, want) } -func choose(language string, entries []delivery.NodeToolchain, places *Places, qualifies func(node string) bool, want int) ([]string, error) { +func choose(language string, entries []buildstep.NodeToolchain, places *Places, qualifies func(node string) bool, want int) ([]string, error) { if places == nil { places = &Places{} } diff --git a/modules/mesh-delivery-builder/buildrun/choose_test.go b/modules/mesh-delivery-builder/buildrun/choose_test.go index bb2e69ab..d39e70c6 100644 --- a/modules/mesh-delivery-builder/buildrun/choose_test.go +++ b/modules/mesh-delivery-builder/buildrun/choose_test.go @@ -4,17 +4,17 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) const digest = "sha256:2222222222222222222222222222222222222222222222222222222222222222" func hex64(c byte) string { return strings.Repeat(string(c), 64) } -func tcs(language, version string, nodes ...string) []delivery.NodeToolchain { - var out []delivery.NodeToolchain +func tcs(language, version string, nodes ...string) []buildstep.NodeToolchain { + var out []buildstep.NodeToolchain for _, n := range nodes { - out = append(out, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: language, Version: version, Recipe: "1"}}) + out = append(out, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: language, Version: version, Recipe: "1"}}) } return out } @@ -42,8 +42,8 @@ func chooseWithOneNodeAnswersOneNode(t *testing.T) { // 1607/AC4 func chooseFailsNamingTheLanguageAndTheVersionsWhenNoNodeQualifies(t *testing.T) { // 1607/AC4 _, _, err := ChooseNodes("typescript", nil, nil, nil, 2) - if err == nil || !strings.Contains(err.Error(), "typescript") || !strings.Contains(err.Error(), delivery.ToolchainNoReport) { - t.Errorf("no report: error = %v, want it to name typescript and %q", err, delivery.ToolchainNoReport) + if err == nil || !strings.Contains(err.Error(), "typescript") || !strings.Contains(err.Error(), buildstep.ToolchainNoReport) { + t.Errorf("no report: error = %v, want it to name typescript and %q", err, buildstep.ToolchainNoReport) } entries := append(tcs("go", "go1.27.1", "novox"), tcs("go", "go1.26.0", "ace")...) _, _, err = ChooseNodes("go", entries, nil, nil, 2) diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index fceda6f3..ede9fa15 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -3,6 +3,12 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun go 1.26.0 require ( + git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs 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-contracts/go/clients/nodeengineclient 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/sdk v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/buildrun/have.go b/modules/mesh-delivery-builder/buildrun/have.go index f3c079cf..ae5be958 100644 --- a/modules/mesh-delivery-builder/buildrun/have.go +++ b/modules/mesh-delivery-builder/buildrun/have.go @@ -3,7 +3,7 @@ package buildrun import ( "sync" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" ) // HaveSet holds what each node's node-engine says of each module on it (node-engine_have, key `.`): the @@ -19,17 +19,17 @@ type HaveSet struct { // Put takes a change of the have bucket in and answers whether a module's healthy state changed. A node's own key // (`.node`) and a value that is not a have are left out. func (s *HaveSet) Put(key string, value []byte, deleted bool) bool { - node, module, isNode, err := delivery.ParseHaveKey(key) + node, module, isNode, err := nodeengineclient.ParseHaveKey(key) if err != nil || isNode { return false } ok := false if !deleted { - h, err := delivery.DecodeHave(value) + h, err := nodeengineclient.DecodeHave(value) if err != nil { return false } - ok = h.Phase == delivery.PhaseRunning && h.Health == delivery.HealthOK + ok = h.Phase == nodeengineclient.PhaseRunning && h.Health == nodeengineclient.HealthOK } k := node + "." + module s.mu.Lock() diff --git a/modules/mesh-delivery-builder/buildrun/heardends.go b/modules/mesh-delivery-builder/buildrun/heardends.go index 25b8cbbb..3cae74f1 100644 --- a/modules/mesh-delivery-builder/buildrun/heardends.go +++ b/modules/mesh-delivery-builder/buildrun/heardends.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // **A step's end is never only its own watch's to hear** (the lost check of 2026-10-28: mesh-catalog #553's verdict @@ -22,7 +22,7 @@ import ( // StepReader reads, now, every step a build module's steps state holds for an ask: its keys `*.`. type StepReader interface { - AskSteps(ctx context.Context, module, askID string) ([]delivery.Step, error) + AskSteps(ctx context.Context, module, askID string) ([]buildstep.Step, error) } // reopenTries is how often a wait opens its ask's watch again after it closed, before the wait ends failed. @@ -50,7 +50,7 @@ func (h *HeardEnds) logf(format string, args ...any) { } // WatchAsk is AskWaiter's: what Inner's watch hands, and, at each nudge, what the ask's keys hold now. -func (h *HeardEnds) WatchAsk(ctx context.Context, module, askID string) (<-chan delivery.Step, error) { +func (h *HeardEnds) WatchAsk(ctx context.Context, module, askID string) (<-chan buildstep.Step, error) { in, err := h.Inner.WatchAsk(ctx, module, askID) if err != nil { return nil, err @@ -62,7 +62,7 @@ func (h *HeardEnds) WatchAsk(ctx context.Context, module, askID string) (<-chan } h.waiting[askID] = w h.mu.Unlock() - out := make(chan delivery.Step) + out := make(chan buildstep.Step) go func() { defer close(out) defer func() { @@ -72,7 +72,7 @@ func (h *HeardEnds) WatchAsk(ctx context.Context, module, askID string) (<-chan } h.mu.Unlock() }() - send := func(s delivery.Step) bool { + send := func(s buildstep.Step) bool { select { case out <- s: return true @@ -112,7 +112,7 @@ func (h *HeardEnds) WatchAsk(ctx context.Context, module, askID string) (<-chan continue } for _, s := range steps { - if s.State != delivery.StepRunning { + if s.State != buildstep.StepRunning { h.logf("the ask %s's step on %s ended %s, read at a nudge: its watch had not handed it over", askID, s.Node, s.State) } if !send(s) { @@ -147,26 +147,26 @@ func (h *HeardEnds) Waiting() int { } // AskSteps is StepReader's: the keys `*.` of the build module's steps state, read now. -func (s JetStreamAsks) AskSteps(ctx context.Context, module, askID string) ([]delivery.Step, error) { - pattern, err := delivery.AskStepWatch(askID) +func (s JetStreamAsks) AskSteps(ctx context.Context, module, askID string) ([]buildstep.Step, error) { + pattern, err := buildstep.AskStepWatch(askID) if err != nil { return nil, err } - kv, err := s.JS.KeyValue(ctx, delivery.StepsBucket(module)) + kv, err := s.JS.KeyValue(ctx, StepsBucket(module)) if err != nil { - return nil, fmt.Errorf("the bucket %s: %w", delivery.StepsBucket(module), err) + return nil, fmt.Errorf("the bucket %s: %w", StepsBucket(module), err) } lister, err := kv.ListKeysFiltered(ctx, pattern) if err != nil { return nil, err } - var out []delivery.Step + var out []buildstep.Step for key := range lister.Keys() { e, err := kv.Get(ctx, key) if err != nil { continue // deleted between the list and the read } - if step, err := delivery.DecodeStep(e.Value()); err == nil { + if step, err := buildstep.DecodeStep(e.Value()); err == nil { out = append(out, step) } } @@ -182,7 +182,7 @@ func HearEnds(ctx context.Context, asks JetStreamAsks, inner AskWaiter, modules } h := &HeardEnds{Inner: inner, Read: asks, Logf: logf} for _, module := range modules { - kv, err := asks.JS.KeyValue(ctx, delivery.StepsBucket(module)) + kv, err := asks.JS.KeyValue(ctx, StepsBucket(module)) if err != nil { logf("the steps of %s cannot be read (%v): an ask whose own watch goes deaf is read only at a reconnect", module, err) continue @@ -206,7 +206,7 @@ func (h *HeardEnds) follow(ctx context.Context, kv jetstream.KeyValue, module st continue } heard = true - if step, err := delivery.DecodeStep(e.Value()); err == nil && step.State != delivery.StepRunning { + if step, err := buildstep.DecodeStep(e.Value()); err == nil && step.State != buildstep.StepRunning { h.Nudge(module) } } @@ -226,11 +226,11 @@ func (h *HeardEnds) follow(ctx context.Context, kv jetstream.KeyValue, module st } // AskSteps is StepReader's: the last step written for the ask, if any. -func (f *FakeAsks) AskSteps(_ context.Context, _ string, askID string) ([]delivery.Step, error) { +func (f *FakeAsks) AskSteps(_ context.Context, _ string, askID string) ([]buildstep.Step, error) { f.mu.Lock() defer f.mu.Unlock() if s := f.steps[askID]; len(s) > 0 { - return []delivery.Step{s[len(s)-1]}, nil + return []buildstep.Step{s[len(s)-1]}, nil } return nil, nil } diff --git a/modules/mesh-delivery-builder/buildrun/lostask.go b/modules/mesh-delivery-builder/buildrun/lostask.go index 750acaa4..a3392412 100644 --- a/modules/mesh-delivery-builder/buildrun/lostask.go +++ b/modules/mesh-delivery-builder/buildrun/lostask.go @@ -12,13 +12,13 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/process" ) // **An ask the build seat gave up on ends its step** (novox/hq issue 734). A build seat's worker offers an ask -// delivery.AskMaxDeliver times; one handed out that often without an answer (a bus reload that drops every hand-out, a node +// buildstep.AskMaxDeliver times; one handed out that often without an answer (a bus reload that drops every hand-out, a node // that dies taking it each time) is given up by the server, and the controller keeps a copy of it in DEAD_LETTERS under // mesh.events.dead.., with the ask's message id (its ask id), how often it was handed out and when it // was given up. No build node writes a step for it, so an asker that waits for the step waits for ever: a try stays @@ -193,7 +193,7 @@ func (w LostAskWaiter) now() time.Time { } // WatchAsk is AskWaiter's. -func (w LostAskWaiter) WatchAsk(ctx context.Context, module, askID string) (<-chan delivery.Step, error) { +func (w LostAskWaiter) WatchAsk(ctx context.Context, module, askID string) (<-chan buildstep.Step, error) { if w.Hearing != nil && w.Hearing.Deaf() != "" { w.Hearing.Again() } @@ -215,12 +215,12 @@ func (w LostAskWaiter) WatchAsk(ctx context.Context, module, askID string) (<-ch nt, stopBound = context.WithTimeout(context.Background(), bound) notTaken = nt.Done() } - out := make(chan delivery.Step) + out := make(chan buildstep.Step) go func() { defer close(out) defer cancel() defer func() { stopBound() }() - send := func(s delivery.Step) bool { + send := func(s buildstep.Step) bool { select { case out <- s: return true @@ -228,9 +228,9 @@ func (w LostAskWaiter) WatchAsk(ctx context.Context, module, askID string) (<-ch return false } } - ended := func(reason string) delivery.Step { + ended := func(reason string) buildstep.Step { at := w.now().UTC() - return delivery.Step{ID: askID, Node: LostNode, State: delivery.StepFailed, Reason: reason, Ended: at} + return buildstep.Step{ID: askID, Node: LostNode, State: buildstep.StepFailed, Reason: reason, Ended: at} } for { select { @@ -261,10 +261,12 @@ func (w LostAskWaiter) WatchAsk(ctx context.Context, module, askID string) (<-ch } // IsLost says whether a step was ended by LostAskWaiter because the build seat gave its ask up (LostWords). -func IsLost(s delivery.Step) bool { return s.Node == LostNode && strings.Contains(s.Reason, LostWords) } +func IsLost(s buildstep.Step) bool { + return s.Node == LostNode && strings.Contains(s.Reason, LostWords) +} // IsNotTaken says whether a step was ended by LostAskWaiter because no build node took its ask within the bound. -func IsNotTaken(s delivery.Step) bool { +func IsNotTaken(s buildstep.Step) bool { return s.Node == LostNode && strings.Contains(s.Reason, NotTakenWords) } diff --git a/modules/mesh-delivery-builder/buildrun/names.go b/modules/mesh-delivery-builder/buildrun/names.go new file mode 100644 index 00000000..770324f8 --- /dev/null +++ b/modules/mesh-delivery-builder/buildrun/names.go @@ -0,0 +1,15 @@ +package buildrun + +// The holders' states the runner reads, named from the builder's own manifest (module.json `reads`; a unit row of the +// builder checks them against it) and from the build seats' states: a bucket is `_`, and no contract +// names a holder (novox/hq ADR 0625 §4, ADR 0634 §1). +const ( + // PlacementBucket is the assigner's state placement, which the toolchain gate reads for the placing of a build module. + PlacementBucket = "mesh-delivery-assigner_placement" +) + +// StepsBucket is the bucket of a build module's state steps. +func StepsBucket(module string) string { return module + "_steps" } + +// ToolchainsBucket is the bucket of a build module's state toolchains. +func ToolchainsBucket(module string) string { return module + "_toolchains" } diff --git a/modules/mesh-delivery-builder/buildrun/presence.go b/modules/mesh-delivery-builder/buildrun/presence.go index 63f82435..fafc431b 100644 --- a/modules/mesh-delivery-builder/buildrun/presence.go +++ b/modules/mesh-delivery-builder/buildrun/presence.go @@ -3,7 +3,7 @@ package buildrun import ( "sync" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" ) // PresenceSet holds which nodes are online, as the watchman writes it (mesh-delivery-watchman_presence, key ``, the @@ -17,13 +17,16 @@ type PresenceSet struct { // Put takes a change of the presence bucket in and answers whether a node's online state changed. A part's key and a // value that is not a presence are left out. func (s *PresenceSet) Put(key string, value []byte, deleted bool) bool { - node, part, _, err := delivery.ParsePresenceKey(key) - if err != nil || part != "" { + if _, _, isPart := meshwatchdogs.ParsePresenceByPartPartInstanceKey(key); isPart { + return false + } + node, ok := meshwatchdogs.ParsePresenceByNodeKey(key) + if !ok { return false } on := false if !deleted { - p, err := delivery.DecodePresence(value) + p, err := meshwatchdogs.DecodePresenceByNodeValue(value) if err != nil { return false } diff --git a/modules/mesh-delivery-builder/buildrun/presence_test.go b/modules/mesh-delivery-builder/buildrun/presence_test.go index 5157609a..38e45577 100644 --- a/modules/mesh-delivery-builder/buildrun/presence_test.go +++ b/modules/mesh-delivery-builder/buildrun/presence_test.go @@ -1,14 +1,15 @@ package buildrun import ( + "encoding/json" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" ) func presenceValue(t *testing.T, online bool) []byte { t.Helper() - b, err := delivery.EncodePresence(delivery.Presence{Online: online}) + b, err := json.Marshal(meshwatchdogs.PresenceByNodeValue{Online: online}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-builder/buildrun/standing.go b/modules/mesh-delivery-builder/buildrun/standing.go index fdaf1da6..19913896 100644 --- a/modules/mesh-delivery-builder/buildrun/standing.go +++ b/modules/mesh-delivery-builder/buildrun/standing.go @@ -12,7 +12,8 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -23,7 +24,7 @@ import ( // // ToolchainGate stands between the toolchains buckets and what takes their entries in (the indexer's PutToolchain, a // ToolchainSet): an entry is passed on only while the placement (the seat mesh-placement's state `placement`, held by -// the assigner, delivery.PlacementBucket, read by delivery.Placed; ADR 0567, ADR 0549 §3) places its build module on +// the assigner, PlacementBucket, read by meshmoduleassignerclient.Placed; ADR 0567, ADR 0549 §3) places its build module on // its node, and only once it was written since that placement; it is passed on as deleted otherwise, said once, and // passed on when its node writes it again. A report written before the placement is one an earlier run of the module // left (issue 1940, 2026-10-25 20:56: mesh-build-node put on ace again brought ace.typescript v26.12.0 of 2026-10-23 @@ -39,7 +40,7 @@ type ToolchainGate struct { modules map[string]bool // the build modules whose toolchains pass the gate forward func(module, key string, value []byte, deleted bool) bool // answers whether it changed what it holds logf func(format string, a ...any) - placed *delivery.Placed + placed *meshmoduleassignerclient.Placed known bool // the placement is settled raw map[string]map[string][]byte // module -> toolchains key -> value, as the bucket holds it left map[string]string // / -> why it was said left out, so each reason is said once @@ -51,7 +52,7 @@ type ToolchainGate struct { // NewToolchainGate is a gate over the toolchains of modules, passing what stands on to forward and saying with logf what // it leaves out. func NewToolchainGate(modules []string, forward func(module, key string, value []byte, deleted bool) bool, logf func(string, ...any)) *ToolchainGate { - g := &ToolchainGate{modules: map[string]bool{}, forward: forward, logf: logf, placed: delivery.NewPlaced(), + g := &ToolchainGate{modules: map[string]bool{}, forward: forward, logf: logf, placed: meshmoduleassignerclient.NewPlaced(), raw: map[string]map[string][]byte{}, left: map[string]string{}} for _, m := range modules { g.modules[m] = true @@ -82,7 +83,7 @@ func (g *ToolchainGate) Placement(key string, value []byte, deleted bool, at tim was := g.placed.Seeded() node, changed, err := g.placed.Put(key, value, deleted, at) if err != nil { - g.logf("%s: %v (issue 1940)", delivery.PlacementBucket, err) + g.logf("%s: %v (issue 1940)", PlacementBucket, err) } if !g.known { return false @@ -135,7 +136,7 @@ func (g *ToolchainGate) again(node string, modules []string) bool { continue } for _, k := range slices.Sorted(maps.Keys(g.raw[module])) { - if n, _, err := delivery.ParseToolchainKey(k); err == nil && n == node { + if n, _, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(k); ok && n == node { changed = g.pass(module, k) || changed } } @@ -150,9 +151,9 @@ func (g *ToolchainGate) pass(module, key string) bool { if !held { return g.forward(module, key, nil, true) } - node, _, err := delivery.ParseToolchainKey(key) + node, _, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(key) why := "" - if err == nil && g.known && g.placed.Seeded() { + if ok && g.known && g.placed.Seeded() { why = g.leftOut(module, node, value) } if why == "" { @@ -161,7 +162,7 @@ func (g *ToolchainGate) pass(module, key string) bool { } if g.left[module+"/"+key] != why { g.left[module+"/"+key] = why - g.logf("%s.%s is left out of the agreed toolchain: %s (issue 1940)", delivery.ToolchainsBucket(module), key, why) + g.logf("%s.%s is left out of the agreed toolchain: %s (issue 1940)", ToolchainsBucket(module), key, why) } return g.forward(module, key, nil, true) } @@ -174,8 +175,8 @@ func (g *ToolchainGate) leftOut(module, node string, value []byte) string { if !placed { return fmt.Sprintf("the catalogue does not place %s on %s, so it is no builder", module, node) } - tc, err := delivery.DecodeToolchain(value) - if err == nil && tc.At.Before(since) { + tc, err := nodebuildgo.DecodeToolchainsByNodeLanguageValue(value) + if err == nil && tc.At != nil && tc.At.Before(since) { return fmt.Sprintf("it was written at %s, before %s was placed on %s at %s: it counts once %s writes it again", tc.At.UTC().Format(time.RFC3339), module, node, since.UTC().Format(time.RFC3339), node) } @@ -188,14 +189,14 @@ func (g *ToolchainGate) leftOut(module, node string, value []byte) string { // stops for it stops every delivery (issue 1945). An error of the watch after that ends on the channel. func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *ToolchainGate, changed func(context.Context), logf func(string, ...any)) (<-chan error, error) { errs := make(chan error, 1) - kv, err := js.KeyValue(ctx, delivery.PlacementBucket) + kv, err := js.KeyValue(ctx, PlacementBucket) if err != nil { - logf("the state %s cannot be read, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", delivery.PlacementBucket, err) + logf("the state %s cannot be read, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", PlacementBucket, err) return errs, nil } w, err := kv.WatchAll(ctx, jetstream.IncludeHistory()) if err != nil { - logf("the state %s cannot be watched, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", delivery.PlacementBucket, err) + logf("the state %s cannot be watched, so every node's toolchain report counts, a build node's the catalogue does not place too (issue 1940): %v", PlacementBucket, err) return errs, nil } put := func(e jetstream.KeyValueEntry) bool { @@ -208,7 +209,7 @@ func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *Toolcha return errs, ctx.Err() case e, ok := <-w.Updates(): if !ok { - return errs, errors.New("the watch of " + delivery.PlacementBucket + " ended before it settled") + return errs, errors.New("the watch of " + PlacementBucket + " ended before it settled") } if e == nil { settled = true @@ -232,7 +233,7 @@ func FollowPlacement(ctx context.Context, js process.BucketOpener, gate *Toolcha return case e, ok := <-w.Updates(): if !ok { - errs <- errors.New("the watch of " + delivery.PlacementBucket + " ended") + errs <- errors.New("the watch of " + PlacementBucket + " ended") return } if e != nil && put(e) { diff --git a/modules/mesh-delivery-builder/buildrun/standing_test.go b/modules/mesh-delivery-builder/buildrun/standing_test.go index 6a55ef4b..47093903 100644 --- a/modules/mesh-delivery-builder/buildrun/standing_test.go +++ b/modules/mesh-delivery-builder/buildrun/standing_test.go @@ -3,6 +3,7 @@ package buildrun import ( "bytes" "context" + "encoding/json" "errors" "fmt" "log" @@ -12,16 +13,16 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "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/seats/nodebuildgo" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) func gateReport(t *testing.T, node, version string, at time.Time) (string, []byte) { t.Helper() - key, err := delivery.ToolchainKey(node, "typescript") - if err != nil { - t.Fatal(err) - } - value, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: at}) + key := nodebuildgo.ToolchainsByNodeLanguageKey(node, "typescript") + value, err := json.Marshal(nodebuildgo.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: &at}) if err != nil { t.Fatal(err) } @@ -31,27 +32,27 @@ func gateReport(t *testing.T, node, version string, at time.Time) (string, []byt // placing is a value of the state `placement` placing modules on node. func placing(t *testing.T, node string, modules ...string) (string, []byte) { t.Helper() - 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}) } - values, err := delivery.PlacementValues(p) + values, err := meshmoduleassignerclient.PlacementValues(p) if err != nil { t.Fatal(err) } - key, _ := delivery.PlacementNodeKey(node) + key, _ := meshmoduleassignerclient.NodeKey(node) return key, values[key] } // placingLegacy is placing in the form before, `placement.` (research 072 row 37). func placingLegacy(t *testing.T, node string, modules ...string) (string, []byte) { t.Helper() - 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}) } - key, _ := delivery.LegacyPlacementKey(node) - value, err := delivery.EncodePlacement(p) + key, _ := meshmoduleassignerclient.LegacyKey(node) + value, err := meshmoduleassignerclient.EncodePlacement(p) if err != nil { t.Fatal(err) } @@ -59,7 +60,7 @@ func placingLegacy(t *testing.T, node string, modules ...string) (string, []byte } func agreedOf(set *ToolchainSet) (string, string) { - tc, none := delivery.AgreedToolchain(set.Entries("typescript")) + tc, none := buildstep.AgreedToolchain(set.Entries("typescript")) return tc.Version, none } @@ -91,7 +92,7 @@ func TestToolchainGate(t *testing.T) { gate, set, said := start(t, true) key, value := gateReport(t, "ace", "v26.12.0", old) gate.Toolchain(NodeBuildModule, key, value, false) - if a := delivery.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out + if a := buildstep.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out t.Fatalf("before the placement is settled every report counts; got %+v", a) } k, v := placing(t, "ace", GoBuildModule) // another build module on ace: no matter @@ -111,7 +112,7 @@ func TestToolchainGate(t *testing.T) { // Placed on ace with a report written since: it counts, and the builders really disagree. k, v = placing(t, "ace", GoBuildModule, NodeBuildModule) gate.Placement(k, v, false, old.Add(-time.Minute)) - if a := delivery.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" { + if a := buildstep.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" { t.Fatalf("builders that run different versions: the majority is agreed and ace sits out; got %+v", a) } // Taken off ace again (its key deleted): its report goes again. @@ -188,7 +189,7 @@ func TestToolchainGate(t *testing.T) { }) t.Run("the history: a module in the first revision heard of a key stands since before it", func(t *testing.T) { - p := delivery.NewPlaced() + p := meshmoduleassignerclient.NewPlaced() // ace in the form before, then converted: its module stands since before, the legacy key read no more. k, v := placingLegacy(t, "ace", NodeBuildModule) p.Put(k, v, false, old) diff --git a/modules/mesh-delivery-builder/buildrun/steps.go b/modules/mesh-delivery-builder/buildrun/steps.go index f08e0026..37e9bed8 100644 --- a/modules/mesh-delivery-builder/buildrun/steps.go +++ b/modules/mesh-delivery-builder/buildrun/steps.go @@ -6,7 +6,7 @@ import ( "fmt" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // Caller calls a tool of a module on one named machine (mesh-sdk process.Conn.CallOn; ADR 0469 clause 3). @@ -18,7 +18,7 @@ type Caller interface { // (ADR 0469 clause 2) once its state is not running. It is a watch of that key, not a poll; it returns when the step // ends or ctx does. type Steps interface { - Wait(ctx context.Context, module, key string) (delivery.Step, error) + Wait(ctx context.Context, module, key string) (buildstep.Step, error) } // Runner runs a build module's long steps: the call answers at once with the step's key, and the result is read from that @@ -48,7 +48,7 @@ func (r Runner) Run(ctx context.Context, module, node, tool string, args map[str if answer.Key == "" { return nil, fmt.Errorf("%s on %s answered no step key: %s", tool, node, raw) } - if n, _, err := delivery.ParseStepKey(answer.Key); err != nil || n != node { + if n, _, err := buildstep.ParseStepKey(answer.Key); err != nil || n != node { return nil, fmt.Errorf("%s on %s answered the step key %q, which is not a key of that node", tool, node, answer.Key) } wait, cancel := context.WithTimeoutCause(ctx, r.Bound, fmt.Errorf("the step %s did not end within %s", answer.Key, r.Bound)) @@ -60,7 +60,7 @@ func (r Runner) Run(ctx context.Context, module, node, tool string, args map[str } return nil, fmt.Errorf("%s on %s: %w", tool, node, err) } - if step.State != delivery.StepPassed { + if step.State != buildstep.StepPassed { return nil, fmt.Errorf("%s on %s: the step %s %s: %s", tool, node, answer.Key, step.State, step.Reason) } return step.Result, nil diff --git a/modules/mesh-delivery-builder/buildrun/steps_test.go b/modules/mesh-delivery-builder/buildrun/steps_test.go index fe1d32e0..c94a6314 100644 --- a/modules/mesh-delivery-builder/buildrun/steps_test.go +++ b/modules/mesh-delivery-builder/buildrun/steps_test.go @@ -9,7 +9,7 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) type call struct { @@ -35,12 +35,12 @@ func (f *fakeCaller) CallOn(node, key string, body any) (json.RawMessage, error) // fakeSteps holds the steps values by `/`; Wait on a key it does not hold blocks until the context ends. type fakeSteps struct { mu sync.Mutex - steps map[string]delivery.Step + steps map[string]buildstep.Step // during, when set, is called as a wait starts and the function it answers as the wait ends. during func(key string) func() } -func (f *fakeSteps) Wait(ctx context.Context, module, key string) (delivery.Step, error) { +func (f *fakeSteps) Wait(ctx context.Context, module, key string) (buildstep.Step, error) { f.mu.Lock() s, ok := f.steps[module+"/"+key] during := f.during @@ -52,11 +52,11 @@ func (f *fakeSteps) Wait(ctx context.Context, module, key string) (delivery.Step return s, nil } <-ctx.Done() - return delivery.Step{}, context.Cause(ctx) + return buildstep.Step{}, context.Cause(ctx) } -func stepAt(node, id, state, reason string, result string) delivery.Step { - s := delivery.Step{ID: id, Tool: "build_go_fetch", Node: node, State: state, Reason: reason} +func stepAt(node, id, state, reason string, result string) buildstep.Step { + s := buildstep.Step{ID: id, Tool: "build_go_fetch", Node: node, State: state, Reason: reason} if result != "" { s.Result = json.RawMessage(result) } @@ -65,7 +65,7 @@ func stepAt(node, id, state, reason string, result string) delivery.Step { func runCallsTheToolOnTheNodeAndReadsTheStepsKey(t *testing.T) { // 1607/AC5 caller := &fakeCaller{answer: `{"step":"abc1","key":"ace.abc1","state":"running"}`} - steps := &fakeSteps{steps: map[string]delivery.Step{"mesh-build-go/ace.abc1": stepAt("ace", "abc1", delivery.StepPassed, "", `{"workspace":"w1","commit":"c"}`)}} + steps := &fakeSteps{steps: map[string]buildstep.Step{"mesh-build-go/ace.abc1": stepAt("ace", "abc1", buildstep.StepPassed, "", `{"workspace":"w1","commit":"c"}`)}} res, err := Runner{Caller: caller, Steps: steps, Bound: time.Second}.Run(context.Background(), "mesh-build-go", "ace", "build_go_fetch", map[string]any{"repository": "novox/r", "commit": "c"}) if err != nil { t.Fatal(err) @@ -80,9 +80,9 @@ func runCallsTheToolOnTheNodeAndReadsTheStepsKey(t *testing.T) { // 1607/AC5 } func runFailsOnAFailedOrRefusedStepNamingItsReason(t *testing.T) { // 1607/AC5 - for _, state := range []string{delivery.StepFailed, delivery.StepRefused} { + for _, state := range []string{buildstep.StepFailed, buildstep.StepRefused} { caller := &fakeCaller{answer: `{"step":"abc1","key":"ace.abc1","state":"running"}`} - steps := &fakeSteps{steps: map[string]delivery.Step{"mesh-build-go/ace.abc1": stepAt("ace", "abc1", state, "go is go1.26.0, the recipe builds with go1.27.1", "")}} + steps := &fakeSteps{steps: map[string]buildstep.Step{"mesh-build-go/ace.abc1": stepAt("ace", "abc1", state, "go is go1.26.0, the recipe builds with go1.27.1", "")}} _, err := Runner{Caller: caller, Steps: steps, Bound: time.Second}.Run(context.Background(), "mesh-build-go", "ace", "build_go_build", nil) if err == nil || !strings.Contains(err.Error(), "go1.27.1") || !strings.Contains(err.Error(), state) || !strings.Contains(err.Error(), "ace") { t.Errorf("%s: error = %v, want the node, the state and the reason", state, err) diff --git a/modules/mesh-delivery-builder/buildrun/toolchains.go b/modules/mesh-delivery-builder/buildrun/toolchains.go index 61704252..acd8089b 100644 --- a/modules/mesh-delivery-builder/buildrun/toolchains.go +++ b/modules/mesh-delivery-builder/buildrun/toolchains.go @@ -3,7 +3,8 @@ package buildrun import ( "sync" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // LanguageOf is the language each build module's toolchains state holds (the indexer's ToolchainModules, reversed). @@ -13,7 +14,7 @@ var LanguageOf = map[string]string{GoBuildModule: "go", NodeBuildModule: "typesc // put them in. Safe for use from the watches and from a build at once. type ToolchainSet struct { mu sync.Mutex - by map[string]map[string]delivery.Toolchain // language -> node -> entry + by map[string]map[string]nodebuildgo.ToolchainsByNodeLanguageValue // language -> node -> entry } // Put takes a change of a build module's toolchains bucket in: a put of a node's entry (key .) or its @@ -24,8 +25,8 @@ func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool if !ok { return false } - node, keyLanguage, err := delivery.ParseToolchainKey(key) - if err != nil || keyLanguage != language { + node, keyLanguage, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(key) + if !ok || keyLanguage != language { return false } s.mu.Lock() @@ -37,15 +38,15 @@ func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool delete(s.by[language], node) return true } - tc, err := delivery.DecodeToolchain(value) + tc, err := nodebuildgo.DecodeToolchainsByNodeLanguageValue(value) if err != nil || tc.Language != language { return false } if s.by == nil { - s.by = map[string]map[string]delivery.Toolchain{} + s.by = map[string]map[string]nodebuildgo.ToolchainsByNodeLanguageValue{} } if s.by[language] == nil { - s.by[language] = map[string]delivery.Toolchain{} + s.by[language] = map[string]nodebuildgo.ToolchainsByNodeLanguageValue{} } cur, had := s.by[language][node] s.by[language][node] = tc @@ -53,12 +54,12 @@ func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool } // Entries are the nodes' entries of a language, in no particular order. -func (s *ToolchainSet) Entries(language string) []delivery.NodeToolchain { +func (s *ToolchainSet) Entries(language string) []buildstep.NodeToolchain { s.mu.Lock() defer s.mu.Unlock() - var out []delivery.NodeToolchain + var out []buildstep.NodeToolchain for node, tc := range s.by[language] { - out = append(out, delivery.NodeToolchain{Node: node, Toolchain: tc}) + out = append(out, buildstep.NodeToolchain{Node: node, Toolchain: buildstep.Toolchain{Language: tc.Language, Version: tc.Version, Recipe: tc.Recipe}}) } return out } diff --git a/modules/mesh-delivery-builder/buildrun/toolchains_test.go b/modules/mesh-delivery-builder/buildrun/toolchains_test.go index 4df0d38d..ebf796ac 100644 --- a/modules/mesh-delivery-builder/buildrun/toolchains_test.go +++ b/modules/mesh-delivery-builder/buildrun/toolchains_test.go @@ -1,15 +1,17 @@ package buildrun import ( + "encoding/json" "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" ) func tcValue(t *testing.T, language, version string) []byte { t.Helper() - b, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: "1", At: time.Now()}) + now := time.Now() + b, err := json.Marshal(nodebuildgo.Toolchain{Language: language, Version: version, Recipe: "1", At: &now}) if err != nil { t.Fatal(err) } From 989d41f3cca9c4ee9d11f07732645f7fd330eff0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:19:41 +0100 Subject: [PATCH 03/11] =?UTF-8?q?Delivery=20SDK:=20the=20agreement=20over?= =?UTF-8?q?=20toolchain=20entries=20uses=20the=20neutral=20Toolchain=20val?= =?UTF-8?q?ue=20of=20the=20build=20seats'=20state=20(B2g's),=20not=20a=20s?= =?UTF-8?q?econd=20type=20(ADR=200634=20=C2=A71,=20=C2=A72;=20issue=201966?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- sdk/buildstep/toolchains.go | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/sdk/buildstep/toolchains.go b/sdk/buildstep/toolchains.go index 6dc878f7..00d49555 100644 --- a/sdk/buildstep/toolchains.go +++ b/sdk/buildstep/toolchains.go @@ -13,15 +13,7 @@ import ( // ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain. const ToolchainNoReport = "toolchain not yet reported" -// Toolchain is what a toolchain is identified by: the language, the installed version and the build module's recipe -// version. It has the fields of the mesh-catalog client's Toolchain2, which a caller converts it to. -type Toolchain struct { - Language string - Version string - Recipe string -} - -// NodeToolchain is one node's entry of a language, as read from a build seat's state toolchains. +// NodeToolchain is one node's entry of a language, as read from a build seat's state toolchains (the entry is Toolchain). type NodeToolchain struct { Node string Toolchain @@ -35,7 +27,8 @@ type SittingOut struct { Recipe string } -// Agreement is what the nodes' entries of one language come to. Toolchain is the agreed one, set when Wait is empty. +// Agreement is what the nodes' entries of one language come to. Toolchain is the agreed one (its language, version and +// recipe), set when Wait is empty. // Wait is why nothing is agreed: "toolchain not yet reported" with no entries, or "builders disagree" naming each version // and recipe with the nodes reporting it, when no version has a strict majority. SittingOut lists, sorted by node, the // nodes that report another version than the agreed one. @@ -52,14 +45,15 @@ func ToolchainAgreement(entries []NodeToolchain) Agreement { if len(entries) == 0 { return Agreement{Wait: ToolchainNoReport} } - groups := map[Toolchain][]string{} + type identity struct{ Language, Version, Recipe string } + groups := map[identity][]string{} for _, e := range entries { - k := Toolchain{Language: e.Language, Version: e.Version, Recipe: e.Recipe} + k := identity{e.Language, e.Version, e.Recipe} groups[k] = append(groups[k], e.Node) } for k, nodes := range groups { if 2*len(nodes) > len(entries) { - a := Agreement{Toolchain: k} + a := Agreement{Toolchain: Toolchain{Language: k.Language, Version: k.Version, Recipe: k.Recipe}} for _, e := range entries { if e.Version != k.Version || e.Recipe != k.Recipe { a.SittingOut = append(a.SittingOut, SittingOut{Node: e.Node, Version: e.Version, Recipe: e.Recipe}) From 0f5f8bf4c8764b71b225fd0898bde5990ea0093a Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:19:55 +0100 Subject: [PATCH 04/11] =?UTF-8?q?Builder=20runner=20reads=20a=20node's=20t?= =?UTF-8?q?oolchains=20through=20the=20Delivery=20SDK's=20neutral=20toolch?= =?UTF-8?q?ain=20value=20and=20key=20(B2g),=20which=20gains=20the=20key's?= =?UTF-8?q?=20parse=20(ADR=200634=20=C2=A71,=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-delivery-builder/buildrun/names.go | 3 ++- .../mesh-delivery-builder/buildrun/standing.go | 12 ++++++------ .../buildrun/standing_test.go | 6 ++---- .../mesh-delivery-builder/buildrun/toolchains.go | 15 +++++++-------- .../buildrun/toolchains_test.go | 5 ++--- sdk/buildstep/toolchains.go | 12 ++++++++++++ sdk/buildstep/toolchains_test.go | 15 +++++++++++++++ 7 files changed, 46 insertions(+), 22 deletions(-) diff --git a/modules/mesh-delivery-builder/buildrun/names.go b/modules/mesh-delivery-builder/buildrun/names.go index 770324f8..79def2fb 100644 --- a/modules/mesh-delivery-builder/buildrun/names.go +++ b/modules/mesh-delivery-builder/buildrun/names.go @@ -5,7 +5,8 @@ package buildrun // names a holder (novox/hq ADR 0625 §4, ADR 0634 §1). const ( // PlacementBucket is the assigner's state placement, which the toolchain gate reads for the placing of a build module. - PlacementBucket = "mesh-delivery-assigner_placement" + AssignerModule = "mesh-delivery-assigner" + PlacementBucket = AssignerModule + "_placement" ) // StepsBucket is the bucket of a build module's state steps. diff --git a/modules/mesh-delivery-builder/buildrun/standing.go b/modules/mesh-delivery-builder/buildrun/standing.go index 19913896..adc4adb0 100644 --- a/modules/mesh-delivery-builder/buildrun/standing.go +++ b/modules/mesh-delivery-builder/buildrun/standing.go @@ -13,7 +13,7 @@ import ( "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" - "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -136,7 +136,7 @@ func (g *ToolchainGate) again(node string, modules []string) bool { continue } for _, k := range slices.Sorted(maps.Keys(g.raw[module])) { - if n, _, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(k); ok && n == node { + if n, _, err := buildstep.ParseToolchainKey(k); err == nil && n == node { changed = g.pass(module, k) || changed } } @@ -151,9 +151,9 @@ func (g *ToolchainGate) pass(module, key string) bool { if !held { return g.forward(module, key, nil, true) } - node, _, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(key) + node, _, err := buildstep.ParseToolchainKey(key) why := "" - if ok && g.known && g.placed.Seeded() { + if err == nil && g.known && g.placed.Seeded() { why = g.leftOut(module, node, value) } if why == "" { @@ -175,8 +175,8 @@ func (g *ToolchainGate) leftOut(module, node string, value []byte) string { if !placed { return fmt.Sprintf("the catalogue does not place %s on %s, so it is no builder", module, node) } - tc, err := nodebuildgo.DecodeToolchainsByNodeLanguageValue(value) - if err == nil && tc.At != nil && tc.At.Before(since) { + tc, err := buildstep.DecodeToolchain(value) + if err == nil && tc.At.Before(since) { return fmt.Sprintf("it was written at %s, before %s was placed on %s at %s: it counts once %s writes it again", tc.At.UTC().Format(time.RFC3339), module, node, since.UTC().Format(time.RFC3339), node) } diff --git a/modules/mesh-delivery-builder/buildrun/standing_test.go b/modules/mesh-delivery-builder/buildrun/standing_test.go index 47093903..0ac7afeb 100644 --- a/modules/mesh-delivery-builder/buildrun/standing_test.go +++ b/modules/mesh-delivery-builder/buildrun/standing_test.go @@ -3,7 +3,6 @@ package buildrun import ( "bytes" "context" - "encoding/json" "errors" "fmt" "log" @@ -15,14 +14,13 @@ import ( "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/seats/nodebuildgo" "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) func gateReport(t *testing.T, node, version string, at time.Time) (string, []byte) { t.Helper() - key := nodebuildgo.ToolchainsByNodeLanguageKey(node, "typescript") - value, err := json.Marshal(nodebuildgo.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: &at}) + key, _ := buildstep.ToolchainKey(node, "typescript") + value, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: at}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-builder/buildrun/toolchains.go b/modules/mesh-delivery-builder/buildrun/toolchains.go index acd8089b..a37200b9 100644 --- a/modules/mesh-delivery-builder/buildrun/toolchains.go +++ b/modules/mesh-delivery-builder/buildrun/toolchains.go @@ -3,7 +3,6 @@ package buildrun import ( "sync" - "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) @@ -14,7 +13,7 @@ var LanguageOf = map[string]string{GoBuildModule: "go", NodeBuildModule: "typesc // put them in. Safe for use from the watches and from a build at once. type ToolchainSet struct { mu sync.Mutex - by map[string]map[string]nodebuildgo.ToolchainsByNodeLanguageValue // language -> node -> entry + by map[string]map[string]buildstep.Toolchain // language -> node -> entry } // Put takes a change of a build module's toolchains bucket in: a put of a node's entry (key .) or its @@ -25,8 +24,8 @@ func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool if !ok { return false } - node, keyLanguage, ok := nodebuildgo.ParseToolchainsByNodeLanguageKey(key) - if !ok || keyLanguage != language { + node, keyLanguage, err := buildstep.ParseToolchainKey(key) + if err != nil || keyLanguage != language { return false } s.mu.Lock() @@ -38,15 +37,15 @@ func (s *ToolchainSet) Put(module, key string, value []byte, deleted bool) bool delete(s.by[language], node) return true } - tc, err := nodebuildgo.DecodeToolchainsByNodeLanguageValue(value) + tc, err := buildstep.DecodeToolchain(value) if err != nil || tc.Language != language { return false } if s.by == nil { - s.by = map[string]map[string]nodebuildgo.ToolchainsByNodeLanguageValue{} + s.by = map[string]map[string]buildstep.Toolchain{} } if s.by[language] == nil { - s.by[language] = map[string]nodebuildgo.ToolchainsByNodeLanguageValue{} + s.by[language] = map[string]buildstep.Toolchain{} } cur, had := s.by[language][node] s.by[language][node] = tc @@ -59,7 +58,7 @@ func (s *ToolchainSet) Entries(language string) []buildstep.NodeToolchain { defer s.mu.Unlock() var out []buildstep.NodeToolchain for node, tc := range s.by[language] { - out = append(out, buildstep.NodeToolchain{Node: node, Toolchain: buildstep.Toolchain{Language: tc.Language, Version: tc.Version, Recipe: tc.Recipe}}) + out = append(out, buildstep.NodeToolchain{Node: node, Toolchain: tc}) } return out } diff --git a/modules/mesh-delivery-builder/buildrun/toolchains_test.go b/modules/mesh-delivery-builder/buildrun/toolchains_test.go index ebf796ac..72aeab92 100644 --- a/modules/mesh-delivery-builder/buildrun/toolchains_test.go +++ b/modules/mesh-delivery-builder/buildrun/toolchains_test.go @@ -1,17 +1,16 @@ package buildrun import ( - "encoding/json" "testing" "time" - "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) func tcValue(t *testing.T, language, version string) []byte { t.Helper() now := time.Now() - b, err := json.Marshal(nodebuildgo.Toolchain{Language: language, Version: version, Recipe: "1", At: &now}) + b, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: language, Version: version, Recipe: "1", At: now}) if err != nil { t.Fatal(err) } diff --git a/sdk/buildstep/toolchains.go b/sdk/buildstep/toolchains.go index 00d49555..024bf935 100644 --- a/sdk/buildstep/toolchains.go +++ b/sdk/buildstep/toolchains.go @@ -77,3 +77,15 @@ func AgreedToolchain(entries []NodeToolchain) (tc Toolchain, none string) { a := ToolchainAgreement(entries) return a.Toolchain, a.Wait } + +// 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("buildstep: %q is not a toolchain key, .", key) + } + if _, err := ToolchainKey(node, language); err != nil { + return "", "", fmt.Errorf("buildstep: %q is not a toolchain key: %w", key, err) + } + return node, language, nil +} diff --git a/sdk/buildstep/toolchains_test.go b/sdk/buildstep/toolchains_test.go index a1f376ef..094c7903 100644 --- a/sdk/buildstep/toolchains_test.go +++ b/sdk/buildstep/toolchains_test.go @@ -56,3 +56,18 @@ func TestToolchainAgreement(t *testing.T) { } } } + +func TestToolchainKeyParsesBack(t *testing.T) { + key, err := ToolchainKey("ace", "go") + if err != nil { + t.Fatal(err) + } + if node, lang, err := ParseToolchainKey(key); err != nil || node != "ace" || lang != "go" { + t.Errorf("%q parsed to %q %q %v", key, node, lang, err) + } + for _, bad := range []string{"ace", "ace.rust", ".go", "a.b.go"} { + if _, _, err := ParseToolchainKey(bad); err == nil { + t.Errorf("%q parsed", bad) + } + } +} From 1af40279b122b0ff82ef2f4f05c2719b6f1de5eb Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:33:29 +0100 Subject: [PATCH 05/11] =?UTF-8?q?Builder:=20its=20candidates,=20modules,?= =?UTF-8?q?=20manifests,=20sources,=20steps=20and=20toolchains=20are=20the?= =?UTF-8?q?=20catalog's,=20the=20gate's=20and=20the=20build=20seats'=20typ?= =?UTF-8?q?es=20and=20the=20clients'=20codecs,=20its=20build=20ask=20the?= =?UTF-8?q?=20Delivery=20SDK's,=20its=20artifacts=20the=20forms'=20type;?= =?UTF-8?q?=20the=20holders=20and=20buckets=20it=20names=20are=20its=20own?= =?UTF-8?q?=20constants=20checked=20against=20its=20manifest;=20no=20go/de?= =?UTF-8?q?livery=20left=20(D-11;=20ADR=200634=20=C2=A71,=20=C2=A72,=20?= =?UTF-8?q?=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-delivery-builder/buildrun/go.mod | 1 - .../mesh-delivery-builder/builds/builds.go | 41 +-- .../cmd/mesh-delivery-builder-tools/main.go | 11 +- .../cmd/mesh-delivery-builder/branchend.go | 12 +- .../cmd/mesh-delivery-builder/bus.go | 8 +- .../cmd/mesh-delivery-builder/findings.go | 16 +- .../gone_integration_test.go | 20 +- .../cmd/mesh-delivery-builder/held.go | 43 +-- .../mesh-delivery-builder/integration_test.go | 245 +++++++-------- .../cmd/mesh-delivery-builder/main.go | 55 ++-- .../cmd/mesh-delivery-builder/names.go | 38 +++ .../cmd/mesh-delivery-builder/names_test.go | 50 ++++ .../cmd/mesh-delivery-builder/oldform.go | 8 +- .../cmd/mesh-delivery-builder/optional.go | 12 +- .../cmd/mesh-delivery-builder/reads.go | 4 +- .../cmd/mesh-delivery-builder/reasks.go | 12 +- .../cmd/mesh-delivery-builder/refused_test.go | 51 ++-- .../cmd/mesh-delivery-builder/seats.go | 8 +- .../cmd/mesh-delivery-builder/tagbounds.go | 6 +- .../cmd/mesh-delivery-builder/toolchains.go | 4 +- .../cmd/mesh-delivery-builder/versions.go | 18 +- modules/mesh-delivery-builder/go.mod | 21 +- .../internal/build/announce.go | 10 +- .../internal/build/announce_test.go | 10 +- .../internal/build/askagain.go | 21 +- .../internal/build/askfake_test.go | 27 +- .../internal/build/buildagain.go | 22 +- .../internal/build/candidates.go | 32 +- .../internal/build/candidates_test.go | 34 ++- .../internal/build/claim.go | 51 ++-- .../internal/build/claim_test.go | 38 +-- .../internal/build/fakes_test.go | 23 +- .../internal/build/fixtures_test.go | 17 +- .../internal/build/followups_test.go | 22 +- .../internal/build/gone_test.go | 24 +- .../internal/build/imports_test.go | 19 +- .../internal/build/numbers.go | 40 +-- .../internal/build/plan.go | 10 +- .../internal/build/plan_test.go | 12 +- .../internal/build/queue.go | 7 +- .../internal/build/r4e_test.go | 95 +++--- .../internal/build/round4_test.go | 20 +- .../internal/build/run.go | 216 +++++++------- .../internal/build/run_test.go | 282 +++++++++--------- .../internal/build/since.go | 18 +- .../internal/build/stuck.go | 27 +- .../internal/build/toolchains.go | 28 ++ .../internal/versions/again.go | 4 +- .../internal/versions/claim.go | 4 +- .../internal/versions/names.go | 16 +- .../internal/versions/oldform.go | 13 +- .../internal/versions/tools.go | 10 +- .../internal/versions/versions_test.go | 16 +- 53 files changed, 1007 insertions(+), 845 deletions(-) create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go create mode 100644 modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go create mode 100644 modules/mesh-delivery-builder/internal/build/toolchains.go diff --git a/modules/mesh-delivery-builder/buildrun/go.mod b/modules/mesh-delivery-builder/buildrun/go.mod index ede9fa15..9cb38ea8 100644 --- a/modules/mesh-delivery-builder/buildrun/go.mod +++ b/modules/mesh-delivery-builder/buildrun/go.mod @@ -4,7 +4,6 @@ go 1.26.0 require ( git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs 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-contracts/go/clients/nodeengineclient 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 diff --git a/modules/mesh-delivery-builder/builds/builds.go b/modules/mesh-delivery-builder/builds/builds.go index b52b676d..edabc9d9 100644 --- a/modules/mesh-delivery-builder/builds/builds.go +++ b/modules/mesh-delivery-builder/builds/builds.go @@ -16,6 +16,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry" "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" "git.novox.be/novox/mesh-delivery/sdk/buildstep" + "git.novox.be/novox/mesh-delivery/sdk/forms" "git.novox.be/novox/mesh-delivery/sdk/kept" "git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-sdk/go/keys" @@ -90,39 +91,15 @@ func ParseBuilderBuildKey(key string) (module, fp string, err error) { return module, fp, nil } -// BuiltArtifact is one artifact of a done build: its name in the manifest, its kind, its content digest (`sha256:` and -// 64 hex digits) and the reference that names where it is (novox/hq ADR 0498 clauses 1, 5 and 6; the lead's decision -// L1 of R4e), by kind: -// -// bundle, archive artifact-store:////blobs/ the blob pushed to the artifact store -// image, upstream //@ the image registry's reference, which -// artifact-store:///@ catalogue.Recorded keeps without its address -// package @ the version published from the trunk -// -// From the registry milestone a done key names the reference its seat answered (novox/hq ADR 0605 §9, design 51 *The -// done key names the reference the seat answered*): `//@`, the registry seat it -// came through (oci-artifact-registry for a bundle or an archive, container-image-registry for an image or an upstream -// copy), and `@` for a package as before. A key written in the older form keeps it; this reads both, and -// no build is built again for its form (seats.InArtifactStore reads both the same way). -// -// is Name and is Digest. A reference is empty for a candidate's image or package, which is never -// pushed or published (ADR 0498 clause 6), and in a key done before R4e; the controller delivers only from a key whose -// every artifact names one (Referenced, clause 8). A kind left empty (a key written before kinds) is not checked. -type BuiltArtifact struct { - Name string `json:"name"` - Kind string `json:"kind,omitempty"` - Digest string `json:"digest"` - Reference string `json:"reference,omitempty"` - // References are every form this artifact is named in, Reference among them (novox/hq ADR 0618, ADR 0624): a build - // keeps each form that a counted node-engine states, away nodes included, and Reference stays the form every one - // of them reads. A reader that does not know the field reads Reference alone. - References []string `json:"references,omitempty"` -} +// BuiltArtifact is one artifact of a done build: its name, kind, digest and the reference that names where it is, with every +// form it is named in (novox/hq ADR 0618, ADR 0624). The Delivery SDK's forms package holds the type, since the forms read it +// and forms cannot import this package. +type BuiltArtifact = forms.BuiltArtifact const artifactStore = "artifact-store://" // referenceProblem says what is wrong with a's reference for its kind, or "". -func (a BuiltArtifact) referenceProblem() string { +func referenceProblem(a BuiltArtifact) string { r := a.Reference if r == "" || a.Kind == "" { return "" @@ -284,14 +261,14 @@ func (b BuilderBuild) check() error { if a.Digest == "" { return fmt.Errorf("builds: the artifact %q of the build has no digest", a.Name) } - if p := a.referenceProblem(); p != "" { + if p := referenceProblem(a); p != "" { return fmt.Errorf("builds: the reference %q of the %s %q %s", a.Reference, a.Kind, a.Name, p) } if len(a.References) > 0 && !slices.Contains(a.References, a.Reference) { return fmt.Errorf("builds: the references of the %s %q do not name its reference %q", a.Kind, a.Name, a.Reference) } for _, r := range a.References { - if p := (BuiltArtifact{Name: a.Name, Kind: a.Kind, Digest: a.Digest, Reference: r}).referenceProblem(); p != "" { + if p := referenceProblem(BuiltArtifact{Name: a.Name, Kind: a.Kind, Digest: a.Digest, Reference: r}); p != "" { return fmt.Errorf("builds: the reference %q of the %s %q %s", r, a.Kind, a.Name, p) } } @@ -319,7 +296,7 @@ func (b BuilderBuild) check() error { if b.State != BuildQueued { return fmt.Errorf("builds: a %s build waits for %q; only a queued key waits for a package", b.State, b.Waits) } - if p := (BuiltArtifact{Kind: "package", Reference: b.Waits}).referenceProblem(); p != "" { + if p := referenceProblem(BuiltArtifact{Kind: "package", Reference: b.Waits}); p != "" { return fmt.Errorf("builds: the package a key waits for, %q, %s", b.Waits, p) } } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go index 1858af75..acb6b77c 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder-tools/main.go @@ -16,8 +16,9 @@ import ( "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" 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/enginereads" @@ -192,7 +193,7 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q return map[string]any{"key": key, "revision": r.Revision, "asks": r.Asks, "answer": r.Answer, "next": build.StuckTool + " shows the key and its asks while they wait"}, nil }}, - {Name: delivery.BuildsSinceTool, + {Name: builds.BuildsSinceTool, Description: "Every build that ended since a time, oldest first, from the builder's log build-log (kept 30 days): per build its module, " + "fingerprint, source revision, repository, directory, commit and ref, the build node, when it ended, its outcome (done, failed or " + "queued), the manifest built, the bases it was built against and each artifact made with its digest and reference. more says the " + @@ -250,7 +251,7 @@ func Tools(r build.StuckReader, askAgain func(key string, value []byte) error, q language = build.LanguageOf(str(args, "seat")) } if language == "" { - language = delivery.AskGo + language = buildstep.AskGo } limit, _ := args["limit"].(float64) key, value, err := build.NewQueueAsk(language, int(limit), stdio.CallerOf(args), now()) @@ -406,7 +407,7 @@ func main() { } return e.Value, true, nil }} - tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(delivery.BuilderLog).Since, time.Now, vr, tags, tr, fr, ofr) + tools := Tools(r, put, askQueue(stdio.State(build.QueuesState), 25*time.Second), buildAgain, stdio.Log(builds.BuilderLog).Since, time.Now, vr, tags, tr, fr, ofr) // The builder's expiring keys (state bounds): what each bounds and when it ends. tools = append(tools, stdio.Tool{Name: versions.BoundsTool, Description: versions.BoundsDescription, Input: map[string]any{"type": "object", "properties": map[string]any{}}, @@ -419,7 +420,7 @@ func main() { tools = append(tools, enginereads.Tool(EngineReadsTool, "the builder", enginereads.KeptState(stdio.State("engine-reads")), true, time.Now)) // builder_first_views and builder_first_view_again: where the builder's first views of the indexer's and the assigner's // states stand, and asking one again by hand (ADR 0615, DLV-M1). - tools = append(tools, views.StatusTools(delivery.BuilderModule, "builder_first_views", "builder_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) + tools = append(tools, views.StatusTools(builds.BuilderModule, "builder_first_views", "builder_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) if err := stdio.Serve("", tools); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-builder-tools] %v\n", err) os.Exit(1) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go index a63c2785..bcfc6256 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/branchend.go @@ -9,7 +9,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/health" "mesh-delivery-builder/internal/build" @@ -20,14 +20,14 @@ import ( // branch's end (design 51 *Tagged versions*). const forgeStream = "FORGE" -// forgeBranchDeleted is the forge bridge's kind for a branch's end (delivery.ForgeBranchDeleted in the group's mesh-sdk). +// forgeBranchDeleted is the forge bridge's kind for a branch's end (forgeBranchDeleted in the group's mesh-sdk). const forgeBranchDeleted = "branch-deleted" // checkerAsksStream is the checker's family `asks` as the bus names it, and tagWantedSubject its event tag-wanted // (design 51 *Tagged versions*, DLV-C3): on the checker's own stream, so its subject is mesh.stream..event.*. const ( checkerAsksStream = "ASKS" - tagWantedSubject = "mesh.stream." + delivery.CheckerModule + ".event.tag-wanted" + tagWantedSubject = "mesh.stream." + checkerModule + ".event.tag-wanted" ) // forgeEnd is the part of a forge message the builder reads. @@ -41,10 +41,10 @@ type forgeEnd struct { // ended says whether the message ends a branch: merged, a pull closed, or branch-deleted. func (f forgeEnd) ended() bool { switch f.Kind { - case delivery.ForgeMerged, forgeBranchDeleted: + case forgemsg.ForgeMerged, forgeBranchDeleted: return f.Branch != "" - case delivery.ForgePull: - return f.Action == delivery.PullClosed && f.Branch != "" + case forgemsg.ForgePull: + return f.Action == forgemsg.PullClosed && f.Branch != "" } return false } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go index 4023faf2..34654335 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/bus.go @@ -7,8 +7,8 @@ import ( "github.com/nats-io/nats.go/jetstream" + "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" "mesh-delivery-builder/internal/build" ) @@ -111,7 +111,7 @@ func follow(ctx context.Context, w jetstream.KeyWatcher, each func(jetstream.Key type manifestReader struct{ kv jetstream.KeyValue } func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) { - key, err := delivery.ManifestsKey(digest) + key, err := meshcatalogclient.ManifestsKey(digest) if err != nil { return catalogue.Manifest{}, err } @@ -119,10 +119,10 @@ func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue. if err != nil { return catalogue.Manifest{}, fmt.Errorf("manifests.%s: %w", key, err) } - kept, err := delivery.DecodeManifest(e.Value()) + kept, err := meshcatalogclient.DecodeManifest(e.Value()) if err != nil { return catalogue.Manifest{}, err } // Decoded, never judged (novox/hq ADR 0578 §3): what a build needs of it is read, a field it does not know set aside. - return catalogue.Decode([]byte(kept.JSON)) + return catalogue.Decode([]byte(kept.Manifest)) } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go index 6c458d2c..4d2e8a42 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/findings.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/process" @@ -18,7 +18,7 @@ import ( // findingsBucket is the builder's findings (novox/hq ADR 0564 §2): what it sees wrong now, written at its raise and // deleted at its clear, which the condition keeper raises and clears. -var findingsBucket = health.FindingsBucket(delivery.BuilderModule) +var findingsBucket = health.FindingsBucket(builds.BuilderModule) // findings writes the builder's findings into its state, once bound; until then each is said in the log only. type findings struct { @@ -63,19 +63,19 @@ const BuildFailed = "build-failed" // buildEnded raises the module's build-failed for a trunk build that failed and clears it at a later done one; a // candidate's build (its ref named) is the check's to say. -func (f *findings) buildEnded(ctx context.Context, e delivery.BuildLogEntry) { - if e.Event != delivery.BuildEndedEvent || e.Ref != "" { +func (f *findings) buildEnded(ctx context.Context, e builds.BuildLogEntry) { + if e.Event != builds.BuildEndedEvent || e.Ref != "" { return } switch e.Outcome { - case delivery.BuildFailed: + case builds.BuildFailed: f.raise(ctx, finding{kind: BuildFailed, scope: health.ScopeModule, id: e.Module, summary: "the trunk build of " + e.Module + " at " + e.FP + " (commit " + e.Commit + ") failed", headline: "A module's trunk build failed", explanation: "The builder built " + e.Module + " from the trunk and the build failed, so its new version is not delivered.", resolved: "A later trunk build of " + e.Module + " is done.", needs: "builder_builds_since shows the build; builder_build_again builds it again."}) - case delivery.BuildDone: + case builds.BuildDone: f.clear(ctx, BuildFailed, health.ScopeModule, e.Module) } } @@ -100,7 +100,7 @@ func (f *findings) raise(ctx context.Context, fd finding) { } value, err := health.EncodeFinding(health.Finding{Key: key, Kind: fd.kind, Subject: health.Subject{Scope: fd.scope, ID: fd.id}, Severity: sev, Summary: fd.summary, Headline: fd.headline, Explanation: fd.explanation, Resolved: fd.resolved, Needs: fd.needs, - Evidence: []health.Evidence{{At: at, Said: fd.summary}}, Source: "builder." + fd.kind, Observer: delivery.BuilderModule, At: at}) + Evidence: []health.Evidence{{At: at, Said: fd.summary}}, Source: "builder." + fd.kind, Observer: builds.BuilderModule, At: at}) if err == nil { _, err = kv.Put(ctx, key, value) } @@ -135,7 +135,7 @@ func (f *findings) notHeard(ctx context.Context) heard.Writer { f.kv.Ready(func(kv jetstream.KeyValue) { do(kv) }) } } - return heard.Writer{Observer: delivery.BuilderModule, Logf: f.logf, Now: f.now, + return heard.Writer{Observer: builds.BuilderModule, Logf: f.logf, Now: f.now, Put: func(key string, value []byte) error { on(func(kv findingStore) { if _, err := kv.Put(ctx, key, value); err != nil { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go index ee342ee6..52be8bde 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/gone_integration_test.go @@ -13,8 +13,8 @@ import ( "github.com/nats-io/nats.go/jetstream" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // A done key whose blob the artifact store no longer holds is built again, by the builder whole against the bus @@ -78,10 +78,10 @@ func eventsConsumer(t *testing.T, b *bed, node string) { // doneKey writes builds.. done, naming one archive at reference, and answers its revision. func doneKey(t *testing.T, b *bed, module, manifest, reference, digest string) uint64 { t.Helper() - key, _ := delivery.BuilderBuildKey(module, fp) - data, err := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, By: "n2.mesh-delivery-builder", + key, _ := builds.BuilderBuildKey(module, fp) + data, err := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, By: "n2.mesh-delivery-builder", Manifest: manifest, Step: "built mesh-build-go on n2", - Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Digest: digest, Reference: reference}}}) + Artifacts: []builds.BuiltArtifact{{Name: "files", Kind: "archive", Digest: digest, Reference: reference}}}) if err != nil { t.Fatal(err) } @@ -94,7 +94,7 @@ func doneKey(t *testing.T, b *bed, module, manifest, reference, digest string) u func revisionOf(t *testing.T, b *bed, module string) uint64 { t.Helper() - key, _ := delivery.BuilderBuildKey(module, fp) + key, _ := builds.BuilderBuildKey(module, fp) e, err := b.builds.Get(b.ctx, key) if err != nil { t.Fatal(err) @@ -115,8 +115,8 @@ func TestADoneKeyWhoseBlobIsGoneIsBuiltAgainAtStart(t *testing.T) { h.serve(t, b) b.start(t, "n1", nil) - v := b.await(t, "ca-trust", func(v delivery.BuilderBuild) bool { - return v.State == delivery.BuildDone && len(v.Artifacts) == 1 && v.Artifacts[0].Digest == digestOf("files") + v := b.await(t, "ca-trust", func(v builds.BuilderBuild) bool { + return v.State == builds.BuildDone && len(v.Artifacts) == 1 && v.Artifacts[0].Digest == digestOf("files") }) if !strings.Contains(v.Step, "built again: the artifact store no longer held "+oldRef) { t.Errorf("the rebuilt key's step %q does not say why it was built again", v.Step) @@ -168,7 +168,7 @@ func TestArtifactsGoneLeadsTheBuilderToBuildTheKeyAgain(t *testing.T) { mu.Lock() collected = true mu.Unlock() - key, _ := delivery.BuilderBuildKey("ca-trust", fp) + key, _ := builds.BuilderBuildKey("ca-trust", fp) at := time.Now().UTC() body, err := json.Marshal(sweep.ArtifactsGone{Event: sweep.EventArtifactsGone, Module: "ca-trust", Key: key, Fingerprint: fp, Gone: []string{ref}, At: &at}) @@ -178,8 +178,8 @@ func TestArtifactsGoneLeadsTheBuilderToBuildTheKeyAgain(t *testing.T) { if _, err := b.js.Publish(b.ctx, sweep.SubjectArtifactsGone, body); err != nil { t.Fatal(err) } - v := b.await(t, "ca-trust", func(v delivery.BuilderBuild) bool { - return v.State == delivery.BuildDone && strings.Contains(v.Step, "built again") + v := b.await(t, "ca-trust", func(v builds.BuilderBuild) bool { + return v.State == builds.BuildDone && strings.Contains(v.Step, "built again") }) if got := revisionOf(t, b, "ca-trust"); got <= rev || b.fake.count("mesh-build-go.build_go_build") != 1 { t.Errorf("build = %+v at revision %d (was %d), %d builds: want built again once on the event", v, got, rev, diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/held.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/held.go index 369a245e..acdbd9fe 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/held.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/held.go @@ -10,7 +10,8 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "mesh-delivery-builder/internal/build" ) @@ -22,7 +23,7 @@ type heldOnTheBus struct{ js jetstream.JetStream } // Held is build.Builder's Held: a step key `.` in the build module's steps, or a message of the stream under // the ask's subject whose Nats-Msg-Id is the ask's id. -func (h heldOnTheBus) Held(ctx context.Context, a delivery.StepAsk) (bool, error) { +func (h heldOnTheBus) Held(ctx context.Context, a buildstep.StepAsk) (bool, error) { seen, err := h.seen(ctx, a, false) if err != nil { return false, err @@ -31,7 +32,7 @@ func (h heldOnTheBus) Held(ctx context.Context, a delivery.StepAsk) (bool, error } // Gone is build.Builder's Gone: why the ask can no longer be answered (build.AskSeen.Why), or "". -func (h heldOnTheBus) Gone(ctx context.Context, a delivery.StepAsk) (string, error) { +func (h heldOnTheBus) Gone(ctx context.Context, a buildstep.StepAsk) (string, error) { seen, err := h.seen(ctx, a, true) if err != nil { return "", err @@ -48,17 +49,17 @@ const ( // seen reads what the bus holds of the ask; with later, the ask asked last that a build node took too (AskSeen.Last), when // the ask is on its stream. -func (h heldOnTheBus) seen(ctx context.Context, a delivery.StepAsk, later bool) (build.AskSeen, error) { +func (h heldOnTheBus) seen(ctx context.Context, a buildstep.StepAsk, later bool) (build.AskSeen, error) { seen := build.AskSeen{Kind: a.Kind, At: build.AskedAt(a.ID)} - module, err := delivery.BuildModuleOf(a.Language) + module, err := buildstep.BuildModuleOf(a.Language) if err != nil { return seen, err } - steps, err := h.js.KeyValue(ctx, delivery.StepsBucket(module)) + steps, err := h.js.KeyValue(ctx, buildrun.StepsBucket(module)) if err != nil { - return seen, fmt.Errorf("the bucket %s: %w", delivery.StepsBucket(module), err) + return seen, fmt.Errorf("the bucket %s: %w", buildrun.StepsBucket(module), err) } - pattern, err := delivery.AskStepWatch(a.ID) + pattern, err := buildstep.AskStepWatch(a.ID) if err != nil { return seen, err } @@ -114,13 +115,13 @@ func (h heldOnTheBus) seen(ctx context.Context, a delivery.StepAsk, later bool) // stream is the build seat's stream of language, its name and the subject of kind on it. func (h heldOnTheBus) stream(ctx context.Context, language, kind string) (jetstream.Stream, string, string, error) { - subject, err := delivery.AskSubject(language, kind) + subject, err := buildstep.AskSubject(language, kind) if err != nil { return nil, "", "", err } - // The seat's stream by its name (delivery.AskStream), as the broker grants the builder its information and its + // The seat's stream by its name (buildstep.AskStream), as the broker grants the builder its information and its // messages by sequence and nothing wider (issue 1910). - name, err := delivery.AskStream(language) + name, err := buildstep.AskStream(language) if err != nil { return nil, "", "", err } @@ -145,7 +146,7 @@ func onSubject(ctx context.Context, stream jetstream.Stream, subject string, eac return err } q := build.Queued{Seq: m.Sequence, Ask: m.Header.Get(jetstream.MsgIDHeader), PutAt: m.Time} - if _, kind, err := delivery.ParseAskSubject(m.Subject); err == nil { + if _, kind, err := buildstep.ParseAskSubject(m.Subject); err == nil { q.Kind = kind } if q.At = build.AskedAt(q.Ask); q.At.IsZero() { @@ -185,7 +186,7 @@ func stepState(ctx context.Context, steps jetstream.KeyValue, keys []string) str if err != nil { continue } - if s, err := delivery.DecodeStep(e.Value()); err == nil { + if s, err := buildstep.DecodeStep(e.Value()); err == nil { return s.State } } @@ -214,7 +215,7 @@ func lastTaken(ctx context.Context, steps jetstream.KeyValue, taken map[string][ if err != nil { continue } - s, err := delivery.DecodeStep(e.Value()) + s, err := buildstep.DecodeStep(e.Value()) if err != nil { continue } @@ -230,7 +231,7 @@ func lastTaken(ctx context.Context, steps jetstream.KeyValue, taken map[string][ // Drop is build.Dropper's Drop: it removes the ask's message from its build seat's stream, found by its Nats-Msg-Id on its // subject, when the stream still holds it. The builder's account must be granted $JS.API.STREAM.MSG.DELETE.. -func (h heldOnTheBus) Drop(ctx context.Context, a delivery.StepAsk) error { +func (h heldOnTheBus) Drop(ctx context.Context, a buildstep.StepAsk) error { stream, name, subject, err := h.stream(ctx, a.Language, a.Kind) if errors.Is(err, jetstream.ErrStreamNotFound) { return nil @@ -258,11 +259,11 @@ func (h heldOnTheBus) Drop(ctx context.Context, a delivery.StepAsk) error { // every kind, oldest first and at most limit of each kind, each with its step's state and whether it still waits. func (h heldOnTheBus) Queue(ctx context.Context, language string, limit int) (build.SeatQueue, error) { q := build.SeatQueue{Language: language, Note: build.QueueNote} - module, err := delivery.BuildModuleOf(language) + module, err := buildstep.BuildModuleOf(language) if err != nil { return q, err } - stream, name, _, err := h.stream(ctx, language, delivery.AskDelivery) + stream, name, _, err := h.stream(ctx, language, buildstep.AskDelivery) q.Stream = name if errors.Is(err, jetstream.ErrStreamNotFound) { q.Note = name + " does not exist" @@ -276,16 +277,16 @@ func (h heldOnTheBus) Queue(ctx context.Context, language string, limit int) (bu return q, err } q.Messages, q.FirstSeq, q.LastSeq = info.State.Msgs, info.State.FirstSeq, info.State.LastSeq - steps, err := h.js.KeyValue(ctx, delivery.StepsBucket(module)) + steps, err := h.js.KeyValue(ctx, buildrun.StepsBucket(module)) if err != nil { - return q, fmt.Errorf("the bucket %s: %w", delivery.StepsBucket(module), err) + return q, fmt.Errorf("the bucket %s: %w", buildrun.StepsBucket(module), err) } taken, err := stepKeys(ctx, steps) if err != nil { return q, err } - for _, kind := range delivery.AskKinds { - subject, _ := delivery.AskSubject(language, kind) + for _, kind := range buildstep.AskKinds { + subject, _ := buildstep.AskSubject(language, kind) var ahead []build.Queued if err := onSubject(ctx, stream, subject, func(m build.Queued) bool { ahead = append(ahead, m) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go index 882a56a8..92200937 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/integration_test.go @@ -26,13 +26,20 @@ import ( // The build runner the build modules pull and run asks with: mesh-build-go's own package (novox/hq ADR 0593 §1), // another repository's, by its published version, so the builder is tested against the real pulling code. runner "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" - "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" - "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" - sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" + "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/gopackageregistry" + "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/ociartifactregistry" + "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" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/buscompose" - "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/process" "git.novox.be/novox/mesh-sdk/go/views/viewstest" @@ -139,7 +146,7 @@ func (f *fakeBuildModules) step(module, name string, args map[string]any) (any, d := digestOf(fmt.Sprint(args["artifact"])) return json.RawMessage(`{"artifact":"` + fmt.Sprint(args["artifact"]) + `","digest":"` + d + `","reference":"registry.example/` + fmt.Sprint(args["module"]) + `/` + fmt.Sprint(args["artifact"]) + `@` + d + `","pushed":true}`), nil case tool != "build_image_build" && f.refusePush.Load(): - return nil, fmt.Errorf("%s%v: the artifact store answered 507 Insufficient Storage", delivery.NotStored, args["artifact"]) + return nil, fmt.Errorf("%s%v: the artifact store answered 507 Insufficient Storage", builds.NotStored, args["artifact"]) case tool != "build_image_build": // a bundle or an archive is pushed and its reference answered (ADR 0498 clause 1) d := digestOf(fmt.Sprint(args["artifact"])) return json.RawMessage(`{"digest":"` + d + `","reference":"artifact-store://` + filepath.Base(fmt.Sprint(args["module"])) + `/` + fmt.Sprint(args["artifact"]) + `/blobs/` + d + `"}`), nil @@ -153,7 +160,7 @@ func (f *fakeBuildModules) step(module, name string, args map[string]any) (any, // serve makes module's steps bucket and its build seat's stream and workers, and pulls its asks on nodes. func (f *fakeBuildModules) serve(t *testing.T, module string, nodes ...string) { - kv, err := f.js.CreateKeyValue(f.ctx, jetstream.KeyValueConfig{Bucket: delivery.StepsBucket(module), History: 1}) + kv, err := f.js.CreateKeyValue(f.ctx, jetstream.KeyValueConfig{Bucket: buildrun.StepsBucket(module), History: 1}) if err != nil { t.Fatal(err) } @@ -177,7 +184,7 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, nodes ...string) { } return 2 }, PullWait: 200 * time.Millisecond, - Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { return runner.RunAsk(ctx, steps, a) }} + Run: func(ctx context.Context, a sdkstep.StepAsk) (any, error) { return runner.RunAsk(ctx, steps, a) }} f.mu.Lock() f.pullers = append(f.pullers, p) f.mu.Unlock() @@ -191,11 +198,11 @@ const trunkHead = "3333333333333333333333333333333333333333" // trunk writes the gate's source.main naming head as novox/mesh-catalog's trunk head, and answers its revision. func (b *bed) trunk(t *testing.T, head string) uint64 { t.Helper() - data, err := delivery.EncodeSource(delivery.Source{V: delivery.Version, Group: "g", Heads: map[string]string{"novox/mesh-catalog": head}}) + data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{V: keys.Version, Group: "g", Heads: map[string]string{"novox/mesh-catalog": head}}) if err != nil { t.Fatal(err) } - rev, err := b.source.Put(b.ctx, delivery.SourceMainKey, data) + rev, err := b.source.Put(b.ctx, meshdeliveryclient.SourceMainKey, data) if err != nil { t.Fatal(err) } @@ -273,7 +280,7 @@ func startBedWith(t *testing.T, closed bool) *bed { t.Fatal(err) } } - kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket(module), History: 1}) + kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: buildrun.ToolchainsBucket(module), History: 1}) if err != nil { t.Fatal(err) } @@ -286,19 +293,19 @@ func startBedWith(t *testing.T, closed bool) *bed { } return kv } - b.modules = mk(delivery.ModulesBucket, delivery.ModulesHistory) - b.candidates = mk(delivery.CandidatesBucket, delivery.CandidatesHistory) - b.source = mk(delivery.SourceBucket, delivery.SourceHistory) - mk(delivery.ManifestsBucket, delivery.ManifestsHistory) + b.modules = mk(indexerBucket(modulesState), modulesHistory) + b.candidates = mk(indexerBucket(candidatesState), candidatesHistory) + b.source = mk(sourceBucket, sourceHistory) + mk(indexerBucket(manifestsState), manifestsHistory) // The indexer and the assigner as the owners of the states the builder follows: their read tools and views families // (ADR 0615, DLV-M1); n1 and n2 each have their durable consumer. - readers := []string{"n1_" + delivery.BuilderModule, "n2_" + delivery.BuilderModule} + readers := []string{"n1_" + builds.BuilderModule, "n2_" + builds.BuilderModule} for _, st := range []string{modulesState, manifestsState, candidatesState} { - viewstest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, delivery.IndexerModule+".indexer_read_"+st, readers...) + viewstest.Serve(t, ctx, js, ctl, indexerModule, st, indexerModule+".indexer_read_"+st, readers...) } - b.packages = viewstest.Serve(t, ctx, js, ctl, delivery.IndexerModule, delivery.PackagesState, delivery.IndexerModule+".indexer_read_packages", readers...) - viewstest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule+".assigner_read_placement", readers...) - b.builds = mk(delivery.BuilderBuildsBucket, delivery.BuilderBuildsHistory) + b.packages = viewstest.Serve(t, ctx, js, ctl, indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages", readers...) + viewstest.Serve(t, ctx, js, ctl, assignerModule, "placement", assignerModule+".assigner_read_placement", readers...) + b.builds = mk(builds.BuilderBuildsBucket, builds.BuilderBuildsHistory) b.instances = mk(instancesBucket, 1) // The tag asks and their bounds (design 51 *Tagged versions*): the bounds declared key-ttl, so an ended bound leaves the // bus's marker a watch hears. @@ -308,8 +315,8 @@ func startBedWith(t *testing.T, closed bool) *bed { } b.out = &saidLog{} // The builder's module log, as the runtime raises it (ADR 0297): its stream on the log's subjects. - logStream := buscompose.LogStreamName(delivery.BuilderModule, delivery.BuilderLog) - if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: logStream, Subjects: []string{buscompose.LogSubject(delivery.BuilderModule, delivery.BuilderLog) + ".>"}, + logStream := buscompose.LogStreamName(builds.BuilderModule, builds.BuilderLog) + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: logStream, Subjects: []string{buscompose.LogSubject(builds.BuilderModule, builds.BuilderLog) + ".>"}, AllowDirect: true}); err != nil { t.Fatal(err) } @@ -326,20 +333,20 @@ func startBedWith(t *testing.T, closed bool) *bed { } // logged is every entry of module's key in the builder's log, read through process.Conn.ReadLog on the controller's user. -func (b *bed) logged(t *testing.T, module string) []delivery.BuildLogEntry { +func (b *bed) logged(t *testing.T, module string) []builds.BuildLogEntry { t.Helper() conn, err := process.Connect(process.Account{URL: b.srvURL, Module: "mesh-delivery-view", User: "ctl", Password: "pw"}) if err != nil { t.Fatal(err) } defer conn.Close() - entries, err := conn.ReadLog(delivery.BuilderModule, delivery.BuilderLog).Since(time.Unix(0, 0), module) + entries, err := conn.ReadLog(builds.BuilderModule, builds.BuilderLog).Since(time.Unix(0, 0), module) if err != nil { t.Fatal(err) } - var out []delivery.BuildLogEntry + var out []builds.BuildLogEntry for _, le := range entries { - e, err := delivery.DecodeBuildLogEntry(le.Value) + e, err := builds.DecodeBuildLogEntry(le.Value) if err != nil { t.Fatal(err) } @@ -351,11 +358,11 @@ func (b *bed) logged(t *testing.T, module string) []delivery.BuildLogEntry { // report writes a node's toolchains key of a build module and says so on the module's stream, as the module does. func (b *bed) report(t *testing.T, module, node, language, version, recipe string) { t.Helper() - data, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now().UTC()}) + data, err := sdkstep.EncodeToolchain(sdkstep.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now().UTC()}) if err != nil { t.Fatal(err) } - key, _ := delivery.ToolchainKey(node, language) + key, _ := sdkstep.ToolchainKey(node, language) if _, err := b.tools[module].Put(b.ctx, key, data); err != nil { t.Fatal(err) } @@ -370,39 +377,39 @@ func (b *bed) module(t *testing.T, name, fixture, path string) string { } // bothToolchains are the toolchains the bed's nodes report, as a module's fingerprint names them. -var bothToolchains = map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}} +var bothToolchains = map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}} // moduleWith is module with the toolchains its fingerprint was computed with. -func (b *bed) moduleWith(t *testing.T, name, fixture, path string, toolchains map[string]delivery.ToolchainUsed) string { +func (b *bed) moduleWith(t *testing.T, name, fixture, path string, toolchains map[string]builds.ToolchainUsed) string { return b.moduleCommit(t, name, fixture, path, toolchains, commit) } -func (b *bed) moduleCommit(t *testing.T, name, fixture, path string, toolchains map[string]delivery.ToolchainUsed, at string) string { +func (b *bed) moduleCommit(t *testing.T, name, fixture, path string, toolchains map[string]builds.ToolchainUsed, at string) string { t.Helper() return b.moduleFP(t, name, fixture, path, toolchains, at, fp, 0) } // moduleFP is moduleCommit with the new-form fingerprint fp2 and the source revision src of its merge (0: none). -func (b *bed) moduleFP(t *testing.T, name, fixture, path string, toolchains map[string]delivery.ToolchainUsed, at, fp2 string, src uint64) string { +func (b *bed) moduleFP(t *testing.T, name, fixture, path string, toolchains map[string]builds.ToolchainUsed, at, fp2 string, src uint64) string { t.Helper() raw, err := os.ReadFile(filepath.Join("testdata", fixture)) if err != nil { t.Fatal(err) } - digest := delivery.ManifestDigest(raw) - manifests, err := b.js.KeyValue(b.ctx, delivery.ManifestsBucket) + digest := meshcatalogclient.ManifestDigest(raw) + manifests, err := b.js.KeyValue(b.ctx, indexerBucket(manifestsState)) if err != nil { t.Fatal(err) } - mv, err := delivery.EncodeManifest(delivery.Manifest{Module: name, JSON: string(raw)}) + mv, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: name, Manifest: string(raw)}) if err != nil { t.Fatal(err) } if _, err := manifests.Put(b.ctx, digest, mv); err != nil { t.Fatal(err) } - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: at, FP: "src1:" + strings.Repeat("f", 64), FP2: fp2, - Manifest: digest, Path: path, Toolchains: toolchains, Src: src}) + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: at, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2, + Manifest: digest, Path: path, Toolchains: build.CatalogToolchains(toolchains), Src: int64(src)}) if err != nil { t.Fatal(err) } @@ -476,25 +483,25 @@ func (b *bed) awaitSaid(t *testing.T, want string) string { } // await reads the build key until pred holds of it, within 20 s. -func (b *bed) await(t *testing.T, module string, pred func(delivery.BuilderBuild) bool) delivery.BuilderBuild { +func (b *bed) await(t *testing.T, module string, pred func(builds.BuilderBuild) bool) builds.BuilderBuild { t.Helper() return b.awaitFP(t, module, fp, pred) } // awaitFP is await of the build key of fingerprint at. -func (b *bed) awaitFP(t *testing.T, module, at string, pred func(delivery.BuilderBuild) bool) delivery.BuilderBuild { +func (b *bed) awaitFP(t *testing.T, module, at string, pred func(builds.BuilderBuild) bool) builds.BuilderBuild { t.Helper() - key, err := delivery.BuilderBuildKey(module, at) + key, err := builds.BuilderBuildKey(module, at) if err != nil { t.Fatal(err) } - var last delivery.BuilderBuild + var last builds.BuilderBuild for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { e, err := b.builds.Get(b.ctx, key) if err != nil { continue } - if last, err = delivery.DecodeBuilderBuild(e.Value()); err == nil && pred(last) { + if last, err = builds.DecodeBuilderBuild(e.Value()); err == nil && pred(last) { return last } } @@ -502,8 +509,8 @@ func (b *bed) awaitFP(t *testing.T, module, at string, pred func(delivery.Builde return last } -func isState(state string) func(delivery.BuilderBuild) bool { - return func(v delivery.BuilderBuild) bool { return v.State == state } +func isState(state string) func(builds.BuilderBuild) bool { + return func(v builds.BuilderBuild) bool { return v.State == state } } // TestBuilderMakesADoneKeyWithTheDigests: modules.netcheck (a Go and a TypeScript bundle) and its manifest are in the @@ -529,7 +536,7 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 t.Fatal(err) } b.start(t, "n1", map[string]string{"MESH_BUILDER_TAG_ASK_BOUND": "1s", "MESH_BUILDER_STEP_BOUND": "1s"}) - v := b.await(t, "netcheck", isState(delivery.BuildDone)) + v := b.await(t, "netcheck", isState(builds.BuildDone)) for _, a := range []string{"tools-go", "tools-typescript"} { es := reg.of(t, "netcheck/"+a) if len(es) != 1 || es[0].Version != "0.1.5" || es[0].Fingerprint != fp || es[0].State != versions.Numbered || es[0].Digest != digestOf(a) { @@ -554,15 +561,15 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 // 1955/L7 (ADR 0571 §1): the build's start and end are in the module log build-log under the module's key, read as a // reader with the log's read reads them, the end naming the manifest and what was made. // DLV-B10: between them the build log holds the build's narrations (build.said), each version numbered, and why. - var ended []delivery.BuildLogEntry + var ended []builds.BuildLogEntry for _, e := range b.logged(t, "netcheck") { - if e.Event != delivery.BuildSaidEvent { + if e.Event != builds.BuildSaidEvent { ended = append(ended, e) } else if e.Why != versions.SaidNumbered || !strings.Contains(e.Line, "numbered 0.1.5") || e.FP != fp { t.Errorf("said = %+v, want netcheck's versions numbered", e) } } - if len(ended) != 2 || ended[0].Event != delivery.BuildStartedEvent || ended[1].Event != delivery.BuildEndedEvent || + if len(ended) != 2 || ended[0].Event != builds.BuildStartedEvent || ended[1].Event != builds.BuildEndedEvent || ended[1].Manifest != digest || len(ended[1].Made) != 2 || ended[1].Node != "n1" { t.Errorf("logged = %+v, want the start and the end of netcheck's build", ended) } @@ -576,11 +583,11 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 t.Errorf("the narration = %q, want what the ask waits on", said) } // And in the build log, under the builder's own key (the ruling of 13:17: build.said). - if !slices.ContainsFunc(b.logged(t, delivery.BuilderModule), func(e delivery.BuildLogEntry) bool { - return e.Event == delivery.BuildSaidEvent && e.Why == versions.SaidUnanswered && e.FP == key && e.Commit == "abc" && + if !slices.ContainsFunc(b.logged(t, builds.BuilderModule), func(e builds.BuildLogEntry) bool { + return e.Event == builds.BuildSaidEvent && e.Why == versions.SaidUnanswered && e.FP == key && e.Commit == "abc" && strings.Contains(e.Line, "the check's build of abc is not claimed yet") }) { - t.Errorf("the build log of %s = %+v, want the unanswered tag ask said", delivery.BuilderModule, b.logged(t, delivery.BuilderModule)) + t.Errorf("the build log of %s = %+v, want the unanswered tag ask said", builds.BuilderModule, b.logged(t, builds.BuilderModule)) } if e, err := b.tagAsks.Get(b.ctx, key); err != nil { t.Fatal(err) @@ -647,7 +654,7 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 b.fake.hang.Store(&release) third := "src2:" + strings.Repeat("4", 64) b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, commit, third, 3) - if f := b.awaitFP(t, "netcheck", third, isState(delivery.BuildFailed)); !strings.Contains(f.Error, "did not end within") { + if f := b.awaitFP(t, "netcheck", third, isState(builds.BuildFailed)); !strings.Contains(f.Error, "did not end within") { t.Errorf("the failed build = %+v, want its bound run out", f) } failed := health.FindingKey(health.ScopeModule, "netcheck", BuildFailed) @@ -655,10 +662,10 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 b.fake.hang.Store(nil) close(release) b.moduleFP(t, "netcheck", netcheck, "modules/netcheck", bothToolchains, commit, third, 3) // the indexer writes it again - b.awaitFP(t, "netcheck", third, isState(delivery.BuildDone)) + b.awaitFP(t, "netcheck", third, isState(builds.BuildDone)) b.awaitSaid(t, "kept: fingerprint "+third+" is numbered under it") - if !slices.ContainsFunc(b.logged(t, "netcheck"), func(e delivery.BuildLogEntry) bool { - return e.Event == delivery.BuildSaidEvent && e.Why == versions.SaidKept && e.FP == third && strings.Contains(e.Line, "kept: fingerprint "+third) + if !slices.ContainsFunc(b.logged(t, "netcheck"), func(e builds.BuildLogEntry) bool { + return e.Event == builds.BuildSaidEvent && e.Why == versions.SaidKept && e.FP == third && strings.Contains(e.Line, "kept: fingerprint "+third) }) { t.Errorf("the build log of netcheck says no version kept at %s", third) } @@ -685,8 +692,8 @@ func TestBuilderMakesADoneKeyWithTheDigests(t *testing.T) { // 1607/AC7 if _, err := reg.versions.Put(b.ctx, versions.Key(tagged.Name, tagged.Version), raw); err != nil { t.Fatal(err) } - if _, err := b.js.Publish(b.ctx, process.ModuleStreamSubject(delivery.ForgeBridgeModule, "forge"), - []byte(`{"kind":"`+delivery.ForgeMerged+`","repository":"novox/mesh-sdk","branch":"feat/x"}`)); err != nil { + if _, err := b.js.Publish(b.ctx, process.ModuleStreamSubject(forgeBridgeModule, "forge"), + []byte(`{"kind":"`+forgemsg.ForgeMerged+`","repository":"novox/mesh-sdk","branch":"feat/x"}`)); err != nil { t.Fatal(err) } for deadline := time.Now().Add(20 * time.Second); ; time.Sleep(50 * time.Millisecond) { @@ -715,7 +722,7 @@ func TestAnArchiveOnlyModuleAndARootModuleAreBuilt(t *testing.T) { for _, c := range []struct { name, path string two bool - }{{"no toolchain", "modules/ca-trust", false}, {"root", delivery.RootPath, false}, {"two instances together", "modules/ca-trust", true}} { + }{{"no toolchain", "modules/ca-trust", false}, {"root", meshcatalogclient.RootPath, false}, {"two instances together", "modules/ca-trust", true}} { t.Run(c.name, func(t *testing.T) { b := startBed(t) b.archive(t, "ca-trust", c.path, commit) @@ -723,7 +730,7 @@ func TestAnArchiveOnlyModuleAndARootModuleAreBuilt(t *testing.T) { if c.two { b.start(t, "n2", nil) } - v := b.await(t, "ca-trust", isState(delivery.BuildDone)) + v := b.await(t, "ca-trust", isState(builds.BuildDone)) if c.name == "no toolchain" && (len(v.Toolchains) != 0 || len(v.Artifacts) != 1 || strings.HasPrefix(v.Step, "one node (")) { t.Errorf("build = %+v, want done naming no toolchain", v) } @@ -745,11 +752,11 @@ func TestAFailedKeyIsBuiltAgainAsANewRevisionFromANewModulesValueWithTheSameFing b.fake.failBuild.Store(true) b.archive(t, "ca-trust", "modules/ca-trust", commit) b.start(t, "n1", nil) - v := b.await(t, "ca-trust", isState(delivery.BuildFailed)) + v := b.await(t, "ca-trust", isState(builds.BuildFailed)) if !strings.Contains(v.Error, " on n") || !strings.Contains(v.Error, "boom") { t.Errorf("build = %+v, want failed naming the node and the step's reason", v) } - key, _ := delivery.BuilderBuildKey("ca-trust", fp) + key, _ := builds.BuilderBuildKey("ca-trust", fp) failed, err := b.builds.Get(b.ctx, key) if err != nil { t.Fatal(err) @@ -757,7 +764,7 @@ func TestAFailedKeyIsBuiltAgainAsANewRevisionFromANewModulesValueWithTheSameFing b.fake.failBuild.Store(false) // The indexer writes the module again (here at a later commit), with the same fingerprint. b.archive(t, "ca-trust", "modules/ca-trust", "89abcdef0123456789abcdef0123456789abcdef") - b.await(t, "ca-trust", isState(delivery.BuildDone)) + b.await(t, "ca-trust", isState(builds.BuildDone)) done, err := b.builds.Get(b.ctx, key) if err != nil { t.Fatal(err) @@ -778,8 +785,8 @@ func TestAToolchainChangeDoesNotBuildTheOldFingerprint(t *testing.T) { // 1615/A b.start(t, "n1", nil) // Another module, whose fingerprint names the new toolchain, is built; by then the old one has been looked at. b.moduleCommit(t, "netcheck2", "catalogue-a67f0196-netcheck.module.json", "modules/netcheck", - map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.2", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}}, commit) - b.await(t, "netcheck2", isState(delivery.BuildDone)) + map[string]builds.ToolchainUsed{"go": {Version: "go1.27.2", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}}, commit) + b.await(t, "netcheck2", isState(builds.BuildDone)) if _, err := b.builds.Get(b.ctx, mustKey(t, "netcheck")); err == nil { t.Error("the old fingerprint, naming go1.27.1, was built with go1.27.2") } @@ -789,7 +796,7 @@ func TestAToolchainChangeDoesNotBuildTheOldFingerprint(t *testing.T) { // 1615/A } func mustKey(t *testing.T, module string) string { - k, err := delivery.BuilderBuildKey(module, fp) + k, err := builds.BuilderBuildKey(module, fp) if err != nil { t.Fatal(err) } @@ -799,14 +806,14 @@ func mustKey(t *testing.T, module string) string { // moduleAt is module with the go toolchain version the fingerprint names and the commit it was computed at. func (b *bed) moduleAt(t *testing.T, name, fixture, path, goVersion, at string) { t.Helper() - b.moduleCommit(t, name, fixture, path, map[string]delivery.ToolchainUsed{"go": {Version: goVersion, Recipe: "1"}}, at) + b.moduleCommit(t, name, fixture, path, map[string]builds.ToolchainUsed{"go": {Version: goVersion, Recipe: "1"}}, at) } // TestAKeyLeftBuildingForAModuleThatIsGoneIsClosedAtStart: the builds bucket holds a key left building by another node for // a module the indexer no longer has; the program, at start, fails it naming why (issue 1660, ADR 0478 clause 6). func TestAKeyLeftBuildingForAModuleThatIsGoneIsClosedAtStart(t *testing.T) { b := startBed(t) - data, err := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "n2.mesh-delivery-builder", Manifest: strings.Repeat("a", 64)}) + data, err := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "n2.mesh-delivery-builder", Manifest: strings.Repeat("a", 64)}) if err != nil { t.Fatal(err) } @@ -814,7 +821,7 @@ func TestAKeyLeftBuildingForAModuleThatIsGoneIsClosedAtStart(t *testing.T) { t.Fatal(err) } b.start(t, "n1", nil) - v := b.await(t, "ghost", isState(delivery.BuildFailed)) + v := b.await(t, "ghost", isState(builds.BuildFailed)) if !strings.Contains(v.Error, "no longer has this fingerprint") || !strings.Contains(v.Error, "n2.mesh-delivery-builder") { t.Errorf("build = %+v, want it closed naming who left it", v) } @@ -829,8 +836,8 @@ func TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk(t *tes b := startBed(t) other := "src2:" + strings.Repeat("e", 64) digest := b.module(t, "netcheck", "catalogue-a67f0196-netcheck.module.json", "modules/netcheck") - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: commit, FP: "src1:" + strings.Repeat("f", 64), FP2: other, - Manifest: digest, Path: "modules/netcheck", Toolchains: map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}}}) + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: other, + Manifest: digest, Path: "modules/netcheck", Toolchains: build.CatalogToolchains(map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}})}) if err != nil { t.Fatal(err) } @@ -838,14 +845,14 @@ func TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk(t *tes t.Fatal(err) } const head = "1111111111111111111111111111111111111111" - pulls := []delivery.CandidatePull{{Head: delivery.Head{Repository: "novox/mesh-catalog", Number: 7, Head: head}, Modules: []string{"netcheck"}, - Fps: map[string]delivery.CandidateModule{"netcheck": {FP2: fp, Manifest: digest}}}} - hash, _ := delivery.HeadsHash([]delivery.Head{pulls[0].Head}) - cand, err := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: "feat/x", Base: b.trunk(t, trunkHead), HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}) + pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"netcheck"}, + Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"netcheck": {Fp2: fp, Manifest: digest}}}} + hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) + cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) if err != nil { t.Fatal(err) } - ckey, err := delivery.CandidatesKey("feat/x") + ckey, err := keys.EscapeName("feat/x"), error(nil) if err != nil { t.Fatal(err) } @@ -853,7 +860,7 @@ func TestBuilderBuildsACandidatesFingerprintFromTheHeadMergedOntoTheTrunk(t *tes t.Fatal(err) } b.start(t, "n1", nil) - v := b.await(t, "netcheck", isState(delivery.BuildDone)) + v := b.await(t, "netcheck", isState(builds.BuildDone)) if len(v.Artifacts) != 2 { t.Fatalf("build = %+v", v) } @@ -881,7 +888,7 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 digest := b.moduleCommit(t, "imaged", "catalogue-a67f0196-imaged.module.json", "modules/imaged", nil, commit) // The trunk carries `other`; its key is built and published. trunk := func(fp2 string) { - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: commit, FP: "src1:" + strings.Repeat("f", 64), FP2: fp2, + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: commit, Fp: "src1:" + strings.Repeat("f", 64), Fp2: fp2, Manifest: digest, Path: "modules/imaged"}) if err != nil { t.Fatal(err) @@ -892,12 +899,12 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 } trunk(other) b.start(t, "n1", nil) - okey, _ := delivery.BuilderBuildKey("imaged", other) - awaitKey := func(key string, pred func(delivery.BuilderBuild) bool) delivery.BuilderBuild { - var last delivery.BuilderBuild + okey, _ := builds.BuilderBuildKey("imaged", other) + awaitKey := func(key string, pred func(builds.BuilderBuild) bool) builds.BuilderBuild { + var last builds.BuilderBuild for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { if e, err := b.builds.Get(b.ctx, key); err == nil { - if last, err = delivery.DecodeBuilderBuild(e.Value()); err == nil && pred(last) { + if last, err = builds.DecodeBuilderBuild(e.Value()); err == nil && pred(last) { return last } } @@ -905,24 +912,24 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 t.Fatalf("builds.%s = %+v within 20 s", key, last) return last } - v := awaitKey(okey, isState(delivery.BuildDone)) + v := awaitKey(okey, isState(builds.BuildDone)) if len(v.Artifacts) != 1 || v.Artifacts[0].Digest != digestOf("server") || !strings.HasSuffix(v.Artifacts[0].Reference, "@"+digestOf("server")) { t.Fatalf("trunk build = %+v, want the digest and the reference", v) } // The candidate carries fp: built, not published. const head = "1111111111111111111111111111111111111111" - pulls := []delivery.CandidatePull{{Head: delivery.Head{Repository: "novox/mesh-catalog", Number: 7, Head: head}, Modules: []string{"imaged"}, - Fps: map[string]delivery.CandidateModule{"imaged": {FP2: fp, Manifest: digest}}}} - hash, _ := delivery.HeadsHash([]delivery.Head{pulls[0].Head}) - cand, err := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: "feat/x", Base: b.trunk(t, trunkHead), HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}) + pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: head, Modules: []string{"imaged"}, + Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"imaged": {Fp2: fp, Manifest: digest}}}} + hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) + cand, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Base: int64(b.trunk(t, trunkHead)), Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) if err != nil { t.Fatal(err) } - ckey, _ := delivery.CandidatesKey("feat/x") + ckey := keys.EscapeName("feat/x") if _, err := b.candidates.Put(b.ctx, ckey, cand); err != nil { t.Fatal(err) } - v = b.await(t, "imaged", isState(delivery.BuildDone)) + v = b.await(t, "imaged", isState(builds.BuildDone)) if len(v.Artifacts) != 1 || v.Artifacts[0].Reference != "" { t.Fatalf("candidate build = %+v, want done with no reference", v) } @@ -933,8 +940,8 @@ func TestAnImageIsBuiltOnOneNodeAndOnlyATrunkKeyPublishes(t *testing.T) { // 170 b.fake.mu.Unlock() // The pull request is merged: the trunk's record names fp, and its key ends published, built once more with publish. trunk(fp) - v = b.await(t, "imaged", func(v delivery.BuilderBuild) bool { - return v.State == delivery.BuildDone && len(v.Artifacts) == 1 && v.Artifacts[0].Reference != "" + v = b.await(t, "imaged", func(v builds.BuilderBuild) bool { + return v.State == builds.BuildDone && len(v.Artifacts) == 1 && v.Artifacts[0].Reference != "" }) if v.Artifacts[0].Digest != digestOf("server") { t.Errorf("merged build = %+v", v) @@ -960,13 +967,13 @@ func TestEveryDoneKeyNamesItsReferencesAndARefusedPushIsBuiltAgain(t *testing.T) b.fake.refusePush.Store(true) b.archive(t, "ca-trust", "modules/ca-trust", commit) b.start(t, "n1", nil) - v := b.await(t, "ca-trust", isState(delivery.BuildFailed)) - if !strings.HasPrefix(v.Error, delivery.NotStored) || !strings.Contains(v.Error, "507") { - t.Fatalf("build = %+v, want failed opening %q with the store's words", v, delivery.NotStored) + v := b.await(t, "ca-trust", isState(builds.BuildFailed)) + if !strings.HasPrefix(v.Error, builds.NotStored) || !strings.Contains(v.Error, "507") { + t.Fatalf("build = %+v, want failed opening %q with the store's words", v, builds.NotStored) } b.fake.refusePush.Store(false) b.archive(t, "ca-trust", "modules/ca-trust", "89abcdef0123456789abcdef0123456789abcdef") - v = b.await(t, "ca-trust", isState(delivery.BuildDone)) + v = b.await(t, "ca-trust", isState(builds.BuildDone)) if ok, missing := v.Referenced(); !ok { t.Errorf("build = %+v, artifacts naming no reference: %v", v, missing) } @@ -979,7 +986,7 @@ func TestAPinnedUpstreamIsCopiedByTheImageBuildModule(t *testing.T) { b := startBed(t) b.moduleCommit(t, "hello-web", "catalogue-49caf17f-hello-web.module.json", "modules/hello-web", nil, commit) b.start(t, "n1", nil) - v := b.await(t, "hello-web", isState(delivery.BuildDone)) + v := b.await(t, "hello-web", isState(builds.BuildDone)) if ok, missing := v.Referenced(); !ok { t.Fatalf("build = %+v, artifacts naming no reference: %v", v, missing) } @@ -998,10 +1005,10 @@ func TestAManifestFallingBackToAPredefinedKeyIsBuilt(t *testing.T) { b := startBed(t) b.module(t, "claude-code", "catalogue-835bf22b-claude-code.module.json", "modules/claude-code") b.start(t, "n1", nil) - v := b.await(t, "claude-code", func(v delivery.BuilderBuild) bool { - return v.State == delivery.BuildDone || v.State == delivery.BuildFailed + v := b.await(t, "claude-code", func(v builds.BuilderBuild) bool { + return v.State == builds.BuildDone || v.State == builds.BuildFailed }) - if v.State != delivery.BuildDone { + if v.State != builds.BuildDone { t.Errorf("build = %+v, want done: the manifest is read", v) } } @@ -1053,7 +1060,7 @@ func TestTheBuilderAsksTheBuildSeatsAndCallsNoNode(t *testing.T) { defer s.Unsubscribe() asked := b.asksPut(t, sub) b.start(t, "n1", nil) - v := b.await(t, "netcheck", isState(delivery.BuildDone)) + v := b.await(t, "netcheck", isState(builds.BuildDone)) for _, a := range v.Artifacts { if a.Reference == "" { t.Errorf("artifact %s names no reference", a.Name) @@ -1069,7 +1076,7 @@ func TestTheBuilderAsksTheBuildSeatsAndCallsNoNode(t *testing.T) { t.Fatalf("asks put = %+v, want one Go and one Node delivery ask", got) } for _, a := range got { - if a.Kind != delivery.AskDelivery || a.Build == nil || a.Build.FP != fp { + if a.Kind != sdkstep.AskDelivery || a.Build == nil || a.Build.Fp != fp { t.Errorf("ask %+v, want a delivery ask of the fingerprint", a) } } @@ -1077,12 +1084,12 @@ func TestTheBuilderAsksTheBuildSeatsAndCallsNoNode(t *testing.T) { // asksPut hears every ask put on a build seat's accept subjects (a core subscription beside the stream) and answers the // asks heard so far. -func (b *bed) asksPut(t *testing.T, nc *nats.Conn) func() []delivery.StepAsk { +func (b *bed) asksPut(t *testing.T, nc *nats.Conn) func() []sdkstep.StepAsk { t.Helper() var mu sync.Mutex - var asks []delivery.StepAsk + var asks []sdkstep.StepAsk s, err := nc.Subscribe("mesh.seat.*.accept.>", func(m *nats.Msg) { - if a, err := delivery.DecodeStepAsk(m.Data); err == nil { + if a, err := sdkstep.DecodeStepAsk(m.Data); err == nil { mu.Lock() asks = append(asks, a) mu.Unlock() @@ -1092,10 +1099,10 @@ func (b *bed) asksPut(t *testing.T, nc *nats.Conn) func() []delivery.StepAsk { t.Fatal(err) } t.Cleanup(func() { _ = s.Unsubscribe() }) - return func() []delivery.StepAsk { + return func() []sdkstep.StepAsk { mu.Lock() defer mu.Unlock() - return append([]delivery.StepAsk(nil), asks...) + return append([]sdkstep.StepAsk(nil), asks...) } } @@ -1111,7 +1118,7 @@ func TestARestartedBuilderPutsNoSecondAsk(t *testing.T) { defer sub.Close() asked := b.asksPut(t, sub) b.start(t, "n1", nil) - v := b.await(t, "netcheck", func(v delivery.BuilderBuild) bool { return v.State == delivery.BuildBuilding && len(v.Asks) == 2 }) + v := b.await(t, "netcheck", func(v builds.BuilderBuild) bool { return v.State == builds.BuildBuilding && len(v.Asks) == 2 }) for deadline := time.Now().Add(20 * time.Second); len(asked()) < 2; time.Sleep(20 * time.Millisecond) { if time.Now().After(deadline) { t.Fatalf("asks put = %+v within 20 s, want the two the key names", asked()) @@ -1120,15 +1127,15 @@ func TestARestartedBuilderPutsNoSecondAsk(t *testing.T) { // 1945 note 62, on the real bus: the stream read as builder_seat_queue reads it shows the Go ask waiting; the ask can // still be answered (nothing was taken since); and a message dropped as an ask asked again is gone from the stream. held := heldOnTheBus{b.js} - goAsk := delivery.StepAsk{ID: v.Asks[delivery.AskGo], Language: delivery.AskGo, Kind: delivery.AskDelivery} - stale := delivery.StepAsk{ID: "mvo53jcm70b5f7", Language: delivery.AskGo, Kind: delivery.AskDelivery} - subject, _ := delivery.AskSubject(delivery.AskGo, delivery.AskDelivery) + goAsk := sdkstep.StepAsk{ID: v.Asks[sdkstep.AskGo], Language: sdkstep.AskGo, Kind: sdkstep.AskDelivery} + stale := sdkstep.StepAsk{ID: "mvo53jcm70b5f7", Language: sdkstep.AskGo, Kind: sdkstep.AskDelivery} + subject, _ := sdkstep.AskSubject(sdkstep.AskGo, sdkstep.AskDelivery) if _, err := b.js.Publish(b.ctx, subject, []byte("{}"), jetstream.WithMsgID(stale.ID)); err != nil { t.Fatal(err) } queued := func() []build.QueuedState { - q, err := held.Queue(b.ctx, delivery.AskGo, 10) - if err != nil || len(q.Kinds) != 1 || q.Kinds[0].Kind != delivery.AskDelivery { + q, err := held.Queue(b.ctx, sdkstep.AskGo, 10) + if err != nil || len(q.Kinds) != 1 || q.Kinds[0].Kind != sdkstep.AskDelivery { t.Fatalf("the Go seat's queue = %+v, %v; want its delivery asks", q, err) } return q.Kinds[0].Messages @@ -1148,7 +1155,7 @@ func TestARestartedBuilderPutsNoSecondAsk(t *testing.T) { b.stop() b.start(t, "n1", nil) // the same builder, started again, while both asks wait b.fake.open() - done := b.await(t, "netcheck", isState(delivery.BuildDone)) + done := b.await(t, "netcheck", isState(builds.BuildDone)) if got := asked(); len(got) != 2 { t.Fatalf("asks put = %d (%+v), want the two the key named and none again", len(got), got) } @@ -1174,7 +1181,7 @@ type registry struct { b *bed versions, findings, reasks jetstream.KeyValue mu sync.Mutex - numbered []delivery.VersionNumbered + numbered []builds.VersionNumbered removes []map[string]any } @@ -1193,12 +1200,12 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis // EVENTS with two seats' outcomes, and the forge bridge's stream (a branch's end): each with the builder's durable consumer. for stream, subjects := range map[string][]string{ eventsStream: {"mesh.mod.*.event.>", "mesh.seat.*.event.>"}, - forgeStream: {process.ModuleStreamSubject(delivery.ForgeBridgeModule, ">")}, + forgeStream: {process.ModuleStreamSubject(forgeBridgeModule, ">")}, } { if _, err := b.js.CreateOrUpdateStream(b.ctx, jetstream.StreamConfig{Name: stream, Subjects: subjects}); err != nil { t.Fatal(err) } - if _, err := b.js.CreateOrUpdateConsumer(b.ctx, stream, jetstream.ConsumerConfig{Durable: node + "_" + delivery.BuilderModule, + if _, err := b.js.CreateOrUpdateConsumer(b.ctx, stream, jetstream.ConsumerConfig{Durable: node + "_" + builds.BuilderModule, AckPolicy: jetstream.AckExplicitPolicy}); err != nil { t.Fatal(err) } @@ -1218,8 +1225,8 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis t.Cleanup(func() { _ = rm.Unsubscribe() }) nc := b.js.Conn() // As a build module hears it: on the contract's subject, read by the contract's decoder (BG2's late publish). - sub, err := nc.Subscribe(delivery.VersionNumberedSubject, func(m *nats.Msg) { - if nb, err := delivery.DecodeVersionNumbered(m.Data); err == nil { + sub, err := nc.Subscribe(builds.VersionNumberedSubject, func(m *nats.Msg) { + if nb, err := builds.DecodeVersionNumbered(m.Data); err == nil { r.mu.Lock() r.numbered = append(r.numbered, nb) r.mu.Unlock() @@ -1234,9 +1241,9 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis listed = append(listed, map[string]string{"version": v}) } for subject, answer := range map[string]any{ - "mesh.seat." + ociartifactregistry.Seat + ".call.versions.>": map[string]any{"versions": listed}, - "mesh.mod.mesh-delivery-forge-bridge.call.bridge_tags.>": map[string]any{"tags": []string{}}, - "mesh.mod." + delivery.IndexerModule + ".call.indexer_requirements.>": map[string]any{"requirements": []string{}}, + "mesh.seat." + ociartifactregistry.Seat + ".call.versions.>": map[string]any{"versions": listed}, + "mesh.mod.mesh-delivery-forge-bridge.call.bridge_tags.>": map[string]any{"tags": []string{}}, + "mesh.mod." + indexerModule + ".call.indexer_requirements.>": map[string]any{"requirements": []string{}}, } { reply, _ := json.Marshal(map[string]any{"result": answer, "node": "ctl"}) s, err := nc.Subscribe(subject, func(m *nats.Msg) { _ = m.Respond(reply) }) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index 5559b78b..3e614c9d 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -8,7 +8,6 @@ import ( "cmp" "context" "fmt" - sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings" "io" "log" "maps" @@ -20,14 +19,20 @@ import ( "syscall" "time" + sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/sdk/forms" "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" - "git.novox.be/novox/mesh-sdk/go/delivery" + "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/sdk/buildstep" + "git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/views" @@ -63,7 +68,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro return err } defer conn.Close() - builds, err := process.EssentialBucket(ctx, js, "builder", delivery.BuilderBuildsBucket, "claim or write a build key") + buildsKV, err := process.EssentialBucket(ctx, js, "builder", builds.BuilderBuildsBucket, "claim or write a build key") if err != nil { return err } @@ -79,28 +84,28 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The module has one durable consumer of EVENTS: one hub pulls it and hands each message to the part that hears it (the // views reader and the builder's own events), started once every part is registered. hub := process.NewEventHub(logger.Printf) - reader := &views.ProcessReader{Module: delivery.BuilderModule, Call: calls.Call, Logf: logger.Printf, Hub: hub} + reader := &views.ProcessReader{Module: builds.BuilderModule, Call: calls.Call, Logf: logger.Printf, Hub: hub} findingsKV := process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings, a first view unanswered past its bound among them, are said in its log only", logger.Printf) reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findingsKV.KV) } - modules, err := reader.Follow(delivery.IndexerModule, modulesState, delivery.IndexerModule+".indexer_read_modules") + modules, err := reader.Follow(indexerModule, modulesState, indexerModule+".indexer_read_modules") if err != nil { return err } - manifests, err := reader.Follow(delivery.IndexerModule, manifestsState, delivery.IndexerModule+".indexer_read_manifests") + manifests, err := reader.Follow(indexerModule, manifestsState, indexerModule+".indexer_read_manifests") if err != nil { return err } // The candidates' fingerprints are built too (issue 1684). - candidates, err := reader.Follow(delivery.IndexerModule, candidatesState, delivery.IndexerModule+".indexer_read_candidates") + candidates, err := reader.Follow(indexerModule, candidatesState, indexerModule+".indexer_read_candidates") if err != nil { return err } // The indexer's packages, whose names a package is numbered under (design 32). - packages, err := reader.Follow(delivery.IndexerModule, delivery.PackagesState, delivery.IndexerModule+".indexer_read_packages") + packages, err := reader.Follow(indexerModule, meshcatalog.StatePackages, indexerModule+".indexer_read_packages") if err != nil { return err } - placement, err := reader.Follow(delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule+".assigner_read_placement") + placement, err := reader.Follow(assignerModule, "placement", assignerModule+".assigner_read_placement") if err != nil { return err } @@ -128,15 +133,15 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro dead := &buildrun.DeadLetters{} // The state may not be on the bus yet when a delivery started the builder (the grants part creates it, issue 1790): it // is bound when the bus says it was created, and the latest hearing is written then. - hearingKV := process.AwaitBucket(ctx, js, conn, delivery.BuilderModule+"_"+buildrun.HearingState, "builder_stuck cannot say whether DEAD_LETTERS is heard", logger.Printf) - hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, delivery.AskLanguages, dead, logger.Printf, + hearingKV := process.AwaitBucket(ctx, js, conn, builds.BuilderModule+"_"+buildrun.HearingState, "builder_stuck cannot say whether DEAD_LETTERS is heard", logger.Printf) + hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, buildstep.AskLanguages, dead, logger.Printf, buildrun.SayHearing(ctx, hearingKV, logger.Printf)) defer hearing.Stop() // Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read // (buildrun.HearEnds): a watch that goes deaf loses no end (the lost check of 2026-10-28). var buildModules []string - for _, l := range delivery.AskLanguages { - if m, err := delivery.BuildModuleOf(l); err == nil { + for _, l := range buildstep.AskLanguages { + if m, err := buildstep.BuildModuleOf(l); err == nil { buildModules = append(buildModules, m) } } @@ -158,7 +163,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // A package is numbered under its name in its ecosystem, read from the indexer's packages state (design 32). numberer := &versions.Numberer{Versions: versions.Versions{Store: awaitedVersions{versionsKV}}, Floor: floorVia(calls.Call), Waits: floorWaits, Names: versions.NamesIn(indexerPackages{packages}), - Said: func(nb delivery.VersionNumbered) { + Said: func(nb builds.VersionNumbered) { if err := emitNumbered(context.WithoutCancel(ctx), js, cred, nb); err != nil { logger.Printf("%v", err) } @@ -169,7 +174,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro pubs := &publishes{bounds: pubBoundsKV.KV, versions: numberer.Versions, findings: found, bound: versions.PublishBound, logf: logger.Printf} numberer.Publishing = pubs.arm b := &build.Builder{ - Claimer: build.Claimer{Store: kvStore{builds}, By: cred.Node + "." + cred.Module}, + Claimer: build.Claimer{Store: kvStore{buildsKV}, By: cred.Node + "." + cred.Module}, Asks: buildrun.Asker{Publisher: asks, Waiter: buildrun.LostAskWaiter{Inner: heard, Dead: dead, Hearing: hearing}, Bound: bound}, Held: heldOnTheBus{js}, // An ask that can no longer be answered is asked again (issue 1945), read at the step bound: a context's bound. @@ -189,7 +194,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // An ask not answered within five minutes is narrated with what it waits on: its bound's end on the bus says when. boundsKV := process.AwaitBucket(ctx, js, conn, versions.TagAskBoundsBucket, "an unanswered tag ask is not narrated", logger.Printf) bounds := &tagBounds{asks: tagAsksKV.KV, bounds: boundsKV.KV, bound: tagAskBound, waits: floorWaits.Waiting, logf: logger.Printf, now: time.Now, - log: func(e delivery.BuildLogEntry) { b.Log(e) }} + log: func(e builds.BuildLogEntry) { b.Log(e) }} // A state bound later numbers the builds looked at without it. versionsKV.Ready(func(jetstream.KeyValue) { b.Reconsider(ctx) }) // The versions' hand acts (builder_publish_again, builder_floor_again) come through reasks, as builder_build_again. @@ -218,7 +223,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro b.Trunk = trunks.head // The builder's log (ADR 0571 §1): each build's start and end in its module log `build-log`, read by the view, the // indexer and the assigner through their reads, and by the operator's command nox builds through the tool builder_builds_since. - b.Log = func(e delivery.BuildLogEntry) { + b.Log = func(e builds.BuildLogEntry) { if e.Node == "" { // a narration of no one build (a tag ask, the old form's window) is this instance's e.Node = cred.Node } @@ -262,7 +267,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The old reference form's window over them (DLV-B9), kept in the state old-form once it is bound; builder_old_form // reads it and drops a retired node by hand. reads.ctx = context.WithoutCancel(ctx) - reads.window = &oldForm{forms: forms.Current, findings: found, logf: logger.Printf, now: time.Now, log: func(e delivery.BuildLogEntry) { b.Log(e) }, + reads.window = &oldForm{forms: forms.Current, findings: found, logf: logger.Printf, now: time.Now, log: func(e builds.BuildLogEntry) { b.Log(e) }, bounds: pubBoundsKV.KV, said: func(body versions.OldFormClosedBody) { if err := emitEvent(context.WithoutCancel(ctx), js, cred, versions.OldFormClosedEvent, "old-form-closed-"+body.At.Format(time.RFC3339Nano), body); err != nil { @@ -432,7 +437,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro }{ {forgeStream, "a branch's end", func(m jetstream.Msg) { ends.heard(ctx, m) }}, {checkerAsksStream, "the checker's tag-wanted", func(m jetstream.Msg) { - tagWanted(ctx, tagAsksKV.KV(), m, delivery.CheckerModule, time.Now(), logger.Printf) + tagWanted(ctx, tagAsksKV.KV(), m, checkerModule, time.Now(), logger.Printf) }}, } { hear(c.stream, c.what+" is not heard until the builder starts again", c.hear) @@ -461,9 +466,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The builder's own state of its first views (views.StatusState); its findings (findingsBucket) are where the condition // keeper raises a first view unanswered past its bound (ADR 0615 §6). -var firstViewsBucket = delivery.BuilderModule + "_" + views.StatusState +var firstViewsBucket = builds.BuilderModule + "_" + views.StatusState -// The indexer's states the builder follows (delivery.ModulesBucket, ManifestsBucket, CandidatesBucket), and the owners' +// The indexer's states the builder follows (indexerBucket(modulesState), ManifestsBucket, CandidatesBucket), and the owners' // views families as their manifests' `streams` name them. const ( modulesState = "modules" @@ -513,7 +518,7 @@ func buildFromModules(ctx context.Context, b *build.Builder, modules, manifests, } // sourceHeads are the trunk heads of each revision of the gate's source.main the watch delivered, the newest -// delivery.SourceHistory of them: what a candidate's base revision names. +// sourceHistory of them: what a candidate's base revision names. type sourceHeads struct { mu sync.Mutex byRev map[uint64]map[string]string @@ -524,7 +529,7 @@ func (s *sourceHeads) put(e jetstream.KeyValueEntry) bool { if !opOf(e) { return false } - src, err := delivery.DecodeSource(e.Value()) + src, err := meshdeliveryclient.DecodeSource(e.Value()) if err != nil { return false } @@ -537,7 +542,7 @@ func (s *sourceHeads) put(e jetstream.KeyValueEntry) bool { return false } s.byRev[e.Revision()] = src.Heads - for len(s.byRev) > delivery.SourceHistory { + for len(s.byRev) > sourceHistory { delete(s.byRev, slices.Min(slices.Collect(maps.Keys(s.byRev)))) } return true @@ -552,7 +557,7 @@ func (s *sourceHeads) head(_ context.Context, rev uint64, repository string) (st return "", fmt.Errorf("source revision %d is not heard (it is built once it is)", rev) } for r, head := range heads { - if delivery.SameRepository(r, repository) { + if trunk.SameRepository(r, repository) { return head, nil } } diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go new file mode 100644 index 00000000..5c425e7b --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names.go @@ -0,0 +1,38 @@ +package main + +import ( + "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/buildrun" +) + +// The holders and buckets the builder names, taken from its own manifest (module.json: its `reads`) and from the holders' +// seats' state names; a unit row checks them against the manifest (TestTheNamesAreTheManifests). No contract names a holder +// (novox/hq ADR 0625 §4, ADR 0634 §1), except the clients of the indexer's and the assigner's states, whose holder names +// are used from there. +const ( + gateModule = "mesh-delivery-gate" + checkerModule = "mesh-delivery-checker" + forgeBridgeModule = "mesh-delivery-forge-bridge" + + // indexerModule is the holder of the states modules, manifests, candidates and packages the builder follows. + indexerModule = meshcatalogclient.Module + // assignerModule is the holder of the state placement. + assignerModule = buildrun.AssignerModule + + // sourceBucket is the gate's state source (reads: mesh-delivery-gate.source), the heads a candidate's base names. + sourceBucket = gateModule + "_source" + // packagesBucket is the indexer's state packages. + packagesBucket = indexerModule + "_" + meshcatalog.StatePackages + + // The revisions each keeps per key (the beds of the integration test create them alike). + sourceHistory = 64 + candidatesHistory = 10 + modulesHistory = 20 + manifestsHistory = 1 +) + +func indexerBucket(state string) string { return indexerModule + "_" + state } + +// namedTreePrefix opens the tree input of a module whose every artifact names its paths. +const namedTreePrefix = "named:" diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go new file mode 100644 index 00000000..544f3090 --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/names_test.go @@ -0,0 +1,50 @@ +package main + +import ( + "encoding/json" + "os" + "slices" + "testing" + + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" +) + +// TestTheNamesAreTheManifests: the holders and buckets the builder names are its manifest's `reads` (the gate's source, the +// build modules' steps and toolchains) and its `consumes` of the indexer's and assigner's views, since no contract names a +// holder (novox/hq ADR 0634 §1). +func TestTheNamesAreTheManifests(t *testing.T) { + raw, err := os.ReadFile("../../module.json") + if err != nil { + t.Fatal(err) + } + var m struct { + Reads []string `json:"reads"` + Consumes []string `json:"consumes"` + } + if err := json.Unmarshal(raw, &m); err != nil { + t.Fatal(err) + } + for _, read := range []string{ + gateModule + ".source", + "mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains", + } { + if !slices.Contains(m.Reads, read) { + t.Errorf("the builder reads %s, its manifest's reads are %v", read, m.Reads) + } + } + for _, view := range []string{indexerModule + ".views.modules", indexerModule + ".views.manifests", indexerModule + ".views.candidates", + indexerModule + ".views.packages", assignerModule + ".views.placement"} { + if !slices.Contains(m.Consumes, view) { + t.Errorf("the builder follows %s, its manifest consumes %v", view, m.Consumes) + } + } + for want, got := range map[string]string{ + "mesh-delivery-gate_source": sourceBucket, "mesh-build-go_steps": buildrun.StepsBucket("mesh-build-go"), + "mesh-build-node_toolchains": buildrun.ToolchainsBucket("mesh-build-node"), "mesh-delivery-indexer_packages": packagesBucket, + "mesh-delivery-assigner_placement": buildrun.PlacementBucket, + } { + if want != got { + t.Errorf("a bucket is %q, want %q", got, want) + } + } +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/oldform.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/oldform.go index 20dfb4e5..fa1189ce 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/oldform.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/oldform.go @@ -12,7 +12,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-delivery/sdk/forms" @@ -30,7 +30,7 @@ type oldForm struct { // said emits version.old-form-closed. said func(versions.OldFormClosedBody) // log writes the close into the builder's build log (build.said); nil writes none. - log func(delivery.BuildLogEntry) + log func(builds.BuildLogEntry) logf func(string, ...any) now func() time.Time @@ -85,7 +85,7 @@ func (o *oldForm) dropped() map[string]bool { // step moves the window to what counted states. // named is every node the read named (the placement's nodes before any is dropped), counted the statements of those -// still counted: a read naming no node judges nothing (delivery.StepWindow), so it never closes the window. +// still counted: a read naming no node judges nothing (window.StepWindow), so it never closes the window. func (o *oldForm) step(ctx context.Context, named []string, counted map[string][]string) { o.mu.Lock() defer o.mu.Unlock() @@ -119,7 +119,7 @@ func (o *oldForm) step(ctx context.Context, named []string, counted map[string][ // In the build log too (design 51 *The node-engine's own build*), under the builder's own key, the window's key // standing for the fingerprint: the close is no one build's. if o.log != nil { - o.log(delivery.BuildLogEntry{Event: delivery.BuildSaidEvent, Module: delivery.BuilderModule, FP: versions.OldFormKey, + o.log(builds.BuildLogEntry{Event: builds.BuildSaidEvent, Module: builds.BuilderModule, FP: versions.OldFormKey, At: body.At.UTC(), Line: line, Why: versions.SaidOldFormClosed}) } if o.said != nil { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go index 02afe10c..09324c26 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/optional.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" ) // **A part does not stop for a bucket it can work without** (mesh-sdk process.EssentialBucket, OptionalBucket) (novox/hq issue 1945, met twice on 2026-10-25: the builder of @@ -39,17 +39,17 @@ func (t *trunkSource) ensure() error { if t.opened { return nil } - kv, err := t.js.KeyValue(t.ctx, delivery.SourceBucket) + kv, err := t.js.KeyValue(t.ctx, sourceBucket) if err != nil { - return fmt.Errorf("the bucket %s cannot be read (%v)", delivery.SourceBucket, err) + return fmt.Errorf("the bucket %s cannot be read (%v)", sourceBucket, err) } - w, err := kv.Watch(t.ctx, delivery.SourceMainKey, jetstream.IncludeHistory()) + w, err := kv.Watch(t.ctx, meshdeliveryclient.SourceMainKey, jetstream.IncludeHistory()) if err != nil { - return fmt.Errorf("the bucket %s cannot be watched (%v)", delivery.SourceBucket, err) + return fmt.Errorf("the bucket %s cannot be watched (%v)", sourceBucket, err) } sw, err := settled(t.ctx, w, func(e jetstream.KeyValueEntry) { t.heads.put(e) }) if err != nil { - return fmt.Errorf("the bucket %s: %w", delivery.SourceBucket, err) + return fmt.Errorf("the bucket %s: %w", sourceBucket, err) } t.opened = true go func() { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reads.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reads.go index a4697771..c2c12614 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reads.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reads.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" @@ -22,7 +22,7 @@ import ( const EngineReadsState = "engine-reads" // engineReadsBucket is that state's bucket. -const engineReadsBucket = delivery.BuilderModule + "_" + EngineReadsState +const engineReadsBucket = builds.BuilderModule + "_" + EngineReadsState // engineReads keeps the statements and the counted nodes for the builder's forms (build.Builder.Forms). type engineReads struct { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go index 3e57a46a..8d7c8d3c 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reasks.go @@ -8,8 +8,8 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/buscompose" - "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" @@ -134,18 +134,18 @@ func announcedAgain(ctx context.Context, b askAgainer, kv jetstream.KeyValue, e } // appendLog appends one entry to the builder's module log `build-log` (ADR 0297, ADR 0571 §1) under the module's key -// (delivery.BuildLogKey), on the log's own subject: a module's program appends to the logs it keeps on its own credential. +// (builds.BuildLogKey), on the log's own subject: a module's program appends to the logs it keeps on its own credential. // The id makes a repeated append of one entry one entry. -func appendLog(ctx context.Context, js jetstream.JetStream, cred process.Account, e delivery.BuildLogEntry) error { - data, err := delivery.EncodeBuildLogEntry(e) +func appendLog(ctx context.Context, js jetstream.JetStream, cred process.Account, e builds.BuildLogEntry) error { + data, err := builds.EncodeBuildLogEntry(e) if err != nil { return err } - key, err := delivery.BuildLogKey(e.Module) + key, err := builds.BuildLogKey(e.Module) if err != nil { return err } - msg := nats.NewMsg(buscompose.LogSubject(cred.Module, delivery.BuilderLog) + "." + key) + msg := nats.NewMsg(buscompose.LogSubject(cred.Module, builds.BuilderLog) + "." + key) msg.Data = data id := e.Event + "." + e.Module + "." + e.FP + "." + e.At.Format(time.RFC3339Nano) pub, cancel := context.WithTimeout(ctx, 10*time.Second) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go index cc000338..2e36a1db 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/refused_test.go @@ -21,11 +21,14 @@ import ( "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" "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" "git.novox.be/novox/mesh-delivery/sdk/enginereads" "git.novox.be/novox/mesh-delivery/sdk/forms" - "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-builder/internal/build" "mesh-delivery-builder/internal/versions" @@ -57,7 +60,7 @@ func TestARefusedBucketIsSaidAndThePartGoesOn(t *testing.T) { // head is told why and opens it again then. src := &trunkSource{ctx: context.Background(), js: refusingBus{refuseOpen: true, err: refused.err}, heads: &sourceHeads{}, errs: make(chan error, 1), logf: logger.Printf} src.start() - if _, err := src.head(context.Background(), 7, "novox/mesh-catalog"); err == nil || !strings.Contains(err.Error(), delivery.SourceBucket) { + if _, err := src.head(context.Background(), 7, "novox/mesh-catalog"); err == nil || !strings.Contains(err.Error(), sourceBucket) { t.Errorf("a trunk head with the source refused: %v, want why", err) } if src.opened { @@ -74,11 +77,11 @@ func theCountedNodesEngineReads(t *testing.T) { gate := buildrun.NewToolchainGate(nil, func(string, string, []byte, bool) bool { return false }, t.Logf) place := func(nodes ...string) { for _, node := range []string{"n1", "n2"} { - pl := delivery.Placement{Node: node} + pl := meshmoduleassigner.Placement{Node: node} if slices.Contains(nodes, node) { - pl.Modules = []delivery.PlacedModule{{Module: "web"}} + pl.Modules = []meshmoduleassigner.PlacedModule{{Module: "web"}} } - values, err := delivery.PlacementValues(pl) + values, err := meshmoduleassignerclient.PlacementValues(pl) if err != nil { t.Fatal(err) } @@ -86,7 +89,7 @@ func theCountedNodesEngineReads(t *testing.T) { gate.Placement(key, raw, false, time.Now()) } if !slices.Contains(nodes, node) { - key, _ := delivery.PlacementModuleKey(node, "web") + key, _ := meshmoduleassignerclient.ModuleKey(node, "web") gate.Placement(key, nil, true, time.Now()) } } @@ -264,11 +267,11 @@ func aBranchsEndIsHeard(t *testing.T) { f forgeEnd want bool }{ - {forgeEnd{Kind: delivery.ForgeMerged, Repository: "o/r", Branch: "feat/x"}, true}, + {forgeEnd{Kind: forgemsg.ForgeMerged, Repository: "o/r", Branch: "feat/x"}, true}, {forgeEnd{Kind: forgeBranchDeleted, Repository: "o/r", Branch: "feat/x"}, true}, - {forgeEnd{Kind: delivery.ForgePull, Repository: "o/r", Branch: "feat/x", Action: delivery.PullClosed}, true}, - {forgeEnd{Kind: delivery.ForgePull, Repository: "o/r", Branch: "feat/x", Action: delivery.PullOpened}, false}, - {forgeEnd{Kind: delivery.ForgeMerged, Repository: "o/r"}, false}, + {forgeEnd{Kind: forgemsg.ForgePull, Repository: "o/r", Branch: "feat/x", Action: forgemsg.PullClosed}, true}, + {forgeEnd{Kind: forgemsg.ForgePull, Repository: "o/r", Branch: "feat/x", Action: forgemsg.PullOpened}, false}, + {forgeEnd{Kind: forgemsg.ForgeMerged, Repository: "o/r"}, false}, } { if got := c.f.ended(); got != c.want { t.Errorf("%+v ended = %v, want %v", c.f, got, c.want) @@ -290,7 +293,7 @@ func aBranchsEndIsHeard(t *testing.T) { verdict := &wakeMsg{subject: "mesh.stream.mesh-delivery-checker.event.verdict", data: []byte(`{"repository":"o/r"}`)} wanted := &wakeMsg{subject: tagWantedSubject, data: []byte(`{"repository":"o/r","branch":"feat/x","head":"abc","tag":"feat-x"}`)} for _, m := range []*wakeMsg{verdict, wanted} { - tagWanted(context.Background(), nil, m, delivery.CheckerModule, time.Now(), t.Logf) + tagWanted(context.Background(), nil, m, checkerModule, time.Now(), t.Logf) } if !verdict.acked || verdict.delayed != 0 || wanted.acked || wanted.delayed != versions.RemoveAgainAfter || !strings.HasPrefix(tagWantedSubject, "mesh.stream.") { t.Errorf("a verdict acked %v (delayed %v), a tag-wanted acked %v (delayed %v)", verdict.acked, verdict.delayed, wanted.acked, wanted.delayed) @@ -313,20 +316,20 @@ func (failingVersions) KeysOf(context.Context, string) ([]string, error) { func aFailedTrunkBuildIsAFinding(t *testing.T) { st := &memFindings{vals: map[string][]byte{}} f := &findings{store: st, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }} - ended := func(outcome, ref string) delivery.BuildLogEntry { - return delivery.BuildLogEntry{Event: delivery.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Ref: ref, Outcome: outcome} + ended := func(outcome, ref string) builds.BuildLogEntry { + return builds.BuildLogEntry{Event: builds.BuildEndedEvent, Module: "m", FP: "f1", Commit: "c", Ref: ref, Outcome: outcome} } key := health.FindingKey(health.ScopeModule, "m", BuildFailed) - f.buildEnded(context.Background(), ended(delivery.BuildFailed, "refs/pull/3/head")) + f.buildEnded(context.Background(), ended(builds.BuildFailed, "refs/pull/3/head")) if len(st.vals) != 0 { t.Fatalf("a candidate's failed build is a finding: %v", st.vals) } - f.buildEnded(context.Background(), ended(delivery.BuildFailed, "")) + f.buildEnded(context.Background(), ended(builds.BuildFailed, "")) got, err := health.DecodeFinding(st.vals[key]) - if err != nil || got.Observer != delivery.BuilderModule || !strings.Contains(got.Summary, "trunk build of m at f1") { + if err != nil || got.Observer != builds.BuilderModule || !strings.Contains(got.Summary, "trunk build of m at f1") { t.Fatalf("the finding = %+v, %v", got, err) } - f.buildEnded(context.Background(), ended(delivery.BuildDone, "")) + f.buildEnded(context.Background(), ended(builds.BuildDone, "")) if _, ok := st.vals[key]; ok { t.Error("a later done trunk build leaves build-failed") } @@ -375,10 +378,10 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) { t.Fatalf("a bound not written: the finding = %+v, %v", got, err) } watchBounds(context.Background(), refusedWatch{err: errors.New("permissions violation")}, func(jetstream.KeyValueEntry) {}, f.notHeard(context.Background()), nil, t.Logf) - if got, err := health.DecodeFinding(st.vals[heard.Key(delivery.BuilderModule, versions.BoundsState)]); err != nil || !strings.Contains(got.Summary, "permissions violation") { + if got, err := health.DecodeFinding(st.vals[heard.Key(builds.BuilderModule, versions.BoundsState)]); err != nil || !strings.Contains(got.Summary, "permissions violation") { t.Fatalf("bounds not watched: the finding = %+v, %v", got, err) } - delete(st.vals, heard.Key(delivery.BuilderModule, versions.BoundsState)) + delete(st.vals, heard.Key(builds.BuilderModule, versions.BoundsState)) malformed := &wakeMsg{subject: "mesh.seat.oci-artifact-registry.event.published", data: []byte(`{"version":"0.1.4"}`)} unreadKey := health.FindingKey(health.ScopeModule, "oci-artifact-registry", versions.SeatOutcomeUnread) if !h.heard(context.Background(), malformed) || !malformed.acked || st.vals[unreadKey] == nil { @@ -390,7 +393,7 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) { } // ADR 0615 §5 (review round 1): a consumer not there yet is family-not-heard naming its stream, cleared when it binds. notHeard := f.notHeard(context.Background()) - unheardKey := heard.Key(delivery.BuilderModule, "FORGE") + unheardKey := heard.Key(builds.BuilderModule, "FORGE") _ = notHeard.Bind("FORGE", "a branch's end is not heard", func() error { return errors.New("consumer not found") }) if got, err := health.DecodeFinding(st.vals[unheardKey]); err != nil || got.Kind != heard.Kind || !strings.Contains(got.Summary, "does not hear FORGE") { t.Fatalf("a consumer not there: the finding = %+v, %v", got, err) @@ -405,11 +408,11 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) { now := time.Unix(0, 0) fs := forms.ArtifactForms{{Name: "registry", First: true, Kinds: []string{"image"}}, {Name: "oci", Kinds: []string{"image"}}} var closings []versions.OldFormClosedBody - var logged []delivery.BuildLogEntry + var logged []builds.BuildLogEntry marks := &memBounds{keys: map[string]bool{}} window := &oldForm{forms: fs, findings: f, logf: t.Logf, now: func() time.Time { return now }, bounds: func() jetstream.KeyValue { return marks }, said: func(b versions.OldFormClosedBody) { closings = append(closings, b) }, - log: func(e delivery.BuildLogEntry) { logged = append(logged, e) }} + log: func(e builds.BuildLogEntry) { logged = append(logged, e) }} reads := &engineReads{keeper: &enginereads.Keeper{}, window: window, counted: []string{"a", "b"}} newest := []string{forms.FormNeed("oci")} counted := map[string][]string{"a": newest, "b": nil} @@ -442,10 +445,10 @@ func aFailedTrunkBuildIsAFinding(t *testing.T) { if window.w.Open || len(closings) != 1 || !slices.Equal(closings[0].LastHolding, []string{"b"}) || st.vals[heldKey] != nil { t.Errorf("the node back with the newest form: %+v, closings %+v, finding left %v", window.w, closings, st.vals[heldKey] != nil) } - if len(logged) != 1 || logged[0].Event != delivery.BuildSaidEvent || logged[0].Why != versions.SaidOldFormClosed || + if len(logged) != 1 || logged[0].Event != builds.BuildSaidEvent || logged[0].Why != versions.SaidOldFormClosed || !strings.Contains(logged[0].Line, "last held by b") { t.Errorf("the build log = %+v, want the close said once (build.said)", logged) - } else if _, err := delivery.EncodeBuildLogEntry(logged[0]); err != nil { + } else if _, err := builds.EncodeBuildLogEntry(logged[0]); err != nil { t.Errorf("the close is no entry the build log takes: %v", err) } if _, err := window.drop(context.Background(), "b", "op@shanks", "retired", now); err != nil { diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/seats.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/seats.go index 694f6ba8..d6029f81 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/seats.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/seats.go @@ -8,10 +8,10 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry" - "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" - "git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry" - "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry" + "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/contracts/go/seats/ociartifactregistry" "mesh-delivery-builder/internal/build" "mesh-delivery-builder/internal/versions" diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/tagbounds.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/tagbounds.go index 4c6d3618..c9dc9d67 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/tagbounds.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/tagbounds.go @@ -8,7 +8,7 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "mesh-delivery-builder/internal/versions" ) @@ -22,7 +22,7 @@ type tagBounds struct { waits func() []versions.FloorWait logf func(string, ...any) // log writes a narration into the builder's build log (build.said); nil writes none. - log func(delivery.BuildLogEntry) + log func(builds.BuildLogEntry) now func() time.Time bound time.Duration } @@ -82,7 +82,7 @@ func (tb *tagBounds) ended(ctx context.Context, e jetstream.KeyValueEntry) { // In the build log too (design 51 *Tagged versions*): under the builder's own key, no build being claimed yet, the // ask's key standing for the fingerprint. if tb.log != nil { - tb.log(delivery.BuildLogEntry{Event: delivery.BuildSaidEvent, Module: delivery.BuilderModule, FP: e.Key(), Commit: t.Head, + tb.log(builds.BuildLogEntry{Event: builds.BuildSaidEvent, Module: builds.BuilderModule, FP: e.Key(), Commit: t.Head, At: tb.now().UTC(), Line: line, Why: versions.SaidUnanswered}) } tb.arm(ctx, e.Key()) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchains.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchains.go index 38a9259c..82734a16 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchains.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchains.go @@ -12,8 +12,6 @@ import ( "git.novox.be/novox/mesh-sdk/go/process" "github.com/nats-io/nats.go/jetstream" - - "git.novox.be/novox/mesh-sdk/go/delivery" ) // toolchainStreams are the stream each build module carries its toolchain.reported event on, as the bus names it @@ -57,7 +55,7 @@ func (w *toolchainWatch) ensure(module string) error { if w.watched[module] { return nil } - name := delivery.ToolchainsBucket(module) + name := buildrun.ToolchainsBucket(module) kv, err := w.js.KeyValue(w.ctx, name) switch { case errors.Is(err, jetstream.ErrBucketNotFound): diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go index f2bd2cd3..de3e51e1 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/versions.go @@ -11,9 +11,9 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/health" "git.novox.be/novox/mesh-sdk/go/process" @@ -98,7 +98,7 @@ func followPackages(ctx context.Context, kv jetstream.KeyValue, b *build.Builder module, name, named := versions.NamedModule(e.Key(), e.Value()) if named && heard[e.Key()] != module+" "+name { heard[e.Key()] = module + " " + name - logf("%s.%s names a package of %s: its build is looked at again", delivery.PackagesBucket, e.Key(), module) + logf("%s.%s names a package of %s: its build is looked at again", packagesBucket, e.Key(), module) b.PackageNamed(ctx, module) } }) @@ -203,13 +203,13 @@ func floorVia(call toolCaller) versions.FloorSources { // emitNumbered says the builder's event version.numbered (design 51 *Events of the builder*), on its own family apart // from its builds' ends; the name and version are the bus's dedup id. -func emitNumbered(ctx context.Context, js jetstream.JetStream, cred process.Account, b delivery.VersionNumbered) error { +func emitNumbered(ctx context.Context, js jetstream.JetStream, cred process.Account, b builds.VersionNumbered) error { id := "numbered-" + versions.Key(b.Name, b.Version) if b.Again != "" { id += "-again-" + b.Again } - if err := emitEvent(ctx, js, cred, delivery.VersionNumberedEvent, id, b); err != nil { - return fmt.Errorf("saying %s for %s@%s: %w", delivery.VersionNumberedEvent, b.Name, b.Version, err) + if err := emitEvent(ctx, js, cred, builds.VersionNumberedEvent, id, b); err != nil { + return fmt.Errorf("saying %s for %s@%s: %w", builds.VersionNumberedEvent, b.Name, b.Version, err) } return nil } @@ -295,7 +295,7 @@ func conditionModules(id string) []string { // doneVia says whether a build key is done, read from the builder's own builds state: the build's file is held. func doneVia(b *build.Builder) func(ctx context.Context, module, fp string) (bool, error) { return func(ctx context.Context, module, fp string) (bool, error) { - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return false, err } @@ -303,11 +303,11 @@ func doneVia(b *build.Builder) func(ctx context.Context, module, fp string) (boo if err != nil || !found { return false, err } - cur, err := delivery.DecodeBuilderBuild(raw) + cur, err := builds.DecodeBuilderBuild(raw) if err != nil { return false, err } - return cur.State == delivery.BuildDone, nil + return cur.State == builds.BuildDone, nil } } diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index cfa50977..53501b9f 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,16 +3,27 @@ module mesh-delivery-builder 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/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry 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/npmpackageregistry v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient 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/meshdeliveryclient 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/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg 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-contracts/health-and-repair/go/clients/meshhandactsclient 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 ) -require git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261104174638-125929dbc459 - require ( git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun v0.0.0-00010101000000-000000000000 @@ -31,11 +42,7 @@ require ( replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ./buildrun require ( - git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry v0.0.0-20261104174344-ae5266e77639 - git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104174344-ae5266e77639 git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104174344-ae5266e77639 - git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104174344-ae5266e77639 - git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry v0.0.0-20261104174344-ae5266e77639 git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 ) diff --git a/modules/mesh-delivery-builder/internal/build/announce.go b/modules/mesh-delivery-builder/internal/build/announce.go index 7ccdd9b2..c810d215 100644 --- a/modules/mesh-delivery-builder/internal/build/announce.go +++ b/modules/mesh-delivery-builder/internal/build/announce.go @@ -5,7 +5,7 @@ import ( "errors" "fmt" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // **A done build is announced again by hand** (recovery of photos and link2pay on novox, 2026-10-29). The controller takes a @@ -18,7 +18,7 @@ import ( // not done is refused with why: failed (builder_build_again builds it) or waiting (builder_ask_again). func (b *Builder) AnnounceAgain(ctx context.Context, module, fp, why, by string) BuildAgainResult { b.init() - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return BuildAgainResult{Refused: err.Error()} } @@ -29,13 +29,13 @@ func (b *Builder) AnnounceAgain(ctx context.Context, module, fp, why, by string) if !found { return BuildAgainResult{Refused: fmt.Sprintf("%s has no build key: nothing to announce", key)} } - cur, err := delivery.DecodeBuilderBuild(value) + cur, err := builds.DecodeBuilderBuild(value) if err != nil { return BuildAgainResult{Refused: fmt.Sprintf("%s holds what is not a build: %v", key, err)} } switch cur.State { - case delivery.BuildDone: - case delivery.BuildFailed: + case builds.BuildDone: + case builds.BuildFailed: return BuildAgainResult{Refused: fmt.Sprintf("%s is failed, not done: %s builds it again", key, BuildAgainTool)} default: return BuildAgainResult{Refused: fmt.Sprintf("%s is %q, not done: %s asks its waiting asks again", key, cur.State, AskAgainTool)} diff --git a/modules/mesh-delivery-builder/internal/build/announce_test.go b/modules/mesh-delivery-builder/internal/build/announce_test.go index 021c900a..64101416 100644 --- a/modules/mesh-delivery-builder/internal/build/announce_test.go +++ b/modules/mesh-delivery-builder/internal/build/announce_test.go @@ -6,19 +6,19 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) func TestAnnounceAgainRewritesOnlyADoneKeyUnchanged(t *testing.T) { // recovery of photos and link2pay, 2026-10-29 for _, c := range []struct { name string - key *delivery.BuilderBuild + key *builds.BuilderBuild fp string refuse string }{ - {name: "a done key is written again, unchanged, as a new revision", key: &delivery.BuilderBuild{State: delivery.BuildDone, Step: "built"}}, - {name: "a failed key is refused, builder_build_again is for it", refuse: "builder_build_again", key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "x"}}, - {name: "a building key is refused, builder_ask_again is for it", refuse: "builder_ask_again", key: &delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Step: "asked"}}, + {name: "a done key is written again, unchanged, as a new revision", key: &builds.BuilderBuild{State: builds.BuildDone, Step: "built"}}, + {name: "a failed key is refused, builder_build_again is for it", refuse: "builder_build_again", key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "x"}}, + {name: "a building key is refused, builder_ask_again is for it", refuse: "builder_ask_again", key: &builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Step: "asked"}}, {name: "a key with no build is refused", refuse: "has no build key"}, {name: "a fingerprint that is not one is refused", refuse: "fingerprint", fp: "nope"}, } { diff --git a/modules/mesh-delivery-builder/internal/build/askagain.go b/modules/mesh-delivery-builder/internal/build/askagain.go index e91e6ba2..aa9147d3 100644 --- a/modules/mesh-delivery-builder/internal/build/askagain.go +++ b/modules/mesh-delivery-builder/internal/build/askagain.go @@ -12,12 +12,13 @@ import ( "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // **An ask that can no longer be answered is asked again** (novox/hq issue 1945). A build seat's stream gives an ask up -// once it was delivered as often as its worker may (delivery.AskMaxDeliver), and a message given up on, dropped or +// once it was delivered as often as its worker may (sdkstep.AskMaxDeliver), and a message given up on, dropped or // terminated with no step written is answered by no build node: the lane waiting for its step would wait for ever, and on // restart take over the same id and wait again. The builder notices it at the check bound (CheckAfter, the step bound), // a context's bound and no timer of its own: it asks Gone, which reads the bus, and asks such an ask again under a new @@ -90,10 +91,10 @@ type Taken struct { } // Proves is whether a taken ask of kind taken shows that a message of kind waiting asked before it is no longer -// delivered: a holder pulls the kinds in delivery.AskKinds' order, so taking one of the same kind or of a later one means +// delivered: a holder pulls the kinds in sdkstep.AskKinds' order, so taking one of the same kind or of a later one means // its worker had none of waiting's kind left to give. func Proves(taken, waiting string) bool { - t, w := slices.Index(delivery.AskKinds, taken), slices.Index(delivery.AskKinds, waiting) + t, w := slices.Index(sdkstep.AskKinds, taken), slices.Index(sdkstep.AskKinds, waiting) return t >= 0 && w >= 0 && t >= w } @@ -162,11 +163,11 @@ func AskedAt(id string) time.Time { // Dropper removes an ask's message from its build seat's stream (Builder.Gone may be one): the old message of an ask asked // again, so a message given up on does not sit ahead of every later ask of its kind until its age (issue 1945 note 62). type Dropper interface { - Drop(ctx context.Context, a delivery.StepAsk) error + Drop(ctx context.Context, a sdkstep.StepAsk) error } // drop removes the old ask's message when Gone can; a refusal is said and changes nothing else: the new ask is put. -func (b *Builder) drop(ctx context.Context, ask delivery.StepAsk, old string) { +func (b *Builder) drop(ctx context.Context, ask sdkstep.StepAsk, old string) { d, ok := b.Gone.(Dropper) if !ok { return @@ -302,8 +303,8 @@ func (h byHand) Error() string { return "asked again by hand: " + h.why } // answer waits for the step of the lane's ask, and asks it again while it can no longer be answered (Gone) or a person // asked it again (AskAgain); it answers the step and the ask that has it. -func (b *Builder) answer(ctx context.Context, l *lane, ask delivery.StepAsk, asked *askNotes, module, fp string) (delivery.Step, delivery.StepAsk, error) { - key, _ := delivery.BuilderBuildKey(module, fp) +func (b *Builder) answer(ctx context.Context, l *lane, ask sdkstep.StepAsk, asked *askNotes, module, fp string) (sdkstep.Step, sdkstep.StepAsk, error) { + key, _ := builds.BuilderBuildKey(module, fp) again := b.listen(key, ask.Language) defer b.unlisten(key, ask.Language, again) asking := askingOf(asked.step, l.module) @@ -370,7 +371,7 @@ func (b *Builder) answer(ctx context.Context, l *lane, ask delivery.StepAsk, ask // waitOnce waits for the ask's step until it ends, a person asks it again, or the check bound passes: then it asks Gone // and answers errAgain with why when the ask can no longer be answered, errCheck when it waits on. -func (b *Builder) waitOnce(ctx context.Context, ask delivery.StepAsk, again <-chan string) (delivery.Step, string, error) { +func (b *Builder) waitOnce(ctx context.Context, ask sdkstep.StepAsk, again <-chan string) (sdkstep.Step, string, error) { hand, cancel := context.WithCancelCause(ctx) defer cancel(nil) go func() { @@ -387,7 +388,7 @@ func (b *Builder) waitOnce(ctx context.Context, ask delivery.StepAsk, again <-ch defer stop() } step, err := b.Asks.Wait(wait, ask) - if _, declined := delivery.DeclinedOf(step); err == nil && declined { + if _, declined := sdkstep.DeclinedOf(step); err == nil && declined { // A build node off the agreed toolchain sat out (ADR 0584 clause 2). It is no asking: answer() puts the ask again // after a pause, within DeclineBound. return step, step.Reason, errDeclined diff --git a/modules/mesh-delivery-builder/internal/build/askfake_test.go b/modules/mesh-delivery-builder/internal/build/askfake_test.go index 6894884e..c5bcffb2 100644 --- a/modules/mesh-delivery-builder/internal/build/askfake_test.go +++ b/modules/mesh-delivery-builder/internal/build/askfake_test.go @@ -7,7 +7,7 @@ import ( "maps" "strings" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // The build runner a build node runs an ask with is mesh-build-go's (novox/hq ADR 0593 §1), another repository's, @@ -21,18 +21,19 @@ type stepFunc func(ctx context.Context, args map[string]any) (any, error) // askSteps are a build module's steps an ask is run with. type askSteps struct{ Fetch, Build, Copy stepFunc } -// runAsk runs a build ask with steps and answers its delivery.BuildAnswer, or the error that fails the step. -func runAsk(ctx context.Context, steps askSteps, ask delivery.StepAsk) (any, error) { +// runAsk runs a build ask with steps and answers its buildstep.BuildAnswer, or the error that fails the step. +func runAsk(ctx context.Context, steps askSteps, ask buildstep.StepAsk) (any, error) { if ask.Build == nil { return nil, fmt.Errorf("the ask %s holds no build job", ask.ID) } job := *ask.Build fetch := job.Fetch if len(ask.Packages) > 0 && fetch != nil { - fetch = maps.Clone(fetch) - fetch["packages"] = maps.Clone(ask.Packages) + withPackages := *fetch + withPackages.Packages = maps.Clone(ask.Packages) + fetch = &withPackages } - answer := delivery.BuildAnswer{} + answer := buildstep.BuildAnswer{} workspace := "" for _, a := range job.Artifacts { var out any @@ -44,7 +45,7 @@ func runAsk(ctx context.Context, steps askSteps, ask delivery.StepAsk) (any, err out, err = steps.Copy(ctx, map[string]any{"module": job.Module, "artifact": a.Artifact, "from": a.From}) } else { if workspace == "" { - raw, err := asRaw(steps.Fetch(ctx, fetch)) + raw, err := asRaw(steps.Fetch(ctx, fetchArgs(fetch))) if err != nil { return nil, err } @@ -56,7 +57,7 @@ func runAsk(ctx context.Context, steps askSteps, ask delivery.StepAsk) (any, err if err := json.Unmarshal(raw, &f); err != nil || f.Workspace == "" { return nil, fmt.Errorf("the fetch answered no workspace: %s", raw) } - if _, candidate := fetch["base"]; candidate && (len(f.Conflicts) > 0 || f.Merge == "") { + if candidate := fetch.Base != ""; candidate && (len(f.Conflicts) > 0 || f.Merge == "") { return nil, fmt.Errorf("the head does not merge into the trunk: %s", strings.Join(f.Conflicts, ", ")) } workspace = f.Workspace @@ -78,7 +79,7 @@ func runAsk(ctx context.Context, steps askSteps, ask delivery.StepAsk) (any, err if err := json.Unmarshal(raw, &built); err != nil || built.Digest == "" { return nil, fmt.Errorf("the build of %s answered no digest: %s", a.Artifact, raw) } - answer.Artifacts = append(answer.Artifacts, delivery.AskBuilt{Artifact: a.Artifact, Digest: built.Digest, Reference: built.Reference}) + answer.Artifacts = append(answer.Artifacts, buildstep.AskBuilt{Artifact: a.Artifact, Digest: built.Digest, Reference: built.Reference}) } return answer, nil } @@ -92,3 +93,11 @@ func asRaw(v any, err error) (json.RawMessage, error) { } return json.Marshal(v) } + +// fetchArgs is the fetch of an ask as the fetch tool is called with it: its JSON object. +func fetchArgs(f *buildstep.Fetch) map[string]any { + raw, _ := json.Marshal(f) + var args map[string]any + _ = json.Unmarshal(raw, &args) + return args +} diff --git a/modules/mesh-delivery-builder/internal/build/buildagain.go b/modules/mesh-delivery-builder/internal/build/buildagain.go index 62d3deee..6f39187e 100644 --- a/modules/mesh-delivery-builder/internal/build/buildagain.go +++ b/modules/mesh-delivery-builder/internal/build/buildagain.go @@ -8,7 +8,7 @@ import ( "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // **A failed key is built again by hand** (novox/hq issue 1126 note 72; ADR 0429 clause 18: "a fingerprint is built again @@ -88,7 +88,7 @@ func (b *Builder) handEnded(key string) { // ctx is the builder's own context: the build it starts lives on it. func (b *Builder) BuildAgain(ctx context.Context, module, fp, why, by string, bound time.Duration) BuildAgainResult { b.init() - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return BuildAgainResult{Refused: err.Error()} } @@ -99,18 +99,18 @@ func (b *Builder) BuildAgain(ctx context.Context, module, fp, why, by string, bo if !found { return BuildAgainResult{Refused: fmt.Sprintf("%s has no build key: nothing failed. The builder builds a fingerprint by itself once the indexer writes it to its module", key)} } - cur, err := delivery.DecodeBuilderBuild(value) + cur, err := builds.DecodeBuilderBuild(value) if err != nil { return BuildAgainResult{Refused: fmt.Sprintf("%s holds what is not a build: %v", key, err)} } switch { - case cur.State == delivery.BuildDone: + case cur.State == builds.BuildDone: return BuildAgainResult{Refused: fmt.Sprintf("%s is done: nothing to do", key)} - case cur.State == delivery.BuildBuilding || cur.State == delivery.BuildQueued: + case cur.State == builds.BuildBuilding || cur.State == builds.BuildQueued: return BuildAgainResult{Refused: fmt.Sprintf("%s is waiting (%s: %s): %s asks its waiting asks again", key, cur.State, cur.Step, AskAgainTool)} - case cur.State != delivery.BuildFailed: + case cur.State != builds.BuildFailed: return BuildAgainResult{Refused: fmt.Sprintf("%s is %q, not failed", key, cur.State)} - case delivery.FailedUntilChanged(cur.Error) || strings.HasPrefix(cur.Error, buildrun.DigestsDifferPrefix): + case builds.FailedUntilChanged(cur.Error) || strings.HasPrefix(cur.Error, buildrun.DigestsDifferPrefix): return BuildAgainResult{Refused: fmt.Sprintf("%s failed for a reason the builder does not retry on the same fingerprint, it waits for another fingerprint: %s", key, cur.Error)} } // One critical section: the key is refused when it is looked at or asked already, the module still has the fingerprint, @@ -119,7 +119,7 @@ func (b *Builder) BuildAgain(ctx context.Context, module, fp, why, by string, bo b.mu.Lock() m, known := b.known[module] switch { - case !known || m.FP2 != fp: + case !known || m.Fp2 != fp: b.mu.Unlock() return BuildAgainResult{Refused: fmt.Sprintf("module %s does not have the fingerprint %s now (a newer one superseded it, or its modules value is not read yet): the builder builds the module's current fingerprint", module, fp)} case m.Manifest == "": @@ -159,11 +159,11 @@ func (b *Builder) BuildAgain(ctx context.Context, module, fp, why, by string, bo case ended && len(res.Asks) < max(lanes, 0)+boolInt(lanes < 0): // The look ended before every ask was put: say how the key stands, not that it was asked. if v, _, found, err := b.Claimer.Store.Get(ctx, key); err == nil && found { - if cur, err := delivery.DecodeBuilderBuild(v); err == nil { + if cur, err := builds.DecodeBuilderBuild(v); err == nil { switch cur.State { - case delivery.BuildFailed: + case builds.BuildFailed: res.Failed = cur.Error - case delivery.BuildDone: + case builds.BuildDone: default: res.Pending = fmt.Sprintf("the look ended with the key %s (%s) before every ask was put: its log says why", cur.State, cur.Step) } diff --git a/modules/mesh-delivery-builder/internal/build/candidates.go b/modules/mesh-delivery-builder/internal/build/candidates.go index fd9cc4de..fc65815c 100644 --- a/modules/mesh-delivery-builder/internal/build/candidates.go +++ b/modules/mesh-delivery-builder/internal/build/candidates.go @@ -5,8 +5,10 @@ import ( "fmt" "sort" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // How a candidate's fingerprint is built (novox/hq ADR 0484 clause 3, issue 1684): the indexer writes, for each module a @@ -24,7 +26,7 @@ type via struct{ Ref, Base string } // carries. A value that is not a candidate is said and left. func (b *Builder) Candidate(ctx context.Context, key string, value []byte) { b.init() - c, err := delivery.DecodeCandidate(value) + c, err := meshcatalog.DecodeCandidatesByGroupValue(value) if err != nil { b.logf("candidates.%s holds what is not a candidate, left: %v", key, err) return @@ -43,7 +45,7 @@ func (b *Builder) ForgetCandidate(key string) { b.mu.Unlock() } -func (b *Builder) considerCandidate(ctx context.Context, c delivery.Candidate) { +func (b *Builder) considerCandidate(ctx context.Context, c meshcatalog.CandidatesByGroupValue) { for _, p := range c.Pulls { names := make([]string, 0, len(p.Fps)) for name := range p.Fps { @@ -52,33 +54,33 @@ func (b *Builder) considerCandidate(ctx context.Context, c delivery.Candidate) { sort.Strings(names) for _, name := range names { cm := p.Fps[name] - if cm.FP2 == "" || cm.Manifest == "" { + if cm.Fp2 == "" || cm.Manifest == "" { continue // no fingerprint of the new form for it (a reason in none2) or no manifest to build by } b.mu.Lock() trunk, known := b.known[name] b.mu.Unlock() if !known || trunk.Path == "" { - b.logf("candidates.%s: the module %s has no record of the trunk yet, so its fingerprint %s is not built", c.Group, name, cm.FP2) + b.logf("candidates.%s: the module %s has no record of the trunk yet, so its fingerprint %s is not built", c.Group, name, cm.Fp2) continue } - key, err := delivery.BuilderBuildKey(name, cm.FP2) + key, err := builds.BuilderBuildKey(name, cm.Fp2) if err != nil { b.logf("candidates.%s: %v", c.Group, err) continue } - m := delivery.Module{Repository: p.Repository, Commit: p.Head.Head, FP2: cm.FP2, Manifest: cm.Manifest, Path: trunk.Path, - Toolchains: b.agreedFor(ctx, cm.Manifest), Packages: cm.Packages} // the group's package heads (ADR 0573 §4) + m := meshcatalog.ModulesByModuleValue{Repository: p.Repository, Commit: p.Head, Fp2: cm.Fp2, Manifest: cm.Manifest, Path: trunk.Path, + Toolchains: CatalogToolchains(b.agreedFor(ctx, cm.Manifest)), Packages: cm.Packages} // the group's package heads (ADR 0573 §4) if len(cm.Paths) > 0 { // The named paths' objects the preview gave it: the fetch names the paths (runLane, ADR 0499 clause 4). - m.Inputs2 = &delivery.KeptInputs2{Paths: cm.Paths} + m.Inputs2 = &meshcatalog.ModulesByModuleValueInputs2{Paths: cm.Paths} } // The preview is merged onto the trunk head the candidate was previewed onto: the head of the pull request's // repository in the gate's source revision c.Base (ADR 0484 §3). Never the module's value's commit: that is the // commit its fingerprint first appeared at, and may be many merges behind the trunk. - base, err := b.trunkHead(ctx, c.Base, p.Repository) + base, err := b.trunkHead(ctx, uint64(c.Base), p.Repository) if err != nil { - b.logf("candidates.%s: the module %s's fingerprint %s is not built: %v", c.Group, name, cm.FP2, err) + b.logf("candidates.%s: the module %s's fingerprint %s is not built: %v", c.Group, name, cm.Fp2, err) continue } b.mu.Lock() @@ -94,19 +96,19 @@ func (b *Builder) considerCandidate(ctx context.Context, c delivery.Candidate) { // agreedFor are the toolchains the build nodes agree on now, for the languages of the manifest's plan: the toolchains the // candidate's fingerprint was computed with, as the indexer recomputes it whenever they change. A language with none agreed // is left out, and the build then waits for it as the trunk's does. -func (b *Builder) agreedFor(ctx context.Context, manifest string) map[string]delivery.ToolchainUsed { +func (b *Builder) agreedFor(ctx context.Context, manifest string) map[string]builds.ToolchainUsed { plan, err, _ := b.planOf(ctx, manifest) if err != nil { return nil } - out := map[string]delivery.ToolchainUsed{} + out := map[string]builds.ToolchainUsed{} for _, bm := range plan.Modules() { if !plan.NeedsToolchain(bm) { continue } language := buildrun.LanguageOf[bm] - if tc, none := delivery.AgreedToolchain(b.Toolchains.Entries(language)); none == "" { - out[language] = delivery.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe} + if tc, none := buildstep.AgreedToolchain(b.Toolchains.Entries(language)); none == "" { + out[language] = builds.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe} } } if len(out) == 0 { diff --git a/modules/mesh-delivery-builder/internal/build/candidates_test.go b/modules/mesh-delivery-builder/internal/build/candidates_test.go index 41ca4f10..a20e2c40 100644 --- a/modules/mesh-delivery-builder/internal/build/candidates_test.go +++ b/modules/mesh-delivery-builder/internal/build/candidates_test.go @@ -8,16 +8,20 @@ import ( "strings" "testing" + "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/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) // trunkModule writes the trunk's record of the module m at a fingerprint of its own, so that the candidate's fingerprint (fp1) // is one the trunk does not carry. func trunkModule(t *testing.T, r *rig) { t.Helper() - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP: "src1:" + hex64('f'), - FP2: "src2:" + hex64('e'), Manifest: mdigest('a'), Path: "modules/m", Toolchains: agreedToolchains}) + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: "src1:" + hex64('f'), + Fp2: "src2:" + hex64('e'), Manifest: mdigest('a'), Path: "modules/m", Toolchains: CatalogToolchains(agreedToolchains)}) if err != nil { t.Fatal(err) } @@ -30,13 +34,13 @@ func trunkModule(t *testing.T, r *rig) { func candidateValue(t *testing.T, group string, number int, head string) []byte { t.Helper() - pulls := []delivery.CandidatePull{{Head: delivery.Head{Repository: "novox/mesh-catalog", Number: number, Head: head}, Modules: []string{"m"}, - Fps: map[string]delivery.CandidateModule{"m": {FP2: fp1, Manifest: mdigest('a')}}}} - hash, err := delivery.HeadsHash([]delivery.Head{pulls[0].Head}) + pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: int64(number), Head: head, Modules: []string{"m"}, + Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"m": {Fp2: fp1, Manifest: mdigest('a')}}}} + hash, err := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) if err != nil { t.Fatal(err) } - data, err := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: group, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}) + data, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: group, Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) if err != nil { t.Fatal(err) } @@ -53,7 +57,7 @@ const ( // indexerCandidate is a candidate as the indexer writes it, with the gate's source revision it names (mesh-delivery-indexer // internal/index testdata/candidate-written.json, kept equal to what the indexer writes by its own test). -func indexerCandidate(t *testing.T) (rev uint64, heads map[string]string, c delivery.Candidate, raw []byte) { +func indexerCandidate(t *testing.T) (rev uint64, heads map[string]string, c meshcatalog.CandidatesByGroupValue, raw []byte) { t.Helper() data, err := os.ReadFile("../../../mesh-delivery-indexer/internal/index/testdata/candidate-written.json") if err != nil { @@ -69,7 +73,7 @@ func indexerCandidate(t *testing.T) (rev uint64, heads map[string]string, c deli if err := json.Unmarshal(data, &w); err != nil { t.Fatal(err) } - if c, err = delivery.DecodeCandidate(w.Candidate); err != nil { + if c, err = meshcatalog.DecodeCandidatesByGroupValue(w.Candidate); err != nil { t.Fatal(err) } return w.Source.Rev, w.Source.Heads, c, w.Candidate @@ -91,8 +95,8 @@ func TestACandidatesFingerprintIsBuiltOnceAsAKeyOfItsOwnFromTheHeadMergedOntoThe return heads[repository], nil } const valueCommit = "7a74cee83f88e6585a5511c3cc53bdf0f5303d20" - value, err := delivery.EncodeModule(delivery.Module{Repository: p.Repository, Commit: valueCommit, FP2: "src2:" + hex64('e'), - Manifest: cm.Manifest, Path: "modules/avahi", Toolchains: agreedToolchains}) + value, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: p.Repository, Commit: valueCommit, Fp2: "src2:" + hex64('e'), + Manifest: cm.Manifest, Path: "modules/avahi", Toolchains: CatalogToolchains(agreedToolchains)}) if err != nil { t.Fatal(err) } @@ -108,7 +112,7 @@ func TestACandidatesFingerprintIsBuiltOnceAsAKeyOfItsOwnFromTheHeadMergedOntoThe t.Fatalf("%d fetches of the indexer's candidate, want one", len(fetches)) } f := fetches[0].body.(map[string]any) - if want := heads[p.Repository]; want == "" || f["base"] != want || f["base"] == valueCommit || f["commit"] != p.Head.Head { + if want := heads[p.Repository]; want == "" || f["base"] != want || f["base"] == valueCommit || f["commit"] != p.Head { t.Errorf("fetch args = %+v, want the head merged onto the trunk head %.8s of source revision %d", f, want, rev) } } @@ -117,7 +121,7 @@ func TestACandidatesFingerprintIsBuiltOnceAsAKeyOfItsOwnFromTheHeadMergedOntoThe r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) r.b.Wait() v := valueOf(t, r.st, r.key) - if v.State != delivery.BuildDone || len(v.Artifacts) != 1 { + if v.State != builds.BuildDone || len(v.Artifacts) != 1 { t.Fatalf("build = %+v, want the candidate's fingerprint built", v) } fetches := r.bm.callsOf("build_go_fetch") @@ -158,7 +162,7 @@ func TestAFingerprintTheTrunkCarriesIsBuiltFromTheTrunksCommit(t *testing.T) { / t.Errorf("fetch args = %+v: a trunk fingerprint is built from the trunk's commit alone", f) } } - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone { t.Errorf("build = %+v", v) } } @@ -174,7 +178,7 @@ func TestACandidateWhoseHeadDoesNotMergeIsFailedAndNoTrunkBuildIsStoredUnderIt(t r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) r.b.Wait() v := valueOf(t, r.st, r.key) - if v.State != delivery.BuildFailed || (name == "conflict" && !strings.Contains(v.Error, "go.mod")) || len(r.bm.callsOf("build_go_build")) != 0 { + if v.State != builds.BuildFailed || (name == "conflict" && !strings.Contains(v.Error, "go.mod")) || len(r.bm.callsOf("build_go_build")) != 0 { t.Errorf("%s: build = %+v, builds %d, want failed and nothing built", name, v, len(r.bm.callsOf("build_go_build"))) } } diff --git a/modules/mesh-delivery-builder/internal/build/claim.go b/modules/mesh-delivery-builder/internal/build/claim.go index 50b933a9..a45baac7 100644 --- a/modules/mesh-delivery-builder/internal/build/claim.go +++ b/modules/mesh-delivery-builder/internal/build/claim.go @@ -9,8 +9,9 @@ import ( "strings" "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-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) // ErrConflict is what a Store answers when a Create finds the key or an Update finds another revision than the one given. @@ -37,7 +38,7 @@ type Claimer struct { type Claim struct { Key string Rev uint64 - Build delivery.BuilderBuild + Build builds.BuilderBuild // Gone are the references of a done key the artifact store no longer holds, for which it was claimed again // (ClaimOptions.Stored; issue 1937): said in the log and the key's step. Gone []string @@ -71,7 +72,7 @@ type ClaimOptions struct { // StoredReferences are the references of a done build's bundles and archives that name a blob in the artifact store // (`artifact-store:////blobs/`): what the store's collector can delete. An image or an upstream // copy is a manifest, which the collector keeps, and a package lives in the package registry. -func StoredReferences(b delivery.BuilderBuild) []string { +func StoredReferences(b builds.BuilderBuild) []string { var out []string for _, a := range b.Artifacts { if strings.HasPrefix(a.Reference, "artifact-store://") && strings.Contains(a.Reference, "/blobs/") && @@ -89,12 +90,12 @@ func GoneStep(gone []string) string { // Unpublished is whether a done build holds an image built without a push or a package built without a publish: it has // no reference (a candidate's, ADR 0487 clause 12, ADR 0498 clauses 5 and 6). -func Unpublished(b delivery.BuilderBuild) bool { return len(UnpublishedModules(b)) > 0 } +func Unpublished(b builds.BuilderBuild) bool { return len(UnpublishedModules(b)) > 0 } // UnpublishedModules are the build modules that publish what a done build holds unpublished: the image build module for // an image, the Node build module for a package; sorted, none when it holds nothing unpublished. -func UnpublishedModules(b delivery.BuilderBuild) []string { - if b.State != delivery.BuildDone { +func UnpublishedModules(b builds.BuilderBuild) []string { + if b.State != builds.BuildDone { return nil } var image, pkg bool @@ -115,7 +116,7 @@ func UnpublishedModules(b delivery.BuilderBuild) []string { // publishable are the build modules of UnpublishedModules that can publish now: the image build module only while a node // runs it (imageBuilder). -func publishable(b delivery.BuilderBuild, imageBuilder func() bool) []string { +func publishable(b builds.BuilderBuild, imageBuilder func() bool) []string { var out []string for _, bm := range UnpublishedModules(b) { if bm != ImageBuildModule || (imageBuilder != nil && imageBuilder()) { @@ -128,8 +129,8 @@ func publishable(b delivery.BuilderBuild, imageBuilder func() bool) []string { // Unreferenced is whether a done build holds a bundle, an archive or an upstream copy that names no reference: a key done // before R4e, whose artifacts stayed on their build nodes (ADR 0498 clause 8). An artifact of no kind (a key written before // kinds) is one too. Claim takes such a key again, once: its new revision names them. -func Unreferenced(b delivery.BuilderBuild) bool { - if b.State != delivery.BuildDone { +func Unreferenced(b builds.BuilderBuild) bool { + if b.State != builds.BuildDone { return false } for _, a := range b.Artifacts { @@ -151,7 +152,7 @@ func Unreferenced(b delivery.BuilderBuild) bool { // - a done key of the trunk's fingerprint whose bundle or archive blob the artifact store no longer holds // (ClaimOptions.Stored, issue 1937) is claimed building as a new revision and built whole, its step saying which; // - a failed key is taken again as a new revision of the same key (ADR 0429 clause 18), except one failed for a kind no -// builder builds, for an upstream pinned by no digest or a package version taken (delivery.FailedUntilChanged: ADR 0478 +// builder builds, for an upstream pinned by no digest or a package version taken (builds.FailedUntilChanged: ADR 0478 // clause 2, ADR 0498 clauses 4 and 5) or for digests that differ (buildrun.DigestsDifferPrefix, clause 4), // which wait for another fingerprint, and one failed for want of an image builder (NoImageBuilderPrefix), which waits for // another fingerprint or a node that runs the image build module (ClaimOptions.ImageBuilder); @@ -160,11 +161,11 @@ func Unreferenced(b delivery.BuilderBuild) bool { // - a key left queued or building by this builder is taken as it stands (ADR 0478 clause 5: a stopped builder takes its // own again when it starts), and so is one left by another builder when ClaimOptions.TakeOver (clause 6). func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string, opt ClaimOptions) (*Claim, error) { - key, err := delivery.BuilderBuildKey(module, fingerprint) + key, err := builds.BuilderBuildKey(module, fingerprint) if err != nil { return nil, err } - building := delivery.BuilderBuild{State: delivery.BuildBuilding, By: c.By, Step: "claimed", Manifest: manifest} + building := builds.BuilderBuild{State: builds.BuildBuilding, By: c.By, Step: "claimed", Manifest: manifest} value, rev, found, err := c.Store.Get(ctx, key) if err != nil { return nil, fmt.Errorf("%s: %w", key, err) @@ -172,12 +173,12 @@ func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string if !found { return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Create(ctx, key, data) }) } - cur, err := delivery.DecodeBuilderBuild(value) + cur, err := builds.DecodeBuilderBuild(value) if err != nil { return nil, fmt.Errorf("%s holds what is not a build: %w", key, err) } switch { - case cur.State == delivery.BuildDone: + case cur.State == builds.BuildDone: if Unreferenced(cur) { return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) }) } @@ -207,13 +208,13 @@ func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string return &Claim{Key: key, Rev: rev, Build: cur}, nil } return nil, nil - case cur.State == delivery.BuildFailed && (delivery.FailedUntilChanged(cur.Error) || strings.HasPrefix(cur.Error, buildrun.DigestsDifferPrefix)): + case cur.State == builds.BuildFailed && (builds.FailedUntilChanged(cur.Error) || strings.HasPrefix(cur.Error, buildrun.DigestsDifferPrefix)): return nil, nil - case cur.State == delivery.BuildFailed && strings.HasPrefix(cur.Error, NoImageBuilderPrefix) && (opt.ImageBuilder == nil || !opt.ImageBuilder()): + case cur.State == builds.BuildFailed && strings.HasPrefix(cur.Error, NoImageBuilderPrefix) && (opt.ImageBuilder == nil || !opt.ImageBuilder()): return nil, nil - case cur.State == delivery.BuildFailed: + case cur.State == builds.BuildFailed: return c.write(ctx, key, building, func(data []byte) (uint64, error) { return c.Store.Update(ctx, key, data, rev) }) - case cur.State == delivery.BuildQueued && cur.Waits != "": + case cur.State == builds.BuildQueued && cur.Waits != "": if opt.Published == nil || !opt.Published(cur.Waits) { return nil, nil // the package it waits for is not published yet } @@ -224,8 +225,8 @@ func (c Claimer) Claim(ctx context.Context, module, fingerprint, manifest string return nil, nil } -func (c Claimer) write(_ context.Context, key string, b delivery.BuilderBuild, put func([]byte) (uint64, error)) (*Claim, error) { - data, err := delivery.EncodeBuilderBuild(b) +func (c Claimer) write(_ context.Context, key string, b builds.BuilderBuild, put func([]byte) (uint64, error)) (*Claim, error) { + data, err := builds.EncodeBuilderBuild(b) if err != nil { return nil, err } @@ -236,18 +237,18 @@ func (c Claimer) write(_ context.Context, key string, b delivery.BuilderBuild, p if err != nil { return nil, fmt.Errorf("%s: %w", key, err) } - b.V = delivery.Version + b.V = keys.Version return &Claim{Key: key, Rev: rev, Build: b}, nil } // Finish moves the claimed key to b by compare-and-set at the claim's revision, and writes nothing when b is the value // the key holds. A key that changed meanwhile is ErrConflict: the claim is no longer this builder's. -func (c Claimer) Finish(ctx context.Context, cl *Claim, b delivery.BuilderBuild) error { - b.V = delivery.Version - if delivery.SameBuilderBuild(cl.Build, b) { +func (c Claimer) Finish(ctx context.Context, cl *Claim, b builds.BuilderBuild) error { + b.V = keys.Version + if builds.SameBuilderBuild(cl.Build, b) { return nil } - data, err := delivery.EncodeBuilderBuild(b) + data, err := builds.EncodeBuilderBuild(b) if err != nil { return err } diff --git a/modules/mesh-delivery-builder/internal/build/claim_test.go b/modules/mesh-delivery-builder/internal/build/claim_test.go index 54cf5e55..51e93a86 100644 --- a/modules/mesh-delivery-builder/internal/build/claim_test.go +++ b/modules/mesh-delivery-builder/internal/build/claim_test.go @@ -6,7 +6,7 @@ import ( "sync" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // memStore is a bucket's create and compare-and-set in memory, with a revision per write. @@ -78,13 +78,13 @@ const ( digest = "sha256:2222222222222222222222222222222222222222222222222222222222222222" ) -func seed(t *testing.T, st *memStore, b delivery.BuilderBuild) string { +func seed(t *testing.T, st *memStore, b builds.BuilderBuild) string { t.Helper() - key, err := delivery.BuilderBuildKey("m", fp1) + key, err := builds.BuilderBuildKey("m", fp1) if err != nil { t.Fatal(err) } - data, err := delivery.EncodeBuilderBuild(b) + data, err := builds.EncodeBuilderBuild(b) if err != nil { t.Fatal(err) } @@ -92,9 +92,9 @@ func seed(t *testing.T, st *memStore, b delivery.BuilderBuild) string { return key } -func valueOf(t *testing.T, st *memStore, key string) delivery.BuilderBuild { +func valueOf(t *testing.T, st *memStore, key string) builds.BuilderBuild { t.Helper() - b, err := delivery.DecodeBuilderBuild(st.vals[key]) + b, err := builds.DecodeBuilderBuild(st.vals[key]) if err != nil { t.Fatal(err) } @@ -108,7 +108,7 @@ func TestClaimCreatesTheKeyBuildingAndASecondBuilderGetsNothing(t *testing.T) { if err != nil || got == nil { t.Fatalf("first claim = %v, %v, want a claim", got, err) } - if v := valueOf(t, st, got.Key); v.State != delivery.BuildBuilding || v.By != "novox.a" || v.Manifest != hex64('a') { + if v := valueOf(t, st, got.Key); v.State != builds.BuildBuilding || v.By != "novox.a" || v.Manifest != hex64('a') { t.Errorf("the key holds %+v, want building by novox.a with the manifest digest", v) } if second, err := b.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}); err != nil || second != nil { @@ -117,7 +117,7 @@ func TestClaimCreatesTheKeyBuildingAndASecondBuilderGetsNothing(t *testing.T) { // A claim losing the create race gets nothing (merged into this test for the test budget, design 51). st = newMem() - st.before = func() { seed(t, st, delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.b"}) } + st.before = func() { seed(t, st, builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.b"}) } got, err = Claimer{Store: st, By: "novox.a"}.Claim(context.Background(), "m", fp1, "", ClaimOptions{}) if err != nil || got != nil { t.Errorf("claim = %v, %v, want none: the create failed because the key exists", got, err) @@ -126,7 +126,7 @@ func TestClaimCreatesTheKeyBuildingAndASecondBuilderGetsNothing(t *testing.T) { func TestADoneKeyIsNeverTakenAgain(t *testing.T) { // 1607/AC3; a key whose artifacts name references (ADR 0498 clause 8) st := newMem() - seed(t, st, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []delivery.BuiltArtifact{{Name: "x", Kind: "bundle", Digest: digest, Reference: "artifact-store://m/x/blobs/" + digest}}}) + seed(t, st, builds.BuilderBuild{State: builds.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []builds.BuiltArtifact{{Name: "x", Kind: "bundle", Digest: digest, Reference: "artifact-store://m/x/blobs/" + digest}}}) writes := st.writes got, err := Claimer{Store: st, By: "novox.a"}.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) if err != nil || got != nil || st.writes != writes { @@ -136,25 +136,25 @@ func TestADoneKeyIsNeverTakenAgain(t *testing.T) { // 1607/AC3; a key whose arti func TestAFailedKeyIsTakenAgainAsANewRevisionExceptARefusedKind(t *testing.T) { // 1607/AC3, ADR 0478 clause 2 st := newMem() - key := seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: "the step failed"}) + key := seed(t, st, builds.BuilderBuild{State: builds.BuildFailed, By: "novox.a", Error: "the step failed"}) rev := st.revs[key] got, err := Claimer{Store: st, By: "novox.a"}.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) if err != nil || got == nil || st.revs[key] <= rev { t.Fatalf("claim = %v, %v, revision %d (was %d), want a claim as a new revision of the same key", got, err, st.revs[key], rev) } - if v := valueOf(t, st, key); v.State != delivery.BuildBuilding || v.Error != "" { + if v := valueOf(t, st, key); v.State != builds.BuildBuilding || v.Error != "" { t.Errorf("the key holds %+v, want building with no error", v) } st = newMem() - key = seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: delivery.RefusedKind + "package of language \"python\" (artifact x)"}) + key = seed(t, st, builds.BuilderBuild{State: builds.BuildFailed, By: "novox.a", Error: builds.RefusedKind + "package of language \"python\" (artifact x)"}) writes := st.writes if got, err := (Claimer{Store: st, By: "novox.a"}).Claim(context.Background(), "m", fp1, "", ClaimOptions{}); err != nil || got != nil || st.writes != writes { t.Errorf("a refused kind: claim = %v, %v, writes %d (was %d), want none and no write", got, err, st.writes, writes) } - for _, e := range []string{delivery.NotPinned + "up: nats:2 names no digest", delivery.VersionTaken + "@novox/mesh-sdk@0.1.15"} { // B3, B5: ADR 0498 clauses 4, 5 + for _, e := range []string{builds.NotPinned + "up: nats:2 names no digest", builds.VersionTaken + "@novox/mesh-sdk@0.1.15"} { // B3, B5: ADR 0498 clauses 4, 5 st = newMem() - seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: e}) + seed(t, st, builds.BuilderBuild{State: builds.BuildFailed, By: "novox.a", Error: e}) writes = st.writes if got, err := (Claimer{Store: st, By: "novox.a"}).Claim(context.Background(), "m", fp1, "", ClaimOptions{}); err != nil || got != nil || st.writes != writes { t.Errorf("%s: claim = %v, %v, writes %d (was %d), want none until the fingerprint changes", e, got, err, st.writes, writes) @@ -164,7 +164,7 @@ func TestAFailedKeyIsTakenAgainAsANewRevisionExceptARefusedKind(t *testing.T) { func TestAKeyFailedForNoImageBuilderIsTakenAgainOnlyWhenANodeHasTheModule(t *testing.T) { // 1705b st := newMem() - key := seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, By: "novox.a", Error: NoImageBuilderPrefix + " no node has mesh-build-image"}) + key := seed(t, st, builds.BuilderBuild{State: builds.BuildFailed, By: "novox.a", Error: NoImageBuilderPrefix + " no node has mesh-build-image"}) writes := st.writes c := Claimer{Store: st, By: "novox.a"} for _, opt := range []ClaimOptions{{}, {ImageBuilder: func() bool { return false }}} { @@ -173,7 +173,7 @@ func TestAKeyFailedForNoImageBuilderIsTakenAgainOnlyWhenANodeHasTheModule(t *tes } } got, err := c.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{ImageBuilder: func() bool { return true }}) - if err != nil || got == nil || valueOf(t, st, key).State != delivery.BuildBuilding { + if err != nil || got == nil || valueOf(t, st, key).State != builds.BuildBuilding { t.Errorf("claim = %v, %v, want a claim with the key building", got, err) } } @@ -182,11 +182,11 @@ func TestFinishMovesTheKeyByCompareAndSetAndWritesOnlyADifferentValue(t *testing st := newMem() c := Claimer{Store: st, By: "novox.a"} got, _ := c.Claim(context.Background(), "m", fp1, hex64('a'), ClaimOptions{}) - done := delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []delivery.BuiltArtifact{{Name: "x", Kind: "archive", Digest: digest}}} + done := builds.BuilderBuild{State: builds.BuildDone, By: "novox.a", Manifest: hex64('a'), Artifacts: []builds.BuiltArtifact{{Name: "x", Kind: "archive", Digest: digest}}} if err := c.Finish(context.Background(), got, done); err != nil { t.Fatal(err) } - if v := valueOf(t, st, got.Key); v.State != delivery.BuildDone || v.Artifacts[0].Digest != digest { + if v := valueOf(t, st, got.Key); v.State != builds.BuildDone || v.Artifacts[0].Digest != digest { t.Errorf("the key holds %+v", v) } writes := st.writes @@ -202,7 +202,7 @@ func TestFinishMovesTheKeyByCompareAndSetAndWritesOnlyADifferentValue(t *testing c = Claimer{Store: st, By: "novox.a"} got, _ = c.Claim(context.Background(), "m", fp1, "", ClaimOptions{}) st.put(got.Key, []byte(`{"v":1,"state":"failed","error":"someone else"}`)) - err := c.Finish(context.Background(), got, delivery.BuilderBuild{State: delivery.BuildFailed, Error: "mine"}) + err := c.Finish(context.Background(), got, builds.BuilderBuild{State: builds.BuildFailed, Error: "mine"}) if !errors.Is(err, ErrConflict) { t.Errorf("Finish = %v, want ErrConflict", err) } diff --git a/modules/mesh-delivery-builder/internal/build/fakes_test.go b/modules/mesh-delivery-builder/internal/build/fakes_test.go index 2b993dbf..bd18bba5 100644 --- a/modules/mesh-delivery-builder/internal/build/fakes_test.go +++ b/modules/mesh-delivery-builder/internal/build/fakes_test.go @@ -10,7 +10,8 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) type call struct { @@ -36,12 +37,12 @@ func (f *fakeCaller) CallOn(node, key string, body any) (json.RawMessage, error) // fakeSteps holds the steps values by `/`; Wait on a key it does not hold blocks until the context ends. type fakeSteps struct { mu sync.Mutex - steps map[string]delivery.Step + steps map[string]buildstep.Step // during, when set, is called as a wait starts and the function it answers as the wait ends. during func(key string) func() } -func (f *fakeSteps) Wait(ctx context.Context, module, key string) (delivery.Step, error) { +func (f *fakeSteps) Wait(ctx context.Context, module, key string) (buildstep.Step, error) { f.mu.Lock() s, ok := f.steps[module+"/"+key] during := f.during @@ -53,11 +54,11 @@ func (f *fakeSteps) Wait(ctx context.Context, module, key string) (delivery.Step return s, nil } <-ctx.Done() - return delivery.Step{}, context.Cause(ctx) + return buildstep.Step{}, context.Cause(ctx) } -func stepAt(node, id, state, reason string, result string) delivery.Step { - s := delivery.Step{ID: id, Tool: "build_go_fetch", Node: node, State: state, Reason: reason} +func stepAt(node, id, state, reason string, result string) buildstep.Step { + s := buildstep.Step{ID: id, Tool: "build_go_fetch", Node: node, State: state, Reason: reason} if result != "" { s.Result = json.RawMessage(result) } @@ -67,7 +68,7 @@ func stepAt(node, id, state, reason string, result string) delivery.Step { // presenceValue is a watchman presence value, as its writer encodes it (mesh-sdk go/delivery EncodePresence). func presenceValue(t *testing.T, online bool) []byte { t.Helper() - b, err := delivery.EncodePresence(delivery.Presence{Online: online}) + b, err := json.Marshal(meshwatchdogs.PresenceByNodeValue{Online: online}) if err != nil { t.Fatal(err) } @@ -77,7 +78,7 @@ func presenceValue(t *testing.T, online bool) []byte { // tcValue is a toolchains value, as the build module's writer encodes it (mesh-sdk go/delivery EncodeToolchain). func tcValue(t *testing.T, language, version string) []byte { t.Helper() - b, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: "1", At: time.Now()}) + b, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: language, Version: version, Recipe: "1", At: time.Now().UTC()}) if err != nil { t.Fatal(err) } @@ -85,10 +86,10 @@ func tcValue(t *testing.T, language, version string) []byte { } // tcs are the toolchain entries of a language on nodes, as the toolchains set gives them. -func tcs(language, version string, nodes ...string) []delivery.NodeToolchain { - var out []delivery.NodeToolchain +func tcs(language, version string, nodes ...string) []buildstep.NodeToolchain { + var out []buildstep.NodeToolchain for _, n := range nodes { - out = append(out, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: language, Version: version, Recipe: "1"}}) + out = append(out, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: language, Version: version, Recipe: "1"}}) } return out } diff --git a/modules/mesh-delivery-builder/internal/build/fixtures_test.go b/modules/mesh-delivery-builder/internal/build/fixtures_test.go index 467f91d8..cb04934c 100644 --- a/modules/mesh-delivery-builder/internal/build/fixtures_test.go +++ b/modules/mesh-delivery-builder/internal/build/fixtures_test.go @@ -8,8 +8,9 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // result reads a captured step value of mesh-build-image (testdata/captured/README) and returns its result. @@ -19,11 +20,11 @@ func captured(t *testing.T, name string) json.RawMessage { if err != nil { t.Fatal(err) } - var step delivery.Step + var step sdkstep.Step if err := json.Unmarshal(raw, &step); err != nil { t.Fatalf("%s: %v", name, err) } - if step.State != delivery.StepPassed || len(step.Result) == 0 { + if step.State != sdkstep.StepPassed || len(step.Result) == 0 { t.Fatalf("%s: state %q, result %s, want a passed step with a result", name, step.State, step.Result) } return step.Result @@ -43,15 +44,15 @@ func TestCapturedImageStepsDecodeWithTheBuildersOwnTypes(t *testing.T) { // revi }, Build: func(context.Context, map[string]any) (any, error) { return json.RawMessage(captured(t, tc.build)), nil }, } - ask := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskDelivery, Language: delivery.AskImage, Asker: delivery.BuilderModule, - At: time.Now(), Build: &delivery.BuildJob{Module: "m", Path: "modules/m", Publish: tc.publish, - Fetch: map[string]any{"repository": "novox/mesh-catalog", "commit": strings.Repeat("a", 40)}, - Artifacts: []delivery.AskArtifact{{Artifact: "server"}}}} + ask := sdkstep.StepAsk{ID: buildstep.NewID(), Kind: sdkstep.AskDelivery, Language: sdkstep.AskImage, Asker: builds.BuilderModule, + At: time.Now(), Build: &sdkstep.BuildJob{Module: "m", Path: "modules/m", Publish: tc.publish, + Fetch: &sdkstep.Fetch{Repository: "novox/mesh-catalog", Commit: strings.Repeat("a", 40)}, + Artifacts: []sdkstep.AskArtifact{{Artifact: "server"}}}} out, err := runAsk(context.Background(), steps, ask) if err != nil { t.Fatalf("%s: %v", tc.build, err) } - a := out.(delivery.BuildAnswer) + a := out.(sdkstep.BuildAnswer) if len(a.Artifacts) != 1 || a.Artifacts[0].Digest == "" || (a.Artifacts[0].Reference != "") != tc.ref { t.Errorf("%s answered %+v, want a digest and a reference only for the trunk", tc.build, a) } diff --git a/modules/mesh-delivery-builder/internal/build/followups_test.go b/modules/mesh-delivery-builder/internal/build/followups_test.go index ce0efd65..ac03518e 100644 --- a/modules/mesh-delivery-builder/internal/build/followups_test.go +++ b/modules/mesh-delivery-builder/internal/build/followups_test.go @@ -5,20 +5,22 @@ 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/modules/mesh-delivery-builder/builds" ) // 1660: at start the builder closes a key left building for a fingerprint the module no longer has. func TestAKeyLeftBuildingForAFingerprintTheModuleNoLongerHasIsClosedAtStart(t *testing.T) { // issue 1660 r := newRig(t, goBundle) - stale := seed(t, r.st, delivery.BuilderBuild{State: delivery.BuildBuilding, By: "ace.builder", Manifest: manifestD}) // m at fp1 - other, _ := delivery.BuilderBuildKey("gone", fp1) - data, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "ace.builder", Manifest: manifestD}) + stale := seed(t, r.st, builds.BuilderBuild{State: builds.BuildBuilding, By: "ace.builder", Manifest: manifestD}) // m at fp1 + other, _ := builds.BuilderBuildKey("gone", fp1) + data, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "ace.builder", Manifest: manifestD}) r.st.put(other, data) // The module m has moved on to another fingerprint. - moved := delivery.Module{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP: "src1:" + hex64('f'), - FP2: "src2:" + hex64('7'), Manifest: manifestD, Path: "modules/m", Toolchains: agreedToolchains} - enc, _ := delivery.EncodeModule(moved) + moved := meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: "src1:" + hex64('f'), + Fp2: "src2:" + hex64('7'), Manifest: manifestD, Path: "modules/m", Toolchains: CatalogToolchains(agreedToolchains)} + enc, _ := meshcatalogclient.EncodeModule(moved) r.b.Module(context.Background(), "m", enc) r.b.Wait() if err := r.b.Sweep(context.Background()); err != nil { @@ -26,7 +28,7 @@ func TestAKeyLeftBuildingForAFingerprintTheModuleNoLongerHasIsClosedAtStart(t *t } for _, k := range []string{stale, other} { v := valueOf(t, r.st, k) - if v.State != delivery.BuildFailed || !strings.Contains(v.Error, "no longer") || !strings.Contains(v.Error, "ace.builder") { + if v.State != builds.BuildFailed || !strings.Contains(v.Error, "no longer") || !strings.Contains(v.Error, "ace.builder") { t.Errorf("%s = %+v, want it closed as failed, naming why and who left it", k, v) } } @@ -34,13 +36,13 @@ func TestAKeyLeftBuildingForAFingerprintTheModuleNoLongerHasIsClosedAtStart(t *t func TestSweepLeavesAKeyOfTheModulesCurrentFingerprintForTheBuildToTakeOver(t *testing.T) { // issue 1660 r := newRig(t, goBundle) - seed(t, r.st, delivery.BuilderBuild{State: delivery.BuildBuilding, By: "ace.builder", Manifest: manifestD}) + seed(t, r.st, builds.BuilderBuild{State: builds.BuildBuilding, By: "ace.builder", Manifest: manifestD}) r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) // takes it over and builds it r.b.Wait() if err := r.b.Sweep(context.Background()); err != nil { t.Fatal(err) } - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || v.By != "novox.builder" { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || v.By != "novox.builder" { t.Errorf("build = %+v, want done by novox.builder", v) } } diff --git a/modules/mesh-delivery-builder/internal/build/gone_test.go b/modules/mesh-delivery-builder/internal/build/gone_test.go index c114010a..fea331ca 100644 --- a/modules/mesh-delivery-builder/internal/build/gone_test.go +++ b/modules/mesh-delivery-builder/internal/build/gone_test.go @@ -10,7 +10,7 @@ import ( "testing" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // A done key whose blob the artifact store no longer holds is built again (novox/hq issue 1937): on 2026-10-24 the @@ -76,9 +76,9 @@ func (l *logged) has(words ...string) bool { const goneRef = "artifact-store://m/tool/blobs/" + digest -func doneKeyNaming(ref string) delivery.BuilderBuild { - return delivery.BuilderBuild{State: delivery.BuildDone, By: "shanks.builder", Manifest: manifestD, Step: "built mesh-build-go on shanks", - Artifacts: []delivery.BuiltArtifact{{Name: "tool", Kind: "bundle", Digest: digest, Reference: ref}}} +func doneKeyNaming(ref string) builds.BuilderBuild { + return builds.BuilderBuild{State: builds.BuildDone, By: "shanks.builder", Manifest: manifestD, Step: "built mesh-build-go on shanks", + Artifacts: []builds.BuiltArtifact{{Name: "tool", Kind: "bundle", Digest: digest, Reference: ref}}} } // 1937/AC3 (unit, the claim): a done key of the trunk's fingerprint whose blob is gone is claimed building as a new @@ -86,7 +86,7 @@ func doneKeyNaming(ref string) delivery.BuilderBuild { func TestADoneTrunkKeyWhoseBlobIsGoneIsClaimedAgain(t *testing.T) { st := newMem() c := Claimer{Store: st, By: "novox.builder"} - key, _ := delivery.BuilderBuildKey("m", fp1) + key, _ := builds.BuilderBuildKey("m", fp1) rev := putBuild(t, st, key, doneKeyNaming(goneRef)) held := &fakeHeld{gone: map[string]bool{goneRef: true}} @@ -98,11 +98,11 @@ func TestADoneTrunkKeyWhoseBlobIsGoneIsClaimedAgain(t *testing.T) { if err != nil || cl == nil { t.Fatalf("claim = %+v, %v; want the key claimed again", cl, err) } - if cl.Rev <= rev || cl.Build.State != delivery.BuildBuilding || !slices.Equal(cl.Gone, []string{goneRef}) { + if cl.Rev <= rev || cl.Build.State != builds.BuildBuilding || !slices.Equal(cl.Gone, []string{goneRef}) { t.Errorf("claim = %+v (revision was %d); want building as a new revision naming the gone reference", cl, rev) } v := valueOf(t, st, key) - if v.State != delivery.BuildBuilding || !strings.Contains(v.Step, "no longer holds") || !strings.Contains(v.Step, goneRef) { + if v.State != builds.BuildBuilding || !strings.Contains(v.Step, "no longer holds") || !strings.Contains(v.Step, goneRef) { t.Errorf("the key holds %+v; want building, its step naming the gone reference", v) } } @@ -112,9 +112,9 @@ func TestADoneTrunkKeyWhoseBlobIsGoneIsClaimedAgain(t *testing.T) { func TestADoneTrunkKeyStaysDoneWhileTheStoreHoldsItsBlobs(t *testing.T) { st := newMem() c := Claimer{Store: st, By: "novox.builder"} - key, _ := delivery.BuilderBuildKey("m", fp1) + key, _ := builds.BuilderBuildKey("m", fp1) b := doneKeyNaming(goneRef) - b.Artifacts = append(b.Artifacts, delivery.BuiltArtifact{Name: "img", Kind: "image", Digest: digest, Reference: "registry.example/m/img@" + digest}) + b.Artifacts = append(b.Artifacts, builds.BuiltArtifact{Name: "img", Kind: "image", Digest: digest, Reference: "registry.example/m/img@" + digest}) rev := putBuild(t, st, key, b) held := &fakeHeld{gone: map[string]bool{}} if cl, err := c.Claim(t.Context(), "m", fp1, manifestD, ClaimOptions{Trunk: true, Stored: held.stored}); err != nil || cl != nil { @@ -143,7 +143,7 @@ func TestTheBuilderBuildsAgainADoneKeyWhoseBlobIsGone(t *testing.T) { r.b.Stored = held.stored rev := putBuild(t, r.st, r.key, doneKeyNaming(goneRef)) v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || r.st.revs[r.key] <= rev || len(r.bm.callsOf("build_go_build")) == 0 { + if v.State != builds.BuildDone || r.st.revs[r.key] <= rev || len(r.bm.callsOf("build_go_build")) == 0 { t.Fatalf("build = %+v at revision %d (was %d); want built again", v, r.st.revs[r.key], rev) } if !strings.Contains(v.Step, "built again: the artifact store no longer held "+goneRef) { @@ -177,7 +177,7 @@ func TestArtifactsGoneMakesTheBuilderAskAndBuildAgain(t *testing.T) { held := &fakeHeld{gone: map[string]bool{}} r.b.Stored = held.stored putBuild(t, r.st, r.key, doneKeyNaming(goneRef)) - if v := r.consider(t, "modules/m"); v.State != delivery.BuildDone || len(r.bm.callsOf("build_go_build")) != 0 { + if v := r.consider(t, "modules/m"); v.State != builds.BuildDone || len(r.bm.callsOf("build_go_build")) != 0 { t.Fatalf("a key whose blob is held was built: %+v", v) } rev := r.st.revs[r.key] @@ -196,7 +196,7 @@ func TestArtifactsGoneMakesTheBuilderAskAndBuildAgain(t *testing.T) { Key: r.key, Fingerprint: fp1, Gone: []string{goneRef}}) r.b.Wait() v := valueOf(t, r.st, r.key) - if v.State != delivery.BuildDone || r.st.revs[r.key] <= rev || len(r.bm.callsOf("build_go_build")) == 0 { + if v.State != builds.BuildDone || r.st.revs[r.key] <= rev || len(r.bm.callsOf("build_go_build")) == 0 { t.Fatalf("build = %+v at revision %d (was %d); want built again on the event", v, r.st.revs[r.key], rev) } if !log.has("heard artifacts-gone") || !log.has(r.key, "built again", goneRef) { diff --git a/modules/mesh-delivery-builder/internal/build/imports_test.go b/modules/mesh-delivery-builder/internal/build/imports_test.go index 77215db7..6f81e2fe 100644 --- a/modules/mesh-delivery-builder/internal/build/imports_test.go +++ b/modules/mesh-delivery-builder/internal/build/imports_test.go @@ -6,7 +6,8 @@ import ( "sync" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // ADR 0593 §2 (F3): the imports a Go build module answers at a build's end reach the done key and the log's end entry, @@ -14,7 +15,7 @@ import ( func importsABuildAnswersReachTheDoneKeyAndTheLog(t *testing.T) { r := newRig(t, goBundle) imports := []string{"git.novox.be/novox/mesh-sdk/go", "git.novox.be/novox/mesh-sdk/go/announce"} - r.asks.Answer = func(a delivery.StepAsk) delivery.Step { + r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { step := r.answer(a) var answer map[string]any if err := json.Unmarshal(step.Result, &answer); err == nil && a.Language == "go" { @@ -23,11 +24,11 @@ func importsABuildAnswersReachTheDoneKeyAndTheLog(t *testing.T) { } return step } - var logged []delivery.BuildLogEntry + var logged []builds.BuildLogEntry var mu sync.Mutex - r.b.Node, r.b.Log = "novox", func(e delivery.BuildLogEntry) { mu.Lock(); logged = append(logged, e); mu.Unlock() } + r.b.Node, r.b.Log = "novox", func(e builds.BuildLogEntry) { mu.Lock(); logged = append(logged, e); mu.Unlock() } v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || strings.Join(v.Imports, ",") != strings.Join(imports, ",") { + if v.State != builds.BuildDone || strings.Join(v.Imports, ",") != strings.Join(imports, ",") { t.Fatalf("done key %+v, want the imports", v) } if len(logged) != 2 || strings.Join(logged[1].Imports, ",") != strings.Join(imports, ",") || len(logged[0].Imports) != 0 { @@ -51,7 +52,7 @@ func outOfStepFilesABuildAnswersReachTheLog(t *testing.T) { } { t.Run(row.name, func(t *testing.T) { r := newRig(t, goBundle) - r.asks.Answer = func(a delivery.StepAsk) delivery.Step { + r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { step := r.answer(a) answer := map[string]any{} _ = json.Unmarshal(step.Result, &answer) @@ -60,13 +61,13 @@ func outOfStepFilesABuildAnswersReachTheLog(t *testing.T) { } step.Result, _ = json.Marshal(answer) if row.fail { - step.State, step.Reason = delivery.StepFailed, "the generated files are out of step" + step.State, step.Reason = buildstep.StepFailed, "the generated files are out of step" } return step } - var logged []delivery.BuildLogEntry + var logged []builds.BuildLogEntry var mu sync.Mutex - r.b.Node, r.b.Log = "novox", func(e delivery.BuildLogEntry) { mu.Lock(); logged = append(logged, e); mu.Unlock() } + r.b.Node, r.b.Log = "novox", func(e builds.BuildLogEntry) { mu.Lock(); logged = append(logged, e); mu.Unlock() } r.consider(t, "modules/m") mu.Lock() defer mu.Unlock() diff --git a/modules/mesh-delivery-builder/internal/build/numbers.go b/modules/mesh-delivery-builder/internal/build/numbers.go index d8b14932..6cf6250f 100644 --- a/modules/mesh-delivery-builder/internal/build/numbers.go +++ b/modules/mesh-delivery-builder/internal/build/numbers.go @@ -6,7 +6,9 @@ import ( "strconv" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "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/sdk/trunk" "mesh-delivery-builder/internal/versions" ) @@ -16,8 +18,8 @@ import ( type Numbers interface { // tag is a check's build's tag (TagOf), empty for a trunk build; done the digests of its key when it is done already // (versions.BuildClaim.Done), nil otherwise. - Number(ctx context.Context, module string, m delivery.Module, plan Plan, tag string, done map[string]string) (versions.Numbering, error) - Built(ctx context.Context, module string, m delivery.Module, plan Plan, got versions.Numbering, made []delivery.BuiltArtifact) error + Number(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, tag string, done map[string]string) (versions.Numbering, error) + Built(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, got versions.Numbering, made []builds.BuiltArtifact) error } // VersionNumbers is Numbers over the builder's state versions. @@ -30,11 +32,11 @@ type VersionNumbers struct { // ClaimOf is the claim a module value's trunk build is numbered by: a package per source revision (Src), the rest per // fingerprint, in merge order (Src). -func ClaimOf(module string, m delivery.Module, plan Plan) versions.BuildClaim { - c := versions.BuildClaim{Module: module, Path: m.Path, Repository: delivery.RepositoryName(m.Repository), Fingerprint: m.FP2, - Commit: m.Commit, Merge: m.Src} - if m.Src != 0 { - c.Revision = strconv.FormatUint(m.Src, 10) +func ClaimOf(module string, m meshcatalog.ModulesByModuleValue, plan Plan) versions.BuildClaim { + c := versions.BuildClaim{Module: module, Path: m.Path, Repository: trunk.RepositoryName(m.Repository), Fingerprint: m.Fp2, + Commit: m.Commit, Merge: uint64(m.Src)} + if uint64(m.Src) != 0 { + c.Revision = strconv.FormatUint(uint64(m.Src), 10) } for _, t := range plan.Tasks { c.Artifacts = append(c.Artifacts, versions.ClaimArtifact{Artifact: t.Artifact, Kind: t.Kind, Language: t.Language}) @@ -42,7 +44,7 @@ func ClaimOf(module string, m delivery.Module, plan Plan) versions.BuildClaim { return c } -func (v VersionNumbers) Number(ctx context.Context, module string, m delivery.Module, plan Plan, tag string, done map[string]string) (versions.Numbering, error) { +func (v VersionNumbers) Number(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, tag string, done map[string]string) (versions.Numbering, error) { c := ClaimOf(module, m, plan) c.Done = done if tag != "" { @@ -56,7 +58,7 @@ func (v VersionNumbers) Number(ctx context.Context, module string, m delivery.Mo return got, err } -func (v VersionNumbers) Built(ctx context.Context, module string, m delivery.Module, plan Plan, got versions.Numbering, made []delivery.BuiltArtifact) error { +func (v VersionNumbers) Built(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, got versions.Numbering, made []builds.BuiltArtifact) error { digests := map[string]string{} for _, a := range made { digests[a.Name] = a.Digest @@ -71,32 +73,32 @@ type TagOf func(ctx context.Context, repository, head string) (tag string, ok bo // number numbers a build before its claim, a trunk build's artifacts or a check's build's packages under its tag: false // when it waits for its floor (a package, design 51 *The floor*), and then nothing is claimed; the floor's wake looks at // the module again. -func (b *Builder) number(ctx context.Context, module string, m delivery.Module, plan Plan, tag string) (versions.Numbering, bool) { +func (b *Builder) number(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, tag string) (versions.Numbering, bool) { if b.Numbers == nil { return versions.Numbering{}, true } var done map[string]string if tag == "" { - done = b.doneDigests(ctx, module, m.FP2) + done = b.doneDigests(ctx, module, m.Fp2) } got, err := b.Numbers.Number(ctx, module, m, plan, tag, done) for _, s := range got.Said { - b.logf("%s at %s: %s", module, m.FP2, s.Line) + b.logf("%s at %s: %s", module, m.Fp2, s.Line) // And in the build log (design 51 *The versions*: it says a version was kept, and why), between the build's start and end. // A look at a key already done builds nothing and has no start or end: a version kept there says nothing new, and is // not logged (each later look, a first view answered or a name heard, would add one). if b.Log != nil && !(done != nil && s.Why == versions.SaidKept) { - b.Log(delivery.BuildLogEntry{Event: delivery.BuildSaidEvent, Module: module, FP: m.FP2, Commit: m.Commit, Node: b.Node, + b.Log(builds.BuildLogEntry{Event: builds.BuildSaidEvent, Module: module, FP: m.Fp2, Commit: m.Commit, Node: b.Node, At: time.Now().UTC(), Line: s.Line, Why: s.Why}) } } if err != nil { // Said, and built as before: an unnumbered build is published by its digest (ADR 0617 §1), numbered at its next look. - b.logf("%s at %s: %v", module, m.FP2, fmt.Errorf("not numbered, the versions cannot be used: %w", err)) + b.logf("%s at %s: %v", module, m.Fp2, fmt.Errorf("not numbered, the versions cannot be used: %w", err)) return versions.Numbering{}, true } if got.Wait != "" { - b.logf("%s at %s is not built yet: %s", module, m.FP2, got.Wait) + b.logf("%s at %s is not built yet: %s", module, m.Fp2, got.Wait) return got, false } return got, true @@ -105,7 +107,7 @@ func (b *Builder) number(ctx context.Context, module string, m delivery.Module, // doneDigests are the digests of a trunk build's key by artifact when the key is done, nil otherwise or when it cannot be // read: a look at a done build numbers what it left unnumbered with them (versions.BuildClaim.Done). func (b *Builder) doneDigests(ctx context.Context, module, fp string) map[string]string { - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil || b.Claimer.Store == nil { return nil } @@ -113,8 +115,8 @@ func (b *Builder) doneDigests(ctx context.Context, module, fp string) map[string if err != nil || !found { return nil } - cur, err := delivery.DecodeBuilderBuild(raw) - if err != nil || cur.State != delivery.BuildDone { + cur, err := builds.DecodeBuilderBuild(raw) + if err != nil || cur.State != builds.BuildDone { return nil } out := map[string]string{} diff --git a/modules/mesh-delivery-builder/internal/build/plan.go b/modules/mesh-delivery-builder/internal/build/plan.go index 4a5969a1..11c678d5 100644 --- a/modules/mesh-delivery-builder/internal/build/plan.go +++ b/modules/mesh-delivery-builder/internal/build/plan.go @@ -9,8 +9,8 @@ import ( "strings" "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-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // ImageBuildModule is the build module that builds an image artifact (novox/hq ADR 0487): buildah, rootless, on the nodes that @@ -63,19 +63,19 @@ func (p Plan) NeedsToolchain(module string) bool { } // Refused is the error of a plan the builder does not build: an artifact of a kind (or of a language) no build module -// builds. It opens delivery.RefusedKind, so its key is not built again until the fingerprint changes. +// builds. It opens builds.RefusedKind, so its key is not built again until the fingerprint changes. type Refused struct{ Artifact, What string } func (r *Refused) Error() string { - return fmt.Sprintf("%s%s (artifact %s): no build module builds it; the builder builds Go and TypeScript bundles, archives, images, upstream copies and TypeScript packages", delivery.RefusedKind, r.What, r.Artifact) + return fmt.Sprintf("%s%s (artifact %s): no build module builds it; the builder builds Go and TypeScript bundles, archives, images, upstream copies and TypeScript packages", builds.RefusedKind, r.What, r.Artifact) } // NotPinned is the error of a plan with an upstream artifact whose reference names no digest (ADR 0498 clause 4): it opens -// delivery.NotPinned, so its key is not built again until the fingerprint changes. +// builds.NotPinned, so its key is not built again until the fingerprint changes. type NotPinned struct{ Artifact, From string } func (n *NotPinned) Error() string { - return fmt.Sprintf("%s%s: %q names no digest (@sha256:), so what would be copied is whatever its tag means today", delivery.NotPinned, n.Artifact, n.From) + return fmt.Sprintf("%s%s: %q names no digest (@sha256:), so what would be copied is whatever its tag means today", builds.NotPinned, n.Artifact, n.From) } // PlanOf is the plan of a manifest (ADR 0478 clauses 1 and 2, ADR 0498 clauses 4 and 5): a Go bundle and an archive are diff --git a/modules/mesh-delivery-builder/internal/build/plan_test.go b/modules/mesh-delivery-builder/internal/build/plan_test.go index 9914ca92..897f3e1c 100644 --- a/modules/mesh-delivery-builder/internal/build/plan_test.go +++ b/modules/mesh-delivery-builder/internal/build/plan_test.go @@ -7,7 +7,7 @@ import ( "testing" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/catalogue" ) @@ -58,21 +58,21 @@ func TestPlanSendsAPinnedUpstreamToTheImageBuildModulesCopyAndRefusesAnUnpinnedO t.Error("a copy needs no toolchain") } _, err = PlanOf(manifestOf(catalogue.Artifact{Name: "up", Kind: "upstream", From: "nats:2.11.17-alpine"})) - if err == nil || !strings.HasPrefix(err.Error(), delivery.NotPinned+"up") || !delivery.FailedUntilChanged(err.Error()) { - t.Errorf("an upstream with no digest: error = %v, want it to open %q naming the artifact", err, delivery.NotPinned) + if err == nil || !strings.HasPrefix(err.Error(), builds.NotPinned+"up") || !builds.FailedUntilChanged(err.Error()) { + t.Errorf("an upstream with no digest: error = %v, want it to open %q naming the artifact", err, builds.NotPinned) } } func TestPlanRefusesAPackageOfAnotherLanguageByName(t *testing.T) { // ADR 0498 clause 5 _, err := PlanOf(manifestOf(catalogue.Artifact{Name: "ok", Kind: "archive"}, catalogue.Artifact{Name: "pkg", Kind: "package", Language: "python"})) var r *Refused - if err == nil || !asRefused(err, &r) || !strings.HasPrefix(err.Error(), delivery.RefusedKind) || !strings.Contains(err.Error(), "python") || !strings.Contains(err.Error(), "pkg") { - t.Errorf("error = %v, want it to open %q naming the artifact and its language", err, delivery.RefusedKind) + if err == nil || !asRefused(err, &r) || !strings.HasPrefix(err.Error(), builds.RefusedKind) || !strings.Contains(err.Error(), "python") || !strings.Contains(err.Error(), "pkg") { + t.Errorf("error = %v, want it to open %q naming the artifact and its language", err, builds.RefusedKind) } } func TestPlanRefusesABundleOfAnotherLanguageAndAModuleWithNoArtifact(t *testing.T) { - if _, err := PlanOf(manifestOf(catalogue.Artifact{Name: "x", Kind: "bundle", Language: "python"})); err == nil || !strings.HasPrefix(err.Error(), delivery.RefusedKind) { + if _, err := PlanOf(manifestOf(catalogue.Artifact{Name: "x", Kind: "bundle", Language: "python"})); err == nil || !strings.HasPrefix(err.Error(), builds.RefusedKind) { t.Errorf("a python bundle: error = %v, want a refusal", err) } p, err := PlanOf(manifestOf()) diff --git a/modules/mesh-delivery-builder/internal/build/queue.go b/modules/mesh-delivery-builder/internal/build/queue.go index 7542a503..68d94081 100644 --- a/modules/mesh-delivery-builder/internal/build/queue.go +++ b/modules/mesh-delivery-builder/internal/build/queue.go @@ -6,7 +6,8 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // **The tool builder_seat_queue reads a build seat's stream as the builder reads it** (novox/hq issue 1945 note 62; the @@ -19,7 +20,7 @@ const ( SeatQueueTool = "builder_seat_queue" // QueuesState is the builder's state of queue reads: the bucket mesh-delivery-builder_queues, keyed by the language. QueuesState = "queues" - QueuesBucket = delivery.BuilderModule + "_" + QueuesState + QueuesBucket = builds.BuilderModule + "_" + QueuesState // DefaultQueueLimit and MaxQueueLimit bound the messages read of each kind. DefaultQueueLimit = 50 MaxQueueLimit = 500 @@ -87,7 +88,7 @@ func NewKindQueue(kind string, seen AskSeen, now time.Time) KindQueue { // NewQueueAsk is the value builder_seat_queue writes for a language: its key and the value. func NewQueueAsk(language string, limit int, by string, at time.Time) (string, []byte, error) { - if _, err := delivery.AskStream(language); err != nil { + if _, err := buildstep.AskStream(language); err != nil { return "", nil, fmt.Errorf("language: %w", err) } if limit <= 0 { diff --git a/modules/mesh-delivery-builder/internal/build/r4e_test.go b/modules/mesh-delivery-builder/internal/build/r4e_test.go index a9ab4f29..562010ce 100644 --- a/modules/mesh-delivery-builder/internal/build/r4e_test.go +++ b/modules/mesh-delivery-builder/internal/build/r4e_test.go @@ -2,12 +2,17 @@ package build import ( "context" + "encoding/json" "strings" "testing" + "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/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" ) // R4e: references in the key (novox/hq ADR 0498), upstream copies, packages published from the trunk, the package wait @@ -19,7 +24,7 @@ func TestACandidatesBundleKeyNamesItsReferenceAndTheTrunkBuildsItNoMore(t *testi r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) r.b.Wait() v := valueOf(t, r.st, r.key) - if v.State != delivery.BuildDone { + if v.State != builds.BuildDone { t.Fatalf("build = %+v, want done", v) } if ok, missing := v.Referenced(); !ok { @@ -40,21 +45,21 @@ func TestABundleWhosePushAnsweredNoReferenceFailsNotStoredAndIsBuiltAgain(t *tes r := newRig(t, goBundle) r.bm.noRef["ace/tool"] = true v := r.consider(t, "modules/m") - if v.State != delivery.BuildFailed || !strings.HasPrefix(v.Error, delivery.NotStored) || !strings.Contains(v.Error, "tool") { - t.Fatalf("build = %+v, want failed opening %q and naming the artifact", v, delivery.NotStored) + if v.State != builds.BuildFailed || !strings.HasPrefix(v.Error, builds.NotStored) || !strings.Contains(v.Error, "tool") { + t.Fatalf("build = %+v, want failed opening %q and naming the artifact", v, builds.NotStored) } delete(r.bm.noRef, "ace/tool") - if v = r.consider(t, "modules/m"); v.State != delivery.BuildDone { + if v = r.consider(t, "modules/m"); v.State != builds.BuildDone { t.Errorf("build = %+v, want a not-stored key built again as a new revision", v) } } func TestAPushRefusedByTheStoreFailsTheKeyOpeningNotStored(t *testing.T) { // B1, ADR 0498 clause 2 r := newRig(t, goBundle) - r.bm.fails["ace/build_go_build"] = delivery.NotStored + "tool: the artifact store answered 507 insufficient storage" + r.bm.fails["ace/build_go_build"] = builds.NotStored + "tool: the artifact store answered 507 insufficient storage" v := r.consider(t, "modules/m") - if v.State != delivery.BuildFailed || !strings.HasPrefix(v.Error, delivery.NotStored) || !strings.Contains(v.Error, "507") || !strings.Contains(v.Error, "ace") { - t.Fatalf("build = %+v, want failed opening %q with the store's words and the node", v, delivery.NotStored) + if v.State != builds.BuildFailed || !strings.HasPrefix(v.Error, builds.NotStored) || !strings.Contains(v.Error, "507") || !strings.Contains(v.Error, "ace") { + t.Fatalf("build = %+v, want failed opening %q with the store's words and the node", v, builds.NotStored) } } @@ -78,9 +83,9 @@ func TestOnlyATrunkKeysBuildsAreAskedToPublish(t *testing.T) { // B1, the lead's } // putBuild writes a builder key's value as the store would hold it. -func putBuild(t *testing.T, st *memStore, key string, b delivery.BuilderBuild) uint64 { +func putBuild(t *testing.T, st *memStore, key string, b builds.BuilderBuild) uint64 { t.Helper() - data, err := delivery.EncodeBuilderBuild(b) + data, err := builds.EncodeBuilderBuild(b) if err != nil { t.Fatal(err) } @@ -91,10 +96,10 @@ func putBuild(t *testing.T, st *memStore, key string, b delivery.BuilderBuild) u func TestADoneKeyWrittenBeforeR4eIsBuiltAgainOnceAsANewRevision(t *testing.T) { // B2, ADR 0498 clause 8 r := newRig(t, goBundle, archiveA) - rev := putBuild(t, r.st, r.key, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.builder", Manifest: manifestD, Step: "built mesh-build-go on ace and novox", - Artifacts: []delivery.BuiltArtifact{{Name: "files", Kind: "archive", Digest: digest}, {Name: "tool", Kind: "bundle", Digest: digest}}}) + rev := putBuild(t, r.st, r.key, builds.BuilderBuild{State: builds.BuildDone, By: "novox.builder", Manifest: manifestD, Step: "built mesh-build-go on ace and novox", + Artifacts: []builds.BuiltArtifact{{Name: "files", Kind: "archive", Digest: digest}, {Name: "tool", Kind: "bundle", Digest: digest}}}) v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || r.st.revs[r.key] <= rev { + if v.State != builds.BuildDone || r.st.revs[r.key] <= rev { t.Fatalf("build = %+v at revision %d (was %d), want built again as a new revision", v, r.st.revs[r.key], rev) } if ok, missing := v.Referenced(); !ok { @@ -113,7 +118,7 @@ func TestAPinnedUpstreamIsCopiedOnceOnOneImageNodeWithNoFetchAndItsKeyNamesTheRe r := newRig(t, up) r.puller = "ace" v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || len(v.Artifacts) != 1 { + if v.State != builds.BuildDone || len(v.Artifacts) != 1 { t.Fatalf("build = %+v, want done with the copy", v) } if a := v.Artifacts[0]; a.Kind != "upstream" || a.Digest != digest || a.Reference != "registry.example/m/up@"+digest { @@ -146,13 +151,13 @@ func TestAnUpstreamBesideAnImageIsCopiedAndTheImageFetchedAndBuiltOnTheSameNode( func TestACopyThatFailsNotStoredFailsTheKeyAndIsBuiltAgain(t *testing.T) { // B4, ADR 0498 clause 2 r := newRig(t, catalogue.Artifact{Name: "up", Kind: "upstream", From: "nats:2.11.17-alpine@" + digest}) r.puller = "ace" - r.bm.fails["ace/build_image_copy"] = delivery.NotStored + "up: the registry answered 500" + r.bm.fails["ace/build_image_copy"] = builds.NotStored + "up: the registry answered 500" v := r.consider(t, "modules/m") - if v.State != delivery.BuildFailed || !strings.HasPrefix(v.Error, delivery.NotStored) || delivery.FailedUntilChanged(v.Error) { + if v.State != builds.BuildFailed || !strings.HasPrefix(v.Error, builds.NotStored) || builds.FailedUntilChanged(v.Error) { t.Fatalf("build = %+v, want failed 'not stored:' and built again", v) } delete(r.bm.fails, "ace/build_image_copy") - if v = r.consider(t, "modules/m"); v.State != delivery.BuildDone { + if v = r.consider(t, "modules/m"); v.State != builds.BuildDone { t.Errorf("build = %+v, want done on the new revision", v) } } @@ -166,7 +171,7 @@ func TestATypeScriptPackageIsBuiltByTheNodeModuleAndPublishedOnlyForTheTrunk(t * c.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) c.b.Wait() v := valueOf(t, c.st, c.key) - if v.State != delivery.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "package" || v.Artifacts[0].Reference != "" { + if v.State != builds.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "package" || v.Artifacts[0].Reference != "" { t.Fatalf("candidate build = %+v, want done with the packed digest and no reference", v) } for _, call := range c.bm.callsOf("build_node_build") { @@ -178,7 +183,7 @@ func TestATypeScriptPackageIsBuiltByTheNodeModuleAndPublishedOnlyForTheTrunk(t * r := newRig(t, tsPackage) r.bm.packages["lib"] = "@novox/mesh-sdk@0.1.15" v = r.consider(t, "modules/m") - if v.State != delivery.BuildDone || v.Artifacts[0].Reference != "@novox/mesh-sdk@0.1.15" { + if v.State != builds.BuildDone || v.Artifacts[0].Reference != "@novox/mesh-sdk@0.1.15" { t.Fatalf("trunk build = %+v, want done naming the published version", v) } if n := len(r.bm.callsOf("build_node_build")); n == 0 { @@ -189,11 +194,11 @@ func TestATypeScriptPackageIsBuiltByTheNodeModuleAndPublishedOnlyForTheTrunk(t * func TestAVersionTakenFailsTheKeyUntilTheFingerprintChanges(t *testing.T) { // B5, ADR 0498 clause 5 r := newRig(t, tsPackage) r.bm.packages["lib"] = "@novox/mesh-sdk@0.1.15" - r.bm.fails["ace/build_node_build"] = delivery.VersionTaken + "@novox/mesh-sdk@0.1.15 is published with another packed file" + r.bm.fails["ace/build_node_build"] = builds.VersionTaken + "@novox/mesh-sdk@0.1.15 is published with another packed file" r.bm.fails["novox/build_node_build"] = r.bm.fails["ace/build_node_build"] v := r.consider(t, "modules/m") - if v.State != delivery.BuildFailed || !strings.HasPrefix(v.Error, delivery.VersionTaken) || !strings.Contains(v.Error, "0.1.15") { - t.Fatalf("build = %+v, want failed opening %q naming the version", v, delivery.VersionTaken) + if v.State != builds.BuildFailed || !strings.HasPrefix(v.Error, builds.VersionTaken) || !strings.Contains(v.Error, "0.1.15") { + t.Fatalf("build = %+v, want failed opening %q naming the version", v, builds.VersionTaken) } writes := r.st.writes r.consider(t, "modules/m") @@ -208,7 +213,7 @@ func TestACandidatesPackageIsPublishedOnceWhenTheTrunkNamesItsFingerprint(t *tes trunkModule(t, r) r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone { t.Fatalf("candidate build = %+v", v) } rev := r.st.revs[r.key] @@ -237,12 +242,12 @@ func TestAPublishThatFailsVersionTakenOnTheTrunkKeepsThePrefixFirst(t *testing.T r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) r.b.Wait() for _, n := range []string{"ace", "novox"} { - r.bm.fails[n+"/build_node_build"] = delivery.VersionTaken + "@novox/mesh-sdk@0.1.15 is published with another packed file" + r.bm.fails[n+"/build_node_build"] = builds.VersionTaken + "@novox/mesh-sdk@0.1.15 is published with another packed file" } r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildFailed || !strings.HasPrefix(v.Error, delivery.VersionTaken) { - t.Errorf("build = %+v, want failed opening %q", v, delivery.VersionTaken) + if v := valueOf(t, r.st, r.key); v.State != builds.BuildFailed || !strings.HasPrefix(v.Error, builds.VersionTaken) { + t.Errorf("build = %+v, want failed opening %q", v, builds.VersionTaken) } } @@ -251,8 +256,8 @@ func sdkModule(t *testing.T, r *rig, ref string) []byte { t.Helper() r.mf.byDigest[mdigest('b')] = catalogue.Manifest{Module: "sdk", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{tsPackage}}} r.bm.packages["lib"] = ref - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-sdk", Commit: "8a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP2: "src2:" + hex64('5'), - Manifest: mdigest('b'), Path: ".", Toolchains: agreedToolchains}) + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-sdk", Commit: "8a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp2: "src2:" + hex64('5'), + Manifest: mdigest('b'), Path: ".", Toolchains: CatalogToolchains(agreedToolchains)}) if err != nil { t.Fatal(err) } @@ -266,7 +271,7 @@ func TestAModuleWhoseLockNamesAnUnpublishedPackageWaitsQueuedAndIsBuiltOncePubli r.bm.fails[n+"/build_node_fetch"] = waitsFor + ref + ": the lock names a version the registry does not hold" } v := r.consider(t, "modules/m") - if v.State != delivery.BuildQueued || v.Waits != ref || v.Error != "" { + if v.State != builds.BuildQueued || v.Waits != ref || v.Error != "" { t.Fatalf("build = %+v, want queued, waiting for %s, not failed", v, ref) } delete(r.bm.fails, "ace/build_node_fetch") @@ -279,7 +284,7 @@ func TestAModuleWhoseLockNamesAnUnpublishedPackageWaitsQueuedAndIsBuiltOncePubli } r.b.Module(context.Background(), "sdk", sdkModule(t, r, ref)) // the package's trunk key is built and published r.b.Wait() - if v = valueOf(t, r.st, r.key); v.State != delivery.BuildDone || v.Waits != "" { + if v = valueOf(t, r.st, r.key); v.State != builds.BuildDone || v.Waits != "" { t.Errorf("build = %+v, want built once the package it waits for is published", v) } } @@ -287,16 +292,16 @@ func TestAModuleWhoseLockNamesAnUnpublishedPackageWaitsQueuedAndIsBuiltOncePubli func TestAKeyWaitingForAPackageAlreadyPublishedIsBuiltWhenTheBuilderStarts(t *testing.T) { // B7: the scan at start const ref = "@novox/mesh-sdk@0.1.15" r := newRig(t, tsBundle) - putBuild(t, r.st, r.key, delivery.BuilderBuild{State: delivery.BuildQueued, By: "novox.builder", Manifest: manifestD, Waits: ref, Step: "waits"}) - sdkKey, _ := delivery.BuilderBuildKey("sdk", "src2:"+hex64('5')) - putBuild(t, r.st, sdkKey, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.builder", Manifest: mdigest('b'), Step: "built", - Artifacts: []delivery.BuiltArtifact{{Name: "lib", Kind: "package", Digest: digest, Reference: ref}}}) + putBuild(t, r.st, r.key, builds.BuilderBuild{State: builds.BuildQueued, By: "novox.builder", Manifest: manifestD, Waits: ref, Step: "waits"}) + sdkKey, _ := builds.BuilderBuildKey("sdk", "src2:"+hex64('5')) + putBuild(t, r.st, sdkKey, builds.BuilderBuild{State: builds.BuildDone, By: "novox.builder", Manifest: mdigest('b'), Step: "built", + Artifacts: []builds.BuiltArtifact{{Name: "lib", Kind: "package", Digest: digest, Reference: ref}}}) r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) if err := r.b.Sweep(context.Background()); err != nil { t.Fatal(err) } r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone { t.Errorf("build = %+v, want built: the package it waits for was published before the builder started", v) } } @@ -307,9 +312,9 @@ func TestTheFetchOfAModuleNamingItsPathsCarriesItsDirectoryAndThosePaths(t *test paths := map[string]string{"src": hex40('1'), "package.json": hex40('2')} // The named tree the indexer computes from those paths (its own, ADR 0593 §2), given as data: the builder reads it, // never computes it. - const tree = delivery.NamedTreePrefix + "5b5e0f3b6f3c1f2a9d8e7c6b5a4938271605f4e3d2c1b0a9f8e7d6c5b4a39281" - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-sdk", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP2: fp1, Manifest: manifestD, Path: ".", - Toolchains: agreedToolchains, Inputs2: &delivery.KeptInputs2{Tree: tree, Paths: paths}}) + const tree = "named:" + "5b5e0f3b6f3c1f2a9d8e7c6b5a4938271605f4e3d2c1b0a9f8e7d6c5b4a39281" + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-sdk", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp2: fp1, Manifest: manifestD, Path: ".", + Toolchains: CatalogToolchains(agreedToolchains), Inputs2: &meshcatalog.ModulesByModuleValueInputs2{Tree: tree, Paths: paths}}) if err != nil { t.Fatal(err) } @@ -335,7 +340,7 @@ func hex40(c byte) string { return strings.Repeat(string(c), 40) } func TestOneNodeBuildsEachFingerprint(t *testing.T) { // B9, ADR 0478 clause 4: "From R4e one node builds each fingerprint" r := newRig(t, goBundle, archiveA, tsBundle) // ace and novox both qualify for both build modules v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || strings.Contains(v.Step, " and ") || strings.HasPrefix(v.Step, "one node (") { + if v.State != builds.BuildDone || strings.Contains(v.Step, " and ") || strings.HasPrefix(v.Step, "one node (") { t.Fatalf("build = %+v, want done on one node of each build module, unmarked", v) } if f, b := len(r.bm.callsOf("build_go_fetch")), len(r.bm.callsOf("build_go_build")); f != 1 || b != 2 { @@ -348,9 +353,9 @@ func TestOneNodeBuildsEachFingerprint(t *testing.T) { // B9, ADR 0478 clause 4: func TestAKeyDoneOnOneNodeIsNeverBuiltAgainForASecondNode(t *testing.T) { // B9: the second-node retake is gone r := newRig(t, goBundle) - putBuild(t, r.st, r.key, delivery.BuilderBuild{State: delivery.BuildDone, By: "novox.builder", Manifest: manifestD, + putBuild(t, r.st, r.key, builds.BuilderBuild{State: builds.BuildDone, By: "novox.builder", Manifest: manifestD, Step: "one node (mesh-build-go): built mesh-build-go on one node, novox", - Artifacts: []delivery.BuiltArtifact{{Name: "tool", Kind: "bundle", Digest: digest, Reference: "artifact-store://m/tool/blobs/" + digest}}}) + Artifacts: []builds.BuiltArtifact{{Name: "tool", Kind: "bundle", Digest: digest, Reference: "artifact-store://m/tool/blobs/" + digest}}}) writes := r.st.writes r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) r.b.NodesChanged(context.Background()) @@ -364,10 +369,10 @@ func TestACandidatesFetchNamesTheNamedPathsTheIndexerGaveIt(t *testing.T) { // A r := newRig(t, tsPackage) r.bm.packages["lib"] = "@novox/mesh-sdk@0.1.15" trunkModule(t, r) - pulls := []delivery.CandidatePull{{Head: delivery.Head{Repository: "novox/mesh-catalog", Number: 7, Head: headX}, Modules: []string{"m"}, - Fps: map[string]delivery.CandidateModule{"m": {FP2: fp1, Manifest: mdigest('a'), Paths: map[string]string{"src": hex40('1'), "package.json": hex40('2')}}}}} - hash, _ := delivery.HeadsHash([]delivery.Head{pulls[0].Head}) - data, err := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: "feat/x", HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}) + pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: headX, Modules: []string{"m"}, + Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"m": {Fp2: fp1, Manifest: mdigest('a'), Paths: map[string]string{"src": hex40('1'), "package.json": hex40('2')}}}}} + hash, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: int(pulls[0].Number), Head: pulls[0].Head}}) + data, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-builder/internal/build/round4_test.go b/modules/mesh-delivery-builder/internal/build/round4_test.go index f09fa154..8d7d8a1d 100644 --- a/modules/mesh-delivery-builder/internal/build/round4_test.go +++ b/modules/mesh-delivery-builder/internal/build/round4_test.go @@ -6,13 +6,15 @@ import ( "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) -func haveValue(t *testing.T, phase delivery.Phase, health delivery.Health) []byte { +func haveValue(t *testing.T, phase, health string) []byte { t.Helper() - b, err := delivery.EncodeHave(delivery.Have{Phase: phase, Health: health}) + b, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Phase: phase, Health: health}) if err != nil { t.Fatal(err) } @@ -23,7 +25,7 @@ func haveValue(t *testing.T, phase delivery.Phase, health delivery.Health) []byt // fingerprint names no toolchain, and the builder builds it on the build module's nodes with no toolchain agreed. func TestAnArchiveOnlyModuleIsBuiltAlthoughItsFingerprintNamesNoToolchain(t *testing.T) { // review round 4, 1 r := newRig(t, archiveA) - v := func() delivery.BuilderBuild { + v := func() builds.BuilderBuild { r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", nil)) r.b.Wait() if _, ok := r.st.vals[r.key]; !ok { @@ -31,7 +33,7 @@ func TestAnArchiveOnlyModuleIsBuiltAlthoughItsFingerprintNamesNoToolchain(t *tes } return valueOf(t, r.st, r.key) }() - if v.State != delivery.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "archive" || len(v.Toolchains) != 0 { + if v.State != builds.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "archive" || len(v.Toolchains) != 0 { t.Fatalf("build = %+v, want done with the archive and no toolchain named", v) } if n := len(r.bm.callsOf("build_go_build")); n != 1 { @@ -52,7 +54,7 @@ func TestABundleAndAnArchiveOfOneBuildModuleStillNeedTheAgreedToolchain(t *testi func TestClaimLeavesAKeyFailedForDifferingDigestsAlone(t *testing.T) { // review round 4, 2 st := newMem() - seed(t, st, delivery.BuilderBuild{State: delivery.BuildFailed, Error: buildrun.DigestsDifferPrefix + " the nodes built tool to different digests: ace x; novox y"}) + seed(t, st, builds.BuilderBuild{State: builds.BuildFailed, Error: buildrun.DigestsDifferPrefix + " the nodes built tool to different digests: ace x; novox y"}) writes := st.writes got, err := (Claimer{Store: st, By: "novox.a"}).Claim(context.Background(), "m", fp1, "", ClaimOptions{TakeOver: true}) if err != nil || got != nil || st.writes != writes { @@ -63,7 +65,7 @@ func TestClaimLeavesAKeyFailedForDifferingDigestsAlone(t *testing.T) { // review // 6. A key found building is taken over, whoever holds it. func TestABuildLeftBuildingByAnyBuilderIsTakenOverAtStart(t *testing.T) { // review round 4, 6; 1607/AC3, ADR 0478 clauses 5, 6 st := newMem() - seed(t, st, delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.a"}) + seed(t, st, builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.a"}) if got, err := (Claimer{Store: st, By: "novox.a"}).Claim(context.Background(), "m", fp1, "", ClaimOptions{}); err != nil || got == nil { t.Errorf("own building key: claim = %v, %v, want it taken again", got, err) } @@ -77,9 +79,9 @@ func TestABuildLeftBuildingByAnyBuilderIsTakenOverAtStart(t *testing.T) { // rev func TestABuilderFindingAKeyBuildingByAnotherNodeBuildsItOnStart(t *testing.T) { // review round 4, 6 r := newRig(t, goBundle) - seed(t, r.st, delivery.BuilderBuild{State: delivery.BuildBuilding, By: "ace.builder", Manifest: manifestD}) + seed(t, r.st, builds.BuilderBuild{State: builds.BuildBuilding, By: "ace.builder", Manifest: manifestD}) v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || v.By != "novox.builder" { + if v.State != builds.BuildDone || v.By != "novox.builder" { t.Errorf("build = %+v, want it taken over and done by novox.builder", v) } } diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index e651e436..50271829 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -13,19 +13,23 @@ import ( "sync" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/forms" + repotrunk "git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-builder/internal/versions" ) // ToolchainEntries are the nodes' toolchain entries of a language (buildrun.ToolchainSet). type ToolchainEntries interface { - Entries(language string) []delivery.NodeToolchain + Entries(language string) []sdkstep.NodeToolchain } // Builder builds, dark, every new-form fingerprint the indexer writes (novox/hq ADR 0453 clause 4, ADR 0478): it is @@ -38,14 +42,14 @@ type Builder struct { // Asks puts each build module's part of a build as one ask on its build seat's stream and waits for its step (novox/hq // ADR 0525 §1, §3, §6; issue 1887 A4): the builder chooses no node, whichever build node pulls the ask builds it. Asks interface { - Put(ctx context.Context, a delivery.StepAsk) error - Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, error) + Put(ctx context.Context, a sdkstep.StepAsk) error + Wait(ctx context.Context, a sdkstep.StepAsk) (sdkstep.Step, error) } // Held says whether an ask a key names is on its stream or a build node wrote a step for it (issue 1910): a key taken // over at start whose ask is neither was left by a builder that stopped between naming the ask and putting it, and // the ask is put then, under the same id. Nil takes every named ask as held. Held interface { - Held(ctx context.Context, a delivery.StepAsk) (bool, error) + Held(ctx context.Context, a sdkstep.StepAsk) (bool, error) } Toolchains ToolchainEntries Manifests Manifests @@ -66,7 +70,7 @@ type Builder struct { // ask has waited CheckAfter, and again each CheckAfter it goes on waiting. An ask that cannot be answered is asked // again under a new id, at most Askings times. Nil, or CheckAfter 0, never asks it. Gone interface { - Gone(ctx context.Context, a delivery.StepAsk) (string, error) + Gone(ctx context.Context, a sdkstep.StepAsk) (string, error) } CheckAfter time.Duration // Askings is how many times one build module's part of a build is asked, the first included, before its key is failed @@ -78,7 +82,7 @@ type Builder struct { // Said is told each ask asked again (the builder's event build.asked-again). Nil tells nobody. Said func(AskedAgain) // Log is told each build's start and end, the entries of the builder's log (novox/hq ADR 0571 §1). Nil tells nobody. - Log func(delivery.BuildLogEntry) + Log func(builds.BuildLogEntry) // Now is the builder's clock: of the log, and of the decline bound (DeclineBound runs from the first decline); nil is // time.Now. Node is the node the builder runs on, which asked the build. Now func() time.Time @@ -98,12 +102,12 @@ type Builder struct { sem chan struct{} wg sync.WaitGroup mu sync.Mutex - known map[string]delivery.Module // by modules key - inflight map[string]bool // by build key - again map[string]delivery.Module // by build key: the newest value that arrived while the key was being built - plans map[string]planned // by manifest digest - cands map[string]delivery.Candidate // by candidates key (candidates.go) - via map[string]via // by build key: a candidate's fetch (candidates.go) + known map[string]meshcatalog.ModulesByModuleValue // by modules key + inflight map[string]bool // by build key + again map[string]meshcatalog.ModulesByModuleValue // by build key: the newest value that arrived while the key was being built + plans map[string]planned // by manifest digest + cands map[string]meshcatalog.CandidatesByGroupValue // by candidates key (candidates.go) + via map[string]via // by build key: a candidate's fetch (candidates.go) // outOfStep are, by build key, the out-of-step files a build's lanes answered, until its end entry takes them. outOfStep map[string][]string trunkFP map[string]bool // by build key: a trunk module value carries this fingerprint @@ -132,8 +136,8 @@ func (b *Builder) init() { limit = 256 } b.sem = make(chan struct{}, limit) - b.cands, b.via, b.trunkFP = map[string]delivery.Candidate{}, map[string]via{}, map[string]bool{} - b.known, b.inflight, b.again, b.plans, b.tried = map[string]delivery.Module{}, map[string]bool{}, map[string]delivery.Module{}, map[string]planned{}, map[string]bool{} + b.cands, b.via, b.trunkFP = map[string]meshcatalog.CandidatesByGroupValue{}, map[string]via{}, map[string]bool{} + b.known, b.inflight, b.again, b.plans, b.tried = map[string]meshcatalog.ModulesByModuleValue{}, map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{} b.published, b.held, b.waiting = map[string]bool{}, map[string]bool{}, map[string]map[string]chan string{} b.hands = map[string]*handWatch{} }) @@ -152,14 +156,14 @@ func (b *Builder) Wait() { b.wg.Wait() } // to be built. A value that is not a module is said and left. func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.init() - m, err := delivery.DecodeModule(value) + m, err := meshcatalogclient.DecodeModule(value) if err != nil { b.logf("modules.%s holds what is not a module, left: %v", key, err) return } b.mu.Lock() b.known[key] = m - if bk, err := delivery.BuilderBuildKey(key, m.FP2); err == nil && m.FP2 != "" { + if bk, err := builds.BuilderBuildKey(key, m.Fp2); err == nil && m.Fp2 != "" { // A fingerprint the trunk carries is built from the trunk's commit, whoever asked for it first. b.trunkFP[bk] = true delete(b.via, bk) @@ -185,8 +189,8 @@ func (b *Builder) Sweep(ctx context.Context) error { b.mu.Lock() current := map[string]bool{} for name, m := range b.known { - if m.FP2 != "" { - if k, err := delivery.BuilderBuildKey(name, m.FP2); err == nil { + if m.Fp2 != "" { + if k, err := builds.BuilderBuildKey(name, m.Fp2); err == nil { current[k] = true } } @@ -198,17 +202,17 @@ func (b *Builder) Sweep(ctx context.Context) error { if err != nil || !found { continue } - cur, err := delivery.DecodeBuilderBuild(value) + cur, err := builds.DecodeBuilderBuild(value) if err != nil { continue } newly = b.notePublished(cur) || newly - if current[key] || cur.State != delivery.BuildBuilding { + if current[key] || cur.State != builds.BuildBuilding { continue } - closed := delivery.BuilderBuild{State: delivery.BuildFailed, By: b.Claimer.By, Manifest: cur.Manifest, Step: cur.Step, + closed := builds.BuilderBuild{State: builds.BuildFailed, By: b.Claimer.By, Manifest: cur.Manifest, Step: cur.Step, Error: fmt.Sprintf("closed at start: left building by %s, and the module no longer has this fingerprint, so nothing will build it", cur.By)} - data, err := delivery.EncodeBuilderBuild(closed) + data, err := builds.EncodeBuilderBuild(closed) if err != nil { return err } @@ -225,8 +229,8 @@ func (b *Builder) Sweep(ctx context.Context) error { } // notePublished keeps the package versions a done build names as references, and says whether one was new. -func (b *Builder) notePublished(v delivery.BuilderBuild) bool { - if v.State != delivery.BuildDone { +func (b *Builder) notePublished(v builds.BuilderBuild) bool { + if v.State != builds.BuildDone { return false } b.mu.Lock() @@ -286,7 +290,7 @@ func (b *Builder) ArtifactsGone(ctx context.Context, e sweep.ArtifactsGone) { b.init() b.mu.Lock() m, ok := b.known[e.Module] - if key, err := delivery.BuilderBuildKey(e.Module, e.Fingerprint); err == nil { + if key, err := builds.BuilderBuildKey(e.Module, e.Fingerprint); err == nil { for id := range b.held { if strings.HasPrefix(id, key+" ") { delete(b.held, id) @@ -294,7 +298,7 @@ func (b *Builder) ArtifactsGone(ctx context.Context, e sweep.ArtifactsGone) { } } b.mu.Unlock() - if !ok || m.FP2 != e.Fingerprint { + if !ok || m.Fp2 != e.Fingerprint { b.logf("%s: heard %s for %s, a fingerprint the module does not have now: nothing to build", e.Module, sweep.EventArtifactsGone, e.Fingerprint) return @@ -308,7 +312,7 @@ func (b *Builder) ArtifactsGone(ctx context.Context, e sweep.ArtifactsGone) { func (b *Builder) Reconsider(ctx context.Context) { b.init() b.mu.Lock() - known := make(map[string]delivery.Module, len(b.known)) + known := make(map[string]meshcatalog.ModulesByModuleValue, len(b.known)) for k, m := range b.known { known[k] = m } @@ -324,7 +328,7 @@ func (b *Builder) Reconsider(ctx context.Context) { func (b *Builder) TrunkMoved(ctx context.Context) { b.init() b.mu.Lock() - cands := make([]delivery.Candidate, 0, len(b.cands)) + cands := make([]meshcatalog.CandidatesByGroupValue, 0, len(b.cands)) for _, c := range b.cands { cands = append(cands, c) } @@ -335,7 +339,7 @@ func (b *Builder) TrunkMoved(ctx context.Context) { } } -func sortedKeys(m map[string]delivery.Module) []string { +func sortedKeys(m map[string]meshcatalog.ModulesByModuleValue) []string { keys := make([]string, 0, len(m)) for k := range m { keys = append(keys, k) @@ -364,11 +368,11 @@ func (b *Builder) planOf(ctx context.Context, digest string) (Plan, error, bool) return plan, err, true } -func (b *Builder) consider(ctx context.Context, module string, m delivery.Module) { - if m.FP2 == "" || m.Manifest == "" { +func (b *Builder) consider(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue) { + if m.Fp2 == "" || m.Manifest == "" { return } - key, err := delivery.BuilderBuildKey(module, m.FP2) + key, err := builds.BuilderBuildKey(module, m.Fp2) if err != nil { b.logf("modules.%s: %v", module, err) return @@ -404,7 +408,7 @@ func (b *Builder) consider(ctx context.Context, module string, m delivery.Module // startOnce looks at the module for a key the caller has set inflight already (BuildAgain): the by-hand look runs once, and // a look the builder queued for the key meanwhile (again) still runs after it, as consider's loop does. -func (b *Builder) startOnce(ctx context.Context, key, module string, m delivery.Module) { +func (b *Builder) startOnce(ctx context.Context, key, module string, m meshcatalog.ModulesByModuleValue) { b.wg.Add(1) go func() { defer b.wg.Done() @@ -428,8 +432,8 @@ func (b *Builder) startOnce(ctx context.Context, key, module string, m delivery. // try claims the fingerprint and builds it, writing the key done or failed. A build that ends because ctx did is left // building by this builder: it takes it again when it starts (ADR 0478 clause 5). -func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { - key, _ := delivery.BuilderBuildKey(module, m.FP2) +func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue) { + key, _ := builds.BuilderBuildKey(module, m.Fp2) defer b.handEnded(key) plan, planErr, read := b.planOf(ctx, m.Manifest) nothing := read && planErr == nil && len(plan.Tasks) == 0 @@ -437,7 +441,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { if why := b.stale(m, plan); why != "" { // The fingerprint names a toolchain the build nodes no longer agree on: a new fingerprint is coming, and a build // now would be of this source with another toolchain (ADR 0584 clause 1, which changes ADR 0477 clause 2). Nothing is written. - b.logf("%s at %s is not built: %s", module, m.FP2, why) + b.logf("%s at %s is not built: %s", module, m.Fp2, why) return } } @@ -448,8 +452,8 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { b.mu.Unlock() var numbering versions.Numbering tag := "" - if !trunk && b.Tags != nil && b.refOf(module, m.FP2) != "" { - tag, _ = b.Tags(ctx, delivery.RepositoryName(m.Repository), m.Commit) + if !trunk && b.Tags != nil && b.refOf(module, m.Fp2) != "" { + tag, _ = b.Tags(ctx, repotrunk.RepositoryName(m.Repository), m.Commit) } if (trunk || tag != "") && planErr == nil && !nothing { var goOn bool @@ -457,7 +461,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { return } } - cl, err := b.Claimer.Claim(ctx, module, m.FP2, m.Manifest, ClaimOptions{TakeOver: first, ImageBuilder: imageSeat, Trunk: trunk, Published: b.isPublished, + cl, err := b.Claimer.Claim(ctx, module, m.Fp2, m.Manifest, ClaimOptions{TakeOver: first, ImageBuilder: imageSeat, Trunk: trunk, Published: b.isPublished, Stored: b.storedOnce(key)}) if err != nil { b.logf("%s: %v", module, err) @@ -467,7 +471,7 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { return } b.handClaimed(key, cl.Rev) - b.logBuild(LogStarted(module, m, b.refOf(module, m.FP2), b.Node, b.now())) + b.logBuild(LogStarted(module, m, b.refOf(module, m.Fp2), b.Node, b.now())) if cl.Unasked != nil { b.logf("%v", cl.Unasked) } @@ -477,10 +481,10 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { } // A done key taken again builds only what is missing: the image or the package built unpublished as a candidate's; the // other artifacts stay as the done key has them (ADR 0487 clauses 5 and 8, ADR 0498 clause 6). - var prior *delivery.BuilderBuild + var prior *builds.BuilderBuild var only map[string]bool publishing := false - if cl.Build.State == delivery.BuildDone { + if cl.Build.State == builds.BuildDone { prior, only = &cl.Build, map[string]bool{} if trunk { for _, bm := range publishable(cl.Build, imageSeat) { @@ -489,11 +493,11 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { } } } - var result delivery.BuilderBuild + var result builds.BuilderBuild if nothing { // A manifest with no artifact is built by building nothing: its key is done with no artifact, claimed and written as // any other, so the controller has a done key to register it from (issue 1613, ADR 0478 clause 1, ADR 0501 clause 1). - result = delivery.BuilderBuild{State: delivery.BuildDone, Step: "nothing to build: the manifest names no artifact"} + result = builds.BuilderBuild{State: builds.BuildDone, Step: "nothing to build: the manifest names no artifact"} } else { result = b.build(ctx, module, m, plan, planErr, prior, only, b.notes(cl)) } @@ -505,10 +509,10 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { return } result.By, result.Manifest = b.Claimer.By, m.Manifest - if len(cl.Gone) > 0 && result.State == delivery.BuildDone { + if len(cl.Gone) > 0 && result.State == builds.BuildDone { result.Step += " (built again: the artifact store no longer held " + strings.Join(cl.Gone, ", ") + ")" } - if publishing && result.State == delivery.BuildFailed { + if publishing && result.State == builds.BuildFailed { // The trunk's image or package was built only as a candidate's and its push or publish failed: the key says so, so the builds view, the // checker and the failed-key retake see it, and a look does not build the image again for nothing. result.Error = keyError([]string{"publishing what the merged commit built failed: " + result.Error}) @@ -517,18 +521,18 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { b.logf("%s: the build could not be written: %v", cl.Key, err) return } - ended := LogEnded(module, m, b.refOf(module, m.FP2), b.Node, b.now(), result) + ended := LogEnded(module, m, b.refOf(module, m.Fp2), b.Node, b.now(), result) ended.OutOfStep = b.takeOutOfStep(key) // the generated files the build module's compare found out of step (design 26) b.logBuild(ended) - if result.State == delivery.BuildDone && b.Numbers != nil && len(numbering.Versions) > 0 { + if result.State == builds.BuildDone && b.Numbers != nil && len(numbering.Versions) > 0 { if err := b.Numbers.Built(ctx, module, m, plan, numbering, result.Artifacts); err != nil { b.logf("%s: %v", cl.Key, err) } } - if result.State == delivery.BuildFailed { + if result.State == builds.BuildFailed { b.logf("%s: failed: %s", cl.Key, result.Error) } - if result.State == delivery.BuildQueued { + if result.State == builds.BuildQueued { b.logf("%s: waits for the package %s to be published", cl.Key, result.Waits) } if b.notePublished(result) { @@ -539,13 +543,13 @@ func (b *Builder) try(ctx context.Context, module string, m delivery.Module) { // stale is why the module's fingerprint is not built now, or empty when it is: the toolchain each language of the plan // has agreed (novox/hq ADR 0465) must be the one the fingerprint was computed with (Module.Toolchains). -func (b *Builder) stale(m delivery.Module, plan Plan) string { +func (b *Builder) stale(m meshcatalog.ModulesByModuleValue, plan Plan) string { for _, bm := range plan.Modules() { if !plan.NeedsToolchain(bm) { continue // an archive is packed with no toolchain } language := buildrun.LanguageOf[bm] - ag := delivery.ToolchainAgreement(b.Toolchains.Entries(language)) + ag := sdkstep.ToolchainAgreement(b.Toolchains.Entries(language)) if ag.Wait != "" { return fmt.Sprintf("the %s toolchain is not agreed: %s", language, ag.Wait) } @@ -558,15 +562,15 @@ func (b *Builder) stale(m delivery.Module, plan Plan) string { return "" } -func failed(format string, args ...any) delivery.BuilderBuild { - return delivery.BuilderBuild{State: delivery.BuildFailed, Error: fmt.Sprintf(format, args...)} +func failed(format string, args ...any) builds.BuilderBuild { + return builds.BuilderBuild{State: builds.BuildFailed, Error: fmt.Sprintf(format, args...)} } // lane is the build of a plan's tasks of one build module on one node. type lane struct { module, node string // node is the one that pulled the lane's ask tasks []Task - toolchain *delivery.ToolchainUsed + toolchain *builds.ToolchainUsed digests map[string]string // artifact -> digest references map[string]string // artifact -> the reference an image was pushed to (a trunk build only) imports []string // the mesh packages the lane's programs import, as its build module answered them @@ -574,7 +578,7 @@ type lane struct { err error } -func (b *Builder) build(ctx context.Context, module string, m delivery.Module, plan Plan, planErr error, prior *delivery.BuilderBuild, only map[string]bool, asked *askNotes) delivery.BuilderBuild { +func (b *Builder) build(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, planErr error, prior *builds.BuilderBuild, only map[string]bool, asked *askNotes) builds.BuilderBuild { switch { case planErr != nil: return failed("%s", planErr) @@ -582,9 +586,9 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p // Empty is not known (a root module's path is "."): the indexer writes it again (issue 1615). return failed("the module's key names no directory (path), so the build tools cannot be told where %s lives", module) } - repository := delivery.RepositoryName(m.Repository) + repository := repotrunk.RepositoryName(m.Repository) var lanes []*lane - used := map[string]delivery.ToolchainUsed{} + used := map[string]builds.ToolchainUsed{} var sitting []string // the nodes that sit out of a language's build, said in the key's step (ADR 0584 clause 2) var kept []string // build modules whose artifacts stay as the prior done key has them for _, bm := range plan.Modules() { @@ -592,20 +596,20 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p kept = append(kept, bm) continue } - var toolchain *delivery.ToolchainUsed + var toolchain *builds.ToolchainUsed if bm != ImageBuildModule && plan.NeedsToolchain(bm) { // The ask names the toolchain the fingerprint was computed with; a node building with another refuses it. language := buildrun.LanguageOf[bm] - ag := delivery.ToolchainAgreement(b.Toolchains.Entries(language)) + ag := sdkstep.ToolchainAgreement(b.Toolchains.Entries(language)) if ag.Wait != "" { return failed("the %s toolchain is not agreed: %s", language, ag.Wait) } tc := ag.Toolchain sitting = append(sitting, SittingOutWords(language, ag)...) if want := m.Toolchains[language]; want.Version != tc.Version || want.Recipe != tc.Recipe { - return delivery.BuilderBuild{} // the agreed toolchain moved since try looked: nothing is written + return builds.BuilderBuild{} // the agreed toolchain moved since try looked: nothing is written } - u := delivery.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe} + u := builds.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe} used[language], toolchain = u, &u } var tasks []Task @@ -617,7 +621,7 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p // One ask builds each fingerprint's part of a build module, on whichever node pulls it (ADR 0525 §3). lanes = append(lanes, &lane{module: bm, tasks: tasks, toolchain: toolchain, digests: map[string]string{}, references: map[string]string{}}) } - if key, err := delivery.BuilderBuildKey(module, m.FP2); err == nil { + if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { b.handLanes(key, len(lanes)) } var wg sync.WaitGroup @@ -626,7 +630,7 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p go func() { defer wg.Done(); b.runLane(ctx, l, repository, m, module, asked) }() } wg.Wait() - if key, err := delivery.BuilderBuildKey(module, m.FP2); err == nil { + if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { b.keepOutOfStep(key, lanes) } var errs []string @@ -639,11 +643,11 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p if ref := waitsOf(errs); ref != "" { // The module's lock names a version of a package the registry does not hold yet: the key waits for it, queued, and // is not failed (ADR 0500 clause 6). - return delivery.BuilderBuild{State: delivery.BuildQueued, Waits: ref, Step: "waits for the package " + ref + " to be published"} + return builds.BuilderBuild{State: builds.BuildQueued, Waits: ref, Step: "waits for the package " + ref + " to be published"} } return failed("%s", keyError(errs)) } - var artifacts []delivery.BuiltArtifact + var artifacts []builds.BuiltArtifact var present, counted map[string][]string if b.Forms != nil { present, counted = b.Forms() @@ -675,7 +679,7 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p if err != nil { return failed("%s", err) } - built := delivery.BuiltArtifact{Name: t.Artifact, Kind: t.Kind, Digest: d} + built := builds.BuiltArtifact{Name: t.Artifact, Kind: t.Kind, Digest: d} var answered []string for _, l := range lanes { if l.module != t.Module { @@ -685,7 +689,7 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p if r == "" && stored(t.Kind) { // A bundle, an archive or an upstream copy is done only when its push was answered with where it is (ADR 0498 // clause 1); a candidate's included (clause 3). - return failed("%s%s on %s: the build answered no reference, so its push to the artifact store was not answered", delivery.NotStored, t.Artifact, l.node) + return failed("%s%s on %s: the build answered no reference, so its push to the artifact store was not answered", builds.NotStored, t.Artifact, l.node) } if r != "" { answered = append(answered, r) @@ -713,11 +717,11 @@ func (b *Builder) build(ctx context.Context, module string, m delivery.Module, p if len(used) == 0 { used = nil } - done := delivery.BuilderBuild{State: delivery.BuildDone, By: b.Claimer.By, Manifest: m.Manifest, Step: step, Artifacts: artifacts, Toolchains: used, + done := builds.BuilderBuild{State: builds.BuildDone, By: b.Claimer.By, Manifest: m.Manifest, Step: step, Artifacts: artifacts, Toolchains: used, Imports: importsOf(lanes), Needs: forms.Current.Needs(artifacts)} // the forms it is written in (ADR 0618 §1) - if _, err := delivery.EncodeBuilderBuild(done); err != nil { + if _, err := builds.EncodeBuilderBuild(done); err != nil { // A reference the build module answered that does not name the artifact and its digest is no reference. - return failed("%s%v", delivery.NotStored, err) + return failed("%s%v", builds.NotStored, err) } return done } @@ -730,7 +734,7 @@ func stored(kind string) bool { // waitsFor opens the reason of a fetch step whose module's lock names a package version the registry does not hold, then // `@` (the Node build module's, ADR 0500 clause 6). -const waitsFor = delivery.WaitsFor +const waitsFor = builds.WaitsFor // waitsOf is the package version a lane's error says the module waits for, or "" when none does or what it names is not // `@`. @@ -742,7 +746,7 @@ func waitsOf(errs []string) string { } ref, _, _ := strings.Cut(e[at+len(waitsFor):], " ") ref = strings.TrimRight(ref, ":;,") - if _, err := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildQueued, Waits: ref}); err == nil { + if _, err := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildQueued, Waits: ref}); err == nil { return ref } } @@ -750,8 +754,8 @@ func waitsOf(errs []string) string { } // keyPrefixes are the errors a build module's step may open its reason with that the key's error must open with too, so -// the claim reads them (delivery.FailedUntilChanged), first the ones not built again. -var keyPrefixes = []string{delivery.NotPinned, delivery.VersionTaken, delivery.RefusedKind, delivery.NotStored} +// the claim reads them (builds.FailedUntilChanged), first the ones not built again. +var keyPrefixes = []string{builds.NotPinned, builds.VersionTaken, builds.RefusedKind, builds.NotStored} // keyError is the error of a key from its lanes' errors: when one names a prefix of keyPrefixes inside the step's words // (` on : the step failed: not stored: ...`), the key's error opens with that prefix, so a reader of the @@ -775,8 +779,8 @@ func keyError(errs []string) string { // want of an image builder before R4g is taken again (ADR 0525 §1). func imageSeat() bool { return true } -// askLanguage is the build seat language of each build module (delivery.AskSubject). -var askLanguage = map[string]string{buildrun.GoBuildModule: delivery.AskGo, buildrun.NodeBuildModule: delivery.AskNode, ImageBuildModule: delivery.AskImage} +// askLanguage is the build seat language of each build module (sdkstep.AskSubject). +var askLanguage = map[string]string{buildrun.GoBuildModule: sdkstep.AskGo, buildrun.NodeBuildModule: sdkstep.AskNode, ImageBuildModule: sdkstep.AskImage} // askNotes writes a claimed key's asks into it, building, before each is put (ADR 0525 §1; issue 1887 A4b), and holds // the asks a key taken over at start already names, so they are waited for and never put again. @@ -791,7 +795,7 @@ type askNotes struct { func (b *Builder) notes(cl *Claim) *askNotes { n := &askNotes{b: b, cl: cl} - if cl.Build.State == delivery.BuildBuilding && len(cl.Build.Asks) > 0 { + if cl.Build.State == builds.BuildBuilding && len(cl.Build.Asks) > 0 { n.resumed, n.step = maps.Clone(cl.Build.Asks), cl.Build.Step } return n @@ -809,7 +813,7 @@ func (n *askNotes) note(ctx context.Context, language, id, step string) error { n.mu.Lock() defer n.mu.Unlock() v := n.cl.Build - v.State, v.By, v.Step, v.Error, v.Artifacts, v.Needs = delivery.BuildBuilding, n.b.Claimer.By, step, "", nil, nil + v.State, v.By, v.Step, v.Error, v.Artifacts, v.Needs = builds.BuildBuilding, n.b.Claimer.By, step, "", nil, nil v.Asks = maps.Clone(v.Asks) if v.Asks == nil { v.Asks = map[string]string{} @@ -826,9 +830,9 @@ func (n *askNotes) note(ctx context.Context, language, id, step string) error { // check stream for a candidate's) and reads what the node that pulled it built (ADR 0525 §1, §3, §6). An upstream artifact // is copied by its pinned reference with no fetch (ADR 0498 clause 4); a lane of copies alone fetches nothing. A lane of a // key taken over at start waits for the ask the key names and puts none. -func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m delivery.Module, module string, asked *askNotes) { - req := buildstep.FetchRequest{Repository: repository, Commit: m.Commit} - if key, err := delivery.BuilderBuildKey(module, m.FP2); err == nil { +func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m meshcatalog.ModulesByModuleValue, module string, asked *askNotes) { + req := sdkstep.Fetch{Repository: repository, Commit: m.Commit} + if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { b.mu.Lock() v, candidate := b.via[key] b.mu.Unlock() @@ -847,26 +851,26 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m del } candidate := req.Base != "" language := askLanguage[l.module] - kind := delivery.AskDelivery + kind := sdkstep.AskDelivery if candidate { - kind = delivery.AskCheck + kind = sdkstep.AskCheck } - job := &delivery.BuildJob{Module: module, Path: m.Path, FP: m.FP2, Toolchain: l.toolchain, + job := &sdkstep.BuildJob{Module: module, Path: m.Path, Fp: m.Fp2, Toolchain: buildToolchain(language, l.toolchain), // Only a trunk key's build pushes an image or publishes a package; a candidate's never does (ADR 0487 clause 12, // ADR 0498 clauses 5 and 6). A bundle or an archive is pushed either way (clause 3). Publish: !candidate} for _, t := range l.tasks { - a := delivery.AskArtifact{Artifact: t.Artifact} + a := sdkstep.AskArtifact{Artifact: t.Artifact} if t.Kind == catalogue.ArtifactUpstream { a.From = t.From // the copy runs no code of a change: the module's name, the artifact and its pin } job.Artifacts = append(job.Artifacts, a) } if needsFetch(l.tasks) { - job.Fetch = req.Args() + job.Fetch = &req } - ask := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: language, Asker: delivery.BuilderModule, At: time.Now(), Build: job} - if language == delivery.AskGo && job.Fetch != nil && len(m.Packages) > 0 { + ask := sdkstep.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: language, Asker: builds.BuilderModule, At: time.Now(), Build: job} + if language == sdkstep.AskGo && job.Fetch != nil && len(m.Packages) > 0 { // The versions of the mesh packages the fingerprint was computed with (novox/hq ADR 0573 §2): the build module writes // them into the workspace, whatever the repository's go.mod names. ask.Packages = maps.Clone(m.Packages) @@ -899,29 +903,29 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m del return } } - step, ask, err := b.answer(ctx, l, ask, asked, module, m.FP2) + step, ask, err := b.answer(ctx, l, ask, asked, module, m.Fp2) if err != nil { l.err = fmt.Errorf("%s: %w", l.module, err) return } l.node = step.Node // The build module's compare of a contracts module's generated files (design 26, CAT-BG5/BN2) answers the files out - // of step as delivery.BuildAnswer.OutOfStep, in a failed step's result too: read before the step's state is. - var compared delivery.BuildAnswer + // of step as sdkstep.BuildAnswer.OutOfStep, in a failed step's result too: read before the step's state is. + var compared sdkstep.BuildAnswer if len(step.Result) > 0 && json.Unmarshal(step.Result, &compared) == nil { l.outOfStep = compared.OutOfStep } - if step.State != delivery.StepPassed { + if step.State != sdkstep.StepPassed { l.err = fmt.Errorf("%s on %s: the ask %s %s: %s", l.module, step.Node, ask.ID, step.State, step.Reason) return } - var answer delivery.BuildAnswer + var answer sdkstep.BuildAnswer if err := json.Unmarshal(step.Result, &answer); err != nil { l.err = fmt.Errorf("the ask of %s on %s answered what is not a build: %s", l.module, step.Node, step.Result) return } l.imports = answer.Imports - built := map[string]delivery.AskBuilt{} + built := map[string]sdkstep.AskBuilt{} for _, a := range answer.Artifacts { built[a.Artifact] = a } @@ -959,11 +963,11 @@ func (b *Builder) now() time.Time { } // logBuild tells the log an entry, said in the journal when it cannot be one. -func (b *Builder) logBuild(e delivery.BuildLogEntry) { +func (b *Builder) logBuild(e builds.BuildLogEntry) { if b.Log == nil { return } - if _, err := delivery.EncodeBuildLogEntry(e); err != nil { + if _, err := builds.EncodeBuildLogEntry(e); err != nil { b.logf("%s at %s: no log entry: %v", e.Module, e.FP, err) return } @@ -972,7 +976,7 @@ func (b *Builder) logBuild(e delivery.BuildLogEntry) { // refOf is the ref a candidate's build of module's fingerprint is built for, "" for the trunk's. func (b *Builder) refOf(module, fp string) string { - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return "" } @@ -984,27 +988,27 @@ func (b *Builder) refOf(module, fp string) string { // LogStarted is the log's entry of a build of module's fingerprint starting (ADR 0571 §1): the module, the fingerprint, the // source revision, where it is built from (repository, directory, commit, the candidate's ref), the node of the builder // that asked it and the time. -func LogStarted(module string, m delivery.Module, ref, by string, at time.Time) delivery.BuildLogEntry { +func LogStarted(module string, m meshcatalog.ModulesByModuleValue, ref, by string, at time.Time) builds.BuildLogEntry { dir := m.Path if dir == "" { - dir = delivery.RootPath + dir = meshcatalogclient.RootPath } - return delivery.BuildLogEntry{Event: delivery.BuildStartedEvent, Module: module, FP: m.FP2, Source: strconv.FormatUint(m.Src, 10), + return builds.BuildLogEntry{Event: builds.BuildStartedEvent, Module: module, FP: m.Fp2, Source: strconv.FormatUint(uint64(m.Src), 10), Repository: m.Repository, Directory: dir, Commit: m.Commit, Ref: ref, Node: by, At: at.UTC()} } // LogEnded is the log's entry of that build ending: the outcome (done, failed or queued), the manifest built, the bases it // was built against and each artifact made, as the done key has them: what the operator's command nox builds shows of a build. -func LogEnded(module string, m delivery.Module, ref, by string, at time.Time, result delivery.BuilderBuild) delivery.BuildLogEntry { +func LogEnded(module string, m meshcatalog.ModulesByModuleValue, ref, by string, at time.Time, result builds.BuilderBuild) builds.BuildLogEntry { e := LogStarted(module, m, ref, by, at) - e.Event, e.Outcome, e.Manifest = delivery.BuildEndedEvent, result.State, m.Manifest + e.Event, e.Outcome, e.Manifest = builds.BuildEndedEvent, result.State, m.Manifest if result.Manifest != "" { e.Manifest = result.Manifest } if m.Inputs2 != nil { e.Against = append([]string(nil), m.Inputs2.Bases...) } - e.Made = append([]delivery.BuiltArtifact(nil), result.Artifacts...) + e.Made = append([]builds.BuiltArtifact(nil), result.Artifacts...) e.Imports = append([]string(nil), result.Imports...) // the indexer keeps them on the module (novox/hq ADR 0593 §2) return e } @@ -1057,7 +1061,7 @@ func importsOf(lanes []*lane) []string { // SittingOutWords says, per node that is not on the agreed toolchain of a language, that it sits out and which version // it has against the agreed one: "ace sits out: typescript 26.12.1, agreed 26.13.0" (novox/hq ADR 0584 clause 2). -func SittingOutWords(language string, a delivery.Agreement) []string { +func SittingOutWords(language string, a sdkstep.Agreement) []string { var out []string for _, s := range a.SittingOut { out = append(out, fmt.Sprintf("%s sits out: %s %s, agreed %s", s.Node, language, s.Version, a.Toolchain.Version)) diff --git a/modules/mesh-delivery-builder/internal/build/run_test.go b/modules/mesh-delivery-builder/internal/build/run_test.go index 83c965e7..93081d0a 100644 --- a/modules/mesh-delivery-builder/internal/build/run_test.go +++ b/modules/mesh-delivery-builder/internal/build/run_test.go @@ -14,11 +14,15 @@ 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/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/forms" 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/keys" "mesh-delivery-builder/internal/versions" ) @@ -45,7 +49,7 @@ type buildModules struct { } func newBuildModules() *buildModules { - return &buildModules{steps: &fakeSteps{steps: map[string]delivery.Step{}}, digests: map[string]string{}, fails: map[string]string{}, noRef: map[string]bool{}, packages: map[string]string{}} + return &buildModules{steps: &fakeSteps{steps: map[string]buildstep.Step{}}, digests: map[string]string{}, fails: map[string]string{}, noRef: map[string]bool{}, packages: map[string]string{}} } func (b *buildModules) CallOn(node, key string, body any) (json.RawMessage, error) { @@ -63,9 +67,9 @@ func (b *buildModules) CallOn(node, key string, body any) (json.RawMessage, erro id := fmt.Sprintf("s%d", b.n) module, tool, _ := strings.Cut(key, ".") args, _ := body.(map[string]any) - step := delivery.Step{ID: id, Tool: tool, Node: node, State: delivery.StepPassed} + step := buildstep.Step{ID: id, Tool: tool, Node: node, State: buildstep.StepPassed} if reason, ok := b.fails[node+"/"+tool]; ok { - step.State, step.Reason = delivery.StepFailed, reason + step.State, step.Reason = buildstep.StepFailed, reason } else if strings.HasSuffix(tool, "_fetch") { step.Result = json.RawMessage(`{"workspace":"w-` + node + `"}`) if args["base"] != nil { // a fetch with a base answers the merge, or the files in conflict (mesh-build-go FetchResult) @@ -96,7 +100,7 @@ func (b *buildModules) CallOn(node, key string, body any) (json.RawMessage, erro step.Result = json.RawMessage(`{"digest":"` + d + `"}`) } } - sk, _ := delivery.StepKey(node, id) + sk, _ := buildstep.StepKey(node, id) b.steps.mu.Lock() b.steps.steps[module+"/"+sk] = step b.steps.mu.Unlock() @@ -145,12 +149,12 @@ var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun // answer runs an ask as the build module that pulls it does (runAsk), with the fake build modules' tools on the // rig's pulling node, and answers the step that node writes. -func (r *rig) answer(a delivery.StepAsk) delivery.Step { +func (r *rig) answer(a buildstep.StepAsk) buildstep.Step { node := r.puller if node == "" { node = "ace" } - module, _ := delivery.BuildModuleOf(a.Language) + module, _ := buildstep.BuildModuleOf(a.Language) runner := buildrun.Runner{Caller: r.bm, Steps: r.bm.steps, Bound: time.Second} tool := func(name string) stepFunc { return func(ctx context.Context, args map[string]any) (any, error) { @@ -163,9 +167,9 @@ func (r *rig) answer(a delivery.StepAsk) delivery.Step { } steps := askSteps{Fetch: tool("fetch"), Build: tool("build"), Copy: tool("copy")} out, err := runAsk(context.Background(), steps, a) - step := delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: delivery.StepPassed} + step := buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: buildstep.StepPassed} if err != nil { - step.State, step.Reason = delivery.StepFailed, err.Error() + step.State, step.Reason = buildstep.StepFailed, err.Error() return step } step.Result, _ = json.Marshal(out) @@ -186,27 +190,27 @@ func newRig(t *testing.T, artifacts ...catalogue.Artifact) *rig { Trunk: func(context.Context, uint64, string) (string, error) { return trunkHead, nil }, } var err error - if r.key, err = delivery.BuilderBuildKey("m", fp1); err != nil { + if r.key, err = builds.BuilderBuildKey("m", fp1); err != nil { t.Fatal(err) } return r } // agreedToolchains are what the rig's nodes report, and so what a fingerprint computed now names. -var agreedToolchains = map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}} +var agreedToolchains = map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "1"}, "typescript": {Version: "tsc5", Recipe: "1"}} func moduleValue(t *testing.T, path string) []byte { return moduleValueWith(t, path, agreedToolchains) } -func moduleValueWith(t *testing.T, path string, toolchains map[string]delivery.ToolchainUsed) []byte { +func moduleValueWith(t *testing.T, path string, toolchains map[string]builds.ToolchainUsed) []byte { t.Helper() - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP: "src1:" + hex64('f'), FP2: fp1, Manifest: mdigest('a'), Path: path, Toolchains: toolchains}) + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: "src1:" + hex64('f'), Fp2: fp1, Manifest: mdigest('a'), Path: path, Toolchains: CatalogToolchains(toolchains)}) if err != nil { t.Fatal(err) } return data } -func (r *rig) consider(t *testing.T, path string) delivery.BuilderBuild { +func (r *rig) consider(t *testing.T, path string) builds.BuilderBuild { t.Helper() r.b.Module(context.Background(), "m", moduleValue(t, path)) r.b.Wait() @@ -237,43 +241,43 @@ func TestABundleAndAnArchiveAreBuiltOnOneNodeAndDone(t *testing.T) { // 1607/AC7 t.Errorf("the key does not say ace sits out: %s", st) } }() - var logged []delivery.BuildLogEntry + var logged []builds.BuildLogEntry var lmu sync.Mutex - r.b.Node, r.b.Log = "novox", func(e delivery.BuildLogEntry) { lmu.Lock(); logged = append(logged, e); lmu.Unlock() } + r.b.Node, r.b.Log = "novox", func(e builds.BuildLogEntry) { lmu.Lock(); logged = append(logged, e); lmu.Unlock() } v := r.consider(t, "modules/m") // 1955/L7 (ADR 0571 §1): the build is one start and one end in the builder's log, every field named, its duration read // from the two. - if len(logged) != 2 || logged[0].Event != delivery.BuildStartedEvent || logged[1].Event != delivery.BuildEndedEvent || - logged[1].Outcome != delivery.BuildDone || len(logged[1].Made) != 3 || logged[1].Made[0].Reference == "" || + if len(logged) != 2 || logged[0].Event != builds.BuildStartedEvent || logged[1].Event != builds.BuildEndedEvent || + logged[1].Outcome != builds.BuildDone || len(logged[1].Made) != 3 || logged[1].Made[0].Reference == "" || logged[1].Manifest != manifestD || logged[0].Manifest != "" || logged[0].Repository != "novox/mesh-catalog" || logged[0].Directory != "modules/m" || logged[0].Commit != "7a74cee83f88e6585a5511c3cc53bdf0f5303d20" || logged[0].Node != "novox" || logged[0].FP != fp1 || logged[0].Module != "m" { t.Fatalf("logged = %+v", logged) } for _, e := range logged { - if _, err := delivery.EncodeBuildLogEntry(e); err != nil { + if _, err := builds.EncodeBuildLogEntry(e); err != nil { t.Errorf("an entry the log refuses: %v", err) } } - if _, ok := delivery.BuildDuration(logged[0], logged[1]); !ok { + if _, ok := builds.BuildDuration(logged[0], logged[1]); !ok { t.Error("no duration from the two entries") } // builder_builds_since (ADR 0571 clause 9) answers the end alone from the log's page, and says when the log holds more. var page stdio.LogPage for i, e := range logged { - raw, _ := delivery.EncodeBuildLogEntry(e) + raw, _ := builds.EncodeBuildLogEntry(e) page.Entries = append(page.Entries, stdio.LogEntry{Sequence: uint64(i + 1), Key: e.Module, Value: raw, At: e.At}) } page.Next = 3 since, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At) - if err != nil || len(since.Builds) != 1 || since.Builds[0].Event != delivery.BuildEndedEvent || len(since.Builds[0].Made) != 3 || !since.More { + if err != nil || len(since.Builds) != 1 || since.Builds[0].Event != builds.BuildEndedEvent || len(since.Builds[0].Made) != 3 || !since.More { t.Errorf("builds since = %+v, %v, want the one end and more", since, err) } page.Entries[0].Value = []byte(`{"v":0}`) if _, err := BuildsSince(func(time.Time, string, int) (*stdio.LogPage, error) { return &page, nil }, logged[0].At); err == nil { t.Error("an entry that does not read was skipped, want it refused") } - if v.State != delivery.BuildDone || v.By != "novox.builder" || v.Manifest != manifestD || len(v.Artifacts) != 3 { + if v.State != builds.BuildDone || v.By != "novox.builder" || v.Manifest != manifestD || len(v.Artifacts) != 3 { t.Fatalf("build = %+v, want done by novox.builder with three artifacts", v) } for i, want := range []string{"files", "tool", "web"} { @@ -321,14 +325,14 @@ func TestATrunkFingerprintIsOneAskOnTheDeliveryStream(t *testing.T) { r := newRig(t, goBundle) r.asks.Answer = nil // no node has pulled it yet // Issue 1585 (ADR 0573 §2): the ask carries the package versions the fingerprint was computed with. - sdk := delivery.PackageVersions{"git.novox.be/novox/mesh-sdk/go": hex40('5')} - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", - FP2: fp1, Manifest: mdigest('a'), Path: "modules/m", Toolchains: agreedToolchains, Packages: sdk}) + sdk := buildstep.PackageVersions{"git.novox.be/novox/mesh-sdk/go": hex40('5')} + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", + Fp2: fp1, Manifest: mdigest('a'), Path: "modules/m", Toolchains: CatalogToolchains(agreedToolchains), Packages: sdk}) if err != nil { t.Fatal(err) } r.b.Module(context.Background(), "m", data) - var asks []delivery.StepAsk + var asks []buildstep.StepAsk for deadline := time.Now().Add(10 * time.Second); len(asks) == 0; time.Sleep(time.Millisecond) { if time.Now().After(deadline) { t.Fatal("no ask was put") @@ -336,13 +340,13 @@ func TestATrunkFingerprintIsOneAskOnTheDeliveryStream(t *testing.T) { asks = r.asks.Asks() } a := asks[0] - if len(asks) != 1 || a.Kind != delivery.AskDelivery || a.Language != delivery.AskGo || a.Build == nil || a.Build.FP != fp1 { + if len(asks) != 1 || a.Kind != buildstep.AskDelivery || a.Language != buildstep.AskGo || a.Build == nil || a.Build.Fp != fp1 { t.Fatalf("asks = %+v, want one Go delivery ask of the fingerprint", asks) } if !reflect.DeepEqual(a.Packages, sdk) { t.Errorf("the ask's packages = %v, want %v", a.Packages, sdk) } - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildBuilding || v.Asks[delivery.AskGo] != a.ID { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildBuilding || v.Asks[buildstep.AskGo] != a.ID { t.Fatalf("the key = %+v, want building naming the ask %s", v, a.ID) } if n := len(r.bm.callsOf("build_go_build")) + len(r.bm.callsOf("build_go_fetch")); n != 0 { @@ -350,7 +354,7 @@ func TestATrunkFingerprintIsOneAskOnTheDeliveryStream(t *testing.T) { } r.asks.End(r.answer(a)) // a node pulled it and ran it r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || !strings.Contains(v.Step, "on ace") || v.Artifacts[0].Reference == "" || len(v.Asks) != 0 { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || !strings.Contains(v.Step, "on ace") || v.Artifacts[0].Reference == "" || len(v.Asks) != 0 { t.Fatalf("the key = %+v, want done on ace naming the reference", v) } } @@ -360,17 +364,17 @@ func TestATrunkFingerprintIsOneAskOnTheDeliveryStream(t *testing.T) { func TestARestartedBuilderWaitsForTheAskTheKeyNames(t *testing.T) { r := newRig(t, goBundle) const id = "mvl0frv18d1957" - data, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Manifest: manifestD, - Step: "asked mesh-build-go", Asks: map[string]string{delivery.AskGo: id}}) + data, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Manifest: manifestD, + Step: "asked mesh-build-go", Asks: map[string]string{buildstep.AskGo: id}}) r.st.vals[r.key] = data r.asks.Answer = nil - r.asks.End(delivery.Step{ID: id, Node: "shanks", State: delivery.StepPassed, + r.asks.End(buildstep.Step{ID: id, Node: "shanks", State: buildstep.StepPassed, Result: []byte(`{"artifacts":[{"artifact":"tool","digest":"` + digest + `","reference":"artifact-store://m/tool/blobs/` + digest + `"}]}`)}) v := r.consider(t, "modules/m") if n := len(r.asks.Asks()); n != 0 { t.Fatalf("%d asks put again", n) } - if v.State != delivery.BuildDone || !strings.Contains(v.Step, "on shanks") { + if v.State != builds.BuildDone || !strings.Contains(v.Step, "on shanks") { t.Fatalf("the key = %+v, want done from the ask's step on shanks", v) } } @@ -379,13 +383,13 @@ func TestAFailingStepFailsTheBuildWithItsReasonAndNodeAndIsTriedAgainAsANewRevis r := newRig(t, goBundle) r.bm.fails["ace/build_go_build"] = "go build of tool: undefined: x" v := r.consider(t, "modules/m") - if v.State != delivery.BuildFailed || !strings.Contains(v.Error, "ace") || !strings.Contains(v.Error, "undefined: x") { + if v.State != builds.BuildFailed || !strings.Contains(v.Error, "ace") || !strings.Contains(v.Error, "undefined: x") { t.Fatalf("build = %+v, want failed with the node and the reason", v) } rev := r.st.revs[r.key] delete(r.bm.fails, "ace/build_go_build") v = r.consider(t, "modules/m") - if v.State != delivery.BuildDone || r.st.revs[r.key] <= rev { + if v.State != builds.BuildDone || r.st.revs[r.key] <= rev { t.Errorf("build = %+v at revision %d (was %d), want done as a new revision of the same key", v, r.st.revs[r.key], rev) } } @@ -394,7 +398,7 @@ func TestAnImageGoesToOneNodeAndGetsOneDoneKeyWithItsReference(t *testing.T) { / r := newRig(t, imageArt) r.puller = "ace" // two nodes qualify; an image still builds on one v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "image" || v.Artifacts[0].Digest != digest { + if v.State != builds.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Kind != "image" || v.Artifacts[0].Digest != digest { t.Fatalf("build = %+v, want done with one image artifact and its digest", v) } if v.Artifacts[0].Reference == "" { @@ -414,7 +418,7 @@ func TestABundleAndAnImageOfOneModuleBuildOnOneNodeEach(t *testing.T) { // 1705a r := newRig(t, goBundle, imageArt) r.puller = "ace" v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || len(v.Artifacts) != 2 { + if v.State != builds.BuildDone || len(v.Artifacts) != 2 { t.Fatalf("build = %+v", v) } if n := len(r.bm.callsOf("build_go_build")); n != 1 { @@ -443,7 +447,7 @@ func TestACandidatesImageCallCarriesNoPublishAndItsKeyNoReference(t *testing.T) t.Errorf("the check's build numbered with tags %v (asked %v), want feat-x for its head", nums.tags, tagAsked) } v := valueOf(t, r.st, r.key) - if v.State != delivery.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Reference != "" { + if v.State != builds.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Reference != "" { t.Fatalf("build = %+v, want done with no reference", v) } calls := r.bm.callsOf("build_image_build") @@ -503,14 +507,14 @@ type fakeNumbers struct { done map[string]string // the done key's digests the last look numbered with } -func (f *fakeNumbers) Number(_ context.Context, _ string, _ delivery.Module, _ Plan, tag string, done map[string]string) (versions.Numbering, error) { +func (f *fakeNumbers) Number(_ context.Context, _ string, _ meshcatalog.ModulesByModuleValue, _ Plan, tag string, done map[string]string) (versions.Numbering, error) { f.tags = append(f.tags, tag) f.done = done f.numbered++ return versions.Numbering{Wait: f.wait, Versions: f.versions}, nil } -func (f *fakeNumbers) Built(_ context.Context, _ string, _ delivery.Module, _ Plan, got versions.Numbering, made []delivery.BuiltArtifact) error { +func (f *fakeNumbers) Built(_ context.Context, _ string, _ meshcatalog.ModulesByModuleValue, _ Plan, got versions.Numbering, made []builds.BuiltArtifact) error { f.built = map[string]string{} for _, a := range made { if got.Versions[a.Name] != "" { @@ -523,7 +527,7 @@ func (f *fakeNumbers) Built(_ context.Context, _ string, _ delivery.Module, _ Pl func TestAnUnpinnedUpstreamFailsNotPinnedAndIsNeverBuiltAgain(t *testing.T) { // B3, ADR 0498 clause 4 r := newRig(t, archiveA, catalogue.Artifact{Name: "up", Kind: "upstream", From: "nats:2.11.17-alpine"}) v := r.consider(t, "modules/m") - if v.State != delivery.BuildFailed || !strings.HasPrefix(v.Error, delivery.NotPinned) || !strings.Contains(v.Error, "up") { + if v.State != builds.BuildFailed || !strings.HasPrefix(v.Error, builds.NotPinned) || !strings.Contains(v.Error, "up") { t.Fatalf("build = %+v, want failed 'not pinned:' naming the artifact", v) } if len(r.bm.calls) != 0 { @@ -539,7 +543,7 @@ func TestAnUnpinnedUpstreamFailsNotPinnedAndIsNeverBuiltAgain(t *testing.T) { // func TestAModuleKeyWithNoPathFailsNamingThatReason(t *testing.T) { // 1607/AC8, ADR 0478 clause 8 r := newRig(t, goBundle) v := r.consider(t, "") - if v.State != delivery.BuildFailed || !strings.Contains(v.Error, "path") || len(r.bm.calls) != 0 { + if v.State != builds.BuildFailed || !strings.Contains(v.Error, "path") || len(r.bm.calls) != 0 { t.Errorf("build = %+v with %d calls, want failed naming the missing path and nothing called", v, len(r.bm.calls)) } } @@ -548,14 +552,14 @@ func TestAnUnreadableManifestFailsTheBuild(t *testing.T) { r := newRig(t, goBundle) delete(r.mf.byDigest, mdigest('a')) v := r.consider(t, "modules/m") - if v.State != delivery.BuildFailed || !strings.Contains(v.Error, "manifest") { + if v.State != builds.BuildFailed || !strings.Contains(v.Error, "manifest") { t.Errorf("build = %+v, want failed naming the manifest", v) } } func TestAModuleWithNoFingerprintIsNotBuilt(t *testing.T) { // 1607/AC2 r := newRig(t) - data, _ := delivery.EncodeModule(delivery.Module{Repository: "novox/r", Commit: "c", FP: "src1:" + hex64('f'), None2: "toolchain not yet reported"}) + data, _ := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/r", Commit: "c", Fp: "src1:" + hex64('f'), None2: "toolchain not yet reported"}) r.b.Module(context.Background(), "n", data) r.b.Wait() if len(r.st.vals) != 0 { @@ -568,7 +572,7 @@ func TestAModuleWithNoArtifactGetsADoneKeyWithNoArtifactAndNoBuildCall(t *testin r := newRig(t) r.mf.byDigest[mdigest('a')] = manifest v := r.consider(t, "") // a module with nothing to build needs no directory - if v.State != delivery.BuildDone || len(v.Artifacts) != 0 || v.Manifest != manifestD || v.By != "novox.builder" { + if v.State != builds.BuildDone || len(v.Artifacts) != 0 || v.Manifest != manifestD || v.By != "novox.builder" { t.Errorf("%s: build = %+v, want done with no artifact", name, v) } if len(r.bm.calls) != 0 { @@ -585,8 +589,8 @@ func TestAModuleWithNoArtifactGetsADoneKeyWithNoArtifactAndNoBuildCall(t *testin func TestAKeyTheBuilderLeftBuildingIsBuiltAtStartAndDoneKeysAreNotBuiltTwice(t *testing.T) { // 1607/AC3, AC7 r := newRig(t, goBundle) - seed(t, r.st, delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Manifest: manifestD}) - if v := r.consider(t, "modules/m"); v.State != delivery.BuildDone { + seed(t, r.st, builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Manifest: manifestD}) + if v := r.consider(t, "modules/m"); v.State != builds.BuildDone { t.Fatalf("build = %+v, want the builder's own building key built", v) } calls := len(r.bm.calls) @@ -611,7 +615,7 @@ func TestTwoInstancesStartingTogetherEndWithOneDoneKey(t *testing.T) { // 1607/A go func() { defer wg.Done(); b.Module(context.Background(), "m", value); b.Wait() }() } wg.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || len(v.Artifacts) != 1 { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || len(v.Artifacts) != 1 { t.Errorf("build = %+v, want one done key", v) } if n := len(r.bm.callsOf("build_go_build")); n < 1 { @@ -631,14 +635,14 @@ func TestAModulesValueThatArrivesWhileItsKeyIsBeingBuiltIsLookedAtAgainWhenItEnd delete(r.bm.fails, "ace/build_go_build") // what the event is about: the build can succeed now close(r.bm.hold) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone { t.Errorf("build = %+v, want the failed key built again as a new revision after the first build ended", v) } } func TestAKeyIsBuiltOnlyWithTheToolchainItsFingerprintNamesAndNothingIsWrittenOtherwise(t *testing.T) { // 1615/AC4 r := newRig(t, goBundle) - old := map[string]delivery.ToolchainUsed{"go": {Version: "go1.26.0", Recipe: "1"}} + old := map[string]builds.ToolchainUsed{"go": {Version: "go1.26.0", Recipe: "1"}} r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", old)) r.b.Wait() if len(r.st.vals) != 0 || len(r.bm.calls) != 0 { @@ -646,7 +650,7 @@ func TestAKeyIsBuiltOnlyWithTheToolchainItsFingerprintNamesAndNothingIsWrittenOt t.Errorf("keys %v and %d calls for a fingerprint of go1.26.0 while the nodes agree on go1.27.1, want neither", r.st.vals, len(r.bm.calls)) } // A recipe that differs is a different toolchain too, and a value that names none (written before) is not built. - r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "0"}})) + r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "0"}})) r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", nil)) r.b.Wait() if len(r.st.vals) != 0 || len(r.bm.calls) != 0 { @@ -660,9 +664,9 @@ func TestAKeyIsBuiltOnlyWithTheToolchainItsFingerprintNamesAndNothingIsWrittenOt if len(r.st.vals) != 0 { t.Errorf("keys %v: the old fingerprint was built with the new toolchain", r.st.vals) } - r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.2", Recipe: "1"}})) + r.b.Module(context.Background(), "m", moduleValueWith(t, "modules/m", map[string]builds.ToolchainUsed{"go": {Version: "go1.27.2", Recipe: "1"}})) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || v.Toolchains["go"].Version != "go1.27.2" { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || v.Toolchains["go"].Version != "go1.27.2" { t.Errorf("build = %+v, want done with go1.27.2", v) } } @@ -676,7 +680,7 @@ func aRecipeThatMovesBuildsTheModuleAgainUnderItsNewFingerprint(t *testing.T) { calls := len(r.bm.calls) fp2 := "src2:" + hex64('2') for _, n := range []string{"ace", "novox"} { - b, err := delivery.EncodeToolchain(delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "2", At: time.Now()}) + b, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "2", At: time.Now().UTC()}) if err != nil { t.Fatal(err) } @@ -687,21 +691,21 @@ func aRecipeThatMovesBuildsTheModuleAgainUnderItsNewFingerprint(t *testing.T) { if len(r.bm.calls) != calls { t.Errorf("%d calls, was %d: the first fingerprint was built again for the new recipe", len(r.bm.calls), calls) } - data, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", FP: "src1:" + hex64('f'), FP2: fp2, - Manifest: mdigest('a'), Path: "modules/m", Toolchains: map[string]delivery.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "2"}}}) + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: "src1:" + hex64('f'), Fp2: fp2, + Manifest: mdigest('a'), Path: "modules/m", Toolchains: CatalogToolchains(map[string]builds.ToolchainUsed{"go": {Version: "go1.27.1", Recipe: "2"}})}) if err != nil { t.Fatal(err) } r.b.Module(context.Background(), "m", data) r.b.Wait() - second, err := delivery.BuilderBuildKey("m", fp2) + second, err := builds.BuilderBuildKey("m", fp2) if err != nil { t.Fatal(err) } - if v := valueOf(t, r.st, second); v.State != delivery.BuildDone || v.Toolchains["go"].Recipe != "2" { + if v := valueOf(t, r.st, second); v.State != builds.BuildDone || v.Toolchains["go"].Recipe != "2" { t.Errorf("second build = %+v, want done with recipe 2", v) } - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone { t.Errorf("first build = %+v, want it left done", v) } } @@ -718,7 +722,7 @@ func TestNodesTheyDisagreeOnLeaveTheKeyUnwritten(t *testing.T) { // 1615/AC4 func TestARootModulesPathIsDotAndIsGivenToTheBuildTools(t *testing.T) { // 1615/AC4 r := newRig(t, goBundle) - if v := r.consider(t, delivery.RootPath); v.State != delivery.BuildDone { + if v := r.consider(t, meshcatalogclient.RootPath); v.State != builds.BuildDone { t.Fatalf("build = %+v, want done for a root module", v) } if args := r.bm.callsOf("build_go_build")[0].body.(map[string]any); args["module"] != "." { @@ -733,7 +737,7 @@ func TestACandidatesImageIsPublishedOnceWhenTheTrunkNamesItsFingerprint(t *testi r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) r.b.Wait() v := valueOf(t, r.st, r.key) - if v.State != delivery.BuildDone || v.Artifacts[0].Reference != "" { + if v.State != builds.BuildDone || v.Artifacts[0].Reference != "" { t.Fatalf("candidate build = %+v, want done with no reference", v) } if n := len(r.bm.callsOf("build_image_build")); n != 1 { @@ -743,7 +747,7 @@ func TestACandidatesImageIsPublishedOnceWhenTheTrunkNamesItsFingerprint(t *testi r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) r.b.Wait() v = valueOf(t, r.st, r.key) - if v.State != delivery.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Reference == "" { + if v.State != builds.BuildDone || len(v.Artifacts) != 1 || v.Artifacts[0].Reference == "" { t.Fatalf("trunk build = %+v, want done with a reference", v) } calls := r.bm.callsOf("build_image_build") @@ -771,21 +775,21 @@ func TestAFailedPublishOfACandidatesImageIsWrittenToTheKey(t *testing.T) { // re trunkModule(t, r) r.b.Candidate(context.Background(), "feat%2Fx", candidateValue(t, "feat/x", 7, headX)) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone { t.Fatalf("candidate build = %+v", v) } r.bm.fails["ace/build_image_build"] = "push to the registry: connection refused" r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) // the merge: the trunk names the fingerprint r.b.Wait() v := valueOf(t, r.st, r.key) - if v.State != delivery.BuildFailed || !strings.Contains(v.Error, "connection refused") || !strings.Contains(v.Error, "publish") { + if v.State != builds.BuildFailed || !strings.Contains(v.Error, "connection refused") || !strings.Contains(v.Error, "publish") { t.Fatalf("build = %+v, want failed naming the publish and its reason", v) } // The failed key is retaken as any failed key is, and ends published once the registry answers. delete(r.bm.fails, "ace/build_image_build") r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || v.Artifacts[0].Reference == "" { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || v.Artifacts[0].Reference == "" { t.Errorf("build = %+v, want done with a reference", v) } } @@ -795,18 +799,18 @@ func TestAFailedPublishOfACandidatesImageIsWrittenToTheKey(t *testing.T) { // re type goneAsks struct { mu sync.Mutex n int - why func(a delivery.StepAsk) string + why func(a buildstep.StepAsk) string dropped []string } -func (g *goneAsks) Drop(_ context.Context, a delivery.StepAsk) error { +func (g *goneAsks) Drop(_ context.Context, a buildstep.StepAsk) error { g.mu.Lock() defer g.mu.Unlock() g.dropped = append(g.dropped, a.ID) return nil } -func (g *goneAsks) Gone(_ context.Context, a delivery.StepAsk) (string, error) { +func (g *goneAsks) Gone(_ context.Context, a buildstep.StepAsk) (string, error) { g.mu.Lock() g.n++ g.mu.Unlock() @@ -820,7 +824,7 @@ func (g *goneAsks) asked() int { } // waitForAsks waits until n asks were put. -func waitForAsks(t *testing.T, r *rig, n int) []delivery.StepAsk { +func waitForAsks(t *testing.T, r *rig, n int) []buildstep.StepAsk { t.Helper() for deadline := time.Now().Add(10 * time.Second); len(r.asks.Asks()) < n; time.Sleep(time.Millisecond) { if time.Now().After(deadline) { @@ -838,7 +842,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { r := newRig(t, goBundle) r.asks.Answer = nil r.b.Asks = buildrun.Asker{Publisher: r.asks, Waiter: r.asks, Bound: 20 * time.Millisecond} - gone := &goneAsks{why: func(delivery.StepAsk) string { return "" }} + gone := &goneAsks{why: func(buildstep.StepAsk) string { return "" }} r.b.Gone, r.b.CheckAfter = gone, 5*time.Millisecond r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) waitForAsks(t, r, 1) @@ -849,7 +853,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { t.Fatalf("whether the ask can still be answered was read %d times within 10 s, want 5", gone.asked()) } } - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildBuilding { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildBuilding { t.Fatalf("the key = %+v while its ask waits, want building", v) } if gone.asked() == 0 { @@ -857,7 +861,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { } r.asks.End(r.answer(r.asks.Asks()[0])) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || len(r.asks.Asks()) != 1 { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || len(r.asks.Asks()) != 1 { t.Fatalf("the key = %+v after %d asks, want done from the one ask", v, len(r.asks.Asks())) } }) @@ -871,17 +875,17 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { r.b.Now = func() time.Time { return start.Add(time.Duration(clock.Add(1)) * time.Second) } r.b.DeclinePause, r.b.DeclineBound = -1, time.Minute r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) - declined, _ := json.Marshal(delivery.StepDeclined{Declined: true, Lacks: []string{"the agreed toolchain go1.27.1 (recipe 1)"}}) + declined, _ := json.Marshal(buildstep.StepDeclined{Declined: true, Lacks: []string{"the agreed toolchain go1.27.1 (recipe 1)"}}) waitForAsks(t, r, 1) - for ended, deadline := 0, time.Now().Add(20*time.Second); valueOf(t, r.st, r.key).State == delivery.BuildBuilding && time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) { + for ended, deadline := 0, time.Now().Add(20*time.Second); valueOf(t, r.st, r.key).State == builds.BuildBuilding && time.Now().Before(deadline); time.Sleep(5 * time.Millisecond) { if asks := r.asks.Asks(); len(asks) > ended { // each ask put again is declined in its turn - r.asks.End(delivery.Step{ID: asks[ended].ID, Node: "ace", State: delivery.StepRefused, Reason: "this node builds with go1.26.0", Result: declined}) + r.asks.End(buildstep.Step{ID: asks[ended].ID, Node: "ace", State: buildstep.StepRefused, Reason: "this node builds with go1.26.0", Result: declined}) ended++ } } r.b.Wait() v := valueOf(t, r.st, r.key) - if n := len(r.asks.Asks()); v.State != delivery.BuildFailed || n <= DefaultAskings || !strings.Contains(v.Error, "declined for") || !strings.Contains(v.Error, "ace: this node builds with go1.26.0") { + if n := len(r.asks.Asks()); v.State != builds.BuildFailed || n <= DefaultAskings || !strings.Contains(v.Error, "declined for") || !strings.Contains(v.Error, "ace: this node builds with go1.26.0") { t.Fatalf("the key = %+v after %d asks, want it failed after more than %d asks, naming ace and its reason", v, n, DefaultAskings) } }) @@ -901,44 +905,44 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { // it stands (key, absent when nil), the module known at known's fingerprint ("" for the rig's): refuse is a part of why it // is refused, "" when it is built again as a new revision and its ask id answered. again bool - key *delivery.BuilderBuild + key *builds.BuilderBuild known string refuse string // edit changes the module the builder has read, busy marks the key looked at (1) or asked by hand (2), failed is a part of // the error the key then holds when its build ends before its asks are put. - edit func(m *delivery.Module) + edit func(m *meshcatalog.ModulesByModuleValue) busy int failed string // during is a look of the builder's own at the module while the by-hand build runs: it still runs afterwards. during bool }{ {name: "build again: a look the builder queued during the by-hand build still runs afterwards", again: true, during: true, - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: no tun"}}, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}}, {name: "build again: a key looked at by the builder now is refused, not built twice", again: true, refuse: "is being looked at", busy: 1, - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: no tun"}}, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}}, {name: "build again: a key already asked by hand is refused, its watch kept", again: true, refuse: "is being looked at", busy: 2, - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: no tun"}}, - {name: "build again: a module with no manifest is refused", again: true, refuse: "names no manifest", edit: func(m *delivery.Module) { m.Manifest = "" }, - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: no tun"}}, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}}, + {name: "build again: a module with no manifest is refused", again: true, refuse: "names no manifest", edit: func(m *meshcatalog.ModulesByModuleValue) { m.Manifest = "" }, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}}, {name: "build again: a toolchain that moved writes nothing and is said at once", again: true, refuse: "wrote nothing", - edit: func(m *delivery.Module) { - m.Toolchains = map[string]delivery.ToolchainUsed{"go": {Version: "go1.0", Recipe: "1"}} + edit: func(m *meshcatalog.ModulesByModuleValue) { + m.Toolchains = CatalogToolchains(map[string]builds.ToolchainUsed{"go": {Version: "go1.0", Recipe: "1"}}) }, - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: no tun"}}, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}}, {name: "build again: a build that fails before its asks says the key failed, not that it was asked", again: true, failed: "cannot be read", - edit: func(m *delivery.Module) { m.Manifest = mdigest('b') }, - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: no tun"}}, + edit: func(m *meshcatalog.ModulesByModuleValue) { m.Manifest = mdigest('b') }, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}}, {name: "build again: a failed key is written building as a new revision and asks the seat", again: true, - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: the step failed: no tun"}}, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: the step failed: no tun"}}, {name: "build again: a done key is refused, nothing to do", again: true, refuse: "is done: nothing to do", - key: &delivery.BuilderBuild{State: delivery.BuildDone, Step: "built"}}, + key: &builds.BuilderBuild{State: builds.BuildDone, Step: "built"}}, {name: "build again: a building key is refused, builder_ask_again is for it", again: true, refuse: "is waiting", - key: &delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Step: "asked mesh-build-go", Asks: map[string]string{delivery.AskGo: "mvl0frv18d1960"}}}, + key: &builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Step: "asked mesh-build-go", Asks: map[string]string{buildstep.AskGo: "mvl0frv18d1960"}}}, {name: "build again: a key failed for a pin missing is refused, it waits for another fingerprint", again: true, refuse: "does not retry", - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: delivery.NotPinned + "web"}}, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: builds.NotPinned + "web"}}, {name: "build again: a key with no build is refused", again: true, refuse: "has no build key"}, {name: "build again: a fingerprint its module no longer has is refused", again: true, refuse: "does not have the fingerprint", known: "src2:" + strings.Repeat("9", 64), - key: &delivery.BuilderBuild{State: delivery.BuildFailed, Error: "ace: no tun"}}, + key: &builds.BuilderBuild{State: builds.BuildFailed, Error: "ace: no tun"}}, {name: "given up by its stream: asked again under a new id and done from it", puts: 2, done: true, asking: 2}, {name: "asked again by hand: the same, with no check bound", byHand: true, puts: 2, done: true, asking: 2}, {name: "given up into DEAD_LETTERS: its wait ends failed with the words and it is asked again at once", dead: true, puts: 2, done: true, asking: 2}, @@ -954,12 +958,12 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { c.key.Manifest = manifestD rev = putBuild(t, r.st, r.key, *c.key) } - m, err := delivery.DecodeModule(moduleValue(t, "modules/m")) + m, err := meshcatalogclient.DecodeModule(moduleValue(t, "modules/m")) if err != nil { t.Fatal(err) } if c.known != "" { - m.FP2 = c.known + m.Fp2 = c.known } if c.edit != nil { c.edit(&m) @@ -985,7 +989,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { r.b.Module(context.Background(), "m", moduleValue(t, "modules/m")) r.asks.End(r.answer(waitForAsks(t, r, 1)[0])) r.b.Wait() - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || res.Revision <= rev || stored.Load() != 1 { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || res.Revision <= rev || stored.Load() != 1 { t.Fatalf("key %+v, answer %+v, %d asks of the store; want built from the new revision and the queued look run once after it", v, res, stored.Load()) } return @@ -1005,10 +1009,10 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { return } asks := r.asks.Asks() - if res.Refused != "" || res.Pending != "" || res.Revision <= rev || len(asks) != 1 || res.Asks[delivery.AskGo] != asks[0].ID { + if res.Refused != "" || res.Pending != "" || res.Revision <= rev || len(asks) != 1 || res.Asks[buildstep.AskGo] != asks[0].ID { t.Fatalf("answer %+v after %d asks; want the new revision (was %d) and the ask id of the one ask", res, len(asks), rev) } - if v := valueOf(t, r.st, r.key); v.State != delivery.BuildDone || after <= res.Revision { + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone || after <= res.Revision { t.Errorf("the key = %+v at revision %d; want built from the new revision %d", v, after, res.Revision) } if !log.has(r.key, "built again by hand", "no tun on ace", "shanks.lead") { @@ -1024,8 +1028,8 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { first := "" if c.step != "" { first = "mvl0frv18d1960" - data, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Manifest: manifestD, - Step: c.step, Asks: map[string]string{delivery.AskGo: first}}) + data, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Manifest: manifestD, + Step: c.step, Asks: map[string]string{buildstep.AskGo: first}}) r.st.vals[r.key] = data } gone := &goneAsks{} @@ -1039,7 +1043,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { r.b.Asks = buildrun.Asker{Publisher: r.asks, Waiter: heard, Bound: 2 * time.Second} } if !c.byHand && !c.dead && !c.deaf { - gone.why = func(a delivery.StepAsk) string { + gone.why = func(a buildstep.StepAsk) string { if c.always || a.ID == first || first == "" { return "it waits first in line while a node took a later ask" } @@ -1075,7 +1079,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { first = asks[0].ID } last := asks[len(asks)-1] - if v := valueOf(t, r.st, r.key); v.Asks[delivery.AskGo] != last.ID || !strings.Contains(v.Step, "again (asking 2)") { + if v := valueOf(t, r.st, r.key); v.Asks[buildstep.AskGo] != last.ID || !strings.Contains(v.Step, "again (asking 2)") { t.Fatalf("the key = %+v, want building naming the ask asked again %s", v, last.ID) } r.asks.End(r.answer(last)) @@ -1085,10 +1089,10 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { if n := len(r.asks.Asks()); n != c.puts { t.Errorf("%d asks put, want %d", n, c.puts) } - if c.done && v.State != delivery.BuildDone { + if c.done && v.State != builds.BuildDone { t.Errorf("the key = %+v, want done from the ask asked again", v) } - if !c.done && (v.State != delivery.BuildFailed || !strings.Contains(v.Error, "could not be answered and was asked")) { + if !c.done && (v.State != builds.BuildFailed || !strings.Contains(v.Error, "could not be answered and was asked")) { t.Errorf("the key = %+v, want failed saying the ask could not be answered", v) } mu.Lock() @@ -1111,18 +1115,18 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { taken := func(kind string, at time.Time) *Taken { return &Taken{Ask: "later", Kind: kind, At: at, Node: "g14", Started: at} } - later := taken(delivery.AskDelivery, put.Add(time.Minute)) + later := taken(buildstep.AskDelivery, put.Add(time.Minute)) // ahead is n messages put before the ask, a minute apart, each with step (its state, "" for none). ahead := func(n int, step string) []Queued { var qs []Queued for i := range n { at := put.Add(-time.Duration(n-i) * time.Minute) - qs = append(qs, Queued{Seq: uint64(i + 1), Ask: fmt.Sprint("a", i), Kind: delivery.AskDelivery, At: at, PutAt: at, Step: step}) + qs = append(qs, Queued{Seq: uint64(i + 1), Ask: fmt.Sprint("a", i), Kind: buildstep.AskDelivery, At: at, PutAt: at, Step: step}) } return qs } seen := func(qs []Queued, last *Taken) AskSeen { - return AskSeen{Stream: "S", Kind: delivery.AskDelivery, At: put, OnStream: true, PutAt: put, Ahead: qs, Last: last} + return AskSeen{Stream: "S", Kind: buildstep.AskDelivery, At: put, OnStream: true, PutAt: put, Ahead: qs, Last: last} } for _, c := range []struct { name string @@ -1134,14 +1138,14 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { {"no longer on its stream", AskSeen{Stream: "S"}, "no longer holds it", 0}, {"first in line, a later ask taken", seen(nil, later), "no longer delivers it", 0}, {"first in line, nothing taken since", seen(nil, nil), "", 0}, - {"first in line, the ask taken last was asked before it", seen(nil, taken(delivery.AskDelivery, put.Add(-time.Second))), "", 0}, + {"first in line, the ask taken last was asked before it", seen(nil, taken(buildstep.AskDelivery, put.Add(-time.Second))), "", 0}, {"two wait ahead, nothing taken since", seen(ahead(2, ""), nil), "", 2}, - {"two ahead passed over, the ask not yet", seen(ahead(2, ""), taken(delivery.AskDelivery, put.Add(-time.Second))), "", 0}, + {"two ahead passed over, the ask not yet", seen(ahead(2, ""), taken(buildstep.AskDelivery, put.Add(-time.Second))), "", 0}, // Note 62: seven messages SEAT_NODE_BUILD_GO gave up on sat ahead of a Go ask for five hours. {"seven given up on ahead, a later ask taken", seen(ahead(7, ""), later), "behind 7 given up on or taken", 0}, - {"seven taken ahead (failed as poison), a later ask taken", seen(ahead(7, delivery.StepFailed), later), "no longer delivers it", 0}, - {"a check ask taken later proves a delivery ask passed", seen(ahead(7, ""), taken(delivery.AskCheck, put.Add(time.Minute))), "no longer delivers it", 0}, - {"a delivery ask taken later does not prove a check ask passed", func() AskSeen { s := seen(ahead(3, ""), later); s.Kind = delivery.AskCheck; return s }(), "", 3}, + {"seven taken ahead (failed as poison), a later ask taken", seen(ahead(7, buildstep.StepFailed), later), "no longer delivers it", 0}, + {"a check ask taken later proves a delivery ask passed", seen(ahead(7, ""), taken(buildstep.AskCheck, put.Add(time.Minute))), "no longer delivers it", 0}, + {"a delivery ask taken later does not prove a check ask passed", func() AskSeen { s := seen(ahead(3, ""), later); s.Kind = buildstep.AskCheck; return s }(), "", 3}, } { got := c.seen.Why() if (c.want == "") != (got == "") || !strings.Contains(got, c.want) || c.seen.Waiting() != c.waiting { @@ -1168,12 +1172,12 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { return nil, time.Time{}, nil }} } - key, _ := delivery.BuilderBuildKey("m", fp1) - done, _ := delivery.BuilderBuildKey("d", fp1) - building, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Manifest: manifestD, - Step: "asked mesh-build-go again (asking 2): x", Asks: map[string]string{delivery.AskGo: "mvo53jcm70b5f7", delivery.AskImage: "mvo53jcm70b5f8"}}) - finished, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: manifestD, Step: "built"}) - running, _ := json.Marshal(delivery.Step{V: delivery.Version, ID: "mvo53jcm70b5f8", Tool: "delivery ask", Node: "g14", State: delivery.StepRunning, Started: at}) + key, _ := builds.BuilderBuildKey("m", fp1) + done, _ := builds.BuilderBuildKey("d", fp1) + building, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Manifest: manifestD, + Step: "asked mesh-build-go again (asking 2): x", Asks: map[string]string{buildstep.AskGo: "mvo53jcm70b5f7", buildstep.AskImage: "mvo53jcm70b5f8"}}) + finished, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: manifestD, Step: "built"}) + running, _ := json.Marshal(buildstep.Step{V: keys.Version, ID: "mvo53jcm70b5f8", Tool: "delivery ask", Node: "g14", State: buildstep.StepRunning, Started: at}) _, reask, _ := NewReAsk("m", fp1, "stuck", "shanks.lead", at) r := StuckReader{Builds: state(map[string]string{key: string(building), done: string(finished)}), ReAsks: state(map[string]string{key: string(reask)}), Steps: map[string]StateRead{buildrun.GoBuildModule: state(map[string]string{}), ImageBuildModule: state(map[string]string{"g14.mvo53jcm70b5f8": string(running)})}, @@ -1192,7 +1196,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { t.Fatalf("stuck = %+v, want the building key alone, an hour old, one ask with no step, with its ask again", stuck) } a := stuck[0].Asks - if len(a) != 2 || a[0].Language != delivery.AskGo || a[0].Asking != 2 || a[0].Step != "none" || a[0].Asked.IsZero() || + if len(a) != 2 || a[0].Language != buildstep.AskGo || a[0].Asking != 2 || a[0].Step != "none" || a[0].Asked.IsZero() || a[1].Asking != 1 || !strings.HasPrefix(a[1].Step, "running on g14") { t.Fatalf("asks = %+v, want go at asking 2 with no step and image at asking 1 running on g14", a) } @@ -1203,7 +1207,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { var gets, written, inFlight, maxFlight atomic.Int64 vals := map[string]string{} for i := 0; i < 3000; i++ { - k, _ := delivery.BuilderBuildKey(fmt.Sprintf("mod%02d", i%100), fmt.Sprintf("src2:%064x", i)) + k, _ := builds.BuilderBuildKey(fmt.Sprintf("mod%02d", i%100), fmt.Sprintf("src2:%064x", i)) vals[k] = string(finished) if (i/100)%10 == 0 { vals[k] = string(building) @@ -1248,7 +1252,7 @@ func TestABuildAskWaitingInItsStreamIsNotBounded(t *testing.T) { // heldAsks says an ask is held when its id is among held. type heldAsks map[string]bool -func (h heldAsks) Held(_ context.Context, a delivery.StepAsk) (bool, error) { return h[a.ID], nil } +func (h heldAsks) Held(_ context.Context, a buildstep.StepAsk) (bool, error) { return h[a.ID], nil } // 1910: a builder that stopped between naming the ask in its key and putting it left a key naming an ask neither the // stream holds nor any node ran: started again, the builder puts that ask, under the same id, once, and the key is @@ -1257,13 +1261,13 @@ func TestAnAskTheKeyNamesAndNobodyPutIsPutAtStart(t *testing.T) { for _, held := range []bool{false, true} { r := newRig(t, goBundle) const id = "mvl0frv18d1958" - data, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Manifest: manifestD, - Step: "asked mesh-build-go", Asks: map[string]string{delivery.AskGo: id}}) + data, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Manifest: manifestD, + Step: "asked mesh-build-go", Asks: map[string]string{buildstep.AskGo: id}}) r.st.vals[r.key] = data r.b.Held = heldAsks{id: held} if held { r.asks.Answer = nil - r.asks.End(delivery.Step{ID: id, Node: "shanks", State: delivery.StepPassed, + r.asks.End(buildstep.Step{ID: id, Node: "shanks", State: buildstep.StepPassed, Result: []byte(`{"artifacts":[{"artifact":"tool","digest":"` + digest + `","reference":"artifact-store://m/tool/blobs/` + digest + `"}]}`)}) } v := r.consider(t, "modules/m") @@ -1274,7 +1278,7 @@ func TestAnAskTheKeyNamesAndNobodyPutIsPutAtStart(t *testing.T) { case !held && (len(asks) != 1 || asks[0].ID != id): t.Errorf("the ask nobody put: asks %+v, want one under %s", asks, id) } - if v.State != delivery.BuildDone { + if v.State != builds.BuildDone { t.Errorf("held %v: the key = %+v, want done", held, v) } } @@ -1283,7 +1287,7 @@ func TestAnAskTheKeyNamesAndNobodyPutIsPutAtStart(t *testing.T) { // unreadableHeld cannot tell whether an ask is held. type unreadableHeld struct{} -func (unreadableHeld) Held(context.Context, delivery.StepAsk) (bool, error) { +func (unreadableHeld) Held(context.Context, buildstep.StepAsk) (bool, error) { return false, errors.New("permissions violation for publish to $JS.API.STREAM.INFO.SEAT_NODE_BUILD_GO") } @@ -1292,15 +1296,15 @@ func (unreadableHeld) Held(context.Context, delivery.StepAsk) (bool, error) { func TestATakeOverThatCannotReadTheStreamWaitsForTheAsk(t *testing.T) { r := newRig(t, goBundle) const id = "mvl0frv18d1959" - data, _ := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildBuilding, By: "novox.builder", Manifest: manifestD, - Step: "asked mesh-build-go", Asks: map[string]string{delivery.AskGo: id}}) + data, _ := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildBuilding, By: "novox.builder", Manifest: manifestD, + Step: "asked mesh-build-go", Asks: map[string]string{buildstep.AskGo: id}}) r.st.vals[r.key] = data r.b.Held = unreadableHeld{} r.asks.Answer = nil - r.asks.End(delivery.Step{ID: id, Node: "shanks", State: delivery.StepPassed, + r.asks.End(buildstep.Step{ID: id, Node: "shanks", State: buildstep.StepPassed, Result: []byte(`{"artifacts":[{"artifact":"tool","digest":"` + digest + `","reference":"artifact-store://m/tool/blobs/` + digest + `"}]}`)}) v := r.consider(t, "modules/m") - if v.State != delivery.BuildDone || len(r.asks.Asks()) != 0 { + if v.State != builds.BuildDone || len(r.asks.Asks()) != 0 { t.Fatalf("the key = %+v after %d asks, want done from the ask's step, none put", v, len(r.asks.Asks())) } } @@ -1308,8 +1312,8 @@ func TestATakeOverThatCannotReadTheStreamWaitsForTheAsk(t *testing.T) { // silentWatch is a watch of an ask's step that stays open and hears nothing. type silentWatch struct{} -func (silentWatch) WatchAsk(context.Context, string, string) (<-chan delivery.Step, error) { - return make(chan delivery.Step), nil +func (silentWatch) WatchAsk(context.Context, string, string) (<-chan buildstep.Step, error) { + return make(chan buildstep.Step), nil } // aDoneKeyKeepsEachFormACountedNodeReads holds the builder's rows of ADR 0624's check: every build keeps each form a @@ -1344,7 +1348,7 @@ func aDoneKeyKeepsEachFormACountedNodeReads(t *testing.T) { if got := fs.Kept("bundle", row.present, row.counted); !slices.Equal(got, row.kept) { t.Errorf("%s: kept %v, want %v", row.name, got, row.kept) } - a := fs.Choose(delivery.BuiltArtifact{Name: "code", Kind: "bundle", Digest: digest}, []string{newRef, oldRef}, row.present, row.counted) + a := fs.Choose(forms.BuiltArtifact{Name: "code", Kind: "bundle", Digest: digest}, []string{newRef, oldRef}, row.present, row.counted) if a.Reference != row.reference || !slices.Equal(a.References, row.references) { t.Errorf("%s: %q %v, want %q %v", row.name, a.Reference, a.References, row.reference, row.references) } @@ -1354,11 +1358,11 @@ func aDoneKeyKeepsEachFormACountedNodeReads(t *testing.T) { } } // An answered reference no kept form names still names where the artifact is (ADR 0498 clause 1). - if a := fs.Choose(delivery.BuiltArtifact{Name: "code", Kind: "bundle", Digest: digest}, []string{oldRef}, map[string][]string{"a": both}, + if a := fs.Choose(forms.BuiltArtifact{Name: "code", Kind: "bundle", Digest: digest}, []string{oldRef}, map[string][]string{"a": both}, map[string][]string{"a": both}); a.Reference != oldRef { t.Errorf("a lone answered reference was dropped: %+v", a) } - withBoth := []delivery.BuiltArtifact{{Name: "code", Kind: "bundle", Digest: digest, Reference: oldRef, References: []string{oldRef, newRef}}} + withBoth := []forms.BuiltArtifact{{Name: "code", Kind: "bundle", Digest: digest, Reference: oldRef, References: []string{oldRef, newRef}}} if got := fs.Needs(withBoth); !slices.Equal(got, []string{"form:new", "form:old"}) { t.Errorf("needs %v", got) } @@ -1390,11 +1394,11 @@ func aDoneKeyKeepsEachFormACountedNodeReads(t *testing.T) { map[string][]string{"a": seat, "b": nil}, map[string][]string{"a": seat, "b": nil}, storeRef, nil, []string{"form:artifact-store"}}, {"seat form answered, no node present: the store form", nil, map[string][]string{"a": seat}, storeRef, nil, []string{"form:artifact-store"}}, } { - a := forms.Current.Choose(delivery.BuiltArtifact{Name: "code", Kind: k.kind, Digest: digest}, []string{seatRef}, row.present, row.counted) + a := forms.Current.Choose(forms.BuiltArtifact{Name: "code", Kind: k.kind, Digest: digest}, []string{seatRef}, row.present, row.counted) if a.Reference != row.reference || !slices.Equal(a.References, row.references) { t.Errorf("%s %s: %q %v, want %q %v", k.kind, row.name, a.Reference, a.References, row.reference, row.references) } - needs := forms.Current.Needs([]delivery.BuiltArtifact{a}) + needs := forms.Current.Needs([]forms.BuiltArtifact{a}) if !slices.Equal(needs, row.needs) { t.Errorf("%s %s: needs %v, want %v", k.kind, row.name, needs, row.needs) } @@ -1403,7 +1407,7 @@ func aDoneKeyKeepsEachFormACountedNodeReads(t *testing.T) { t.Errorf("%s %s: the want of %s is held", k.kind, row.name, node) } } - if _, err := delivery.EncodeBuilderBuild(delivery.BuilderBuild{State: delivery.BuildDone, Manifest: strings.TrimPrefix(digest, "sha256:"), Artifacts: []delivery.BuiltArtifact{a}, Needs: needs}); err != nil { + if _, err := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildDone, Manifest: strings.TrimPrefix(digest, "sha256:"), Artifacts: []builds.BuiltArtifact{a}, Needs: needs}); err != nil { t.Errorf("%s %s: %v", k.kind, row.name, err) } } diff --git a/modules/mesh-delivery-builder/internal/build/since.go b/modules/mesh-delivery-builder/internal/build/since.go index 968e5979..8875b37d 100644 --- a/modules/mesh-delivery-builder/internal/build/since.go +++ b/modules/mesh-delivery-builder/internal/build/since.go @@ -4,8 +4,8 @@ import ( "fmt" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // BuildsSinceLimit is how many entries one answer of BuildsSinceTool reads of the log; More says the log held more. @@ -14,28 +14,28 @@ const BuildsSinceLimit = 500 // LogSince is one page of the builder's log from a time on: stdio.Log(BuilderLog).Since. type LogSince func(at time.Time, keys string, limit int) (*stdio.LogPage, error) -// BuildsSince answers delivery.BuildsSinceTool (ADR 0571 clause 9): every ended build in the builder's log from since on, +// BuildsSince answers builds.BuildsSinceTool (ADR 0571 clause 9): every ended build in the builder's log from since on, // oldest first, read through the runtime's log verbs; More when the page was not the log's last, so the caller asks again // from the last build's time. An entry the log holds that does not read is refused, naming its sequence: a reader that // skipped it would register too little. -func BuildsSince(read LogSince, since time.Time) (delivery.BuildsSince, error) { - out := delivery.BuildsSince{Since: since.UTC(), Builds: []delivery.BuildLogEntry{}} +func BuildsSince(read LogSince, since time.Time) (builds.BuildsSince, error) { + out := builds.BuildsSince{Since: since.UTC(), Builds: []builds.BuildLogEntry{}} page, err := read(since, "", BuildsSinceLimit) if err != nil { - return out, fmt.Errorf("the builder's log %s from %s: %w", delivery.BuilderLog, since.Format(time.RFC3339), err) + return out, fmt.Errorf("the builder's log %s from %s: %w", builds.BuilderLog, since.Format(time.RFC3339), err) } if page == nil { return out, nil } - var entries []delivery.BuildLogEntry + var entries []builds.BuildLogEntry for _, le := range page.Entries { - e, err := delivery.DecodeBuildLogEntry(le.Value) + e, err := builds.DecodeBuildLogEntry(le.Value) if err != nil { - return out, fmt.Errorf("the builder's log %s, entry %d: %w", delivery.BuilderLog, le.Sequence, err) + return out, fmt.Errorf("the builder's log %s, entry %d: %w", builds.BuilderLog, le.Sequence, err) } entries = append(entries, e) } - if ended := delivery.EndedSince(entries, since); ended != nil { + if ended := builds.EndedSince(entries, since); ended != nil { out.Builds = ended } out.More = page.Next != 0 diff --git a/modules/mesh-delivery-builder/internal/build/stuck.go b/modules/mesh-delivery-builder/internal/build/stuck.go index 93dd5703..e5ef2e86 100644 --- a/modules/mesh-delivery-builder/internal/build/stuck.go +++ b/modules/mesh-delivery-builder/internal/build/stuck.go @@ -14,7 +14,8 @@ import ( "time" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // The builder's tools (novox/hq issue 1945; the instruction "tools are how a module is spoken to"): builder_stuck reads @@ -31,7 +32,7 @@ const ( AnnounceAgainTool = "builder_announce_again" // ReAsksState is the builder's state of asks to ask again: the bucket mesh-delivery-builder_reasks, keyed as the build key. ReAsksState = "reasks" - ReAsksBucket = delivery.BuilderModule + "_" + ReAsksState + ReAsksBucket = builds.BuilderModule + "_" + ReAsksState ) // ReAsk is a value of the reasks state: why a person asked the key's build again, who and when, and, once the builder @@ -76,7 +77,7 @@ func NewPublishAgain(name, fp, why, by string, at time.Time) (string, []byte, er if !ok || module == "" || artifact == "" { return "", nil, fmt.Errorf("name %q is not a numbered name, /, as builder_versions lists it", name) } - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return "", nil, err } @@ -139,7 +140,7 @@ func DecodeReAsk(data []byte) (ReAsk, error) { // NewReAsk is the value builder_ask_again writes for a module's fingerprint: its key and the value. func NewReAsk(module, fp, why, by string, at time.Time) (string, []byte, error) { - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return "", nil, err } @@ -152,7 +153,7 @@ func NewReAsk(module, fp, why, by string, at time.Time) (string, []byte, error) // NewBuildAgain is the value builder_build_again writes for a module's fingerprint: its key and the value. func NewBuildAgain(module, fp, why, by string, at time.Time) (string, []byte, error) { - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return "", nil, err } @@ -165,7 +166,7 @@ func NewBuildAgain(module, fp, why, by string, at time.Time) (string, []byte, er // NewAnnounceAgain is the value builder_announce_again writes for a module's fingerprint: its key and the value. func NewAnnounceAgain(module, fp, why, by string, at time.Time) (string, []byte, error) { - key, err := delivery.BuilderBuildKey(module, fp) + key, err := builds.BuilderBuildKey(module, fp) if err != nil { return "", nil, err } @@ -303,7 +304,7 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { } byID := make(map[string]string, len(ks)) for _, k := range ks { - if _, id, err := delivery.ParseStepKey(k); err == nil { + if _, id, err := buildstep.ParseStepKey(k); err == nil { byID[id] = k } } @@ -314,7 +315,7 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { type parsed struct{ key, mod, fp string } var matching []parsed for _, key := range keys { - mod, fp, err := delivery.ParseBuilderBuildKey(key) + mod, fp, err := builds.ParseBuilderBuildKey(key) if err != nil || (module != "" && mod != module) { continue } @@ -345,8 +346,8 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { if raw == nil { continue } - v, err := delivery.DecodeBuilderBuild(raw) - if err != nil || (v.State != delivery.BuildBuilding && v.State != delivery.BuildQueued) { + v, err := builds.DecodeBuilderBuild(raw) + if err != nil || (v.State != builds.BuildBuilding && v.State != builds.BuildQueued) { continue } if at.IsZero() && r.Builds.Written != nil { @@ -357,7 +358,7 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { s.Age = now().Sub(at).Round(time.Second).String() } for language, id := range v.Asks { - bm, _ := delivery.BuildModuleOf(language) + bm, _ := buildstep.BuildModuleOf(language) a := StuckAsk{Language: language, BuildModule: bm, ID: id, Asking: askingOf(v.Step, bm), Step: "none"} if t, ok := idTime(id); ok { a.Asked = t @@ -366,7 +367,7 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { if k, ok := stepKeys[bm][id]; ok { reads++ if raw, _, err := st.Get(k); err == nil && raw != nil { - if step, err := delivery.DecodeStep(raw); err == nil { + if step, err := buildstep.DecodeStep(raw); err == nil { a.Step = fmt.Sprintf("%s on %s since %s", step.State, step.Node, step.Started.UTC().Format(time.RFC3339)) } } @@ -378,7 +379,7 @@ func (r StuckReader) Stuck(module string) (map[string]any, error) { s.Asks = append(s.Asks, a) } sort.Slice(s.Asks, func(i, j int) bool { return s.Asks[i].Language < s.Asks[j].Language }) - s.Unasked = v.State == delivery.BuildBuilding && len(v.Asks) == 0 + s.Unasked = v.State == builds.BuildBuilding && len(v.Asks) == 0 if r.ReAsks.Get != nil { reads++ if raw, _, err := r.ReAsks.Get(key); err == nil && raw != nil { diff --git a/modules/mesh-delivery-builder/internal/build/toolchains.go b/modules/mesh-delivery-builder/internal/build/toolchains.go new file mode 100644 index 00000000..f2477c0a --- /dev/null +++ b/modules/mesh-delivery-builder/internal/build/toolchains.go @@ -0,0 +1,28 @@ +package build + +import ( + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + sdkstep "git.novox.be/novox/mesh-delivery/sdk/buildstep" +) + +// CatalogToolchains are the toolchains of a build, by language, as a module's value holds them (the catalog's form names +// the language in the entry). +func CatalogToolchains(used map[string]builds.ToolchainUsed) map[string]meshcatalog.ToolchainUsed { + if used == nil { + return nil + } + out := make(map[string]meshcatalog.ToolchainUsed, len(used)) + for language, t := range used { + out[language] = meshcatalog.ToolchainUsed{Language: language, Recipe: t.Recipe, Version: t.Version} + } + return out +} + +// buildToolchain is the toolchain a build ask names for a language (the ask's form names the language in the entry). +func buildToolchain(language string, t *builds.ToolchainUsed) *sdkstep.ToolchainUsed { + if t == nil { + return nil + } + return &sdkstep.ToolchainUsed{Language: language, Recipe: t.Recipe, Version: t.Version} +} diff --git a/modules/mesh-delivery-builder/internal/versions/again.go b/modules/mesh-delivery-builder/internal/versions/again.go index 8ef89b13..9e77235b 100644 --- a/modules/mesh-delivery-builder/internal/versions/again.go +++ b/modules/mesh-delivery-builder/internal/versions/again.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // What builder_publish_again does with one fingerprint of a name (design 51 *Tools of the builder*), after a holder failed. @@ -309,7 +309,7 @@ func (a Acts) PublishAgain(ctx context.Context, name, fp, why, by string, at tim b.Again = at.UTC().Format(time.RFC3339Nano) a.Numberer.Said(b) } - answer += "; " + delivery.VersionNumberedEvent + " said for " + ag.Entry.Version + answer += "; " + builds.VersionNumberedEvent + " said for " + ag.Entry.Version } return ag, answer, nil } diff --git a/modules/mesh-delivery-builder/internal/versions/claim.go b/modules/mesh-delivery-builder/internal/versions/claim.go index a9990b02..16d3e2a9 100644 --- a/modules/mesh-delivery-builder/internal/versions/claim.go +++ b/modules/mesh-delivery-builder/internal/versions/claim.go @@ -7,7 +7,7 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // The registry seats a version is published to (design 51 *The seats*), by what is published. @@ -139,7 +139,7 @@ type Numberer struct { // Holder is the module holding a seat, whose health condition's clear wakes a silent seat source; nil names none. Holder func(seat string) string // Said is told each version newly numbered; nil tells nobody. - Said func(delivery.VersionNumbered) + Said func(builds.VersionNumbered) // Names answers a package's name in its ecosystem; nil numbers a package as NameOf names it. Names PackageNames // Publishing is told each version Built recorded that is still numbered, its holder's word not heard yet: the diff --git a/modules/mesh-delivery-builder/internal/versions/names.go b/modules/mesh-delivery-builder/internal/versions/names.go index 0d01172e..66dd54d0 100644 --- a/modules/mesh-delivery-builder/internal/versions/names.go +++ b/modules/mesh-delivery-builder/internal/versions/names.go @@ -5,7 +5,7 @@ import ( "sort" "strings" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" ) // PackagesState is the indexer's packages state as the builder reads it (its manifest's reads grant): its keys, and one @@ -27,7 +27,7 @@ func NamesIn(s PackagesState) PackageNames { sort.Strings(keys) built := NameOf(module, artifact) for _, key := range keys { - if !delivery.IsSeriesPackageKey(key) { + if !isSeriesKey(key) { continue } value, found, err := s.Get(ctx, key) @@ -37,7 +37,7 @@ func NamesIn(s PackagesState) PackageNames { if !found { continue } - v, err := delivery.DecodeSeriesPackage(value) // the indexer's value of a series key (mesh-sdk's contract, J4) + v, err := meshcatalog.DecodePackagesByPackageSeriesValue(value) // the indexer's value of a series key (mesh-sdk's contract, J4) if err != nil || v.Package != built { continue } @@ -51,10 +51,10 @@ func NamesIn(s PackagesState) PackageNames { // name: a series key whose value names a name and the / it is built from. The builder looks at that module again when // the value is heard, so a done build that left the package unnumbered (its name not known at its claim) is numbered then. func NamedModule(key string, value []byte) (module, name string, ok bool) { - if !delivery.IsSeriesPackageKey(key) { + if !isSeriesKey(key) { return "", "", false } - v, err := delivery.DecodeSeriesPackage(value) + v, err := meshcatalog.DecodePackagesByPackageSeriesValue(value) if err != nil || v.Name == "" { return "", "", false } @@ -64,3 +64,9 @@ func NamedModule(key string, value []byte) (module, name string, ok bool) { } return module, v.Name, true } + +// isSeriesKey says whether a key of the indexer's packages state is a series key (`.`), not a per-module one. +func isSeriesKey(key string) bool { + _, _, ok := meshcatalog.ParsePackagesByPackageSeriesKey(key) + return ok +} diff --git a/modules/mesh-delivery-builder/internal/versions/oldform.go b/modules/mesh-delivery-builder/internal/versions/oldform.go index e2dba2ea..dd066d94 100644 --- a/modules/mesh-delivery-builder/internal/versions/oldform.go +++ b/modules/mesh-delivery-builder/internal/versions/oldform.go @@ -9,8 +9,9 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/sdk/forms" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/window" ) // The old reference form's window (design 51 *Every build keeps the old reference form*, ADR 0624 §1, ADR 0618 §2-§4, @@ -28,7 +29,7 @@ const ( ) // OldFormBucket is that state's bucket. -const OldFormBucket = delivery.BuilderModule + "_" + OldFormState +const OldFormBucket = builds.BuilderModule + "_" + OldFormState // OldFormHeld is the finding of a node holding the window open for FindingAfter, about that node, cleared when the // window closes or the node no longer holds it. @@ -64,11 +65,11 @@ func Holding(fs forms.ArtifactForms, counted map[string][]string, dropped map[st return out } -// OldFormWindow is the window's record: whether it is open, who holds it and since when (delivery.WindowHeld, the part +// OldFormWindow is the window's record: whether it is open, who holds it and since when (window.WindowHeld, the part // every window keeper shares), and the nodes dropped by hand; what builder_old_form answers and what `old-form-closed` // carries when it closes. type OldFormWindow struct { - delivery.WindowHeld + window.WindowHeld Dropped []Dropped `json:"dropped,omitempty"` } @@ -91,13 +92,13 @@ type OldFormClosedBody struct { // FindingAfter is how long a node holds the window open before it is the builder's finding naming the node. const FindingAfter = 7 * 24 * time.Hour -// Step moves the window by mesh-sdk's one window rule (delivery.StepWindow): named is every counted node the read named, +// Step moves the window by mesh-sdk's one window rule (window.StepWindow): named is every counted node the read named, // those dropped by hand included, and holding those of them holding the window (Holding). A read naming no node (no // placement read yet, an empty node_haves) judges nothing, so it never closes the window. It answers the new window, // whether it closed in this step (so the builder says `old-form-closed` and its log line once), and the nodes holding it // past FindingAfter (the finding, cleared for every other node). func (w OldFormWindow) Step(named, holding []string, now time.Time) (next OldFormWindow, closed bool, overdue []string) { - held, closed, overdue := delivery.StepWindow(w.WindowHeld, named, holding, now, FindingAfter) + held, closed, overdue := window.StepWindow(w.WindowHeld, named, holding, now, FindingAfter) return OldFormWindow{WindowHeld: held, Dropped: w.Dropped}, closed, overdue } diff --git a/modules/mesh-delivery-builder/internal/versions/tools.go b/modules/mesh-delivery-builder/internal/versions/tools.go index 726cf915..bb8d94f0 100644 --- a/modules/mesh-delivery-builder/internal/versions/tools.go +++ b/modules/mesh-delivery-builder/internal/versions/tools.go @@ -7,7 +7,7 @@ import ( "fmt" "strings" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // The builder's tools of its versions (design 51 *Events of the builder*, *Tools of the builder*). @@ -19,14 +19,14 @@ const ( TagAsksTool = "builder_tag_asks" UntagTool = "builder_untag" OldFormTool = "builder_old_form" - // OldFormClosedEvent is, with delivery.VersionNumberedEvent, the builder's own family `version`, kept seven days, + // OldFormClosedEvent is, with builds.VersionNumberedEvent, the builder's own family `version`, kept seven days, // apart from its builds' ends, so a waiter on a build's end does not wake on them. OldFormClosedEvent = "version.old-form-closed" ) -// NumberedOf is the version.numbered body of an entry (delivery.VersionNumbered, the contract a build module reads). -func NumberedOf(e Entry, head string) delivery.VersionNumbered { - return delivery.VersionNumbered{Name: e.Name, Kind: e.Kind, Version: e.Version, Tag: e.Tag, Module: e.Module, Fingerprint: e.Fingerprint, +// NumberedOf is the version.numbered body of an entry (builds.VersionNumbered, the contract a build module reads). +func NumberedOf(e Entry, head string) builds.VersionNumbered { + return builds.VersionNumbered{Name: e.Name, Kind: e.Kind, Version: e.Version, Tag: e.Tag, Module: e.Module, Fingerprint: e.Fingerprint, Commit: e.Commit, Digest: e.Digest, Seat: e.Seat, Head: head} } diff --git a/modules/mesh-delivery-builder/internal/versions/versions_test.go b/modules/mesh-delivery-builder/internal/versions/versions_test.go index d875586c..da2fbda5 100644 --- a/modules/mesh-delivery-builder/internal/versions/versions_test.go +++ b/modules/mesh-delivery-builder/internal/versions/versions_test.go @@ -12,8 +12,8 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/sdk/forms" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // mem is a Store in memory; before runs once before the next Create, to stage a race. @@ -364,7 +364,7 @@ func TestTheBuildersVersions(t *testing.T) { if _, err := w.Drop("e", "op@shanks", "", t0); err == nil { t.Error("a drop with no why is taken") } - // Security-upgrade F3 (review round 1), by mesh-sdk's delivery.StepWindow: a read naming no node (an empty + // Security-upgrade F3 (review round 1), by mesh-sdk's window.StepWindow: a read naming no node (an empty // node_haves, the placement not read yet) never closes the window, and its holder's seven days still count. if held, shut, late := w.Step(nil, nil, t0.Add(FindingAfter+time.Minute)); shut || !held.Open || !reflect.DeepEqual(late, []string{"c"}) { t.Fatalf("a read naming no node: %+v closed=%v overdue %v, want the window open and c overdue", held, shut, late) @@ -396,9 +396,9 @@ func TestTheBuildersVersions(t *testing.T) { }, Requirements: func(context.Context, string) ([]string, error) { return nil, nil }, } - var said []delivery.VersionNumbered + var said []builds.VersionNumbered fw := &FloorWaits{} - n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw, Said: func(b delivery.VersionNumbered) { said = append(said, b) }} + n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Waits: fw, Said: func(b builds.VersionNumbered) { said = append(said, b) }} c := BuildClaim{Module: "m", Path: "go", Repository: "novox/m", Fingerprint: "fp1", Commit: "c1", Revision: "7", Merge: 7, Artifacts: []ClaimArtifact{{Artifact: "program", Kind: KindBundle, Language: "go"}, {Artifact: "base", Kind: "upstream"}}} got, err := n.Number(ctx, c) @@ -495,8 +495,8 @@ func TestTheBuildersVersions(t *testing.T) { return nil, nil }, } - var said []delivery.VersionNumbered - n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Names: NamesIn(state), Said: func(b delivery.VersionNumbered) { said = append(said, b) }} + var said []builds.VersionNumbered + n := &Numberer{Versions: Versions{Store: newMem()}, Floor: src, Names: NamesIn(state), Said: func(b builds.VersionNumbered) { said = append(said, b) }} c := BuildClaim{Module: "mesh-sdk", Path: "go", Repository: "novox/mesh-sdk", Fingerprint: "fp1", Commit: "c1", Revision: "3", Merge: 3, Artifacts: []ClaimArtifact{{Artifact: "go", Kind: KindPackage, Language: "go"}, {Artifact: "ts", Kind: KindPackage, Language: "typescript"}, {Artifact: "new", Kind: KindPackage, Language: "go"}, {Artifact: "program", Kind: KindBundle, Language: "go"}}} @@ -642,10 +642,10 @@ func TestTheBuildersVersions(t *testing.T) { t.Error("a version refused for other bytes is published again") } _, _ = v.Mark(ctx, "m/p", "0.1.4", func(e *Entry) { e.State = Published }) - var said []delivery.VersionNumbered + var said []builds.VersionNumbered var rebuilt []string done := true - acts := Acts{Numberer: &Numberer{Versions: v, Said: func(b delivery.VersionNumbered) { said = append(said, b) }, Waits: &FloorWaits{}}, + acts := Acts{Numberer: &Numberer{Versions: v, Said: func(b builds.VersionNumbered) { said = append(said, b) }, Waits: &FloorWaits{}}, Done: func(context.Context, string, string) (bool, error) { return done, nil }, BuildAgain: func(_ context.Context, m, fp, _, _ string) (string, bool) { rebuilt = append(rebuilt, m+" "+fp) From bcd0c8bdacedd52cc14e42a5ad38e9e7e83e204e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:33:57 +0100 Subject: [PATCH 06/11] =?UTF-8?q?Builder=20manifest=20test=20names=20its?= =?UTF-8?q?=20log=20and=20history=20from=20the=20builds=20package=20(ADR?= =?UTF-8?q?=200634=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-delivery-builder/manifest_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 7f5f470f..0d637c69 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -11,7 +11,7 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" ) // The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521, @@ -96,7 +96,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 if m.Module != "mesh-delivery-builder" { t.Errorf("module = %q", m.Module) } - if want := []state{{Name: "builds", History: 5}, {Name: "engine-reads", History: 1}, {Name: "findings", History: 5}, {Name: "floor-waits", History: 1}, {Name: "hearing", History: 1}, {Name: "instances", History: 1}, {Name: "old-form", History: 1}, {Name: "bounds", History: 1, KeyTTL: true}, {Name: "queues", History: 1}, {Name: "reasks", History: 1}, {Name: "tag-ask-bounds", History: 1, KeyTTL: true}, {Name: "tag-asks", History: 1}, {Name: "versions", History: 1}, {Name: "first-views", History: 1}}; !reflect.DeepEqual(m.State, want) { // delivery.BuilderBuildsHistory + if want := []state{{Name: "builds", History: 5}, {Name: "engine-reads", History: 1}, {Name: "findings", History: 5}, {Name: "floor-waits", History: 1}, {Name: "hearing", History: 1}, {Name: "instances", History: 1}, {Name: "old-form", History: 1}, {Name: "bounds", History: 1, KeyTTL: true}, {Name: "queues", History: 1}, {Name: "reasks", History: 1}, {Name: "tag-ask-bounds", History: 1, KeyTTL: true}, {Name: "tag-asks", History: 1}, {Name: "versions", History: 1}, {Name: "first-views", History: 1}}; !reflect.DeepEqual(m.State, want) { // builds.BuilderBuildsHistory t.Errorf("state = %+v, want %+v", m.State, want) } // 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and @@ -158,8 +158,8 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 t.Errorf("emits = %v, want %v", m.Emits, want) } // 1955/L7 (ADR 0571 §1): its module log, each build's start and end, kept 30 days; no bare stream. - if len(m.Logs) != 1 || m.Logs[0].Name != delivery.BuilderLog || m.Logs[0].MaxAgeDays != delivery.BuilderLogDays { - t.Errorf("logs = %+v, want %s kept %d days", m.Logs, delivery.BuilderLog, delivery.BuilderLogDays) + if len(m.Logs) != 1 || m.Logs[0].Name != builds.BuilderLog || m.Logs[0].MaxAgeDays != builds.BuilderLogDays { + t.Errorf("logs = %+v, want %s kept %d days", m.Logs, builds.BuilderLog, builds.BuilderLogDays) } for _, k := range []string{"streams", "claims", "provides", "requires", "defines-seats", "seat", "seats", "bus", "config"} { if _, ok := top[k]; ok { From 508eff8e1bff3baa95c0656f90645a04df62629f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:37:26 +0100 Subject: [PATCH 07/11] =?UTF-8?q?Builder=20declares=20its=20read=20of=20th?= =?UTF-8?q?e=20assigner's=20placement=20state,=20which=20its=20toolchain?= =?UTF-8?q?=20gate=20follows=20through=20meshmoduleassignerclient=20(ADR?= =?UTF-8?q?=200634=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-delivery-builder/manifest_test.go | 6 ++++-- modules/mesh-delivery-builder/module.json | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 0d637c69..bbf16c6f 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -102,7 +102,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 // 1887/A4c: the builder chooses no node, so it reads neither presence nor have, invokes no build module's tool and // holds no capacity; it puts asks on the three build seats and reads their steps (ADR 0525 §1, §4, §5). if want := []string{"mesh-build-go.steps", "mesh-build-go.toolchains", "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains", - "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) { + "mesh-delivery-assigner.placement", "mesh-delivery-gate.source"}; !reflect.DeepEqual(sorted(m.Reads), want) { t.Errorf("reads = %v, want %v", m.Reads, want) } if want := []string{"node-build-go", "node-build-image", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) { @@ -115,7 +115,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 // read tools and followed by their views families, never read; so are the indexer's packages, whose names a package is // numbered under (design 32). for _, r := range m.Reads { - if strings.HasPrefix(r, "mesh-delivery-indexer.") || strings.HasPrefix(r, "mesh-delivery-assigner.") { + // The toolchain gate follows the assigner's placement through meshmoduleassignerclient (buildrun.PlacementBucket), so + // it declares that one read, as the console and route-proxy do; everything else of theirs is asked by tools. + if strings.HasPrefix(r, "mesh-delivery-indexer.") || (strings.HasPrefix(r, "mesh-delivery-assigner.") && r != "mesh-delivery-assigner.placement") { t.Errorf("reads %s: the indexer's and the assigner's state is read by their tools and views families alone", r) } } diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index 25e24e0a..b4c66db8 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -103,6 +103,7 @@ "mesh-build-image.steps", "mesh-build-node.steps", "mesh-build-node.toolchains", + "mesh-delivery-assigner.placement", "mesh-delivery-gate.source" ], "uses": [ From dd6f899b12e7c59dd97e1b0d2efb4abdf42d4579 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:39:50 +0100 Subject: [PATCH 08/11] =?UTF-8?q?Checker=20client:=20the=20codecs=20of=20t?= =?UTF-8?q?he=20verdicts,=20kept=20and=20previews=20states=20the=20checker?= =?UTF-8?q?=20writes=20and=20others=20read=20(ruling=2017:4x;=20ADR=200634?= =?UTF-8?q?=20=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../meshdeliverycheckerclient/client_test.go | 43 +++++++++ .../meshdeliverycheckerclient/codec.go | 87 +++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 contracts/go/clients/meshdeliverycheckerclient/codec.go diff --git a/contracts/go/clients/meshdeliverycheckerclient/client_test.go b/contracts/go/clients/meshdeliverycheckerclient/client_test.go index c9ef2850..1a8334c6 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/client_test.go +++ b/contracts/go/clients/meshdeliverycheckerclient/client_test.go @@ -149,3 +149,46 @@ func TestVerdictKeyAsGoDeliveryWroteIt(t *testing.T) { t.Error("a short hash keys") } } + +// The codecs write a value at the kept values' version after its rules and read it back through them. +func TestTheCodecsWriteAtTheVersionAndReadBackThroughTheRules(t *testing.T) { + raw, err := EncodeVerdict(passing()) + if err != nil { + t.Fatal(err) + } + if got, err := DecodeVerdict(raw); err != nil || got.V == 0 || got.Group != "g" { + t.Errorf("verdict read back = %+v, %v", got, err) + } + bad := passing() + bad.Heads = "x" + if _, err := EncodeVerdict(bad); err == nil { + t.Error("a verdict whose heads are no hash is written") + } + if _, err := DecodeVerdict([]byte(`{"v":99,"group":"g","heads":"0123456789ab","state":"running"}`)); err == nil { + t.Error("a verdict of an unknown version is read") + } + k := checker.KeptByLevelDigestValue{State: VerdictPass, Repository: "novox/mesh-sdk", Package: "./go/keys", Level: LevelUnit, Toolchain: "1.26.0", At: "2026-11-05T10:00:00Z"} + raw, err = EncodeKept(k) + if err != nil { + t.Fatal(err) + } + if got, err := DecodeKept(raw); err != nil || got.Package != k.Package { + t.Errorf("kept read back = %+v, %v", got, err) + } + k.State = VerdictFail + if _, err := EncodeKept(k); err == nil { + t.Error("a failure is kept") + } + p := checker.PreviewsByRepositoryHeadTrunkValue{Repository: "novox/a", Head: "h1", Trunk: "t1", Conflicts: []string{"f.go"}} + raw, err = EncodePreview(p) + if err != nil { + t.Fatal(err) + } + if got, err := DecodePreview(raw); err != nil || got.Head != "h1" { + t.Errorf("preview read back = %+v, %v", got, err) + } + p.Conflicts = nil + if _, err := EncodePreview(p); err == nil { + t.Error("a preview with no merge commit and no conflict is written") + } +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/codec.go b/contracts/go/clients/meshdeliverycheckerclient/codec.go new file mode 100644 index 00000000..2b6b2b0c --- /dev/null +++ b/contracts/go/clients/meshdeliverycheckerclient/codec.go @@ -0,0 +1,87 @@ +package meshdeliverycheckerclient + +import ( + "encoding/json" + "fmt" + + checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" + "git.novox.be/novox/mesh-sdk/go/keys" +) + +// The codecs of the three states the checker writes and others read (the view and the gate read verdicts, the indexer reads +// previews, the checker and the tools read kept verdicts): a value is written at the version of the kept values after its rules +// (CheckVerdict, CheckKept, CheckPreview) and read back through the same rules. The checker's owed state is read by no other +// part and keeps its own codec. + +// EncodeVerdict is v as JSON at the kept values' version, refused by CheckVerdict's rules. +func EncodeVerdict(v checker.VerdictsByGroupHeadsValue) ([]byte, error) { + if err := CheckVerdict(v); err != nil { + return nil, err + } + v.V = keys.Version + return json.Marshal(v) +} + +// DecodeVerdict reads a `verdicts` value; it refuses an unknown version (keys.ErrUnknownVersion) and what EncodeVerdict refuses. +func DecodeVerdict(data []byte) (checker.VerdictsByGroupHeadsValue, error) { + var v checker.VerdictsByGroupHeadsValue + if err := decode(data, &v); err != nil { + return v, err + } + if err := CheckVerdict(v); err != nil { + return checker.VerdictsByGroupHeadsValue{}, err + } + return v, nil +} + +// EncodeKept is k as JSON at the kept values' version, refused by CheckKept's rules. +func EncodeKept(k checker.KeptByLevelDigestValue) ([]byte, error) { + if err := CheckKept(k); err != nil { + return nil, err + } + k.V = keys.Version + return json.Marshal(k) +} + +// DecodeKept reads a `kept` value; it refuses an unknown version and what EncodeKept refuses. +func DecodeKept(data []byte) (checker.KeptByLevelDigestValue, error) { + var k checker.KeptByLevelDigestValue + if err := decode(data, &k); err != nil { + return k, err + } + if err := CheckKept(k); err != nil { + return checker.KeptByLevelDigestValue{}, err + } + return k, nil +} + +// EncodePreview is p as JSON at the kept values' version, refused by CheckPreview's rules. +func EncodePreview(p checker.PreviewsByRepositoryHeadTrunkValue) ([]byte, error) { + if err := CheckPreview(p); err != nil { + return nil, err + } + p.V = keys.Version + return json.Marshal(p) +} + +// DecodePreview reads a `previews` value; it refuses an unknown version and what EncodePreview refuses. +func DecodePreview(data []byte) (checker.PreviewsByRepositoryHeadTrunkValue, error) { + var p checker.PreviewsByRepositoryHeadTrunkValue + if err := decode(data, &p); err != nil { + return p, err + } + if err := CheckPreview(p); err != nil { + return checker.PreviewsByRepositoryHeadTrunkValue{}, err + } + return p, nil +} + +func decode(data []byte, into any) error { + if err := keys.CheckVersion(data); err != nil { + return err + } + if err := json.Unmarshal(data, into); err != nil { + return fmt.Errorf("meshdeliverycheckerclient: value does not fit its shape: %w", err) + } + return nil +} From b79b94b19de121a2502aec1a3fe9c276f1abf77f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:42:19 +0100 Subject: [PATCH 09/11] =?UTF-8?q?Clients:=20the=20kept=20and=20preview=20k?= =?UTF-8?q?eys=20of=20the=20checker's=20states=20and=20the=20candidates=20?= =?UTF-8?q?key=20of=20the=20catalog's=20(ADR=200634=20=C2=A72;=20issue=201?= =?UTF-8?q?966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../go/clients/meshcatalogclient/candidate.go | 24 +++++++ .../clients/meshcatalogclient/catalog_test.go | 16 +++++ .../meshdeliverycheckerclient/client_test.go | 29 +++++++++ .../meshdeliverycheckerclient/codec.go | 63 +++++++++++++++++++ 4 files changed, 132 insertions(+) diff --git a/contracts/go/clients/meshcatalogclient/candidate.go b/contracts/go/clients/meshcatalogclient/candidate.go index 3c0f181c..31a5db92 100644 --- a/contracts/go/clients/meshcatalogclient/candidate.go +++ b/contracts/go/clients/meshcatalogclient/candidate.go @@ -3,10 +3,12 @@ package meshcatalogclient import ( "crypto/sha256" "encoding/hex" + "fmt" "sort" "strings" seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-sdk/go/keys" ) // FingerprintSet is the `fpset` of a candidate's pull requests: the first 12 hex digits of the sha256 over the lines @@ -36,3 +38,25 @@ func FingerprintSet(pulls []seat.CandidatesByGroupValuePullsItem) string { sum := sha256.Sum256([]byte(strings.Join(all, "\n"))) return hex.EncodeToString(sum[:])[:12] } + +// CandidatesKey is the key, in the state candidates, of a group: `` escaped (keys.EscapeName), the branch name the +// group's pull requests share (a slash is allowed); a lone pull request is a group of one under its branch name. +func CandidatesKey(group string) (string, error) { + if group == "" { + return "", fmt.Errorf("meshcatalogclient: the group name is empty") + } + return seat.CandidatesByGroupKey(keys.EscapeName(group)), nil +} + +// ParseCandidatesKey is the inverse of CandidatesKey. +func ParseCandidatesKey(key string) (string, error) { + part, ok := seat.ParseCandidatesByGroupKey(key) + if !ok { + return "", fmt.Errorf("meshcatalogclient: %q is not a candidates key", key) + } + group, err := keys.UnescapeName(part) + if err != nil { + return "", fmt.Errorf("meshcatalogclient: %q is not a candidates key: %w", key, err) + } + return group, nil +} diff --git a/contracts/go/clients/meshcatalogclient/catalog_test.go b/contracts/go/clients/meshcatalogclient/catalog_test.go index 91a4e3ef..d8a60d74 100644 --- a/contracts/go/clients/meshcatalogclient/catalog_test.go +++ b/contracts/go/clients/meshcatalogclient/catalog_test.go @@ -156,3 +156,19 @@ func TestVerdictsAreTakenAsTheReadersTookThem(t *testing.T) { t.Error("ModulesKey accepts a dotted name") } } + +func TestCandidatesKeyEscapesTheGroupAndReadsBack(t *testing.T) { + key, err := CandidatesKey("fix/1.2") + if err != nil { + t.Fatal(err) + } + if key == "fix/1.2" { + t.Errorf("the group is not escaped: %q", key) + } + if group, err := ParseCandidatesKey(key); err != nil || group != "fix/1.2" { + t.Errorf("read back = %q, %v", group, err) + } + if _, err := CandidatesKey(""); err == nil { + t.Error("an empty group keys") + } +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/client_test.go b/contracts/go/clients/meshdeliverycheckerclient/client_test.go index 1a8334c6..d1e1a99f 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/client_test.go +++ b/contracts/go/clients/meshdeliverycheckerclient/client_test.go @@ -192,3 +192,32 @@ func TestTheCodecsWriteAtTheVersionAndReadBackThroughTheRules(t *testing.T) { t.Error("a preview with no merge commit and no conflict is written") } } + +// The kept and preview keys: a key reads back to its parts, and a part that does not key is refused. +func TestKeptAndPreviewKeysReadBack(t *testing.T) { + digest := strings.Repeat("ab", 32) + key, err := KeptKey(LevelUnit, digest) + if err != nil || key != "unit."+digest { + t.Fatalf("kept key = %q, %v", key, err) + } + if level, d, err := ParseKeptKey(key); err != nil || level != LevelUnit || d != digest { + t.Errorf("kept key read back = %q %q, %v", level, d, err) + } + for _, bad := range []string{"fast." + digest, "unit.XY", "unit"} { + if _, _, err := ParseKeptKey(bad); err == nil { + t.Errorf("kept key %q is read", bad) + } + } + key, err = PreviewKey("novox/mesh-sdk", "h1", "t1") + if err != nil { + t.Fatal(err) + } + if repo, h, tr, err := ParsePreviewKey(key); err != nil || repo != "novox/mesh-sdk" || h != "h1" || tr != "t1" { + t.Errorf("preview key %q read back = %q %q %q, %v", key, repo, h, tr, err) + } + for _, bad := range []string{"novox.h1.t1", "a/b.h1", "a/b.h1.t1.x"} { + if _, _, _, err := ParsePreviewKey(bad); err == nil { + t.Errorf("preview key %q is read", bad) + } + } +} diff --git a/contracts/go/clients/meshdeliverycheckerclient/codec.go b/contracts/go/clients/meshdeliverycheckerclient/codec.go index 2b6b2b0c..c684e73d 100644 --- a/contracts/go/clients/meshdeliverycheckerclient/codec.go +++ b/contracts/go/clients/meshdeliverycheckerclient/codec.go @@ -3,6 +3,7 @@ package meshdeliverycheckerclient import ( "encoding/json" "fmt" + "strings" checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-sdk/go/keys" @@ -85,3 +86,65 @@ func decode(data []byte, into any) error { } return nil } + +// KeptKey is the key, in the state kept, of a kept verdict: `.`, the level unit or integration and the digest, +// lowercase hex, of everything that package's tests read at that level as the language's build module names it. +func KeptKey(level, digest string) (string, error) { + if err := checkLevel(level); err != nil { + return "", err + } + if digest == "" || strings.Trim(digest, "0123456789abcdef") != "" { + return "", fmt.Errorf("meshdeliverycheckerclient: %q is not a digest, lowercase hex", digest) + } + return checker.KeptByLevelDigestKey(level, digest), nil +} + +// ParseKeptKey is the inverse of KeptKey. +func ParseKeptKey(key string) (level, digest string, err error) { + level, digest, ok := checker.ParseKeptByLevelDigestKey(key) + if !ok { + return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a kept key, .", key) + } + if _, err = KeptKey(level, digest); err != nil { + return "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a kept key: %w", key, err) + } + return level, digest, nil +} + +// PreviewKey is the key, in the state previews, of a head merged onto a trunk commit: `..`, the +// repository's owner and name each escaped (keys.EscapeName). +func PreviewKey(repository, head, trunk string) (string, error) { + if err := 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 + } + owner, name, _ := strings.Cut(repository, "/") + return checker.PreviewsByRepositoryHeadTrunkKey(keys.EscapeName(owner)+"/"+keys.EscapeName(name), head, trunk), nil +} + +// ParsePreviewKey is the inverse of PreviewKey. +func ParsePreviewKey(key string) (repository, head, trunk string, err error) { + repo, head, trunk, ok := checker.ParsePreviewsByRepositoryHeadTrunkKey(key) + if !ok || strings.Contains(trunk, ".") { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key, ..", key) + } + owner, name, found := strings.Cut(repo, "/") + if !found { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: the repository has no slash", key) + } + owner, err1 := keys.UnescapeName(owner) + name, err2 := keys.UnescapeName(name) + if err1 != nil || err2 != nil { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: the repository does not unescape", key) + } + repository = owner + "/" + name + if _, err := PreviewKey(repository, head, trunk); err != nil { + return "", "", "", fmt.Errorf("meshdeliverycheckerclient: %q is not a preview key: %w", key, err) + } + return repository, head, trunk, nil +} From 6b32e8257dd9da9f81d32c81f2845382cea2856f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:48:37 +0100 Subject: [PATCH 10/11] =?UTF-8?q?Fetch=20part:=20the=20TypeScript=20module?= =?UTF-8?q?'s=20group=20package=20({name,=20version}=20or=20{repository,?= =?UTF-8?q?=20commit,=20ref}),=20which=20the=20checker=20hands=20the=20Nod?= =?UTF-8?q?e=20fetch=20and=20the=20part=20lacked;=20typed=20in=20the=20thr?= =?UTF-8?q?ee=20seats=20and=20the=20sdk,=20a=20node=20check=20ask=20fixtur?= =?UTF-8?q?e=20(B2g=20gap;=20ADR=200607=20=C2=A73,=20ADR=200500=20=C2=A75;?= =?UTF-8?q?=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- contracts/go/seats/nodebuildgo/contract.go | 11 ++++++ contracts/go/seats/nodebuildimage/contract.go | 11 ++++++ contracts/go/seats/nodebuildnode/contract.go | 11 ++++++ .../go/seats/nodebuildnode/contract_test.go | 2 + contracts/parts/build-step.json | 11 ++++++ .../node-build-node/fixtures/states.json | 39 +++++++++++++++++++ contracts/ts/seats/nodebuildgo/src/index.ts | 1 + .../seats/nodebuildgo/src/parts/build-step.ts | 11 ++++++ .../ts/seats/nodebuildimage/src/index.ts | 1 + .../nodebuildimage/src/parts/build-step.ts | 11 ++++++ contracts/ts/seats/nodebuildnode/src/index.ts | 1 + .../nodebuildnode/src/parts/build-step.ts | 11 ++++++ sdk/buildstep/ask.go | 15 ++++++- 13 files changed, 135 insertions(+), 1 deletion(-) diff --git a/contracts/go/seats/nodebuildgo/contract.go b/contracts/go/seats/nodebuildgo/contract.go index d1a9288c..34d570c1 100644 --- a/contracts/go/seats/nodebuildgo/contract.go +++ b/contracts/go/seats/nodebuildgo/contract.go @@ -251,6 +251,15 @@ type FetchBesideItem struct { Repository string `json:"repository"` } +// FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's. +type FetchPackage struct { + Commit string `json:"commit,omitempty"` + Name string `json:"name,omitempty"` + Ref string `json:"ref,omitempty"` + Repository string `json:"repository,omitempty"` + Version string `json:"version,omitempty"` +} + // Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside. type Fetch struct { // the trunk commit a merge preview merges onto @@ -261,6 +270,8 @@ type Fetch struct { Commit string `json:"commit"` // the module's directory in the repository ("." for its root), with paths Dir string `json:"dir,omitempty"` + // the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's + Package *FetchPackage `json:"package,omitempty"` // the version (a full commit) of each mesh package to build against, by its module path Packages map[string]string `json:"packages,omitempty"` // the paths the module names, relative to dir diff --git a/contracts/go/seats/nodebuildimage/contract.go b/contracts/go/seats/nodebuildimage/contract.go index 69fc44b7..d80d6348 100644 --- a/contracts/go/seats/nodebuildimage/contract.go +++ b/contracts/go/seats/nodebuildimage/contract.go @@ -251,6 +251,15 @@ type FetchBesideItem struct { Repository string `json:"repository"` } +// FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's. +type FetchPackage struct { + Commit string `json:"commit,omitempty"` + Name string `json:"name,omitempty"` + Ref string `json:"ref,omitempty"` + Repository string `json:"repository,omitempty"` + Version string `json:"version,omitempty"` +} + // Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside. type Fetch struct { // the trunk commit a merge preview merges onto @@ -261,6 +270,8 @@ type Fetch struct { Commit string `json:"commit"` // the module's directory in the repository ("." for its root), with paths Dir string `json:"dir,omitempty"` + // the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's + Package *FetchPackage `json:"package,omitempty"` // the version (a full commit) of each mesh package to build against, by its module path Packages map[string]string `json:"packages,omitempty"` // the paths the module names, relative to dir diff --git a/contracts/go/seats/nodebuildnode/contract.go b/contracts/go/seats/nodebuildnode/contract.go index 51a5a76e..c40fa83d 100644 --- a/contracts/go/seats/nodebuildnode/contract.go +++ b/contracts/go/seats/nodebuildnode/contract.go @@ -251,6 +251,15 @@ type FetchBesideItem struct { Repository string `json:"repository"` } +// FetchPackage is the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's. +type FetchPackage struct { + Commit string `json:"commit,omitempty"` + Name string `json:"name,omitempty"` + Ref string `json:"ref,omitempty"` + Repository string `json:"repository,omitempty"` + Version string `json:"version,omitempty"` +} + // Fetch is what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside. type Fetch struct { // the trunk commit a merge preview merges onto @@ -261,6 +270,8 @@ type Fetch struct { Commit string `json:"commit"` // the module's directory in the repository ("." for its root), with paths Dir string `json:"dir,omitempty"` + // the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's + Package *FetchPackage `json:"package,omitempty"` // the version (a full commit) of each mesh package to build against, by its module path Packages map[string]string `json:"packages,omitempty"` // the paths the module names, relative to dir diff --git a/contracts/go/seats/nodebuildnode/contract_test.go b/contracts/go/seats/nodebuildnode/contract_test.go index 70e0c3a9..fd58eeb3 100644 --- a/contracts/go/seats/nodebuildnode/contract_test.go +++ b/contracts/go/seats/nodebuildnode/contract_test.go @@ -39,6 +39,8 @@ func TestFixtures(t *testing.T) { }, subjectIs: nil}, {name: "a build ask", subject: "mesh.seat.node-build-node.accept.delivery", given: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", wire: "{\"asker\":\"mesh-delivery-builder\",\"build\":{\"artifacts\":[{\"artifact\":\"go\"}],\"module\":\"nats\"},\"id\":\"a1b2\",\"kind\":\"build\",\"language\":\"go\"}", decode: func(b []byte) (any, error) { var v DeliveryAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.delivery$")}, + {name: "a check ask with the group's tagged package", subject: "mesh.seat.node-build-node.accept.check", given: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"v\":1}", wire: "{\"asker\":\"mesh-delivery-checker\",\"at\":\"2026-11-05T10:00:00Z\",\"check\":{\"fetch\":{\"base\":\"89abcdef0123456789abcdef0123456789abcdef\",\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"package\":{\"name\":\"@novox/mesh-sdk\",\"version\":\"0.1.15-feat-x.1\"},\"ref\":\"refs/pull/7/head\",\"repository\":\"novox/mesh-delivery\"},\"kept\":true,\"levels\":[\"unit\",\"integration\"],\"node-names\":[\"ace\",\"g14\"],\"toolchain\":{\"language\":\"typescript\",\"recipe\":\"1\",\"version\":\"26.12.0\"}},\"id\":\"c3d5\",\"kind\":\"check\",\"language\":\"typescript\",\"v\":1}", + decode: func(b []byte) (any, error) { var v CheckAccept; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.node-build-node\\.accept\\.check$")}, {name: "a finished check step of a TypeScript merge preview", subject: "", given: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", wire: "{\"ended\":\"2026-11-05T10:01:00Z\",\"id\":\"c3d6\",\"node\":\"ace\",\"result\":{\"fetch\":{\"base\":\"abcdef0123456789abcdef0123456789abcdef01\",\"changed\":[\"modules/m/package.json\"],\"commit\":\"0123456789abcdef0123456789abcdef01234567\",\"merge\":\"0123456789abcdef0123456789abcdef01234567\",\"modules\":[\"modules/m\"],\"trees\":{\"modules/m\":{\"manifest\":\"0581b699d9d05517e69b21f5cedbeb2eee8aba2b205280b561be731594d7feef\",\"tree\":\"0123456789abcdef0123456789abcdef01234567\"}},\"workspace\":\"mvii1sxt979f67\"}},\"started\":\"2026-11-05T10:00:00Z\",\"state\":\"passed\",\"tool\":\"check\",\"v\":1}", decode: func(b []byte) (any, error) { var v StepsByNodeIDValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } diff --git a/contracts/parts/build-step.json b/contracts/parts/build-step.json index c272a8c2..82602f89 100644 --- a/contracts/parts/build-step.json +++ b/contracts/parts/build-step.json @@ -467,6 +467,17 @@ }, "description": "the version (a full commit) of each mesh package to build against, by its module path" }, + "package": { + "type": "object", + "description": "the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's", + "properties": { + "name": { "type": "string" }, + "version": { "type": "string" }, + "repository": { "type": "string" }, + "commit": { "type": "string" }, + "ref": { "type": "string" } + } + }, "beside": { "type": "array", "items": { diff --git a/contracts/seats/node-build-node/fixtures/states.json b/contracts/seats/node-build-node/fixtures/states.json index a9f265f5..125819d0 100644 --- a/contracts/seats/node-build-node/fixtures/states.json +++ b/contracts/seats/node-build-node/fixtures/states.json @@ -60,6 +60,45 @@ } } }, + { + "name": "a check ask with the group's tagged package", + "of": "accept:check", + "subject": "mesh.seat.node-build-node.accept.check", + "given": { + "v": 1, + "id": "c3d5", + "kind": "check", + "language": "typescript", + "asker": "mesh-delivery-checker", + "at": "2026-11-05T10:00:00Z", + "check": { + "fetch": { + "repository": "novox/mesh-delivery", + "commit": "0123456789abcdef0123456789abcdef01234567", + "ref": "refs/pull/7/head", + "base": "89abcdef0123456789abcdef0123456789abcdef", + "package": { + "name": "@novox/mesh-sdk", + "version": "0.1.15-feat-x.1" + } + }, + "levels": [ + "unit", + "integration" + ], + "kept": true, + "toolchain": { + "language": "typescript", + "version": "26.12.0", + "recipe": "1" + }, + "node-names": [ + "ace", + "g14" + ] + } + } + }, { "name": "a finished check step of a TypeScript merge preview", "of": "state:steps/.", diff --git a/contracts/ts/seats/nodebuildgo/src/index.ts b/contracts/ts/seats/nodebuildgo/src/index.ts index 83d2a60a..85ee7149 100644 --- a/contracts/ts/seats/nodebuildgo/src/index.ts +++ b/contracts/ts/seats/nodebuildgo/src/index.ts @@ -113,6 +113,7 @@ export type CurrentInput = buildStep.CurrentInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchPackage = buildStep.FetchPackage; export type FetchRefusal = buildStep.FetchRefusal; export type LevelAnswer = buildStep.LevelAnswer; export type LevelAnswerDigest = buildStep.LevelAnswerDigest; diff --git a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts index c61ca3bc..1b4aabcc 100644 --- a/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildgo/src/parts/build-step.ts @@ -67,6 +67,15 @@ export interface FetchBesideItem { "repository": string; } +/** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ +export interface FetchPackage { + "commit"?: string; + "name"?: string; + "ref"?: string; + "repository"?: string; + "version"?: string; +} + /** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */ export interface Fetch { /** the trunk commit a merge preview merges onto */ @@ -77,6 +86,8 @@ export interface Fetch { "commit": string; /** the module's directory in the repository ("." for its root), with paths */ "dir"?: string; + /** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ + "package"?: FetchPackage; /** the version (a full commit) of each mesh package to build against, by its module path */ "packages"?: Record; /** the paths the module names, relative to dir */ diff --git a/contracts/ts/seats/nodebuildimage/src/index.ts b/contracts/ts/seats/nodebuildimage/src/index.ts index d0745fcc..ffa81df0 100644 --- a/contracts/ts/seats/nodebuildimage/src/index.ts +++ b/contracts/ts/seats/nodebuildimage/src/index.ts @@ -113,6 +113,7 @@ export type CurrentInput = buildStep.CurrentInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchPackage = buildStep.FetchPackage; export type FetchRefusal = buildStep.FetchRefusal; export type LevelAnswer = buildStep.LevelAnswer; export type LevelAnswerDigest = buildStep.LevelAnswerDigest; diff --git a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts index c61ca3bc..1b4aabcc 100644 --- a/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildimage/src/parts/build-step.ts @@ -67,6 +67,15 @@ export interface FetchBesideItem { "repository": string; } +/** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ +export interface FetchPackage { + "commit"?: string; + "name"?: string; + "ref"?: string; + "repository"?: string; + "version"?: string; +} + /** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */ export interface Fetch { /** the trunk commit a merge preview merges onto */ @@ -77,6 +86,8 @@ export interface Fetch { "commit": string; /** the module's directory in the repository ("." for its root), with paths */ "dir"?: string; + /** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ + "package"?: FetchPackage; /** the version (a full commit) of each mesh package to build against, by its module path */ "packages"?: Record; /** the paths the module names, relative to dir */ diff --git a/contracts/ts/seats/nodebuildnode/src/index.ts b/contracts/ts/seats/nodebuildnode/src/index.ts index 0c66706f..c2142833 100644 --- a/contracts/ts/seats/nodebuildnode/src/index.ts +++ b/contracts/ts/seats/nodebuildnode/src/index.ts @@ -113,6 +113,7 @@ export type CurrentInput = buildStep.CurrentInput; export type Fetch = buildStep.Fetch; export type FetchAnswer = buildStep.FetchAnswer; export type FetchBesideItem = buildStep.FetchBesideItem; +export type FetchPackage = buildStep.FetchPackage; export type FetchRefusal = buildStep.FetchRefusal; export type LevelAnswer = buildStep.LevelAnswer; export type LevelAnswerDigest = buildStep.LevelAnswerDigest; diff --git a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts index c61ca3bc..1b4aabcc 100644 --- a/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts +++ b/contracts/ts/seats/nodebuildnode/src/parts/build-step.ts @@ -67,6 +67,15 @@ export interface FetchBesideItem { "repository": string; } +/** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ +export interface FetchPackage { + "commit"?: string; + "name"?: string; + "ref"?: string; + "repository"?: string; + "version"?: string; +} + /** what a build module's fetch tool is asked: the repository and the full commit, the pull request's ref when only its head carries the commit, the trunk commit to merge onto for a merge preview, the module's directory and named paths, the mesh packages to build against, and the sibling repositories placed beside */ export interface Fetch { /** the trunk commit a merge preview merges onto */ @@ -77,6 +86,8 @@ export interface Fetch { "commit": string; /** the module's directory in the repository ("." for its root), with paths */ "dir"?: string; + /** the TypeScript module's group package: the group's tagged version of the SDK ({name, version}), placed from the registry in each member whatever its lock names, or the group's package member ({repository, commit, ref}), packed by the fetch and served to the members in place of the registry's */ + "package"?: FetchPackage; /** the version (a full commit) of each mesh package to build against, by its module path */ "packages"?: Record; /** the paths the module names, relative to dir */ diff --git a/sdk/buildstep/ask.go b/sdk/buildstep/ask.go index 7e46704e..9418a2be 100644 --- a/sdk/buildstep/ask.go +++ b/sdk/buildstep/ask.go @@ -213,7 +213,20 @@ type Fetch struct { Dir string `json:"dir,omitempty"` Paths []string `json:"paths,omitempty"` Packages PackageVersions `json:"packages,omitempty"` - Beside []FetchBeside `json:"beside,omitempty"` + // Package is the TypeScript module's group package: the group's tagged version ({name, version}) or the group's package + // member ({repository, commit, ref}). + Package *FetchPackage `json:"package,omitempty"` + Beside []FetchBeside `json:"beside,omitempty"` +} + +// FetchPackage is the group package a TypeScript fetch takes (ADR 0607 §3, ADR 0500 §5): either the group's tagged version +// (Name and Version) or the group's package member (Repository, Commit and Ref). +type FetchPackage struct { + Name string `json:"name,omitempty"` + Version string `json:"version,omitempty"` + Repository string `json:"repository,omitempty"` + Commit string `json:"commit,omitempty"` + Ref string `json:"ref,omitempty"` } // FetchBeside is a sibling repository placed beside the fetched tree. From 84e617d3a9a864f1ed70093e7121e76ec0ba4960 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 17:00:25 +0100 Subject: [PATCH 11/11] =?UTF-8?q?Checker:=20off=20mesh-sdk=20go/delivery?= =?UTF-8?q?=20onto=20the=20seats'=20types=20and=20the=20clients=20(verdict?= =?UTF-8?q?,=20kept,=20preview,=20owed,=20candidates,=20modules,=20manifes?= =?UTF-8?q?ts,=20packages,=20source,=20turns,=20haves),=20its=20holders=20?= =?UTF-8?q?and=20buckets=20its=20own=20constants=20checked=20against=20its?= =?UTF-8?q?=20manifest=20(D-11),=20the=20check=20ask's=20fetch=20typed=20w?= =?UTF-8?q?ith=20the=20group=20package=20and=20the=20check=20answer's=20fe?= =?UTF-8?q?tch=20read=20as=20the=20Delivery=20SDK's=20FetchAnswer,=20a=20m?= =?UTF-8?q?erge=20with=20no=20program=20told=20by=20merge=20(B2g;=20ADR=20?= =?UTF-8?q?0634=20=C2=A71,=20=C2=A72,=20=C2=A76;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../cmd/mesh-delivery-checker-tools/main.go | 15 +- .../cmd/mesh-delivery-checker/bus.go | 8 +- .../cmd/mesh-delivery-checker/engines.go | 41 +-- .../mesh-delivery-checker/integration_test.go | 254 ++++++++++-------- .../cmd/mesh-delivery-checker/judge.go | 87 +++--- .../cmd/mesh-delivery-checker/judge_test.go | 49 ++-- .../cmd/mesh-delivery-checker/lazyturns.go | 10 +- .../cmd/mesh-delivery-checker/main.go | 71 ++--- .../cmd/mesh-delivery-checker/nodes_test.go | 2 +- .../cmd/mesh-delivery-checker/packages.go | 26 +- .../cmd/mesh-delivery-checker/tags.go | 11 +- .../cmd/mesh-delivery-checker/toolchains.go | 4 +- modules/mesh-delivery-checker/go.mod | 18 +- .../internal/check/adr548_test.go | 38 +-- .../internal/check/askagain.go | 34 +-- .../internal/check/binaries.go | 18 +- .../internal/check/binaries_test.go | 36 ++- .../internal/check/bounded.go | 4 +- .../internal/check/build_change.go | 4 +- .../internal/check/builds.go | 40 +-- .../internal/check/case.go | 46 ++-- .../internal/check/checker.go | 212 ++++++++------- .../internal/check/checker_test.go | 126 ++++----- .../internal/check/composition.go | 30 ++- .../internal/check/composition_test.go | 80 +++--- .../internal/check/digestform_test.go | 13 +- .../internal/check/engines.go | 29 +- .../internal/check/engines_test.go | 41 +-- .../internal/check/every_failure_test.go | 31 ++- .../internal/check/examiner_test.go | 187 +++++++------ .../internal/check/failure_words_test.go | 6 +- .../internal/check/gate_test.go | 10 +- .../internal/check/graph_cycle.go | 4 +- .../internal/check/judge.go | 198 +++++++------- .../internal/check/judged_test.go | 19 +- .../internal/check/kept.go | 25 +- .../internal/check/kept_test.go | 32 +-- .../internal/check/line.go | 76 +++--- .../internal/check/line_test.go | 49 ++-- .../internal/check/lostask.go | 19 +- .../internal/check/lostask_test.go | 57 ++-- .../internal/check/manifests_judge.go | 27 +- .../internal/check/names.go | 90 +++++++ .../internal/check/owed.go | 49 ++++ .../internal/check/pins.go | 13 +- .../internal/check/pins_test.go | 59 ++-- .../internal/check/preview.go | 19 +- .../internal/check/rank_test.go | 15 +- .../internal/check/read.go | 85 +++--- .../internal/check/read_test.go | 73 ++--- .../internal/check/renames.go | 11 +- .../internal/check/results.go | 58 +--- .../internal/check/seatdefs.go | 11 +- .../internal/check/series.go | 30 ++- .../internal/check/tagwanted.go | 23 +- .../internal/check/try.go | 27 +- .../internal/check/try_test.go | 16 +- .../internal/check/turns.go | 25 +- .../internal/check/verdictevent.go | 8 +- .../mesh-delivery-checker/manifest_test.go | 26 +- 60 files changed, 1499 insertions(+), 1226 deletions(-) create mode 100644 modules/mesh-delivery-checker/internal/check/names.go create mode 100644 modules/mesh-delivery-checker/internal/check/owed.go diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go index 369df35e..845be149 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker-tools/main.go @@ -10,13 +10,14 @@ import ( "sync/atomic" "time" + "mesh-delivery-checker/internal/check" + "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/clients/meshdeliveryclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/views" - - "mesh-delivery-checker/internal/check" ) // reads counts the values this call read from the bus: the runtime launches the tools for one call, so the count is the call's. @@ -68,7 +69,7 @@ func (r runsOf) CommitOf(module string) string { if err != nil || len(raw) == 0 { return "" } - m, err := delivery.DecodeModule(raw) + m, err := meshcatalogclient.DecodeModule(raw) if err != nil { return "" } @@ -292,11 +293,11 @@ func main() { tries := stdio.State(check.TriesState) source := stdio.State("mesh-delivery-gate.source") trunks := func() (map[string]string, error) { - e, err := source.Get(delivery.SourceMainKey) + e, err := source.Get(meshdeliveryclient.SourceMainKey) if err != nil || e == nil { return nil, err } - s, err := delivery.DecodeSource(e.Value) + s, err := meshdeliveryclient.DecodeSource(e.Value) return s.Heads, err } r.Trunks = trunks @@ -330,7 +331,7 @@ func main() { tools := append(append(append(Tools(r), TryTools(trying)...), SeatTryTools(trying)...), RecoveryTools(r, written)...) // checker_first_views and checker_first_view_again: where the checker's first views of the indexer's and the assigner's // states stand, and asking one again by hand (ADR 0615, DLV-M2). - tools = append(tools, views.StatusTools(delivery.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) + tools = append(tools, views.StatusTools(check.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...) if err := stdio.Serve("", logged(tools)); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err) os.Exit(1) diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go index 62eccf09..52fe097a 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/bus.go @@ -7,8 +7,8 @@ import ( "github.com/nats-io/nats.go/jetstream" + "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/process" "mesh-delivery-checker/internal/check" @@ -110,7 +110,7 @@ func follow(ctx context.Context, w jetstream.KeyWatcher, each func(jetstream.Key type manifestReader struct{ kv jetstream.KeyValue } func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) { - key, err := delivery.ManifestsKey(digest) + key, err := meshcatalogclient.ManifestsKey(digest) if err != nil { return catalogue.Manifest{}, err } @@ -118,11 +118,11 @@ func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue. if err != nil { return catalogue.Manifest{}, fmt.Errorf("manifests.%s: %w", key, err) } - kept, err := delivery.DecodeManifest(e.Value()) + kept, err := meshcatalogclient.DecodeManifest(e.Value()) if err != nil { return catalogue.Manifest{}, err } - return parseManifest(kept.JSON) + return parseManifest(kept.Manifest) } // parseManifest decodes a manifest the indexer kept, as every reader does (novox/hq ADR 0578 §3): judged by nothing here, diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go index bb068040..48697172 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/engines.go @@ -5,19 +5,24 @@ import ( "encoding/json" "errors" "fmt" - nodeengineseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" "sort" "sync" "time" + nodeengineseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "github.com/nats-io/nats.go/jetstream" + "mesh-delivery-checker/internal/check" + nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient" + "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/buildrun" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" - "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // The checker's composition of a check on the bus (novox/hq ADR 0593 §7, §9; check.EngineCompositions): each placed @@ -65,18 +70,18 @@ func (p placedNodes) Placed() ([]string, error) { defer cancel() kv, err := p.kv(ctx) if err != nil { - return nil, fmt.Errorf("the state %s cannot be read: %w", delivery.PlacementBucket, err) + return nil, fmt.Errorf("the state %s cannot be read: %w", buildrun.PlacementBucket, err) } lister, err := kv.ListKeys(ctx) if errors.Is(err, jetstream.ErrNoKeysFound) { return nil, nil // a placement that places nothing } if err != nil { - return nil, fmt.Errorf("the state %s cannot be listed: %w", delivery.PlacementBucket, err) + return nil, fmt.Errorf("the state %s cannot be listed: %w", buildrun.PlacementBucket, err) } seen := map[string]bool{} for key := range lister.Keys() { - if node, _, _, err := delivery.ParsePlacementKey(key); err == nil { + if node, _, _, err := meshmoduleassignerclient.ParseKey(key); err == nil { seen[node] = true } } @@ -89,20 +94,20 @@ func (p placedNodes) Placed() ([]string, error) { } // seatSandbox runs a compose job on the Go build seat's check stream, as the checker's own ask, and answers the -// composer's verdict from the step's result (delivery.ComposeAnswer). +// composer's verdict from the step's result (buildstep.ComposeAnswer). type seatSandbox struct{ asker buildrun.Asker } -func (s seatSandbox) Compose(ctx context.Context, job delivery.ComposeJob) (composer.Verdict, error) { - ask := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule, +func (s seatSandbox) Compose(ctx context.Context, job buildstep.ComposeJob) (composer.Verdict, error) { + ask := buildstep.StepAsk{ID: buildstep.NewID(), Kind: buildstep.AskCheck, Language: buildstep.AskGo, Asker: check.CheckerModule, At: time.Now().UTC(), Compose: &job} step, err := s.asker.Ask(ctx, ask) if err != nil { return composer.Verdict{}, err } - if step.State != delivery.StepPassed { + if step.State != buildstep.StepPassed { return composer.Verdict{}, fmt.Errorf("the compose ask %s ended %s: %s", ask.ID, step.State, step.Reason) } - var a delivery.ComposeAnswer + var a buildstep.ComposeAnswer if err := json.Unmarshal(step.Result, &a); err != nil { return composer.Verdict{}, fmt.Errorf("the compose ask %s answered no compose answer: %w", ask.ID, err) } @@ -114,17 +119,17 @@ func (s seatSandbox) Compose(ctx context.Context, job delivery.ComposeJob) (comp } // manifestVerdicts are the indexer's refusals of the trunk's manifests, by module, from its modules' values -// (delivery.Module.VerdictOf, ADR 0578 §3): what the sandbox's composer holds a refused module by. +// (meshcatalog.ModulesByModuleValue.VerdictOf, ADR 0578 §3): what the sandbox's composer holds a refused module by. type manifestVerdicts struct { mu sync.Mutex - by map[string]delivery.ManifestVerdict + by map[string]meshcatalog.VerdictsByDigestValue } -func (m *manifestVerdicts) put(module string, v *delivery.ManifestVerdict) { +func (m *manifestVerdicts) put(module string, v *meshcatalog.VerdictsByDigestValue) { m.mu.Lock() defer m.mu.Unlock() if m.by == nil { - m.by = map[string]delivery.ManifestVerdict{} + m.by = map[string]meshcatalog.VerdictsByDigestValue{} } if v == nil { delete(m.by, module) @@ -132,13 +137,13 @@ func (m *manifestVerdicts) put(module string, v *delivery.ManifestVerdict) { } // The sandbox's composer may be the node-engine of an older delivery, which does not know the verdict waits: it is handed a // wait as the refusal it is held as. - m.by[module] = v.AsRefused() + m.by[module] = meshcatalogclient.VerdictAsRefused(*v) } -func (m *manifestVerdicts) all() map[string]delivery.ManifestVerdict { +func (m *manifestVerdicts) all() map[string]meshcatalog.VerdictsByDigestValue { m.mu.Lock() defer m.mu.Unlock() - out := make(map[string]delivery.ManifestVerdict, len(m.by)) + out := make(map[string]meshcatalog.VerdictsByDigestValue, len(m.by)) for k, v := range m.by { out[k] = v } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go index 02dcae26..0f6f616a 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/integration_test.go @@ -22,18 +22,30 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "mesh-delivery-checker/internal/check" + gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" - "git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry" - "git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + "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/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry" + "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/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/views/viewstest" - - "mesh-delivery-checker/internal/check" ) // busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream, the @@ -98,7 +110,7 @@ type fakeBuildModules struct { // names are the nodes' names the last check ask carried, for the names check (ADR 0567 §12). names []string // packages are, by repository, the package versions the last Go check ask of it carried (StepAsk.Packages). - packages map[string]delivery.PackageVersions + packages map[string]buildstep.PackageVersions } func (f *fakeBuildModules) count(tool string) int { @@ -158,7 +170,7 @@ func (f *fakeBuildModules) result(t *testing.T, tool string, args map[string]any // does since R4g (ADR 0525; issue 1887 A5): each ask's job runs with step bodies answering the captured results, and a // check takes the kept verdicts of the checker's kept bucket by their digests. func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.KeyValue) { - kv, err := f.js.CreateKeyValue(f.ctx, jetstream.KeyValueConfig{Bucket: delivery.StepsBucket(module), History: 1}) + kv, err := f.js.CreateKeyValue(f.ctx, jetstream.KeyValueConfig{Bucket: buildrun.StepsBucket(module), History: 1}) if err != nil { t.Fatal(err) } @@ -178,7 +190,7 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key } steps := gobuild.AskSteps{Fetch: call("fetch"), Test: call("test"), Kept: func(ctx context.Context, level, digest string) (int, bool, error) { - key, err := delivery.KeptKey(level, digest) + key, err := meshdeliverycheckerclient.KeptKey(level, digest) if err != nil { return 0, false, err } @@ -189,8 +201,8 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key if err != nil { return 0, false, err } - v, err := delivery.DecodeKept(e.Value()) - return v.Tests, err == nil, err + v, err := meshdeliverycheckerclient.DecodeKept(e.Value()) + return int(v.Tests), err == nil, err }} if module == "mesh-build-go" { steps.Digest = call("digest") @@ -201,13 +213,13 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key } p := &gobuild.Puller{Source: &gobuild.JetStreamAsks{JS: f.js, Language: language}, Language: language, Node: "n1", Put: put, Capacity: func() int { return 2 }, PullWait: 200 * time.Millisecond, - Run: func(ctx context.Context, a delivery.StepAsk) (any, error) { + Run: func(ctx context.Context, a buildstep.StepAsk) (any, error) { if a.Check != nil { f.mu.Lock() f.names = a.Check.NodeNames - if repository, _ := a.Check.Fetch["repository"].(string); a.Language == delivery.AskGo { + if repository := a.Check.Fetch.Repository; a.Language == buildstep.AskGo { if f.packages == nil { - f.packages = map[string]delivery.PackageVersions{} + f.packages = map[string]buildstep.PackageVersions{} } f.packages[repository] = a.Packages } @@ -311,13 +323,13 @@ func (b *bed) said(t *testing.T, words string) { // the seat verb's caller-named and plain subjects); the others never answer. It answers what it was asked. func (b *bed) engine(t *testing.T, nodes ...string) func() []ne.ComposeCandidateInput { t.Helper() - kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: delivery.PlacementBucket, History: 1}) + kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buildrun.PlacementBucket, History: 1}) if err != nil { t.Fatal(err) } for _, n := range nodes { - pk, _ := delivery.PlacementNodeKey(n) - raw, _ := json.Marshal(delivery.Placement{Node: n}) + pk, _ := meshmoduleassignerclient.NodeKey(n) + raw, _ := meshmoduleassignerclient.EncodeNode(meshmoduleassigner.PlacementByNodeNodeValue{Node: n}) if _, err := kv.Put(b.ctx, pk, raw); err != nil { t.Fatal(err) } @@ -412,7 +424,7 @@ func startBed(t *testing.T) *bed { if _, err := js.CreateOrUpdateConsumer(ctx, stream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil { t.Fatal(err) } - kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket(module), History: 1}) + kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: buildrun.ToolchainsBucket(module), History: 1}) if err != nil { t.Fatal(err) } @@ -424,11 +436,11 @@ func startBed(t *testing.T) *bed { } // The checker's streams as its manifest declares them (ADR 0619, ADR 0620): checks, which the indexer and the forge // bridge hear, and asks, with verdict and tag-wanted at the subjects the mesh-delivery-checker definition names. - if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "CHECKS", Subjects: []string{meshdeliverychecker.HolderSubjectPreviewWritten(delivery.CheckerModule)}}); err != nil { + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "CHECKS", Subjects: []string{meshdeliverychecker.HolderSubjectPreviewWritten(check.CheckerModule)}}); err != nil { t.Fatal(err) } - if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ASKS", Subjects: []string{meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule), - meshdeliverychecker.HolderSubjectTagWanted(delivery.CheckerModule)}}); err != nil { + if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ASKS", Subjects: []string{meshdeliverychecker.HolderSubjectVerdict(check.CheckerModule), + meshdeliverychecker.HolderSubjectTagWanted(check.CheckerModule)}}); err != nil { t.Fatal(err) } // The shared events stream with the seats' and modules' events (the package registry seats' published among them), and the checker's durable consumer of it. @@ -438,11 +450,11 @@ func startBed(t *testing.T) *bed { if _, err := js.CreateOrUpdateConsumer(ctx, process.EventsStream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil { t.Fatal(err) } - for name, history := range map[string]int{delivery.CheckerVerdictsBucket: delivery.CheckerVerdictsHistory, delivery.CheckerKeptBucket: delivery.CheckerKeptHistory, - delivery.CheckerOwedBucket: delivery.CheckerOwedHistory, delivery.PreviewsBucket: delivery.PreviewsHistory, delivery.CandidatesBucket: delivery.CandidatesHistory, - delivery.ModulesBucket: delivery.ModulesHistory, delivery.ManifestsBucket: delivery.ManifestsHistory, delivery.SourceBucket: delivery.SourceHistory, - delivery.PresenceBucket: delivery.PresenceHistory, delivery.HaveBucket: delivery.HaveHistory, check.TriesBucket: 1, - delivery.TurnsBucket: delivery.TurnsHistory, delivery.BuilderBuildsBucket: 1} { + for name, history := range map[string]int{check.CheckerVerdictsBucket: 5, check.CheckerKeptBucket: 1, + check.CheckerOwedBucket: 5, check.PreviewsBucket: 5, check.CandidatesBucket: 10, + check.ModulesBucket: 20, check.ManifestsBucket: 1, check.SourceBucket: 64, + check.PresenceBucket: 20, check.HaveBucket: 20, check.TriesBucket: 1, + check.TurnsBucket: 5, builds.BuilderBuildsBucket: 1} { kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: uint8(history)}) if err != nil { t.Fatal(err) @@ -451,17 +463,17 @@ func startBed(t *testing.T) *bed { } // The indexer and the assigner as the owners of the states the checker follows: their read tools and views families // (ADR 0615, DLV-M2). - for _, st := range []string{candidatesState, modulesState, manifestsState, delivery.PackagesState, delivery.IndexerPredefinedState, seatDefinitionsState} { - viewstest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, delivery.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+delivery.CheckerModule) + for _, st := range []string{candidatesState, modulesState, manifestsState, meshcatalog.StatePackages, check.IndexerPredefinedState, seatDefinitionsState} { + viewstest.Serve(t, ctx, js, ctl, check.IndexerModule, st, check.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+check.CheckerModule) } - viewstest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule+".assigner_read_placement", "n1_"+delivery.CheckerModule) + viewstest.Serve(t, ctx, js, ctl, buildrun.AssignerModule, meshmoduleassigner.StatePlacement, buildrun.AssignerModule+".assigner_read_placement", "n1_"+check.CheckerModule) // 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone // judges the repository (an unknown repository is judged by both, issue 1887 A12). - known, err := delivery.EncodeModule(delivery.Module{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"}) + known, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"}) if err != nil { t.Fatal(err) } - if _, err := b.buckets[delivery.ModulesBucket].Put(ctx, "m", known); err != nil { + if _, err := b.buckets[check.ModulesBucket].Put(ctx, "m", known); err != nil { t.Fatal(err) } b.setOnline(t, "n1", true) @@ -471,8 +483,8 @@ func startBed(t *testing.T) *bed { } b.serveForge(t, ctl) b.fake = &fakeBuildModules{js: js, ctx: ctx, calls: map[string]int{}} - b.fake.serve(t, "mesh-build-go", b.buckets[delivery.CheckerKeptBucket]) - b.fake.serve(t, "mesh-build-node", b.buckets[delivery.CheckerKeptBucket]) + b.fake.serve(t, "mesh-build-go", b.buckets[check.CheckerKeptBucket]) + b.fake.serve(t, "mesh-build-node", b.buckets[check.CheckerKeptBucket]) b.report(t, "mesh-build-go", "n1", "go", "go1.27.1", "1") b.report(t, "mesh-build-node", "n1", "typescript", "tsc5", "1") b.setTrunk(t, map[string]string{repo: trunk}) @@ -481,19 +493,19 @@ func startBed(t *testing.T) *bed { func (b *bed) setTrunk(t *testing.T, heads map[string]string) { t.Helper() - data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads}) + data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: heads}) if err != nil { t.Fatal(err) } - if _, err := b.buckets[delivery.SourceBucket].Put(b.ctx, delivery.SourceMainKey, data); err != nil { + if _, err := b.buckets[check.SourceBucket].Put(b.ctx, meshdeliveryclient.SourceMainKey, data); err != nil { t.Fatal(err) } } func (b *bed) setOnline(t *testing.T, node string, online bool) { t.Helper() - data, _ := delivery.EncodePresence(delivery.Presence{Online: online}) - if _, err := b.buckets[delivery.PresenceBucket].Put(b.ctx, node, data); err != nil { + data, _ := json.Marshal(meshwatchdogs.PresenceByNodeValue{V: keys.Version, Online: online}) + if _, err := b.buckets[check.PresenceBucket].Put(b.ctx, node, data); err != nil { t.Fatal(err) } } @@ -502,35 +514,35 @@ func (b *bed) setOnline(t *testing.T, node string, online bool) { // check ask (ADR 0567 §12); with no record the checker asks no check. func (b *bed) setNode(t *testing.T, node string) { t.Helper() - data, err := delivery.EncodeNodeRecord(delivery.NodeRecord{V: 1, Node: node}) + data, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: node}}) if err != nil { t.Fatal(err) } - key, _ := delivery.HaveNodeKey(node) - if _, err := b.buckets[delivery.HaveBucket].Put(b.ctx, key, data); err != nil { + key, _ := nodeengineclient.HaveNodeKey(node) + if _, err := b.buckets[check.HaveBucket].Put(b.ctx, key, data); err != nil { t.Fatal(err) } } func (b *bed) setHealth(t *testing.T, node, module string) { t.Helper() - data, err := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK}) + data, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK}) if err != nil { t.Fatal(err) } - key, _ := delivery.HaveKey(node, module) - if _, err := b.buckets[delivery.HaveBucket].Put(b.ctx, key, data); err != nil { + key, _ := nodeengineclient.HaveKey(node, module) + if _, err := b.buckets[check.HaveBucket].Put(b.ctx, key, data); err != nil { t.Fatal(err) } } func (b *bed) report(t *testing.T, module, node, language, version, recipe string) { t.Helper() - data, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now().UTC()}) + data, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now().UTC()}) if err != nil { t.Fatal(err) } - key, _ := delivery.ToolchainKey(node, language) + key, _ := buildstep.ToolchainKey(node, language) if _, err := b.tools[module].Put(b.ctx, key, data); err != nil { t.Fatal(err) } @@ -540,30 +552,30 @@ func (b *bed) report(t *testing.T, module, node, language, version, recipe strin } // candidate writes candidates. as the indexer does (the SDK's EncodeCandidate is the indexer's own encoder). -func (b *bed) candidate(t *testing.T, group string, pulls ...delivery.CandidatePull) string { +func (b *bed) candidate(t *testing.T, group string, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) string { t.Helper() - 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, err := delivery.HeadsHash(heads) + hash, err := meshdeliveryclient.HeadsHash(heads) if err != nil { t.Fatal(err) } - data, err := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: group, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}) + data, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: group, Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}) if err != nil { t.Fatal(err) } - key, _ := delivery.CandidatesKey(group) - if _, err := b.buckets[delivery.CandidatesBucket].Put(b.ctx, key, data); err != nil { + key, _ := meshcatalogclient.CandidatesKey(group) + if _, err := b.buckets[check.CandidatesBucket].Put(b.ctx, key, data); err != nil { t.Fatal(err) } - vk, _ := delivery.CheckerVerdictKey(group, hash) + vk, _ := meshdeliverycheckerclient.VerdictKey(group, hash) return vk } -func pull(repository string, number int, head string) delivery.CandidatePull { - return delivery.CandidatePull{Head: delivery.Head{Repository: repository, Number: number, Head: head}, Modules: []string{}} +func pull(repository string, number int, head string) meshcatalog.CandidatesByGroupValuePullsItem { + return meshcatalog.CandidatesByGroupValuePullsItem{Repository: repository, Number: int64(number), Head: head, Modules: []string{}} } // start runs the program as the checker of a node. @@ -586,15 +598,15 @@ func (b *bed) start(t *testing.T, env map[string]string) { } // verdict reads the verdict key until pred holds of it, within 20 s. -func (b *bed) verdict(t *testing.T, key string, pred func(delivery.Verdict) bool) delivery.Verdict { +func (b *bed) verdict(t *testing.T, key string, pred func(meshdeliverychecker.VerdictsByGroupHeadsValue) bool) meshdeliverychecker.VerdictsByGroupHeadsValue { t.Helper() - var last delivery.Verdict + var last meshdeliverychecker.VerdictsByGroupHeadsValue for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { - e, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, key) + e, err := b.buckets[check.CheckerVerdictsBucket].Get(b.ctx, key) if err != nil { continue } - if last, err = delivery.DecodeVerdict(e.Value()); err == nil && pred(last) { + if last, err = meshdeliverycheckerclient.DecodeVerdict(e.Value()); err == nil && pred(last) { return last } } @@ -602,22 +614,24 @@ func (b *bed) verdict(t *testing.T, key string, pred func(delivery.Verdict) bool return last } -func ended(v delivery.Verdict) bool { return v.State != delivery.VerdictRunning } +func ended(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State != meshdeliverycheckerclient.VerdictRunning +} // owed reads the status the checker owes on a head. -func (b *bed) owed(t *testing.T, number int, head, state string) delivery.Owed { +func (b *bed) owed(t *testing.T, number int, head, state string) meshdeliverychecker.OwedByRepositoryNumberHeadActValue { t.Helper() - key, err := delivery.CheckerOwedKey(repo, number, head) + key, err := check.CheckerOwedKey(repo, number, head) if err != nil { t.Fatal(err) } - var last delivery.Owed + var last meshdeliverychecker.OwedByRepositoryNumberHeadActValue for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { - e, err := b.buckets[delivery.CheckerOwedBucket].Get(b.ctx, key) + e, err := b.buckets[check.CheckerOwedBucket].Get(b.ctx, key) if err != nil { continue } - if last, err = delivery.DecodeCheckerOwed(e.Value()); err == nil && last.State == state { + if last, err = check.DecodeCheckerOwed(e.Value()); err == nil && last.State == state { return last } } @@ -633,25 +647,25 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 // ADR 0603 §10: the candidate changes the module m, whose manifest the trunk holds; the builder has not built m's // candidate fingerprint yet, so the check waits on the builder's key before it composes. manifest := `{"module":"m"}` - digest := delivery.ManifestDigest([]byte(manifest)) - stored, err := delivery.EncodeManifest(delivery.Manifest{Module: "m", JSON: manifest}) + digest := meshcatalogclient.ManifestDigest([]byte(manifest)) + stored, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "m", Manifest: manifest}) if err != nil { t.Fatal(err) } - mkey, _ := delivery.ManifestsKey(digest) - if _, err := b.buckets[delivery.ManifestsBucket].Put(b.ctx, mkey, stored); err != nil { + mkey, _ := meshcatalogclient.ManifestsKey(digest) + if _, err := b.buckets[check.ManifestsBucket].Put(b.ctx, mkey, stored); err != nil { t.Fatal(err) } - known, err := delivery.EncodeModule(delivery.Module{Repository: repo, Commit: trunk, Path: "modules/m", Manifest: digest, None2: "the fingerprint is computed with its chain"}) + known, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repo, Commit: trunk, Path: "modules/m", Manifest: digest, None2: "the fingerprint is computed with its chain"}) if err != nil { t.Fatal(err) } - if _, err := b.buckets[delivery.ModulesBucket].Put(b.ctx, "m", known); err != nil { + if _, err := b.buckets[check.ModulesBucket].Put(b.ctx, "m", known); err != nil { t.Fatal(err) } fp := "src2:" + strings.Repeat("f", 64) p := pull(repo, 7, head1) - p.Modules, p.Fps = []string{"m"}, map[string]delivery.CandidateModule{"m": {FP2: fp, Manifest: digest}} + p.Modules, p.Fps = []string{"m"}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"m": {Fp2: fp, Manifest: digest}} key := b.candidate(t, "feat/x", p) // ADR 0593 §7, §9 (D12): n1 and n2 are placed; n1's engine answers compose-candidate on the bus, n2's never does. asked := b.engine(t, "n1", "n2") @@ -660,34 +674,36 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 if got := asked(); len(got) != 0 { t.Fatalf("the engines were asked %+v before m's build ended", got) } - bkey, _ := delivery.BuilderBuildKey("m", fp) - building, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildBuilding}) - if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, building); err != nil { + bkey, _ := builds.BuilderBuildKey("m", fp) + building, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildBuilding}) + if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, building); err != nil { t.Fatal(err) } - if v := b.verdict(t, key, func(delivery.Verdict) bool { return true }); v.State != delivery.VerdictRunning || len(asked()) != 0 { + if v := b.verdict(t, key, func(meshdeliverychecker.VerdictsByGroupHeadsValue) bool { return true }); v.State != meshdeliverycheckerclient.VerdictRunning || len(asked()) != 0 { t.Fatalf("verdict %s and %d asks while m builds: the check composes only once its build ended", v.State, len(asked())) } // m's build fails: the check fails naming it. Built again (builder_build_again writes the key done), the check asks // itself again for the same heads, with no recheck, and passes. - failedBuild, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildFailed, Error: "go build: undefined: x"}) - if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, failedBuild); err != nil { + failedBuild, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildFailed, Error: "go build: undefined: x"}) + if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, failedBuild); err != nil { t.Fatal(err) } - if v := b.verdict(t, key, ended); v.State != delivery.VerdictFail || v.Failed.Test != check.BuildFailedTest+"m" { + if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Test != check.BuildFailedTest+"m" { t.Fatalf("verdict %+v, want it failed on m's build", v) } - built, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildDone, Manifest: digest}) - if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, built); err != nil { + built, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildDone, Manifest: digest}) + if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, built); err != nil { t.Fatal(err) } b.said(t, "the builds of m it failed on are done again, so it is checked again") - v := b.verdict(t, key, func(v delivery.Verdict) bool { return v.State == delivery.VerdictPass }) + v := b.verdict(t, key, func(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State == meshdeliverycheckerclient.VerdictPass + }) // The check asked again takes the packages the failed check ran from their kept verdicts. - if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Kept != 2 || v.Levels[delivery.LevelIntegration].Kept != 2 || v.Trunk[repo] != trunk { + if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Kept != 2 || v.Levels[buildstep.LevelIntegration].Kept != 2 || v.Trunk[repo] != trunk { t.Fatalf("verdict = %+v", v) } - if c := v.Composition; c == nil || c.State != delivery.CompositionPass || !slices.Equal(c.Machines, []string{"n1"}) || + if c := v.Composition; c == nil || c.State != meshdeliverycheckerclient.CompositionPass || !slices.Equal(c.Machines, []string{"n1"}) || !strings.Contains(c.Words, "n2: "+nec.NoAnswer) || len(v.Machines) != 1 || v.Machines[0] != "n1" { t.Errorf("composition %+v machines %v", v.Composition, v.Machines) } @@ -704,7 +720,7 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 t.Errorf("the check ask named the nodes %v, want [n1] from the have's node record", names) } o := b.owed(t, 7, head1, "success") - if o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Text, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(o.Text, "unit 0 run, 2 kept") { + if o.Context != meshdeliveryclient.StatusContextCheck || !strings.HasPrefix(o.Text, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(o.Text, "unit 0 run, 2 kept") { t.Errorf("owed = %+v", o) } // Two checks of the same heads: the one failed on m's build and the one it asked itself again; the second runs no test. @@ -715,17 +731,17 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300 } // What passed is kept once under each level's digest. for _, k := range []string{"unit." + digU, "integration." + digI} { - if _, err := b.buckets[delivery.CheckerKeptBucket].Get(b.ctx, k); err != nil { + if _, err := b.buckets[check.CheckerKeptBucket].Get(b.ctx, k); err != nil { t.Errorf("kept.%s: %v", k, err) } } // The preview of the head merged onto the trunk, with the tree and the manifest digest the fetch named. - pk, _ := delivery.PreviewKey(repo, head1, trunk) - e, err := b.buckets[delivery.PreviewsBucket].Get(b.ctx, pk) + pk, _ := meshdeliverycheckerclient.PreviewKey(repo, head1, trunk) + e, err := b.buckets[check.PreviewsBucket].Get(b.ctx, pk) if err != nil { t.Fatalf("previews.%s: %v", pk, err) } - pv, err := delivery.DecodePreview(e.Value()) + pv, err := meshdeliverycheckerclient.DecodePreview(e.Value()) if err != nil || pv.Merge != merged || pv.Modules["modules/m"].Tree != "1111111111111111111111111111111111111111" { t.Errorf("preview = %+v, %v", pv, err) } @@ -744,7 +760,7 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128 tests, kept := b.fake.count("mesh-build-go.build_go_test"), keptCount(t, b) second := b.candidate(t, "feat/y", pull(repo, 8, head2)) v := b.verdict(t, second, ended) - if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 0 || v.Levels[delivery.LevelUnit].Kept != 2 || v.Levels[delivery.LevelIntegration].Run != 0 { + if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Run != 0 || v.Levels[buildstep.LevelUnit].Kept != 2 || v.Levels[buildstep.LevelIntegration].Run != 0 { t.Fatalf("verdict = %+v, want everything taken from the kept verdicts", v) } if got := b.fake.count("mesh-build-go.build_go_test"); got != tests { @@ -756,22 +772,22 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128 // §3: the gate's turn on the repository holds a new check off; its delete claims it. at := time.Now().UTC() - turn, err := delivery.EncodeTurn(delivery.Turn{Repository: repo, Waiting: []delivery.TurnWait{{Group: "fix/other", Heads: "0123456789ab", Approved: at, Since: at}}}) + turn, err := meshdeliveryclient.EncodeTurn(meshdelivery.TurnsByRepositoryValue{Repository: repo, Waiting: []meshdelivery.TurnsByRepositoryValueWaitingItem{{Group: "fix/other", Heads: "0123456789ab", Approved: at, Since: at}}}) if err != nil { t.Fatal(err) } - tk, _ := delivery.TurnsKey(repo) - turnRev, err := b.buckets[delivery.TurnsBucket].Put(b.ctx, tk, turn) + tk, _ := meshdeliveryclient.TurnsKey(repo) + turnRev, err := b.buckets[check.TurnsBucket].Put(b.ctx, tk, turn) if err != nil { t.Fatal(err) } b.said(t, fmt.Sprintf("merge turn %s changed (revision %d)", tk, turnRev)) // the checker heard the turn third := b.candidate(t, "feat/z", pull(repo, 9, "3333333333333333333333333333333333333333")) b.said(t, third+": not claimed: the approved merge of fix/other") // it held the check off - if _, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, third); !errors.Is(err, jetstream.ErrKeyNotFound) { + if _, err := b.buckets[check.CheckerVerdictsBucket].Get(b.ctx, third); !errors.Is(err, jetstream.ErrKeyNotFound) { t.Fatalf("verdicts.%s exists (%v) while a merge turn holds its repository", third, err) } - if err := b.buckets[delivery.TurnsBucket].Delete(b.ctx, tk); err != nil { + if err := b.buckets[check.TurnsBucket].Delete(b.ctx, tk); err != nil { t.Fatal(err) } b.verdict(t, third, ended) @@ -780,8 +796,8 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128 tests = b.fake.count("mesh-build-go.build_go_test") const moved = "abababababababababababababababababababab" b.setTrunk(t, map[string]string{repo: moved}) - c := b.verdict(t, first, func(v delivery.Verdict) bool { return ended(v) && v.Trunk[repo] == moved }) - if c.State != delivery.VerdictPass || c.CarriedFrom[repo] != trunk || c.Levels[delivery.LevelUnit].Run != 0 || c.Levels[delivery.LevelIntegration].Run != 0 { + c := b.verdict(t, first, func(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { return ended(v) && v.Trunk[repo] == moved }) + if c.State != meshdeliverycheckerclient.VerdictPass || c.CarriedFrom[repo] != trunk || c.Levels[buildstep.LevelUnit].Run != 0 || c.Levels[buildstep.LevelIntegration].Run != 0 { t.Fatalf("the carried verdict = %+v, want a pass carried from %s with no test run", c, trunk[:12]) } if got := b.fake.count("mesh-build-go.build_go_test"); got != tests { @@ -794,7 +810,7 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128 func keptCount(t *testing.T, b *bed) int { t.Helper() - keys, err := b.buckets[delivery.CheckerKeptBucket].Keys(b.ctx) + keys, err := b.buckets[check.CheckerKeptBucket].Keys(b.ctx) if err != nil { return 0 } @@ -809,7 +825,7 @@ func TestAHeadThatDoesNotMergeFailsNamingTheFilesAndRunsNoTest(t *testing.T) { / key := b.candidate(t, "feat/x", pull(repo, 7, head1)) b.start(t, nil) v := b.verdict(t, key, ended) - if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "files in conflict: a.go, sub/b.go") { + if v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, "files in conflict: a.go, sub/b.go") { t.Fatalf("verdict = %+v", v) } if o := b.owed(t, 7, head1, "failure"); !strings.HasPrefix(o.Text, "failed: n1: ") || !strings.Contains(o.Text, "files in conflict: a.go, sub/b.go") { @@ -818,10 +834,10 @@ func TestAHeadThatDoesNotMergeFailsNamingTheFilesAndRunsNoTest(t *testing.T) { / if n := b.fake.count("mesh-build-go.build_go_digest") + b.fake.count("mesh-build-go.build_go_test"); n != 0 { t.Errorf("%d digest and test steps ran after a conflict", n) } - pk, _ := delivery.PreviewKey(repo, head1, trunk) - if e, err := b.buckets[delivery.PreviewsBucket].Get(b.ctx, pk); err != nil { + pk, _ := meshdeliverycheckerclient.PreviewKey(repo, head1, trunk) + if e, err := b.buckets[check.PreviewsBucket].Get(b.ctx, pk); err != nil { t.Errorf("previews.%s: %v", pk, err) - } else if pv, err := delivery.DecodePreview(e.Value()); err != nil || strings.Join(pv.Conflicts, ",") != "a.go,sub/b.go" || pv.Merge != "" { + } else if pv, err := meshdeliverycheckerclient.DecodePreview(e.Value()); err != nil || strings.Join(pv.Conflicts, ",") != "a.go,sub/b.go" || pv.Merge != "" { t.Errorf("preview = %+v, %v, want the files in conflict alone", pv, err) } } @@ -834,13 +850,13 @@ func TestAnHqPullRequestGetsAVerdictAndAStatus(t *testing.T) { b.setTrunk(t, map[string]string{repo: trunk, "novox/hq": trunk}) hq := b.candidate(t, "feat/hq", pull("novox/hq", 3, head1)) b.start(t, nil) - if v := b.verdict(t, hq, ended); v.State != delivery.VerdictPass { + if v := b.verdict(t, hq, ended); v.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("hq's verdict = %+v", v) } - key, _ := delivery.CheckerOwedKey("novox/hq", 3, head1) + key, _ := check.CheckerOwedKey("novox/hq", 3, head1) for deadline := time.Now().Add(20 * time.Second); ; time.Sleep(50 * time.Millisecond) { - if e, err := b.buckets[delivery.CheckerOwedBucket].Get(b.ctx, key); err == nil { - if o, err := delivery.DecodeCheckerOwed(e.Value()); err == nil && o.State == "success" { + if e, err := b.buckets[check.CheckerOwedBucket].Get(b.ctx, key); err == nil { + if o, err := check.DecodeCheckerOwed(e.Value()); err == nil && o.State == "success" { return } } @@ -855,16 +871,16 @@ func TestAnHqPullRequestGetsAVerdictAndAStatus(t *testing.T) { func TestACheckLeftRunningIsTakenOverWhenTheCheckerStarts(t *testing.T) { // N6/AC2 b := startBed(t) key := b.candidate(t, "feat/x", pull(repo, 7, head1)) - group, heads, _ := delivery.ParseCheckerVerdictKey(key) - left, err := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, Trunk: map[string]string{repo: trunk}, State: delivery.VerdictRunning}) + group, heads, _ := meshdeliverycheckerclient.ParseVerdictKey(key) + left, err := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group, Heads: heads, Trunk: map[string]string{repo: trunk}, State: meshdeliverycheckerclient.VerdictRunning}) if err != nil { t.Fatal(err) } - if _, err := b.buckets[delivery.CheckerVerdictsBucket].Create(b.ctx, key, left); err != nil { + if _, err := b.buckets[check.CheckerVerdictsBucket].Create(b.ctx, key, left); err != nil { t.Fatal(err) } b.start(t, nil) - if v := b.verdict(t, key, ended); v.State != delivery.VerdictPass { + if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictPass { t.Errorf("verdict = %+v", v) } } @@ -877,7 +893,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing. b.stalePin.Store(true) key := b.candidate(t, "feat/x", pull(repo, 7, head1)) b.start(t, nil) - if v := b.verdict(t, key, ended); v.State != delivery.VerdictPass { + if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", v) } @@ -888,12 +904,12 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing. b.setTrunk(t, map[string]string{repo: trunk, sdkRepo: trunk}) // The member carries no preview of its merge yet, so its manifest is read at its head (the package new in the group). b.heads.Store(sdkRepo+" module.json "+head2, `{"module":"mesh-sdk","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]}]}}`) - asks, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectTagWanted(delivery.CheckerModule)}}) + asks, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectTagWanted(check.CheckerModule)}}) if err != nil { t.Fatal(err) } sdk := pull(sdkRepo, 12, head2) - sdk.Modules, sdk.Fps = []string{"mesh-sdk"}, map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("e", 64)}} + sdk.Modules, sdk.Fps = []string{"mesh-sdk"}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-sdk": {Fp2: "src2:" + strings.Repeat("e", 64)}} key = b.candidate(t, "feat/x", pull(repo, 8, head1), sdk) msg, err := asks.Next(jetstream.FetchMaxWait(20 * time.Second)) if err != nil { @@ -908,7 +924,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing. if want := (meshdeliverychecker.TagWantedHolderEvent{Repository: sdkRepo, Branch: "feat/x", Head: head2, Module: "mesh-sdk", Package: "go", Pull: 12}); wanted != want { t.Errorf("tag-wanted = %+v, want %+v", wanted, want) } - if v, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, key); err == nil && !strings.Contains(string(v.Value()), `"running"`) { + if v, err := b.buckets[check.CheckerVerdictsBucket].Get(b.ctx, key); err == nil && !strings.Contains(string(v.Value()), `"running"`) { t.Fatalf("the group was judged before its tagged version was published: %s", v.Value()) } published, _ := json.Marshal(gopackageregistry.PublishedEvent{Holder: "gitea", Kind: "package", Name: "mesh-sdk/go", Module: check.SDKModulePath, Commit: head2, Tag: "feat-x", Version: "0.1.15-feat-x.1", @@ -916,7 +932,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing. if _, err := b.js.Publish(b.ctx, gopackageregistry.SubjectPublished, published); err != nil { t.Fatal(err) } - if v := b.verdict(t, key, ended); v.State != delivery.VerdictPass { + if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("the group's verdict = %+v", v) } b.fake.mu.Lock() @@ -928,7 +944,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing. // DLV-C2, DLV-C6 (design 26 *Out of step*, ADR 0620): each check's end says one verdict per pull request on the stream // asks, at the definition's subject and in its body (decoded strictly): the lone pull request's, then the group's two. - verdicts, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule)}}) + verdicts, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectVerdict(check.CheckerModule)}}) if err != nil { t.Fatal(err) } @@ -949,8 +965,8 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing. } said[e.Pull] = e } - for pull, want := range map[int64]meshdeliverychecker.VerdictHolderEvent{7: {Repository: repo, Pull: 7, Head: head1, Verdict: delivery.VerdictPass}, - 8: {Repository: repo, Pull: 8, Head: head1, Verdict: delivery.VerdictPass}, 12: {Repository: sdkRepo, Pull: 12, Head: head2, Verdict: delivery.VerdictPass}} { + for pull, want := range map[int64]meshdeliverychecker.VerdictHolderEvent{7: {Repository: repo, Pull: 7, Head: head1, Verdict: meshdeliverycheckerclient.VerdictPass}, + 8: {Repository: repo, Pull: 8, Head: head1, Verdict: meshdeliverycheckerclient.VerdictPass}, 12: {Repository: sdkRepo, Pull: 12, Head: head2, Verdict: meshdeliverycheckerclient.VerdictPass}} { if got := said[pull]; got.Repository != want.Repository || got.Head != want.Head || got.Verdict != want.Verdict || len(got.OutOfStep) != 0 { t.Errorf("verdict of pull %d = %+v, want %+v", pull, got, want) } @@ -1009,7 +1025,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) { if b.fake.count("mesh-build-go.build_go_fetch") == 0 { t.Errorf("the try fetched nothing") } - for _, name := range []string{delivery.CheckerOwedBucket, delivery.CheckerVerdictsBucket, delivery.PreviewsBucket} { + for _, name := range []string{check.CheckerOwedBucket, check.CheckerVerdictsBucket, check.PreviewsBucket} { if keys, _ := b.buckets[name].Keys(b.ctx); len(keys) != 0 { t.Errorf("a try wrote %s: %v", name, keys) } @@ -1021,7 +1037,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) { if err != nil { t.Fatal(err) } - steps, err := b.js.KeyValue(b.ctx, delivery.StepsBucket("mesh-build-go")) + steps, err := b.js.KeyValue(b.ctx, buildrun.StepsBucket("mesh-build-go")) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go index c0a1eac6..4249589e 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge.go @@ -13,16 +13,19 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "mesh-delivery-checker/internal/check" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "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/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "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" "git.novox.be/novox/mesh-delivery/sdk/graph" - - "mesh-delivery-checker/internal/check" ) // judgeDeps are the parts the judge of a check is made of. @@ -57,22 +60,22 @@ type judgeDeps struct { logf func(format string, args ...any) } -// keptPredefined reads the indexer's key predefined.list (delivery.PredefinedKept), bound once its state appears. +// keptPredefined reads the indexer's key predefined.list (the seat mesh-catalog's state predefined), bound once its state appears. type keptPredefined struct{ kv jetstream.KeyValue } func (k keptPredefined) PredefinedList(ctx context.Context) (json.RawMessage, error) { - e, err := k.kv.Get(ctx, delivery.PredefinedListKey) + e, err := k.kv.Get(ctx, meshcatalog.PredefinedByListKey()) if errors.Is(err, jetstream.ErrKeyNotFound) { return nil, nil } if err != nil { return nil, err } - var kept delivery.PredefinedKept - if err := json.Unmarshal(e.Value(), &kept); err != nil { + kept, err := meshcatalog.DecodePredefinedByListValue(e.Value()) + if err != nil { return nil, err } - return kept.List, nil + return json.Marshal(kept.List) } // keptSeatDefinitions reads a pull request's entries of the indexer's seat definitions from its first view: each whose @@ -103,7 +106,7 @@ func seatDefinitionsOf(kv jetstream.KeyValue) keptSeatDefinitions { } func (k keptSeatDefinitions) Unparsed(ctx context.Context, repository string, pull int) ([]check.UnparsedSeat, error) { - suffix := ".pull." + delivery.EscapeName(repository) + "." + strconv.Itoa(pull) + suffix := ".pull." + keys.EscapeName(repository) + "." + strconv.Itoa(pull) keys, err := k.keys(ctx) if err != nil { return nil, err @@ -143,14 +146,14 @@ type nodeNames struct{ kv jetstream.KeyValue } // openNodeNames opens the node-engine's have for the nodes' names alone: it is not watched, so no check waits on a node. func openNodeNames(ctx context.Context, js process.BucketOpener, logf func(string, ...any)) *nodeNames { - return &nodeNames{kv: process.OptionalBucket(ctx, js, delivery.HaveBucket, "no check is asked until it is: the names check needs the nodes' names", logf)} + return &nodeNames{kv: process.OptionalBucket(ctx, js, check.HaveBucket, "no check is asked until it is: the names check needs the nodes' names", logf)} } func (n nodeNames) Names(ctx context.Context) ([]string, error) { if n.kv == nil { - return nil, fmt.Errorf("the bucket %s could not be opened at the checker's start", delivery.HaveBucket) + return nil, fmt.Errorf("the bucket %s could not be opened at the checker's start", check.HaveBucket) } - lister, err := n.kv.ListKeysFiltered(ctx, "*."+delivery.HaveNodeWord) + lister, err := n.kv.ListKeysFiltered(ctx, "*."+check.HaveNodeWord) if err != nil { return nil, err } @@ -264,7 +267,7 @@ func (r *repoModules) Manifest(ctx context.Context, digest string) (catalogue.Ma if known { return m, nil } - key, err := delivery.ManifestsKey(digest) + key, err := meshcatalogclient.ManifestsKey(digest) if err != nil { return catalogue.Manifest{}, err } @@ -275,11 +278,11 @@ func (r *repoModules) Manifest(ctx context.Context, digest string) (catalogue.Ma if !found { return catalogue.Manifest{}, fmt.Errorf("the indexer keeps no manifest %s", digest) } - kept, err := delivery.DecodeManifest(raw) + kept, err := meshcatalogclient.DecodeManifest(raw) if err != nil { return catalogue.Manifest{}, err } - if m, err = parseManifest(kept.JSON); err != nil { + if m, err = parseManifest(kept.Manifest); err != nil { return catalogue.Manifest{}, err } r.mu.Lock() @@ -311,19 +314,19 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted return } r.mu.Unlock() - m, err := delivery.DecodeModule(value) + m, err := meshcatalogclient.DecodeModule(value) if err != nil { return } if r.verdicts != nil { - r.verdicts.put(key, m.VerdictOf(key)) + r.verdicts.put(key, meshcatalogclient.ModuleVerdict(key, m)) } r.mu.Lock() r.repoOf[key] = m.Repository r.typed[key] = m.Manifest r.commit[key] = m.Commit r.path[key] = m.Path - r.named[key] = m.ArtifactPaths + r.named[key] = m.Paths _, known := r.langs[m.Manifest] r.mu.Unlock() if m.Manifest == "" || known { @@ -331,10 +334,10 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted } set := map[string]bool{} var pkgs []string - if mk, err := delivery.ManifestsKey(m.Manifest); err == nil { + if mk, err := meshcatalogclient.ManifestsKey(m.Manifest); err == nil { if raw, _, found, err := r.manifests.Get(ctx, mk); err == nil && found { - if kept, err := delivery.DecodeManifest(raw); err == nil { - if man, err := parseManifest(kept.JSON); err == nil && man.Build != nil { + if kept, err := meshcatalogclient.DecodeManifest(raw); err == nil { + if man, err := parseManifest(kept.Manifest); err == nil && man.Build != nil { for _, a := range man.Build.Artifacts { if a.Language != "" { set[a.Language] = true @@ -365,7 +368,7 @@ func (r *repoModules) ModuleDir(module string) (string, bool) { } // PackagePaths are the paths, relative to the repository, that the TypeScript package artifacts of a repository's modules -// are built from, sorted: the named paths the indexer keeps in each module's value (delivery.Module.ArtifactPaths; only the +// are built from, sorted: the named paths the indexer keeps in each module's value (meshcatalog.ModulesByModuleValue.ArtifactPaths; only the // indexer reads them from a manifest, the lead's decision L8), under the module's directory. None for a module whose value // holds no paths. func (r *repoModules) PackagePaths(repository string) []string { @@ -380,7 +383,7 @@ func (r *repoModules) PackagePaths(repository string) []string { for _, name := range r.tsPkgs[r.typed[module]] { for _, p := range r.named[module][name] { p = strings.Trim(strings.TrimPrefix(p, "./"), "/") - if dir != "" && dir != delivery.RootPath { + if dir != "" && dir != meshcatalogclient.RootPath { p = dir + "/" + p } out = append(out, p) @@ -420,16 +423,16 @@ func (r *repoModules) BuildModules(repository string) []string { // buildReader reads the builder's build of a module at a fingerprint (mesh-delivery-builder_builds). type buildReader struct{ kv kvStore } -func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) { - key, err := delivery.BuilderBuildKey(module, fp2) +func (r buildReader) Build(ctx context.Context, module, fp2 string) (builds.BuilderBuild, bool, error) { + key, err := builds.BuilderBuildKey(module, fp2) if err != nil { - return delivery.BuilderBuild{}, false, err + return builds.BuilderBuild{}, false, err } value, _, found, err := r.kv.Get(ctx, key) if err != nil || !found { - return delivery.BuilderBuild{}, false, err + return builds.BuilderBuild{}, false, err } - b, err := delivery.DecodeBuilderBuild(value) + b, err := builds.DecodeBuilderBuild(value) return b, err == nil, err } @@ -438,30 +441,30 @@ func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.Bu // the fact and polls nothing (novox/hq ADR 0603 §10). type buildEnds struct{ kv jetstream.KeyValue } -func (e buildEnds) Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) { - return e.until(ctx, builds, func(b delivery.BuilderBuild) bool { - return b.State == delivery.BuildDone || b.State == delivery.BuildFailed +func (e buildEnds) Ended(ctx context.Context, asked map[string]string) (map[string]builds.BuilderBuild, error) { + return e.until(ctx, asked, func(b builds.BuilderBuild) bool { + return b.State == builds.BuildDone || b.State == builds.BuildFailed }) } -func (e buildEnds) Done(ctx context.Context, builds map[string]string) bool { - done, err := e.until(ctx, builds, func(b delivery.BuilderBuild) bool { return b.State == delivery.BuildDone }) - return err == nil && len(done) == len(builds) +func (e buildEnds) Done(ctx context.Context, asked map[string]string) bool { + done, err := e.until(ctx, asked, func(b builds.BuilderBuild) bool { return b.State == builds.BuildDone }) + return err == nil && len(done) == len(asked) } // until hears the builds' keys until each holds a build that ended holds, or ctx ends, and answers those that do, by module. -func (e buildEnds) until(ctx context.Context, builds map[string]string, ended func(delivery.BuilderBuild) bool) (map[string]delivery.BuilderBuild, error) { +func (e buildEnds) until(ctx context.Context, asked map[string]string, ended func(builds.BuilderBuild) bool) (map[string]builds.BuilderBuild, error) { byKey := map[string]string{} - keys := make([]string, 0, len(builds)) - for module, fp2 := range builds { - key, err := delivery.BuilderBuildKey(module, fp2) + keys := make([]string, 0, len(asked)) + for module, fp2 := range asked { + key, err := builds.BuilderBuildKey(module, fp2) if err != nil { return nil, err } byKey[key] = module keys = append(keys, key) } - out := map[string]delivery.BuilderBuild{} + out := map[string]builds.BuilderBuild{} if len(keys) == 0 { return out, nil } @@ -485,7 +488,7 @@ func (e buildEnds) until(ctx context.Context, builds map[string]string, ended fu if !ours { continue } - b, err := delivery.DecodeBuilderBuild(entry.Value()) + b, err := builds.DecodeBuilderBuild(entry.Value()) if err == nil && ended(b) { out[module] = b } else { @@ -497,7 +500,7 @@ func (e buildEnds) until(ctx context.Context, builds map[string]string, ended fu } // ManifestDigests is the manifest digest of every module the indexer holds, by module: the catalogue the indexer's judge -// judges a change's manifests in (novox/hq ADR 0578 §2). +// judges a change's manifests in (novox/hq ADR 0578 §2).8 §2). func (r *repoModules) ManifestDigests() map[string]string { r.mu.Lock() defer r.mu.Unlock() diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go index 5b9d04cd..dd5a7abd 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/judge_test.go @@ -12,8 +12,11 @@ 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/buildrun" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" + "git.novox.be/novox/mesh-sdk/go/keys" "mesh-delivery-checker/internal/check" ) @@ -27,7 +30,7 @@ func (m memManifests) Get(_ context.Context, key string) ([]byte, uint64, bool, func putModule(t *testing.T, r *repoModules, name, repository, manifest string) { t.Helper() - data, err := delivery.EncodeModule(delivery.Module{Repository: repository, Commit: strings.Repeat("a", 40), FP: "src1:" + strings.Repeat("a", 64), Manifest: delivery.ManifestDigest([]byte(manifest))}) + data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repository, Commit: strings.Repeat("a", 40), Fp: "src1:" + strings.Repeat("a", 64), Manifest: meshcatalogclient.ManifestDigest([]byte(manifest))}) if err != nil { t.Fatal(err) } @@ -58,11 +61,11 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test tsManifest := string(tsRaw) store := memManifests{} for _, m := range []string{goManifest, tsManifest} { - data, err := delivery.EncodeManifest(delivery.Manifest{Module: "demo", JSON: m}) + data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "demo", Manifest: m}) if err != nil { t.Fatal(err) } - store[delivery.ManifestDigest([]byte(m))] = data + store[meshcatalogclient.ManifestDigest([]byte(m))] = data } r := &repoModules{manifests: store} putModule(t, r, "mesh-delivery-checker", "novox/mesh-delivery", goManifest) @@ -89,15 +92,15 @@ const sdkManifest = `{"module":"mesh-sdk","version":"1","slug":"sdk","build":{"a func TestTheTypeScriptPackagesPathsComeFromTheIndexersModulesNotTheManifest(t *testing.T) { // ADR 0500 §5 with lead decision L8 store := memManifests{} - data, err := delivery.EncodeManifest(delivery.Manifest{Module: "mesh-sdk", JSON: sdkManifest}) + data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mesh-sdk", Manifest: sdkManifest}) if err != nil { t.Fatal(err) } - store[delivery.ManifestDigest([]byte(sdkManifest))] = data + store[meshcatalogclient.ManifestDigest([]byte(sdkManifest))] = data r := &repoModules{manifests: store} put := func(path string, paths map[string][]string) { - v, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), FP: "src1:" + strings.Repeat("a", 64), - Manifest: delivery.ManifestDigest([]byte(sdkManifest)), Path: path, ArtifactPaths: paths}) + v, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), Fp: "src1:" + strings.Repeat("a", 64), + Manifest: meshcatalogclient.ManifestDigest([]byte(sdkManifest)), Path: path, Paths: paths}) if err != nil { t.Fatal(err) } @@ -133,10 +136,11 @@ func TestThePublishedVersionsAreTheIndexersPackagesState(t *testing.T) { t.Errorf("read and empty, the known packages = %#v, want an empty map", got) } put := func(module, repository, dir, commit string) { - key, _ := delivery.PackagesKey(module) - raw, err := delivery.EncodePublishedPackages(delivery.PublishedPackages{Module: module, Repository: repository, Dir: dir, + key := meshcatalog.PackagesByModuleKey(keys.EscapeName(module)) + now := time.Now().UTC() + raw, err := json.Marshal(meshcatalog.PackagesByModuleValue{V: keys.Version, Module: module, Repository: repository, Dir: dir, Commit: commit, Requires: strings.Repeat("e", 64), - Packages: []delivery.PackageDigest{{Package: module, Digest: strings.Repeat("d", 64)}}, At: time.Now().UTC()}) + Packages: []meshcatalog.PackagesByModuleValuePackagesItem{{Package: module, Digest: strings.Repeat("d", 64)}}, At: &now}) if err != nil { t.Fatal(err) } @@ -146,30 +150,31 @@ func TestThePublishedVersionsAreTheIndexersPackagesState(t *testing.T) { put(sdk, "novox/mesh-sdk", "go", strings.Repeat("a", 40)) put(build, "novox/mesh-catalog", "modules/mesh-build-go", strings.Repeat("b", 40)) p.Put("broken", []byte("{"), false) - if got := p.LatestPackages(); !reflect.DeepEqual(got, delivery.PackageVersions{sdk: strings.Repeat("a", 40), build: strings.Repeat("b", 40)}) { + if got := p.LatestPackages(); !reflect.DeepEqual(got, buildstep.PackageVersions{sdk: strings.Repeat("a", 40), build: strings.Repeat("b", 40)}) { t.Errorf("latest packages = %v", got) } - key, _ := delivery.PackagesKey(build) + key := meshcatalog.PackagesByModuleKey(keys.EscapeName(build)) p.Put(key, nil, true) if got := p.LatestPackages(); len(got) != 1 || got[sdk] == "" { t.Errorf("after a delete: %v", got) } // A series key names its package by its ecosystem name, the judge's known packages (DLV-I14, design 32), and is no // module's published version. - series, err := delivery.EncodeSeriesPackage(delivery.SeriesPackage{Name: sdk, Package: "mesh-sdk/go", Series: "0.1", At: time.Now().UTC()}) + now := time.Now().UTC() + series, err := json.Marshal(meshcatalog.PackagesByPackageSeriesValue{V: keys.Version, Name: sdk, Package: "mesh-sdk/go", Series: "0.1", At: &now}) if err != nil { t.Fatal(err) } - p.Put(delivery.SeriesPackageKey(sdk, "0.1"), series, false) + p.Put(meshcatalog.PackagesByPackageSeriesKey(keys.EscapeName(sdk), "0.1"), series, false) if got := p.PackageNames(); !reflect.DeepEqual(got, map[string]string{sdk: "mesh-sdk/go"}) || len(p.LatestPackages()) != 1 { t.Errorf("the known packages = %v, the latest %v", got, p.LatestPackages()) } t.Run("a pull request's unparsed seat definitions are its entries of the indexer's seat-definitions", func(t *testing.T) { state := map[string]string{ "web-api.trunk": `{"seat":"web-api","source":"trunk","digest":"d"}`, - "web-api.pull." + delivery.EscapeName("novox/web") + ".7": `{"seat":"web-api","source":"pull","module":"web-contracts","directory":"go","unparsed":"seat.json: bad"}`, - "mail-api.pull." + delivery.EscapeName("novox/web") + ".7": `{"seat":"mail-api","source":"pull","digest":"e"}`, - "web-api.pull." + delivery.EscapeName("novox/web") + ".17": `{"seat":"web-api","source":"pull","unparsed":"another pull request's"}`, + "web-api.pull." + keys.EscapeName("novox/web") + ".7": `{"seat":"web-api","source":"pull","module":"web-contracts","directory":"go","unparsed":"seat.json: bad"}`, + "mail-api.pull." + keys.EscapeName("novox/web") + ".7": `{"seat":"mail-api","source":"pull","digest":"e"}`, + "web-api.pull." + keys.EscapeName("novox/web") + ".17": `{"seat":"web-api","source":"pull","unparsed":"another pull request's"}`, } k := keptSeatDefinitions{keys: func(context.Context) ([]string, error) { return slices.Collect(maps.Keys(state)), nil }, get: func(_ context.Context, key string) ([]byte, error) { return []byte(state[key]), nil }} @@ -189,11 +194,11 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) { t.Fatal(err) } manifest := string(raw) - data, err := delivery.EncodeManifest(delivery.Manifest{Module: "xterm", JSON: manifest}) + data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "xterm", Manifest: manifest}) if err != nil { t.Fatal(err) } - store := memManifests{delivery.ManifestDigest([]byte(manifest)): data} + store := memManifests{meshcatalogclient.ManifestDigest([]byte(manifest)): data} var said []string r := &repoModules{manifests: store, logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }} putModule(t, r, "xterm", "novox/mesh-catalog", manifest) @@ -208,8 +213,8 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) { } // The sandbox's composer may be an older node-engine, which knows no verdict waits: it is handed a wait as a refusal. var held manifestVerdicts - held.put("nats", &delivery.ManifestVerdict{Module: "nats", State: delivery.VerdictManifestWaits, Unknown: []string{"core-credentials"}, Judge: "j"}) - if v := held.all()["nats"]; v.State != delivery.VerdictRefused || len(v.Problems) != 1 || len(v.Unknown) != 0 { + held.put("nats", &meshcatalog.VerdictsByDigestValue{Module: "nats", State: meshcatalogclient.VerdictManifestWaits, Unknown: []string{"core-credentials"}, Judge: "j"}) + if v := held.all()["nats"]; v.State != meshcatalogclient.VerdictRefused || len(v.Problems) != 1 || len(v.Unknown) != 0 { t.Errorf("a waiting verdict is handed to the sandbox as %+v", v) } } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/lazyturns.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/lazyturns.go index 4d84c22b..b39611bc 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/lazyturns.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/lazyturns.go @@ -3,9 +3,9 @@ package main import ( "sync" - "git.novox.be/novox/mesh-sdk/go/delivery" - "mesh-delivery-checker/internal/check" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" ) // lazyTurns is the gate's merge turns as the checker reads them (novox/hq ADR 0548 §3), opened when the gate's bucket is @@ -33,18 +33,18 @@ func (l *lazyTurns) ensure() { if !l.said { l.said = true l.logf("NO MERGE TURN HOLDS A CHECK: the gate's bucket %s cannot be read (%v); every check is claimed as if no approved merge waited, "+ - "and the bucket is opened again before each claim until it is there", delivery.TurnsBucket, err) + "and the bucket is opened again before each claim until it is there", check.TurnsBucket, err) } return } l.opened = true if l.said { - l.logf("the gate's merge turns are read from %s from now on: an approved merge's turn holds checks off its repositories again", delivery.TurnsBucket) + l.logf("the gate's merge turns are read from %s from now on: an approved merge's turn holds checks off its repositories again", check.TurnsBucket) } } // Holding opens the bucket when it is not yet, then answers what the turns hold. -func (l *lazyTurns) Holding(repositories []string, heads string) (string, delivery.TurnWait, bool) { +func (l *lazyTurns) Holding(repositories []string, heads string) (string, meshdelivery.TurnsByRepositoryValueWaitingItem, bool) { l.ensure() return l.turns.Holding(repositories, heads) } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go index 55a908d7..d7b49ed3 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/main.go @@ -10,7 +10,6 @@ import ( "encoding/json" "errors" "fmt" - sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings" "io" "log" "os" @@ -19,23 +18,29 @@ import ( "syscall" "time" + sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings" + "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "mesh-delivery-checker/internal/check" + + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner" + "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-builder/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/announce" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/seatlist" "git.novox.be/novox/mesh-sdk/go/views" - - "mesh-delivery-checker/internal/check" ) // eventSubject is the subject of an event of the checker's stream checks (the bus names the stream CHECKS): the indexer's // watch of the previews bucket and the forge bridge's watch of the owed bucket are woken by them. func eventSubject(event string) string { - return process.ModuleStreamSubject(delivery.CheckerModule, event) + return process.ModuleStreamSubject(check.CheckerModule, event) } const ( @@ -66,11 +71,11 @@ func durationEnv(getenv func(string) string, name string, def time.Duration) (ti } // The checker's own states of its first views (views.StatusState) and of its findings, where the condition keeper raises a -// first view unanswered past its bound (ADR 0615 §6); and the indexer's states it follows (delivery.CandidatesBucket, +// first view unanswered past its bound (ADR 0615 §6); and the indexer's states it follows (check.CandidatesBucket, // ModulesBucket, ManifestsBucket). var ( - firstViewsBucket = delivery.CheckerModule + "_" + views.StatusState - findingsBucket = delivery.CheckerModule + "_findings" + firstViewsBucket = check.CheckerModule + "_" + views.StatusState + findingsBucket = check.CheckerModule + "_findings" ) const ( @@ -120,7 +125,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro defer conn.Close() // Its own states: a check without one of them would be unwritten, so each stops the start, naming its grant (issue 1945). buckets := map[string]jetstream.KeyValue{} - for _, name := range []string{delivery.CheckerVerdictsBucket, delivery.CheckerKeptBucket, delivery.CheckerOwedBucket, delivery.PreviewsBucket, + for _, name := range []string{check.CheckerVerdictsBucket, check.CheckerKeptBucket, check.CheckerOwedBucket, check.PreviewsBucket, check.TriesBucket} { if buckets[name], err = process.EssentialBucket(ctx, js, "checker", name, "judge or keep a check"); err != nil { return err @@ -146,7 +151,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The module has one durable consumer of EVENTS: one hub pulls it for the views reader and for the registry seats' // published, started once both are registered. hub := process.NewEventHub(logger.Printf) - reader := &views.ProcessReader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub} + reader := &views.ProcessReader{Module: check.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub} seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)} findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf) reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) } @@ -157,14 +162,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro followed = errors.Join(followed, err) return p } - candidates := followOwner(delivery.IndexerModule, candidatesState, "indexer_read_candidates", indexerViews) - modules := followOwner(delivery.IndexerModule, modulesState, "indexer_read_modules", indexerViews) - manifests := followOwner(delivery.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews) - packages := followOwner(delivery.IndexerModule, delivery.PackagesState, "indexer_read_packages", indexerViews) - predefined := followOwner(delivery.IndexerModule, delivery.IndexerPredefinedState, "indexer_read_predefined", indexerViews) - placement := followOwner(delivery.AssignerModule, delivery.PlacementState, "assigner_read_placement", assignerViews) + candidates := followOwner(check.IndexerModule, candidatesState, "indexer_read_candidates", indexerViews) + modules := followOwner(check.IndexerModule, modulesState, "indexer_read_modules", indexerViews) + manifests := followOwner(check.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews) + packages := followOwner(check.IndexerModule, meshcatalog.StatePackages, "indexer_read_packages", indexerViews) + predefined := followOwner(check.IndexerModule, check.IndexerPredefinedState, "indexer_read_predefined", indexerViews) + placement := followOwner(buildrun.AssignerModule, meshmoduleassigner.StatePlacement, "assigner_read_placement", assignerViews) // The seventh: the indexer's seat definitions, so a pull request whose head's definition does not parse fails. - seatDefs := followOwner(delivery.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews) + seatDefs := followOwner(check.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews) if followed != nil { return followed } @@ -200,7 +205,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The state may not be on the bus yet when a delivery started the checker (the grants part creates it, issue 1790): it // is bound when the bus says it was created, and the latest hearing is written then. hearingKV := process.AwaitBucket(ctx, js, conn, check.HearingBucket, "checker_status cannot say whether DEAD_LETTERS is heard", logger.Printf) - hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, []string{delivery.AskGo, delivery.AskNode}, dead, + hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, []string{buildstep.AskGo, buildstep.AskNode}, dead, logger.Printf, buildrun.SayHearing(ctx, hearingKV, logger.Printf)) defer hearing.Stop() // Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read @@ -211,8 +216,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro asksKV := process.AwaitBucket(ctx, js, conn, check.AsksBucket, "checker_ask_again refuses every check, as it cannot tell whether its ask waits in a stream", logger.Printf) ch := &check.Checker{ Asks: awaitedKV{asksKV}, - Verdicts: kvStore{buckets[delivery.CheckerVerdictsBucket]}, - Owed: kvStore{buckets[delivery.CheckerOwedBucket]}, + Verdicts: kvStore{buckets[check.CheckerVerdictsBucket]}, + Owed: kvStore{buckets[check.CheckerOwedBucket]}, Trunks: trunks, Turns: lazy, Bound: checkBound, @@ -222,10 +227,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } // The builder's builds say how the candidates' fingerprints stand (issue 1684); where its bucket is not there yet the line // says nothing of the builds, until the checker starts again. - var builds check.BuildStates + var buildStates check.BuildStates var ends check.BuildEnds - if kv := process.OptionalBucket(ctx, js, delivery.BuilderBuildsBucket, "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil { - builds, ends = buildReader{kvStore{kv}}, buildEnds{kv} + if kv := process.OptionalBucket(ctx, js, builds.BuilderBuildsBucket, "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil { + buildStates, ends = buildReader{kvStore{kv}}, buildEnds{kv} } if ends != nil { ch.Rebuilds = ends // a check failed on a build is asked again once that build is done again (ADR 0603 §10) @@ -276,8 +281,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } ch.Judge = newJudge(judgeDeps{ asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}, tools: tools, trunks: trunks, - kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]}, - repos: repos, published: published, builds: builds, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf, + kept: kvStore{buckets[check.CheckerKeptBucket]}, previews: kvStore{buckets[check.PreviewsBucket]}, + repos: repos, published: published, builds: buildStates, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf, announce: announce, compositions: compositions, tags: tags, nodes: openNodeNames(ctx, js, logger.Printf), predefined: keptPredefined{predefined}, @@ -333,9 +338,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // the assigner's own presence says whether a composition is being made (compositionWatch, ADR 0537 §4). A check is no // longer held for a node: its asks wait in the build seats' streams, which redeliver a lost node's ask (ADR 0525 §6, // issue 1912), so the node-engine's have is not watched here. - if kv := process.OptionalBucket(ctx, js, delivery.PresenceBucket, "no node counts as online until the checker starts again", logger.Printf); kv != nil { + if kv := process.OptionalBucket(ctx, js, check.PresenceBucket, "no node counts as online until the checker starts again", logger.Printf); kv != nil { if pw, err := settle(ctx, kv, func(e jetstream.KeyValueEntry) { presence.Put(e.Key(), e.Value(), !opOf(e)) }); err != nil { - process.NotWatched(logger.Printf, delivery.PresenceBucket, err, "no node counts as online until the checker starts again") + process.NotWatched(logger.Printf, check.PresenceBucket, err, "no node counts as online until the checker starts again") } else { go func() { errs <- follow(ctx, pw, func(e jetstream.KeyValueEntry) { @@ -348,18 +353,18 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro } // The gate's source says the trunk head of each repository, which a pull request is merged onto. Where it is not there // yet no trunk is known, so nothing is checked. - if kv := process.OptionalBucket(ctx, js, delivery.SourceBucket, "no trunk is known, so nothing is checked until the checker starts again", logger.Printf); kv != nil { + if kv := process.OptionalBucket(ctx, js, check.SourceBucket, "no trunk is known, so nothing is checked until the checker starts again", logger.Printf); kv != nil { sw, err := settle(ctx, kv, func(e jetstream.KeyValueEntry) { - if e.Key() == delivery.SourceMainKey { + if e.Key() == meshdeliveryclient.SourceMainKey { trunks.Put(e.Value(), !opOf(e)) } }) if err != nil { - process.NotWatched(logger.Printf, delivery.SourceBucket, err, "no trunk is known, so nothing is checked until the checker starts again") + process.NotWatched(logger.Printf, check.SourceBucket, err, "no trunk is known, so nothing is checked until the checker starts again") } else { go func() { errs <- follow(ctx, sw, func(e jetstream.KeyValueEntry) { - if e.Key() == delivery.SourceMainKey && trunks.Put(e.Value(), !opOf(e)) { + if e.Key() == meshdeliveryclient.SourceMainKey && trunks.Put(e.Value(), !opOf(e)) { ch.TrunkMoved(ctx) // the trunk moved: its statuses go pending, and a verdict for the old trunk is judged again } }) @@ -371,7 +376,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // Where the gate's bucket is not there yet it is read as empty, and opened again before each claim until it is there // (lazyTurns), so a gate delivered after the checker started holds checks from its first turn on. lazy.open = func() error { - kv, err := js.KeyValue(ctx, delivery.TurnsBucket) + kv, err := js.KeyValue(ctx, check.TurnsBucket) if err != nil { return err } @@ -444,7 +449,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro }() // The checker's own verdicts, updates only, in one watch: one checker_ask_again wrote asks its check again // (Checker.VerdictWritten); a deleted key of heads a candidate holds is judged again (Checker.VerdictDeleted, issue 1733). - vw, err := buckets[delivery.CheckerVerdictsBucket].WatchAll(ctx, jetstream.UpdatesOnly()) + vw, err := buckets[check.CheckerVerdictsBucket].WatchAll(ctx, jetstream.UpdatesOnly()) if err != nil { return fmt.Errorf("the checker's verdicts cannot be watched, so no check can be asked again by hand and a deleted verdict would never be written again: %w", err) } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/nodes_test.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/nodes_test.go index 550d4025..aa903e5b 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/nodes_test.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/nodes_test.go @@ -14,7 +14,7 @@ func TestTheCheckerWatchesNoHave(t *testing.T) { if err != nil { t.Fatal(err) } - for _, s := range []string{"delivery.HaveBucket", "buildrun.HaveSet"} { + for _, s := range []string{"check.HaveBucket", "buildrun.HaveSet"} { if strings.Contains(string(raw), s) { t.Errorf("the checker program still reads %s", s) } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go index d56cb78c..19b4118c 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/packages.go @@ -1,9 +1,11 @@ package main import ( + "strings" "sync" - "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/buildstep" ) // publishedPackages is the indexer's state `packages` as the checker follows it (novox/hq ADR 0593 §2): by mesh Go @@ -11,13 +13,13 @@ import ( // package's version; the checker reads it, so a Go check builds every mesh package at the version the mesh publishes // (check.Examiner.Published) and computes none. // -// Its series keys (delivery.SeriesPackage, decoded with delivery.DecodeSeriesPackage) name each package by its name in its +// Its series keys (meshcatalog.PackagesByPackageSeriesValue, decoded with meshcatalog.DecodePackagesByPackageSeriesValue) name each package by its name in its // own ecosystem and the `/` it is built from (design 32): the known packages the judge takes // (PackageNames). type publishedPackages struct { mu sync.Mutex - byKey map[string]delivery.PublishedPackages - series map[string]delivery.SeriesPackage + byKey map[string]meshcatalog.PackagesByModuleValue + series map[string]meshcatalog.PackagesByPackageSeriesValue unread map[string]string // answered is whether the state's first reading ended (Answered): until then the checker knows no package's name. answered bool @@ -36,7 +38,7 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) { p.mu.Lock() defer p.mu.Unlock() if p.byKey == nil { - p.byKey, p.series, p.unread = map[string]delivery.PublishedPackages{}, map[string]delivery.SeriesPackage{}, map[string]string{} + p.byKey, p.series, p.unread = map[string]meshcatalog.PackagesByModuleValue{}, map[string]meshcatalog.PackagesByPackageSeriesValue{}, map[string]string{} } delete(p.byKey, key) delete(p.series, key) @@ -44,8 +46,8 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) { if deleted { return } - if delivery.IsSeriesPackageKey(key) { - s, err := delivery.DecodeSeriesPackage(raw) + if isSeriesPackageKey(key) { + s, err := meshcatalog.DecodePackagesByPackageSeriesValue(raw) if err != nil { p.unread[key] = err.Error() return @@ -53,7 +55,7 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) { p.series[key] = s return } - v, err := delivery.DecodePublishedPackages(raw) + v, err := meshcatalog.DecodePackagesByModuleValue(raw) if err != nil { p.unread[key] = err.Error() return @@ -64,10 +66,10 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) { // LatestPackages is the latest published version of every mesh Go module the indexer holds, by module path: the commit // its repository was published at. A module the state does not hold is left out: a check builds it at the version // go.mod names. -func (p *publishedPackages) LatestPackages() delivery.PackageVersions { +func (p *publishedPackages) LatestPackages() buildstep.PackageVersions { p.mu.Lock() defer p.mu.Unlock() - out := delivery.PackageVersions{} + out := buildstep.PackageVersions{} for _, v := range p.byKey { if v.Module != "" && v.Commit != "" { out[v.Module] = v.Commit @@ -93,3 +95,7 @@ func (p *publishedPackages) PackageNames() map[string]string { } return out } + +// isSeriesPackageKey says a key of the indexer's state packages is a package's series (`.`): a module's key +// is its path escaped, which holds no dot (keys.EscapeName). +func isSeriesPackageKey(key string) bool { return strings.Contains(key, ".") } diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/tags.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/tags.go index e75b2b4b..1f8782ec 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/tags.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/tags.go @@ -11,11 +11,10 @@ import ( "github.com/nats-io/nats.go" "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" - "mesh-delivery-checker/internal/check" + + "git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry" ) // publishedSeats are the two package registry seats' published, by subject (their contracts in mesh-contracts): heard on @@ -46,7 +45,7 @@ func (t tagAsks) Want(ctx context.Context, w check.TagWanted) error { msg := nats.NewMsg(eventSubject(check.EventTagWanted)) id := fmt.Sprintf("tag-wanted.%s.%s.%s.%s", w.Repository, w.Head, w.Module, w.Package) msg.Header.Set("x-event-id", id) - msg.Header.Set("x-source", delivery.CheckerModule) + msg.Header.Set("x-source", check.CheckerModule) msg.Header.Set("x-node", t.node) msg.Header.Set("x-time", t.now().UTC().Format(time.RFC3339Nano)) msg.Header.Set("content-type", "application/json") @@ -65,7 +64,7 @@ func (t tagAsks) Say(ctx context.Context, v check.VerdictEvent) error { msg := nats.NewMsg(eventSubject(check.EventVerdict)) id := fmt.Sprintf("verdict.%s.%d.%s.%s.%x", v.Repository, v.Pull, v.Head, v.Verdict, sha256.Sum256([]byte(strings.Join(v.OutOfStep, "\n")))) msg.Header.Set("x-event-id", id) - msg.Header.Set("x-source", delivery.CheckerModule) + msg.Header.Set("x-source", check.CheckerModule) msg.Header.Set("x-node", t.node) msg.Header.Set("x-time", t.now().UTC().Format(time.RFC3339Nano)) msg.Header.Set("content-type", "application/json") diff --git a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go index bfce12e0..7ac53a3b 100644 --- a/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go +++ b/modules/mesh-delivery-checker/cmd/mesh-delivery-checker/toolchains.go @@ -12,8 +12,6 @@ import ( "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-sdk/go/delivery" - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" ) @@ -58,7 +56,7 @@ func (w *toolchainWatch) ensure(module string) error { if w.watched[module] { return nil } - name := delivery.ToolchainsBucket(module) + name := buildrun.ToolchainsBucket(module) kv, err := w.js.KeyValue(w.ctx, name) switch { case errors.Is(err, jetstream.ErrBucketNotFound): diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index cc0535e9..4b97c771 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -4,21 +4,29 @@ go 1.26.0 require ( 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/seats/meshwatchdogs 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-contracts/placement/go/seats/meshwants 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/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-contracts/health-and-repair/go/clients/meshhandactsclient 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 ) -require git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261104174638-125929dbc459 - require ( git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-20261104174344-ae5266e77639 - git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104174344-ae5266e77639 git.novox.be/novox/mesh-contracts/go/seats/meshconfig 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/nodeengine v0.0.0-20261104174344-ae5266e77639 - git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104174344-ae5266e77639 git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun v0.0.0-00010101000000-000000000000 github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect diff --git a/modules/mesh-delivery-checker/internal/check/adr548_test.go b/modules/mesh-delivery-checker/internal/check/adr548_test.go index 6b72ac11..d88c4f6b 100644 --- a/modules/mesh-delivery-checker/internal/check/adr548_test.go +++ b/modules/mesh-delivery-checker/internal/check/adr548_test.go @@ -6,7 +6,11 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "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" ) // One check per delivery (novox/hq ADR 0548): the checker's part. A merge turn holds new checks off its repository (§3); @@ -17,7 +21,7 @@ import ( func turnOf(t testing.TB, repo, group, heads string) []byte { t.Helper() at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC) - data, err := delivery.EncodeTurn(delivery.Turn{Repository: repo, Waiting: []delivery.TurnWait{{Group: group, Heads: heads, Approved: at, Since: at}}}) + data, err := meshdeliveryclient.EncodeTurn(meshdelivery.TurnsByRepositoryValue{Repository: repo, Waiting: []meshdelivery.TurnsByRepositoryValueWaitingItem{{Group: group, Heads: heads, Approved: at, Since: at}}}) if err != nil { t.Fatal(err) } @@ -26,7 +30,7 @@ func turnOf(t testing.TB, repo, group, heads string) []byte { func turnKey(t testing.TB, repo string) string { t.Helper() - k, err := delivery.TurnsKey(repo) + k, err := meshdeliveryclient.TurnsKey(repo) if err != nil { t.Fatal(err) } @@ -45,10 +49,10 @@ func waitFor(t testing.TB, what string, ok func() bool) { } // pullWithFP is a pull request touching the module m at the fingerprint `src2:` and 64 times the hex digit d. -func pullWithFP(repo string, number int, head string, d string) delivery.CandidatePull { +func pullWithFP(repo string, number int, head string, d string) meshcatalog.CandidatesByGroupValuePullsItem { p := pullOf(repo, number, head) p.Modules = []string{"m"} - p.Fps = map[string]delivery.CandidateModule{"m": {FP2: "src2:" + strings.Repeat(d, 64)}} + p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"m": {Fp2: "src2:" + strings.Repeat(d, 64)}} return p } @@ -76,7 +80,7 @@ func TestOneCheckPerDelivery(t *testing.T) { } r.ch.Reconsider(ctx) // what the watch of turns does on the delete r.ch.Wait() - if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass { + if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != meshdeliverycheckerclient.VerdictPass { t.Errorf("judged %d times after the turn went, want once", j.count()) } }, @@ -86,7 +90,7 @@ func TestOneCheckPerDelivery(t *testing.T) { turns := &Turns{} r.ch.Turns = turns cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) - c, _ := delivery.DecodeCandidate(cand) + c, _ := meshcatalog.DecodeCandidatesByGroupValue(cand) turns.Put(turnKey(t, repoSDK), turnOf(t, repoSDK, "fix/other", "0123456789ab"), false) turns.Put(turnKey(t, repoA), turnOf(t, repoA, "feat/x", GateHeads(c)), false) r.ch.Candidate(ctx, "feat%2Fx", cand) @@ -113,7 +117,7 @@ func TestOneCheckPerDelivery(t *testing.T) { r.ch.Reconsider(ctx) close(release) r.ch.Wait() - if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass { + if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != meshdeliverycheckerclient.VerdictPass { t.Errorf("judged %d times; the running check is not ended by the turn", j.count()) } }, @@ -144,7 +148,7 @@ func TestOneCheckPerDelivery(t *testing.T) { if j.count() != 2 || kinds[0] != KindNew || kinds[1] != KindTrunk || from[0] != nil || from[1][repoA] != trunkA { t.Fatalf("judged %d times as %v carried from %v; want a whole check, then one carry from %s", j.count(), kinds, from, trunkA[:12]) } - if v.State != delivery.VerdictPass || v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA { + if v.State != meshdeliverycheckerclient.VerdictPass || v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA { t.Errorf("the verdict is %s on %v carried from %v; want a pass on the new trunk carried from the old", v.State, v.Trunk, v.CarriedFrom) } if s := owedState(t, r, repoA, 7, head1); s != "success" { @@ -160,7 +164,7 @@ func TestOneCheckPerDelivery(t *testing.T) { j := &judgeFunc{f: func(_ context.Context, c *Case, _ int) (Outcome, error) { kept = append(kept, c.Composition != nil) v := passing() - v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "v0.1.14-x", FPSet: c.FPSet} + v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionPass, Sdk: "v0.1.14-x", Fpset: c.FPSet} return Outcome{Verdict: v}, nil }} r := newRig(t, j) @@ -220,7 +224,7 @@ func TestOneCheckPerDelivery(t *testing.T) { j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) { kinds, from = append(kinds, c.Kind), append(from, c.CarriedFrom) if call == 2 { - return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "ace", + return Outcome{Verdict: meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "ace", Words: repoA + "#7 " + ConflictWords + "go.mod"}}}, nil } return Outcome{Verdict: passing()}, nil @@ -260,7 +264,7 @@ func TestOneCheckPerDelivery(t *testing.T) { t.Errorf("the line names %s on %s first, want the head on the new trunk", head, trunk) } cs.CarriedFrom = map[string]string{repoA: trunkA2} - pending := Line(delivery.Verdict{State: delivery.VerdictRunning}, cs, p, nil, nil, nil) + pending := Line(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, cs, p, nil, nil, nil) if !strings.HasPrefix(pending, "being carried to 111111111111 on aaaaaaaaaaaa | from abababababab") { t.Errorf("the pending line of a carry = %q", pending) } @@ -274,10 +278,10 @@ func TestOneCheckPerDelivery(t *testing.T) { r.ch.Candidate(ctx, "feat%2Fa", candA) r.ch.Candidate(ctx, "feat%2Fc", candC) r.ch.Wait() - a, _ := delivery.DecodeCandidate(candA) - c, _ := delivery.DecodeCandidate(candC) + a, _ := meshcatalog.DecodeCandidatesByGroupValue(candA) + c, _ := meshcatalog.DecodeCandidatesByGroupValue(candC) at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC) - turn, err := delivery.EncodeTurn(delivery.Turn{Repository: repoA, Waiting: []delivery.TurnWait{ + turn, err := meshdeliveryclient.EncodeTurn(meshdelivery.TurnsByRepositoryValue{Repository: repoA, Waiting: []meshdelivery.TurnsByRepositoryValueWaitingItem{ {Group: "feat/a", Heads: GateHeads(a), Approved: at, Since: at}, {Group: "feat/c", Heads: GateHeads(c), Approved: at.Add(time.Minute), Since: at}}}) if err != nil { t.Fatal(err) @@ -322,7 +326,7 @@ func TestOneCheckPerDelivery(t *testing.T) { if call == 1 { return Outcome{Verdict: passing()}, nil } - return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}}, nil + return Outcome{Verdict: meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}}, nil }} r := newRig(t, j) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) @@ -332,7 +336,7 @@ func TestOneCheckPerDelivery(t *testing.T) { r.ch.TrunkMoved(ctx) r.ch.Wait() v := r.verdict(t, keyOf(t, "feat/x", cand)) - if v.State != delivery.VerdictFail || v.CarriedFrom != nil || !strings.HasPrefix(v.Failed.Words, "want 4") || + if v.State != meshdeliverycheckerclient.VerdictFail || v.CarriedFrom != nil || !strings.HasPrefix(v.Failed.Words, "want 4") || !strings.Contains(v.Failed.Words, repoA+" "+trunkA[:12]+" to "+trunkA2[:12]) { t.Errorf("the failed carry is %s carried from %v with words %q; want the test's words, then the move", v.State, v.CarriedFrom, v.Failed.Words) } diff --git a/modules/mesh-delivery-checker/internal/check/askagain.go b/modules/mesh-delivery-checker/internal/check/askagain.go index 7540bbeb..0c4ea8ce 100644 --- a/modules/mesh-delivery-checker/internal/check/askagain.go +++ b/modules/mesh-delivery-checker/internal/check/askagain.go @@ -7,7 +7,9 @@ import ( "fmt" "strings" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" ) // **A check is asked again by hand** (the lost check of 2026-10-28; tools-are-how-a-module-is-spoken-to: every recovery @@ -23,7 +25,7 @@ import ( // AsksState is the checker's state of the asks each running check put, by verdict key (CheckAsks); AsksBucket its bucket. const ( AsksState = "asks" - AsksBucket = delivery.CheckerModule + "_" + AsksState + AsksBucket = CheckerModule + "_" + AsksState ) // CheckAsks are the asks a check put, in order, as the state asks keeps them while it runs; its end deletes the key. @@ -70,8 +72,8 @@ const AskedAgainWords = EndedWords + "asked again by hand (" + AskAgainTool + ") var errAskedAgain = errors.New("its verdict was asked again by hand (" + AskAgainTool + ")") // IsAskedAgain says whether a verdict was written by AskAgainTool. -func IsAskedAgain(v delivery.Verdict) bool { - return IsEnded(v) && strings.HasPrefix(v.Failed.Words, AskedAgainWords) +func IsAskedAgain(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return IsEnded(v) && strings.HasPrefix(failedOf(v).Words, AskedAgainWords) } // VerdictEntry is the verdicts state as the ask-again tool writes it: a key's value and revision (nil and 0 for none), @@ -104,7 +106,7 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error) if !ok { return AskedAgain{}, fmt.Errorf("no candidate is held for the group %q", group) } - key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash) + key, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads) if err != nil { return AskedAgain{}, err } @@ -115,11 +117,11 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error) if len(raw) == 0 { return AskedAgain{}, fmt.Errorf("%s holds no verdict: the checker claims its check when it looks at the candidate (%s)", key, StatusReadTool) } - v, err := delivery.DecodeVerdict(raw) + v, err := meshdeliverycheckerclient.DecodeVerdict(raw) if err != nil { return AskedAgain{}, fmt.Errorf("%s holds what is not a verdict: %w", key, err) } - if v.State != delivery.VerdictRunning { + if v.State != meshdeliverycheckerclient.VerdictRunning { return AskedAgain{}, fmt.Errorf("%s is %s, not running: a verdict stands until a push or a trunk move asks it again", key, v.State) } steps := r.readSteps() @@ -150,9 +152,9 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error) } } words := AskedAgainWords + why - ended := delivery.Verdict{Group: v.Group, Heads: v.Heads, Trunk: v.Trunk, State: delivery.VerdictFail, - Failed: delivery.FailedFirst{Machine: "checker", Words: words}} - data, err := delivery.EncodeVerdict(ended) + ended := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: v.Group, Heads: v.Heads, Trunk: v.Trunk, State: meshdeliverycheckerclient.VerdictFail, + Failed: &FailedFirst{Machine: "checker", Words: words}} + data, err := meshdeliverycheckerclient.EncodeVerdict(ended) if err != nil { return AskedAgain{}, err } @@ -168,7 +170,7 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error) // the check of those heads running in this process, which then considers them again; with none running, the candidate // that holds them is considered at once. Any other verdict is the checker's own, and left. func (ch *Checker) VerdictWritten(ctx context.Context, key string, value []byte) { - v, err := delivery.DecodeVerdict(value) + v, err := meshdeliverycheckerclient.DecodeVerdict(value) if err != nil || !IsAskedAgain(v) { return } @@ -177,22 +179,22 @@ func (ch *Checker) VerdictWritten(ctx context.Context, key string, value []byte) if l, ok := ch.live[key]; ok { l.end(errAskedAgain) ch.mu.Unlock() - ch.logf("%s: %s; the check running for it is ended and asked again", key, v.Failed.Words) + ch.logf("%s: %s; the check running for it is ended and asked again", key, failedOf(v).Words) return } var group string - var cand delivery.Candidate + var cand meshcatalog.CandidatesByGroupValue for k, c := range ch.known { - if vk, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash); err == nil && vk == key { + if vk, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads); err == nil && vk == key { group, cand = k, c break } } ch.mu.Unlock() if group == "" { - ch.logf("%s: %s, but no candidate holds these heads, so nothing is asked", key, v.Failed.Words) + ch.logf("%s: %s, but no candidate holds these heads, so nothing is asked", key, failedOf(v).Words) return } - ch.logf("%s: %s; asked again", key, v.Failed.Words) + ch.logf("%s: %s; asked again", key, failedOf(v).Words) ch.consider(ctx, group, cand) } diff --git a/modules/mesh-delivery-checker/internal/check/binaries.go b/modules/mesh-delivery-checker/internal/check/binaries.go index 5329c61d..d649b757 100644 --- a/modules/mesh-delivery-checker/internal/check/binaries.go +++ b/modules/mesh-delivery-checker/internal/check/binaries.go @@ -7,28 +7,18 @@ import ( // A change that adds or modifies a compiled program fails (issue 1577): it is build output, `go build` in a module's folder // writes one beside go.mod, and `git add -A` takes it. The build module's fetch step, which holds the merged clone, reads the -// first bytes of each changed file and reports the programs (mesh-sdk buildstep.MergeInto); the checker judges the answer. - -// NoProgramsReported is the warning of a build module whose fetch answered no programs field: an older module, which cannot -// tell, so the check does not refuse a program it did not look for. -const NoProgramsReported = "the build module did not report programs: a compiled program in the change is not refused" +// first bytes of each changed file and reports the programs (the build modules' fetch tools); the checker judges the answer. // checkBinariesOf fails a pull request whose change adds or modifies a compiled program, naming each file. It judges the -// first fetch answer of the pull request, whichever build module gave it; a build module that reports no programs leaves a -// warning on the check and passes it. +// first fetch answer of the pull request, whichever build module gave it. A fetch that made the merge and names no program has +// none: the answer's `programs` is an omitted empty list (B2g), and `merge` tells a preview was made. func (st *state) checkBinariesOf(t *target) verdictPtr { key := fmt.Sprintf("%s#%d", t.pull.Repository, t.pull.Number) if t.fetched.Merge == "" { return nil // no merge was made: nothing was changed to judge } - if t.fetched.Programs == nil { - st.c.note(fmt.Sprintf("%s: %s (%s)", key, NoProgramsReported, t.module)) - st.c.warn(key, fmt.Sprintf("a compiled program is not refused: %s reported no programs", t.module)) - st.j.logf("%s: %s (%s)", key, NoProgramsReported, t.module) - return nil - } var found []string - for _, p := range *t.fetched.Programs { + for _, p := range t.fetched.Programs { if p.Commit != "" { found = append(found, fmt.Sprintf("%s: a compiled program added in %s and removed later: it stays in history; squash the branch (issue 1577)", p.Path, short12(p.Commit))) continue diff --git a/modules/mesh-delivery-checker/internal/check/binaries_test.go b/modules/mesh-delivery-checker/internal/check/binaries_test.go index 2a3ab5e5..9e9376c5 100644 --- a/modules/mesh-delivery-checker/internal/check/binaries_test.go +++ b/modules/mesh-delivery-checker/internal/check/binaries_test.go @@ -1,52 +1,46 @@ package check import ( - "slices" "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) func TestAChangeWhoseFetchReportsProgramsFailsNamingEachFile(t *testing.T) { // 1577 - prog := func(p ...string) *[]ProgramFile { - out := []ProgramFile{} + prog := func(p ...string) []buildstep.Program { + var out []buildstep.Program // no program is the omitted empty list of the typed answer for _, f := range p { path, commit, _ := strings.Cut(f, "@") // `path@commit`: added by a commit of the branch and removed later - out = append(out, ProgramFile{Path: path, Kind: "ELF", Commit: commit}) + out = append(out, buildstep.Program{Path: path, Kind: "ELF", Commit: commit}) } - return &out + return out } for _, c := range []struct { name string - programs *[]ProgramFile + programs []buildstep.Program fail []string // paths named; none passes - warned bool // the build module reported no programs field }{ - {"none found", prog(), nil, false}, - {"one program", prog("modules/m/m"), []string{"modules/m/m"}, false}, - {"two programs", prog("a/x", "b/y"), []string{"a/x", "b/y"}, false}, - {"added and removed in the branch", prog("gone/prog@0123456789abcdef0123"), []string{"gone/prog: a compiled program added in 0123456789ab and removed later: it stays in history; squash the branch (issue 1577)"}, false}, - {"an older build module reports nothing", nil, nil, true}, + {"none found", prog(), nil}, + {"one program", prog("modules/m/m"), []string{"modules/m/m"}}, + {"two programs", prog("a/x", "b/y"), []string{"a/x", "b/y"}}, + {"added and removed in the branch", prog("gone/prog@0123456789abcdef0123"), []string{"gone/prog: a compiled program added in 0123456789ab and removed later: it stays in history; squash the branch (issue 1577)"}}, } { b := newBench(t) b.fetch.Merge, b.fetch.Programs = "mmmm", c.programs cs := caseOf(t, pullOf(repoA, 7, head1)) v := b.judge(t, cs).Verdict - if got := slices.ContainsFunc(cs.Notes(), func(n string) bool { return strings.Contains(n, NoProgramsReported) }); got != c.warned { - t.Errorf("%s: warned = %v, want %v (%v)", c.name, got, c.warned, cs.Notes()) - } - line := Line(v, cs, cs.Pulls[0], nil, nil, nil) - if got := strings.Contains(line, "a compiled program is not refused: mesh-build-go reported no programs"); got != c.warned { - t.Errorf("%s: the status line holds the warning = %v, want %v: %s", c.name, got, c.warned, line) + if len(cs.Notes()) > 0 { + t.Errorf("%s: notes = %v: a merge with no program is not a fetch that reported none", c.name, cs.Notes()) } if len(c.fail) == 0 { - if v.State != delivery.VerdictPass { + if v.State != meshdeliverycheckerclient.VerdictPass { t.Errorf("%s: verdict = %+v", c.name, v) } continue } - if v.State != delivery.VerdictFail { + if v.State != meshdeliverycheckerclient.VerdictFail { t.Errorf("%s: verdict = %+v, want a failure", c.name, v) continue } diff --git a/modules/mesh-delivery-checker/internal/check/bounded.go b/modules/mesh-delivery-checker/internal/check/bounded.go index 3d432feb..5e8918c9 100644 --- a/modules/mesh-delivery-checker/internal/check/bounded.go +++ b/modules/mesh-delivery-checker/internal/check/bounded.go @@ -6,7 +6,7 @@ import ( "sync" "sync/atomic" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // ReadWorkers is how many values a read tool asks the bus for at once. Each value is a round trip from the tool through the @@ -60,7 +60,7 @@ func capKeys(keys []string, narrow string) ([]string, string) { // longer id is the later and equal lengths compare as text. A key that is no step key goes last. func newestFirst(keys []string) { id := func(k string) (string, bool) { - _, id, err := delivery.ParseStepKey(k) + _, id, err := buildstep.ParseStepKey(k) return id, err == nil } sort.Slice(keys, func(i, j int) bool { diff --git a/modules/mesh-delivery-checker/internal/check/build_change.go b/modules/mesh-delivery-checker/internal/check/build_change.go index 9c4ede7b..998e9f98 100644 --- a/modules/mesh-delivery-checker/internal/check/build_change.go +++ b/modules/mesh-delivery-checker/internal/check/build_change.go @@ -41,10 +41,10 @@ func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []str readable := true for _, p := range st.c.Pulls { for module, cm := range p.Fps { - if cm.FP2 == "" || cm.Manifest == "" { + if cm.Fp2 == "" || cm.Manifest == "" { continue // the builder builds no fingerprint of it (candidates.go: none of the new form, or no manifest) } - wait[module] = cm.FP2 + wait[module] = cm.Fp2 if !readable || st.j.Read == nil { readable = false continue diff --git a/modules/mesh-delivery-checker/internal/check/builds.go b/modules/mesh-delivery-checker/internal/check/builds.go index 2afa2d42..8fe9abf8 100644 --- a/modules/mesh-delivery-checker/internal/check/builds.go +++ b/modules/mesh-delivery-checker/internal/check/builds.go @@ -9,7 +9,9 @@ import ( "strings" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "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-delivery/modules/mesh-delivery-builder/builds" ) // The states of a build the status line says (ADR 0484 clause 7). @@ -22,18 +24,18 @@ const ( // BuildStates reads the builder's build of a module at a fingerprint of the new form (builds..): the // value and whether the key exists. type BuildStates interface { - Build(ctx context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) + Build(ctx context.Context, module, fp2 string) (builds.BuilderBuild, bool, error) } // NoteOf is the state of a build as the line says it: done is built, failed is failed, and one queued, building or not // begun is waiting. -func NoteOf(b delivery.BuilderBuild, found bool) string { +func NoteOf(b builds.BuilderBuild, found bool) string { switch { case !found: return BuildWaiting - case b.State == delivery.BuildDone: + case b.State == builds.BuildDone: return BuildBuilt - case b.State == delivery.BuildFailed: + case b.State == builds.BuildFailed: return BuildFailed } return BuildWaiting @@ -48,8 +50,8 @@ func (j *Examiner) buildNotes(ctx context.Context, c *Case) []BuildNote { fps := map[string]string{} for _, p := range c.Pulls { for module, cm := range p.Fps { - if cm.FP2 != "" { - fps[module] = cm.FP2 + if cm.Fp2 != "" { + fps[module] = cm.Fp2 } } } @@ -79,7 +81,7 @@ const DefaultBuildWait = 30 * time.Minute type BuildEnds interface { // Ended waits until the build of every module at its fingerprint (module to fingerprint) is done or failed, or ctx ends, // and answers the builds that ended, by module. An error is only builds that cannot be heard at all. - Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) + Ended(ctx context.Context, builds map[string]string) (map[string]builds.BuilderBuild, error) // Done waits until the build of every module at its fingerprint is done (a failed one built again, builder_build_again), // and says whether they all are; false when ctx ends first or they cannot be heard. Done(ctx context.Context, builds map[string]string) bool @@ -91,19 +93,19 @@ const BuildFailedTest = "build: " // failedBuilds are the candidate's builds a failed verdict names as failed (BuildFailedTest), by module, at the fingerprint // the case carries for it; none for a verdict that names no failed build. -func failedBuilds(v delivery.Verdict, cs *Case) map[string]string { - if v.State != delivery.VerdictFail { +func failedBuilds(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case) map[string]string { + if v.State != meshdeliverycheckerclient.VerdictFail { return nil } out := map[string]string{} - for _, f := range append([]delivery.FailedFirst{v.Failed}, v.Failures...) { + for _, f := range append([]FailedFirst{failedOf(v)}, failedList(v.Failures)...) { module, named := strings.CutPrefix(f.Test, BuildFailedTest) if !named { continue } for _, p := range cs.Pulls { - if cm, ok := p.Fps[module]; ok && cm.FP2 != "" { - out[module] = cm.FP2 + if cm, ok := p.Fps[module]; ok && cm.Fp2 != "" { + out[module] = cm.Fp2 } } } @@ -124,13 +126,13 @@ func (j *Examiner) buildWait() time.Duration { // builds of the candidate and composes with it. A failed build fails the check, naming the module and the builder's words; // a build not ended within the bound is named, and the check composes without it. It answers the words of the composition's // warnings: what the check composes without waiting on its build, and why. -func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string { +func (st *state) awaitBuilds(ctx context.Context, v *meshdeliverychecker.VerdictsByGroupHeadsValue) []string { wait, words := st.buildsOfTheCheck(ctx) if len(wait) == 0 { return words } modules := slices.Sorted(maps.Keys(wait)) - if v.State == delivery.VerdictFail { + if v.State == meshdeliverycheckerclient.VerdictFail { // The check has failed already: its red status is never held back by a build (the lead, 2026-11-03). return append(words, fmt.Sprintf("the check failed before its builds were waited on, so it composes at once, without waiting on the builds of %s (ADR 0603 §10)", strings.Join(modules, ", "))) } @@ -154,10 +156,10 @@ func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string switch { case !done: late = append(late, module) - case b.State == delivery.BuildFailed: - f := delivery.FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)} - if v.State != delivery.VerdictFail { - v.State, v.Failed = delivery.VerdictFail, f + case b.State == builds.BuildFailed: + f := FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)} + if v.State != meshdeliverycheckerclient.VerdictFail { + v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &f continue } addFailure(v, f) diff --git a/modules/mesh-delivery-checker/internal/check/case.go b/modules/mesh-delivery-checker/internal/check/case.go index 936e282b..7aee3f1f 100644 --- a/modules/mesh-delivery-checker/internal/check/case.go +++ b/modules/mesh-delivery-checker/internal/check/case.go @@ -9,7 +9,11 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "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/sdk/buildstep" ) // Pull is one pull request the checker judges: its head, the trunk head of its repository it is merged onto and the @@ -20,7 +24,7 @@ type Pull struct { Head string Trunk string Modules []string - Fps map[string]delivery.CandidateModule + Fps map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue } // Case is one check: a candidate's pull requests that are judged (those of a repository whose trunk is not known are left @@ -48,7 +52,7 @@ type Case struct { // seriesOf are, by pull request, the changes of a package series it makes (series.go), named in its status line. seriesOf map[string][]string // seriesChanges are, by pull request, the same changes as structures (series.go), named in the verdict and its event. - seriesChanges map[string][]delivery.SeriesChange + seriesChanges map[string][]meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem notes []string // asks are the ids of the asks the check put on the build seats' streams, in order: a try names them (Try.Asks). asks []string @@ -56,7 +60,7 @@ type Case struct { // the check waits for it: the node that took it, the phase it says, its end. A try writes them into its record, so a // running try names its asks and where each stands (issue 1890). Called from the check's goroutines; it must not block // for long. - Watch func(askID string, step *delivery.Step) + Watch func(askID string, step *buildstep.Step) // Progress says what the check is doing, so a check that ran past its bound names the step it was stuck at. Progress *Progress // Kind is what asked the check (KindNew, KindNodes, KindTrunk): a carry after a trunk or sibling move is asked on the @@ -65,11 +69,11 @@ type Case struct { // CarriedFrom are, for the carry of a verdict that passed, the trunk and sibling commits it was judged on (ADR 0548 §5); // nil for a whole check. CarriedFrom map[string]string - // FPSet is the candidate's fingerprint set (Candidate.FPSet): a composition names the set it judged. + // FPSet is the candidate's fingerprint set (Candidate.Fpset): a composition names the set it judged. FPSet string // Composition is, for a carry, the composition that passed on the same fingerprint set: it is kept and not asked again, // since no fingerprint of a module it composed changed (ADR 0548 §5). Nil composes. - Composition *delivery.Composition + Composition *meshdeliverychecker.VerdictsByGroupHeadsValueComposition } // Asked records the id of an ask the check put. @@ -110,18 +114,18 @@ func (p *Progress) Now() (stage, node string) { // The checker judges pull requests into main only (ADR 0484 clause 11): the pull message carries no base, a merge into // another branch is a closed message, so every candidate is into the trunk; a repository whose trunk head is not known has // none to merge onto. It answers nil when no pull request is left to judge. -func CaseOf(c delivery.Candidate, trunk map[string]string) *Case { - key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash) +func CaseOf(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) *Case { + key, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads) if err != nil { return nil } - cs := &Case{Key: key, Group: c.Group, HeadsHash: c.HeadsHash, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}, FPSet: c.FPSet} + cs := &Case{Key: key, Group: c.Group, HeadsHash: c.Heads, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}, FPSet: c.Fpset} for _, p := range c.Pulls { switch head, known := trunk[p.Repository]; { case !known || head == "": cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: %s", p.Repository, p.Number, UntrunkedWordsOf([]string{p.Repository}))) default: - cs.Pulls = append(cs.Pulls, Pull{Repository: p.Repository, Number: p.Number, Head: p.Head.Head, Trunk: head, Modules: p.Modules, Fps: p.Fps}) + cs.Pulls = append(cs.Pulls, Pull{Repository: p.Repository, Number: int(p.Number), Head: p.Head, Trunk: head, Modules: p.Modules, Fps: p.Fps}) cs.Trunk[p.Repository] = head } } @@ -151,8 +155,8 @@ func UntrunkedWordsOf(repos []string) string { } // IsUntrunked says whether a verdict says its heads cannot be judged for want of a trunk head (UntrunkedWords). -func IsUntrunked(v delivery.Verdict) bool { - return v.State == delivery.VerdictFail && v.Failed.Machine == "checker" && strings.HasPrefix(v.Failed.Words, UntrunkedWords) +func IsUntrunked(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State == meshdeliverycheckerclient.VerdictFail && failedOf(v).Machine == "checker" && strings.HasPrefix(failedOf(v).Words, UntrunkedWords) } // **A candidate outside the mesh is not the checker's** (sweep 2026-10-30 item 7): the forge carries repositories the mesh @@ -168,7 +172,7 @@ const OutsideWords = "outside the mesh: the gate's source.main names no trunk he // Outside says every pull request of c is of a repository trunk names no head of and touches no module. Never with // trunk unknown (nil): before the gate's source is read nothing is outside. -func Outside(c delivery.Candidate, trunk map[string]string) bool { +func Outside(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) bool { if trunk == nil || len(c.Pulls) == 0 { return false } @@ -182,11 +186,11 @@ func Outside(c delivery.Candidate, trunk map[string]string) bool { // leftOf are the pull requests of a candidate whose repository has no trunk head known, with Trunk empty, in repository and // number order; nil when every repository's is known. -func leftOf(c delivery.Candidate, trunk map[string]string) []Pull { +func leftOf(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) []Pull { var out []Pull for _, p := range c.Pulls { if trunk[p.Repository] == "" { - out = append(out, Pull{Repository: p.Repository, Number: p.Number, Head: p.Head.Head, Modules: p.Modules, Fps: p.Fps}) + out = append(out, Pull{Repository: p.Repository, Number: int(p.Number), Head: p.Head, Modules: p.Modules, Fps: p.Fps}) } } sort.SliceStable(out, func(i, j int) bool { @@ -199,15 +203,15 @@ func leftOf(c delivery.Candidate, trunk map[string]string) []Pull { } // GateHeads is the hash the gate matches the members an Approve covers by (mesh-catalog mesh-delivery-gate onestatus.go -// headsOf): delivery.HeadsHash over every pull request of the candidate, hq's included, as +// headsOf): meshdeliveryclient.Heads over every pull request of the candidate, hq's included, as // `#=`. A lone pull request is a group of one. Empty for a candidate // with no pull request. -func GateHeads(c delivery.Candidate) string { - heads := make([]delivery.Head, 0, len(c.Pulls)) +func GateHeads(c meshcatalog.CandidatesByGroupValue) string { + heads := make([]meshdeliveryclient.Head, 0, len(c.Pulls)) for _, p := range c.Pulls { - heads = append(heads, delivery.Head{Repository: strings.ToLower(p.Repository), Number: p.Number, Head: strings.ToLower(p.Head.Head)}) + heads = append(heads, meshdeliveryclient.Head{Repository: strings.ToLower(p.Repository), Number: int(p.Number), Head: strings.ToLower(p.Head)}) } - hash, err := delivery.HeadsHash(heads) + hash, err := meshdeliveryclient.HeadsHash(heads) if err != nil { return "" } @@ -227,7 +231,7 @@ type Trunks struct { func (t *Trunks) Put(value []byte, deleted bool) bool { var heads map[string]string if !deleted { - s, err := delivery.DecodeSource(value) + s, err := meshdeliveryclient.DecodeSource(value) if err != nil { return false } diff --git a/modules/mesh-delivery-checker/internal/check/checker.go b/modules/mesh-delivery-checker/internal/check/checker.go index f42b47fc..c6d5fa60 100644 --- a/modules/mesh-delivery-checker/internal/check/checker.go +++ b/modules/mesh-delivery-checker/internal/check/checker.go @@ -11,7 +11,14 @@ import ( "sync" "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/clients/meshdeliverycheckerclient" + "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/sdk/buildstep" + "git.novox.be/novox/mesh-sdk/go/keys" ) // DefaultBound is how long a check may run before it is ended failed naming the step it was stuck at (ADR 0484, issue 1678): @@ -38,7 +45,7 @@ type Announcer func(ctx context.Context, event, key string) // Outcome is what judging a case gives: the verdict (state, failure, levels, times, composition; the checker adds the // group, the heads and the trunk) and, for the line, what the check knows of the builds of the modules it touches. type Outcome struct { - Verdict delivery.Verdict + Verdict meshdeliverychecker.VerdictsByGroupHeadsValue Builds []BuildNote // Slowest names the slowest tests of a level that went over its bound ("unit pkg.Test 1.2s"). Slowest []string @@ -80,7 +87,7 @@ type Checker struct { // Turns are the gate's merge turns (ADR 0548 §3): no check is claimed of a repository a turn names, but the check of the // waiting delivery's own heads; nil holds none. Turns interface { - Holding(repositories []string, heads string) (string, delivery.TurnWait, bool) + Holding(repositories []string, heads string) (string, meshdelivery.TurnsByRepositoryValueWaitingItem, bool) } // Bound is how long one check may run; none when 0 (DefaultBound). Bound time.Duration @@ -105,17 +112,17 @@ type Checker struct { once sync.Once wg sync.WaitGroup mu sync.Mutex - known map[string]delivery.Candidate // by candidates key - inflight map[string]bool // by verdict key - again map[string]bool // by verdict key: asked again while it ran - tried map[string]bool // by verdict key: looked at since this process started - epoch uint64 // changes of the nodes seen since this process started - nodesFail map[string]uint64 // by verdict key: the epoch a failure of the nodes was written at - held map[string]string // by verdict key: the turn that holds its check off, as last said - live map[string]liveCheck // by verdict key: the check running in this process, ended when its heads go - settling map[string]*window // by candidates key: the window its heads settle in - rebuilt map[string]bool // by verdict key: every failed build its verdict names is done again - awaiting map[string]bool // by verdict key: its failed builds are heard (awaitRebuilt) + known map[string]meshcatalog.CandidatesByGroupValue // by candidates key + inflight map[string]bool // by verdict key + again map[string]bool // by verdict key: asked again while it ran + tried map[string]bool // by verdict key: looked at since this process started + epoch uint64 // changes of the nodes seen since this process started + nodesFail map[string]uint64 // by verdict key: the epoch a failure of the nodes was written at + held map[string]string // by verdict key: the turn that holds its check off, as last said + live map[string]liveCheck // by verdict key: the check running in this process, ended when its heads go + settling map[string]*window // by candidates key: the window its heads settle in + rebuilt map[string]bool // by verdict key: every failed build its verdict names is done again + awaiting map[string]bool // by verdict key: its failed builds are heard (awaitRebuilt) // outsideSaid are the candidates outside the mesh said in the log, by group and heads. outsideSaid map[string]bool } @@ -155,8 +162,8 @@ const DefaultSettle = 15 * time.Second const EndedWords = "ended: " // IsEnded says whether a verdict was ended without being judged (EndedWords). -func IsEnded(v delivery.Verdict) bool { - return v.State == delivery.VerdictFail && v.Failed.Machine == "checker" && strings.HasPrefix(v.Failed.Words, EndedWords) +func IsEnded(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State == meshdeliverycheckerclient.VerdictFail && failedOf(v).Machine == "checker" && strings.HasPrefix(failedOf(v).Words, EndedWords) } // superseded is the cause a running check is ended with: the heads that replaced its own, or none when its group went. @@ -171,7 +178,7 @@ func (s superseded) Error() string { func (ch *Checker) init() { ch.once.Do(func() { - ch.known, ch.inflight, ch.again, ch.tried = map[string]delivery.Candidate{}, map[string]bool{}, map[string]bool{}, map[string]bool{} + ch.known, ch.inflight, ch.again, ch.tried = map[string]meshcatalog.CandidatesByGroupValue{}, map[string]bool{}, map[string]bool{}, map[string]bool{} ch.nodesFail, ch.held = map[string]uint64{}, map[string]string{} ch.live, ch.settling = map[string]liveCheck{}, map[string]*window{} ch.rebuilt, ch.awaiting = map[string]bool{}, map[string]bool{} @@ -216,19 +223,19 @@ func (ch *Checker) Wait() { ch.wg.Wait() } // checked. A value that is not a candidate is said and left. func (ch *Checker) Candidate(ctx context.Context, key string, value []byte) { ch.init() - c, err := delivery.DecodeCandidate(value) + c, err := meshcatalog.DecodeCandidatesByGroupValue(value) if err != nil { ch.logf("candidates.%s holds what is not a candidate, left: %v", key, err) return } - vkey, _ := delivery.CheckerVerdictKey(c.Group, c.HeadsHash) + vkey, _ := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads) ch.mu.Lock() prev, had := ch.known[key] ch.known[key] = c // A check of heads this candidate no longer holds is ended at once (issue 1890): a member's new head, a member joining // or leaving. ch.endOthers(key, vkey, GateHeads(c)) - moved := !had || prev.HeadsHash != c.HeadsHash + moved := !had || prev.Heads != c.Heads if ch.Settle > 0 && (moved || ch.settling[key] != nil) { // The group's heads changed: its check waits until they have been the same for Settle, so a group whose members // are announced one after another is checked once, whole. A later change starts the window again. The wait is a @@ -279,9 +286,9 @@ func (ch *Checker) VerdictDeleted(ctx context.Context, key string) { return } var group string - var cand delivery.Candidate + var cand meshcatalog.CandidatesByGroupValue for k, c := range ch.known { - if vk, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash); err == nil && vk == key { + if vk, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads); err == nil && vk == key { group, cand = k, c break } @@ -324,7 +331,7 @@ func (ch *Checker) NodesChanged(ctx context.Context) { func (ch *Checker) Reconsider(ctx context.Context) { ch.init() ch.mu.Lock() - known := make(map[string]delivery.Candidate, len(ch.known)) + known := make(map[string]meshcatalog.CandidatesByGroupValue, len(ch.known)) for k, c := range ch.known { known[k] = c } @@ -341,7 +348,7 @@ func (ch *Checker) heads() map[string]string { return ch.Trunks.Heads() } -func (ch *Checker) consider(ctx context.Context, key string, c delivery.Candidate) { +func (ch *Checker) consider(ctx context.Context, key string, c meshcatalog.CandidatesByGroupValue) { heads := ch.heads() if Outside(c, heads) { ch.outside(ctx, c) @@ -422,10 +429,10 @@ func (ch *Checker) consider(ctx context.Context, key string, c delivery.Candidat // UntrunkedWords, the members whose trunk is known too, as their group is not judged. It is logged once per head: when a // status or the verdict is written, never at a look that finds them already said. Once the trunk head is known the verdict // is stale (it names no trunk) and the heads are judged whole (prepare, claim). -func (ch *Checker) untrunked(ctx context.Context, c delivery.Candidate, left []Pull) { - key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash) +func (ch *Checker) untrunked(ctx context.Context, c meshcatalog.CandidatesByGroupValue, left []Pull) { + key, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads) if err != nil { - ch.logf("candidates of %s: no verdict key for heads %q, so it cannot be said that they cannot be judged: %v", c.Group, c.HeadsHash, err) + ch.logf("candidates of %s: no verdict key for heads %q, so it cannot be said that they cannot be judged: %v", c.Group, c.Heads, err) return } repos := make([]string, 0, len(left)) @@ -433,37 +440,37 @@ func (ch *Checker) untrunked(ctx context.Context, c delivery.Candidate, left []P repos = append(repos, p.Repository) } words := UntrunkedWordsOf(repos) - v := delivery.Verdict{Group: c.Group, Heads: GateHeads(c), Trunk: map[string]string{}, State: delivery.VerdictFail, - Failed: delivery.FailedFirst{Machine: "checker", Words: words}} - data, err := delivery.EncodeVerdict(v) + v := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: c.Group, Heads: GateHeads(c), Trunk: map[string]string{}, State: meshdeliverycheckerclient.VerdictFail, + Failed: &FailedFirst{Machine: "checker", Words: words}} + data, err := meshdeliverycheckerclient.EncodeVerdict(v) if err != nil { ch.logf("%s: %s, and the verdict cannot say so: %v", key, words, err) return } said, err := putOnce(ctx, ch.Verdicts, key, data, func(cur []byte) bool { - held, err := delivery.DecodeVerdict(cur) - return err == nil && IsUntrunked(held) && held.Failed.Words == words // the same words stand; anything else is replaced + held, err := meshdeliverycheckerclient.DecodeVerdict(cur) + return err == nil && IsUntrunked(held) && failedOf(held).Words == words // the same words stand; anything else is replaced }) if err != nil { ch.logf("%s: %s, and the verdict cannot say so: %v", key, words, err) } for _, cp := range c.Pulls { - okey, err := delivery.CheckerOwedKey(cp.Repository, cp.Number, cp.Head.Head) + okey, err := CheckerOwedKey(cp.Repository, int(cp.Number), cp.Head) if err != nil { ch.logf("%s: %v", key, err) continue } - o := delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "error", + o := meshdeliverychecker.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "error", Target: strings.TrimSuffix(ch.Link, "/") + "/" + key, Text: clipLine(words, MaxLine)} - data, err := delivery.EncodeCheckerOwed(o) + data, err := EncodeCheckerOwed(o) if err != nil { ch.logf("%s: the status of %s cannot be written: %v", key, okey, err) continue } wrote, err := putOnce(ctx, ch.Owed, okey, data, func(cur []byte) bool { - held, err := delivery.DecodeCheckerOwed(cur) - o.V = delivery.Version - return err == nil && delivery.SameOwed(held, o) + held, err := DecodeCheckerOwed(cur) + o.V = keys.Version + return err == nil && held == o }) if err != nil { ch.logf("%s: the status of %s cannot be written: %v", key, okey, err) @@ -488,11 +495,11 @@ func (ch *Checker) untrunked(ctx context.Context, c delivery.Candidate, left []P // outside takes a candidate outside the mesh (case.go Outside): no verdict, and every status owed for its pull requests // taken away, so the forge module stops writing one into a repository the mesh's forge account may not write. Said once // per heads in the log. -func (ch *Checker) outside(ctx context.Context, c delivery.Candidate) { +func (ch *Checker) outside(ctx context.Context, c meshcatalog.CandidatesByGroupValue) { var names []string for _, p := range c.Pulls { - names = append(names, fmt.Sprintf("%s#%d@%s", p.Repository, p.Number, short12(p.Head.Head))) - key, err := delivery.CheckerOwedKey(p.Repository, p.Number, p.Head.Head) + names = append(names, fmt.Sprintf("%s#%d@%s", p.Repository, p.Number, short12(p.Head))) + key, err := CheckerOwedKey(p.Repository, int(p.Number), p.Head) if err != nil { continue } @@ -508,7 +515,7 @@ func (ch *Checker) outside(ctx context.Context, c delivery.Candidate) { ch.logf("%s: the status owed of a pull request outside the mesh could not be taken away: %v", key, err) } } - said := c.Group + "@" + c.HeadsHash + said := c.Group + "@" + c.Heads ch.mu.Lock() first := !ch.outsideSaid[said] ch.outsideSaid[said] = true @@ -538,9 +545,9 @@ func (ch *Checker) prepare(ctx context.Context, group string, cs *Case) { if err != nil || !found { return } - v, err := delivery.DecodeVerdict(value) + v, err := meshdeliverycheckerclient.DecodeVerdict(value) switch { - case err != nil || v.State == delivery.VerdictRunning: + case err != nil || v.State == meshdeliverycheckerclient.VerdictRunning: return case IsNodesFailure(v): cs.Kind = KindNodes @@ -555,9 +562,9 @@ func (ch *Checker) prepare(ctx context.Context, group string, cs *Case) { return // ended unjudged when its heads went (issue 1890), or never judged for want of a trunk head (issue 1422): checked whole } cs.Kind = KindTrunk - if v.State == delivery.VerdictPass && ch.stale(v, cs) { + if v.State == meshdeliverycheckerclient.VerdictPass && ch.stale(v, cs) { cs.CarriedFrom = judgedOn(v) - if c := v.Composition; c != nil && c.State == delivery.CompositionPass && c.FPSet != "" && c.FPSet == cs.FPSet { + if c := v.Composition; c != nil && c.State == meshdeliverycheckerclient.CompositionPass && c.Fpset != "" && c.Fpset == cs.FPSet { kept := *c cs.Composition = &kept } @@ -566,7 +573,7 @@ func (ch *Checker) prepare(ctx context.Context, group string, cs *Case) { // stale says whether a verdict was judged on commits that are no longer the case's: a trunk it merged moved, or a sibling it // read at its trunk's head (ADR 0548 §6). -func (ch *Checker) stale(v delivery.Verdict, cs *Case) bool { +func (ch *Checker) stale(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case) bool { return !sameTrunk(v.Trunk, cs.Trunk) || besideMoved(v.Beside, ch.besideNow()) } @@ -603,7 +610,7 @@ func besideMoved(beside, now map[string]string) bool { } // judgedOn are the trunk and sibling commits a verdict was judged on, by repository. -func judgedOn(v delivery.Verdict) map[string]string { +func judgedOn(v meshdeliverychecker.VerdictsByGroupHeadsValue) map[string]string { out := make(map[string]string, len(v.Trunk)+len(v.Beside)) for r, c := range v.Beside { out[r] = c @@ -616,7 +623,7 @@ func judgedOn(v delivery.Verdict) map[string]string { // carried is what a carry's verdict names as carried from (ADR 0548 §5): the commits it was judged on before, of the // repositories the new verdict still judges or reads; nil when none of them moved. -func carried(from map[string]string, v delivery.Verdict) map[string]string { +func carried(from map[string]string, v meshdeliverychecker.VerdictsByGroupHeadsValue) map[string]string { out, moved := map[string]string{}, false for r, c := range from { now, ok := v.Trunk[r] @@ -636,7 +643,7 @@ func carried(from map[string]string, v delivery.Verdict) map[string]string { // moveWords say a carry's moves, for the words of a failure: "carried from to of ", each moved // repository, sorted. -func moveWords(from map[string]string, v delivery.Verdict) string { +func moveWords(from map[string]string, v meshdeliverychecker.VerdictsByGroupHeadsValue) string { var moves []string for r, c := range from { now, ok := v.Trunk[r] @@ -651,7 +658,7 @@ func moveWords(from map[string]string, v delivery.Verdict) string { return strings.Join(moves, ", ") } -func (ch *Checker) candidate(key string, fallback delivery.Candidate) delivery.Candidate { +func (ch *Checker) candidate(key string, fallback meshcatalog.CandidatesByGroupValue) meshcatalog.CandidatesByGroupValue { ch.mu.Lock() defer ch.mu.Unlock() if c, ok := ch.known[key]; ok { @@ -670,8 +677,8 @@ type claimed struct{ rev uint64 } // - a pass or a failure for the trunk heads the case has stands; one for other trunk heads (the trunk moved) is taken // again by compare-and-set on the same key, and so is a failure of the nodes after the nodes changed. func (ch *Checker) claim(ctx context.Context, cs *Case, first bool, epoch uint64) (*claimed, error) { - running := delivery.Verdict{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: delivery.VerdictRunning} - data, err := delivery.EncodeVerdict(running) + running := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: meshdeliverycheckerclient.VerdictRunning} + data, err := meshdeliverycheckerclient.EncodeVerdict(running) if err != nil { return nil, err } @@ -692,22 +699,22 @@ func (ch *Checker) claim(ctx context.Context, cs *Case, first bool, epoch uint64 } return &claimed{rev}, nil } - cur, err := delivery.DecodeVerdict(value) + cur, err := meshdeliverycheckerclient.DecodeVerdict(value) if err != nil { return nil, fmt.Errorf("%s holds what is not a verdict: %w", cs.Key, err) } retake := false switch { - case cur.State == delivery.VerdictRunning: + case cur.State == meshdeliverycheckerclient.VerdictRunning: if first { return &claimed{rev}, nil } return nil, nil case IsEnded(cur), ch.stale(cur, cs): retake = true // heads held again after their check was ended unjudged (issue 1890), or a trunk that moved - case cur.State == delivery.VerdictFail && ch.takeRebuilt(cs.Key): + case cur.State == meshdeliverycheckerclient.VerdictFail && ch.takeRebuilt(cs.Key): retake = true // every failed build it names is done again (ADR 0603 §10) - case cur.State == delivery.VerdictFail && IsNodesFailure(cur): + case cur.State == meshdeliverycheckerclient.VerdictFail && IsNodesFailure(cur): ch.mu.Lock() at, ok := ch.nodesFail[cs.Key] ch.mu.Unlock() @@ -775,15 +782,15 @@ const ConflictWords = "does not merge into the trunk: files in conflict: " // IsConflictFailure says whether a verdict failed because its head did not merge onto its trunk (ADR 0548 §7): it has no // pass to carry, so its next check, at a push or at a later trunk move, is a whole check of its head. -func IsConflictFailure(v delivery.Verdict) bool { - return v.State == delivery.VerdictFail && strings.Contains(v.Failed.Words, ConflictWords) +func IsConflictFailure(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State == meshdeliverycheckerclient.VerdictFail && strings.Contains(failedOf(v).Words, ConflictWords) } // IsNodesFailure says whether a verdict failed because a part of the mesh the check reads was not heard (UnheardWords): // it is judged again when the nodes change. No check fails for want of a build node any more: its asks wait in the build // seats' streams (ADR 0525 §6, issue 1912). -func IsNodesFailure(v delivery.Verdict) bool { - return v.State == delivery.VerdictFail && strings.Contains(v.Failed.Words, UnheardWords) +func IsNodesFailure(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State == meshdeliverycheckerclient.VerdictFail && strings.Contains(failedOf(v).Words, UnheardWords) } // try claims the check, judges it within its bound under run and writes the verdict and the owed statuses. A check that ends @@ -808,14 +815,14 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) { } began := ch.now() if cs.Watch == nil && ch.Asks != nil { - cs.Watch = func(_ string, step *delivery.Step) { + cs.Watch = func(_ string, step *buildstep.Step) { if step == nil { // put: kept before its wait starts ch.recordAsks(ctx, cs.Key, cs.Asks()) } } defer ch.forgetAsks(ctx, cs.Key) } - ch.writeOwed(ctx, cs, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{}) + ch.writeOwed(ctx, cs, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, Outcome{}) out := ch.judge(run, cs) // An ask the build seat gave up on, or whose step the build module's next process ended because it was replaced under // it, judged nothing (issue 734, IsLostAsk): the check is asked again under the same claim, its status pending, and after @@ -823,11 +830,11 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) { // read at the next nudge (HeardEnds) and judged. for try := 1; IsLostAsk(out.Verdict) && run.Err() == nil; try++ { if try >= LostAskTries { - out.Verdict.Failed.Words = strings.TrimSpace(out.Verdict.Failed.Words + "\nasked " + fmt.Sprint(LostAskTries) + + out.Verdict.Failed.Words = strings.TrimSpace(failedOf(out.Verdict).Words + "\nasked " + fmt.Sprint(LostAskTries) + " times and given up each time: " + UnheardWords) break } - ch.logf("%s: %s; asked again (%d of %d)", cs.Key, out.Verdict.Failed.Words, try+1, LostAskTries) + ch.logf("%s: %s; asked again (%d of %d)", cs.Key, failedOf(out.Verdict).Words, try+1, LostAskTries) out = ch.judge(run, cs) } if ctx.Err() != nil || errors.Is(context.Cause(run), errAskedAgain) { @@ -851,23 +858,26 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) { if cs.CarriedFrom != nil { // A carry (ADR 0548 §5): a pass names the commits it was carried from; a failure names the move it failed on. switch v.State { - case delivery.VerdictPass: + case meshdeliverycheckerclient.VerdictPass: v.CarriedFrom = carried(cs.CarriedFrom, v) - case delivery.VerdictFail: + case meshdeliverycheckerclient.VerdictFail: if moves := moveWords(cs.CarriedFrom, v); moves != "" { - v.Failed.Words = strings.TrimSpace(v.Failed.Words + "\nfailed on the carry over the move of " + moves + ": the move changed what these tests read") + v.Failed.Words = strings.TrimSpace(failedOf(v).Words + "\nfailed on the carry over the move of " + moves + ": the move changed what these tests read") } } } - if v.Took.Queue == 0 && !cs.Queued.IsZero() && began.After(cs.Queued) { + if tookOf(v).Queue == 0 && !cs.Queued.IsZero() && began.After(cs.Queued) { + if v.Took == nil { + v.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{} + } v.Took.Queue = began.Sub(cs.Queued).Milliseconds() } - data, err := delivery.EncodeVerdict(v) + data, err := meshdeliverycheckerclient.EncodeVerdict(v) if err != nil { ch.logf("%s: the verdict cannot be written: %v", cs.Key, err) - v = delivery.Verdict{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: delivery.VerdictFail, - Failed: delivery.FailedFirst{Machine: "checker", Words: "the verdict could not be written: " + err.Error()}} - if data, err = delivery.EncodeVerdict(v); err != nil { + v = meshdeliverychecker.VerdictsByGroupHeadsValue{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: meshdeliverycheckerclient.VerdictFail, + Failed: &FailedFirst{Machine: "checker", Words: "the verdict could not be written: " + err.Error()}} + if data, err = meshdeliverycheckerclient.EncodeVerdict(v); err != nil { return } } @@ -902,9 +912,9 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) { // be taken back (buildrun.Asker has no withdraw), and are no longer waited for. No status is written: the heads that // replaced these have their own check, whose pending status stands. func (ch *Checker) end(ctx context.Context, cs *Case, cl *claimed, why superseded) { - v := delivery.Verdict{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: delivery.VerdictFail, - Failed: delivery.FailedFirst{Machine: "checker", Words: why.Error()}} - data, err := delivery.EncodeVerdict(v) + v := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: meshdeliverycheckerclient.VerdictFail, + Failed: &FailedFirst{Machine: "checker", Words: why.Error()}} + data, err := meshdeliverycheckerclient.EncodeVerdict(v) if err == nil { _, err = ch.Verdicts.Update(ctx, cs.Key, data, cl.rev) } @@ -926,32 +936,32 @@ func (ch *Checker) judge(ctx context.Context, cs *Case) Outcome { var errBound = errors.New("the check ran past its bound") -func failure(machine, words string) delivery.Verdict { - return delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: machine, Words: words}} +func failure(machine, words string) meshdeliverychecker.VerdictsByGroupHeadsValue { + return meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: machine, Words: words}} } // writeOwed writes, for each pull request judged, the status the forge bridge performs: pending while the check runs, success // or failure at its end (ADR 0484 clause 2). A key already holding the same value is not written again, so the bridge // performs one act for each change; an older head's key is never touched. -func (ch *Checker) writeOwed(ctx context.Context, cs *Case, v delivery.Verdict, out Outcome) { +func (ch *Checker) writeOwed(ctx context.Context, cs *Case, v meshdeliverychecker.VerdictsByGroupHeadsValue, out Outcome) { for _, p := range cs.Pulls { - key, err := delivery.CheckerOwedKey(p.Repository, p.Number, p.Head) + key, err := CheckerOwedKey(p.Repository, int(p.Number), p.Head) if err != nil { ch.logf("%s: %v", cs.Key, err) continue } - state := map[string]string{delivery.VerdictRunning: "pending", delivery.VerdictPass: "success", delivery.VerdictFail: "failure"}[v.State] - o := delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: state, + state := map[string]string{meshdeliverycheckerclient.VerdictRunning: "pending", meshdeliverycheckerclient.VerdictPass: "success", meshdeliverycheckerclient.VerdictFail: "failure"}[v.State] + o := meshdeliverychecker.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: state, Target: strings.TrimSuffix(ch.Link, "/") + "/" + cs.Key, Text: Line(v, cs, p, out.Builds, out.Slowest, out.Judged)} - data, err := delivery.EncodeCheckerOwed(o) + data, err := EncodeCheckerOwed(o) if err != nil { ch.logf("%s: the status of %s cannot be written: %v", cs.Key, key, err) continue } wrote, err := putOnce(ctx, ch.Owed, key, data, func(cur []byte) bool { - held, err := delivery.DecodeCheckerOwed(cur) - o.V = delivery.Version - return err == nil && delivery.SameOwed(held, o) + held, err := DecodeCheckerOwed(cur) + o.V = keys.Version + return err == nil && held == o }) if err != nil { ch.logf("%s: the status of %s cannot be written: %v", cs.Key, key, err) @@ -997,7 +1007,7 @@ func (ch *Checker) Sweep(ctx context.Context) error { ch.mu.Lock() current := map[string]bool{} for _, c := range ch.known { - if k, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash); err == nil { + if k, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads); err == nil { current[k] = true } } @@ -1010,13 +1020,13 @@ func (ch *Checker) Sweep(ctx context.Context) error { if err != nil || !found { continue } - cur, err := delivery.DecodeVerdict(value) - if err != nil || cur.State != delivery.VerdictRunning { + cur, err := meshdeliverycheckerclient.DecodeVerdict(value) + if err != nil || cur.State != meshdeliverycheckerclient.VerdictRunning { continue } - cur.State = delivery.VerdictFail - cur.Failed = delivery.FailedFirst{Machine: "checker", Words: EndedWords + "closed at start: left running, and no candidate holds these heads any more, so nothing will check them"} - data, err := delivery.EncodeVerdict(cur) + cur.State = meshdeliverycheckerclient.VerdictFail + cur.Failed = &FailedFirst{Machine: "checker", Words: EndedWords + "closed at start: left running, and no candidate holds these heads any more, so nothing will check them"} + data, err := meshdeliverycheckerclient.EncodeVerdict(cur) if err != nil { return err } @@ -1035,7 +1045,7 @@ func (ch *Checker) Sweep(ctx context.Context) error { func (ch *Checker) TrunkMoved(ctx context.Context) { ch.init() ch.mu.Lock() - known := make(map[string]delivery.Candidate, len(ch.known)) + known := make(map[string]meshcatalog.CandidatesByGroupValue, len(ch.known)) for k, c := range ch.known { known[k] = c } @@ -1050,23 +1060,23 @@ func (ch *Checker) TrunkMoved(ctx context.Context) { if err != nil || !found { continue // never judged: no status to put out of date } - v, err := delivery.DecodeVerdict(value) + v, err := meshdeliverycheckerclient.DecodeVerdict(value) if err == nil && !ch.stale(v, cs) { continue } - if err == nil && v.State == delivery.VerdictPass { + if err == nil && v.State == meshdeliverycheckerclient.VerdictPass { cs.CarriedFrom = judgedOn(v) // the status says it is being carried to the new commit (ADR 0548 §6) } - ch.writeOwed(ctx, cs, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{}) + ch.writeOwed(ctx, cs, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, Outcome{}) } ch.Reconsider(ctx) } // owePending owes mesh/check pending on the heads of a verdict judged on commits that moved while it ran: a pass says it is // being carried to the trunk's head now (ADR 0548 §6), anything else that it is checked again. -func (ch *Checker) owePending(ctx context.Context, judged *Case, v delivery.Verdict) { +func (ch *Checker) owePending(ctx context.Context, judged *Case, v meshdeliverychecker.VerdictsByGroupHeadsValue) { next := judged - ckey, err := delivery.CandidatesKey(v.Group) + ckey, err := meshcatalogclient.CandidatesKey(v.Group) ch.mu.Lock() c, ok := ch.known[ckey] ch.mu.Unlock() @@ -1075,10 +1085,10 @@ func (ch *Checker) owePending(ctx context.Context, judged *Case, v delivery.Verd next = now } } - if v.State == delivery.VerdictPass { + if v.State == meshdeliverycheckerclient.VerdictPass { next.CarriedFrom = judgedOn(v) } - ch.writeOwed(ctx, next, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{}) + ch.writeOwed(ctx, next, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, Outcome{}) } // movedSince says whether a trunk the case was judged on is no longer the trunk's head. @@ -1134,7 +1144,7 @@ func (ch *Checker) hearFailedBuilds(ctx context.Context, group string, cs *Case) if err != nil || !found { return } - if v, err := delivery.DecodeVerdict(value); err == nil { + if v, err := meshdeliverycheckerclient.DecodeVerdict(value); err == nil { if builds := failedBuilds(v, cs); builds != nil { ch.awaitRebuilt(ctx, group, cs.Key, builds) } diff --git a/modules/mesh-delivery-checker/internal/check/checker_test.go b/modules/mesh-delivery-checker/internal/check/checker_test.go index 60e203af..3ead2d80 100644 --- a/modules/mesh-delivery-checker/internal/check/checker_test.go +++ b/modules/mesh-delivery-checker/internal/check/checker_test.go @@ -9,8 +9,14 @@ import ( "testing" "time" - "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/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "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/sdk/buildstep" + "git.novox.be/novox/mesh-sdk/go/keys" ) const hqRepo = "novox/hq" @@ -27,30 +33,30 @@ const ( ) // candidateOf is a candidate of a group with the given pull requests, as the indexer writes one. -func candidateOf(t testing.TB, group string, pulls ...delivery.CandidatePull) []byte { +func candidateOf(t testing.TB, group string, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) []byte { t.Helper() - 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, err := delivery.HeadsHash(heads) + hash, err := meshdeliveryclient.HeadsHash(heads) if err != nil { t.Fatal(err) } - c := delivery.Candidate{V: delivery.Version, Group: group, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)} - data, err := delivery.EncodeCandidate(c) + c := meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: group, Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)} + data, err := json.Marshal(c) if err != nil { t.Fatal(err) } return data } -func pullOf(repo string, number int, head string) delivery.CandidatePull { - return delivery.CandidatePull{Head: delivery.Head{Repository: repo, Number: number, Head: head}, Modules: []string{}} +func pullOf(repo string, number int, head string) meshcatalog.CandidatesByGroupValuePullsItem { + return meshcatalog.CandidatesByGroupValuePullsItem{Repository: repo, Number: int64(number), Head: head, Modules: []string{}} } // newRepoPullOf is a pull request of a new repository of the mesh: its trunk not named yet, its first module added. -func newRepoPullOf(repo string, number int, head string) delivery.CandidatePull { +func newRepoPullOf(repo string, number int, head string) meshcatalog.CandidatesByGroupValuePullsItem { p := pullOf(repo, number, head) p.Modules = []string{"mod-u"} return p @@ -73,9 +79,9 @@ func (j *judgeFunc) Judge(ctx context.Context, c *Case) (Outcome, error) { func (j *judgeFunc) count() int { j.mu.Lock(); defer j.mu.Unlock(); return j.calls } -func passing() delivery.Verdict { - return delivery.Verdict{State: delivery.VerdictPass, Levels: map[string]delivery.LevelResult{ - delivery.LevelUnit: {Run: 2, Kept: 3}, delivery.LevelIntegration: {Run: 1}}} +func passing() meshdeliverychecker.VerdictsByGroupHeadsValue { + return meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictPass, Levels: map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{ + buildstep.LevelUnit: {Run: 2, Kept: 3}, buildstep.LevelIntegration: {Run: 1}}} } func passJudge() *judgeFunc { @@ -99,20 +105,20 @@ func newRig(t testing.TB, j Judge) *rig { func (r *rig) setTrunks(t testing.TB, heads map[string]string) bool { t.Helper() - data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads}) + data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: heads}) if err != nil { t.Fatal(err) } return r.trunks.Put(data, false) } -func (r *rig) verdict(t testing.TB, key string) delivery.Verdict { +func (r *rig) verdict(t testing.TB, key string) meshdeliverychecker.VerdictsByGroupHeadsValue { t.Helper() raw, _, found, _ := r.verdicts.Get(context.Background(), key) if !found { t.Fatalf("no verdict %s; the bucket holds %v", key, r.verdicts.vals) } - v, err := delivery.DecodeVerdict(raw) + v, err := meshdeliverycheckerclient.DecodeVerdict(raw) if err != nil { t.Fatal(err) } @@ -121,11 +127,11 @@ func (r *rig) verdict(t testing.TB, key string) delivery.Verdict { func keyOf(t testing.TB, group string, value []byte) string { t.Helper() - c, err := delivery.DecodeCandidate(value) + c, err := meshcatalog.DecodeCandidatesByGroupValue(value) if err != nil { t.Fatal(err) } - k, err := delivery.CheckerVerdictKey(group, c.HeadsHash) + k, err := meshdeliverycheckerclient.VerdictKey(group, c.Heads) if err != nil { t.Fatal(err) } @@ -139,7 +145,7 @@ func TestACandidateNotYetCheckedGetsItsVerdictKeyCreatedOnceAndIsJudged(t *testi r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() key := keyOf(t, "feat/x", cand) - if v := r.verdict(t, key); v.State != delivery.VerdictPass || v.Group != "feat/x" || v.Trunk[repoA] != trunkA { + if v := r.verdict(t, key); v.State != meshdeliverycheckerclient.VerdictPass || v.Group != "feat/x" || v.Trunk[repoA] != trunkA { t.Fatalf("verdict = %+v", v) } if r.verdicts.creates[key] != 1 || j.count() != 1 { @@ -171,14 +177,14 @@ func TestASecondInstancesCreateIsRefusedAndItDoesNothing(t *testing.T) { // N6/A r := newRig(t, j) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) key := keyOf(t, "feat/x", cand) - other, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], State: delivery.VerdictRunning}) + other, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], State: meshdeliverycheckerclient.VerdictRunning}) r.ch.Verdicts = racyKV{r.verdicts, other} r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() if j.count() != 0 { t.Errorf("the instance that lost the create judged %d times, want none", j.count()) } - if v := r.verdict(t, key); v.State != delivery.VerdictRunning { + if v := r.verdict(t, key); v.State != meshdeliverycheckerclient.VerdictRunning { t.Errorf("the other instance's key was changed: %+v", v) } } @@ -188,13 +194,13 @@ func TestARestartWithARunningKeyJudgesItAgain(t *testing.T) { // N6/AC2 r := newRig(t, j) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) key := keyOf(t, "feat/x", cand) - left, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], Trunk: map[string]string{repoA: trunkA}, State: delivery.VerdictRunning}) + left, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], Trunk: map[string]string{repoA: trunkA}, State: meshdeliverycheckerclient.VerdictRunning}) if _, err := r.verdicts.Create(ctx, key, left); err != nil { t.Fatal(err) } r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() - if j.count() != 1 || r.verdict(t, key).State != delivery.VerdictPass { + if j.count() != 1 || r.verdict(t, key).State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("judged %d times, verdict %+v, want the left check judged once and passed", j.count(), r.verdict(t, key)) } } @@ -212,7 +218,7 @@ func TestACheckPastItsBoundFailsNamingTheStuckStep(t *testing.T) { // N6/AC3 r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() v := r.verdict(t, keyOf(t, "feat/x", cand)) - if v.State != delivery.VerdictFail || v.Failed.Machine != "shanks" || !strings.Contains(v.Failed.Words, "stuck at: test the unit level of mesh-delivery") { + if v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Machine != "shanks" || !strings.Contains(v.Failed.Words, "stuck at: test the unit level of mesh-delivery") { t.Fatalf("verdict = %+v, want a failure naming shanks and the stuck step", v) } } @@ -242,7 +248,7 @@ func TestAnUnheardForgeFailsWithTheReasonAndIsJudgedAgainWhenTheNodesChange(t *t } r.ch.NodesChanged(ctx) r.ch.Wait() - if j.count() != 2 || r.verdict(t, key).State != delivery.VerdictPass { + if j.count() != 2 || r.verdict(t, key).State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("after the nodes changed: judged %d times, verdict %+v", j.count(), r.verdict(t, key)) } } @@ -275,7 +281,7 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi t.Errorf("said %d times in the log, want once for the head: %q", n, logged) } reader := func() Reader { - ckey, _ := delivery.CandidatesKey("feat/y") + ckey, _ := meshcatalogclient.CandidatesKey("feat/y") return Reader{CandidateState: fakeState{ckey: cand}.read(), VerdictState: fakeState(r.verdicts.vals).read(), Trunks: func() (map[string]string, error) { return r.trunks.Heads(), nil }} } @@ -294,7 +300,7 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS, unknown: trunkA2}) r.ch.TrunkMoved(ctx) r.ch.Wait() - if v := r.verdict(t, key); j.count() != 1 || v.State != delivery.VerdictPass || v.Trunk[unknown] != trunkA2 { + if v := r.verdict(t, key); j.count() != 1 || v.State != meshdeliverycheckerclient.VerdictPass || v.Trunk[unknown] != trunkA2 { t.Errorf("judged %d times, verdict %+v, want one pass on %s", j.count(), v, trunkA2) } if s := owedState(t, r, unknown, 4, head1); s != "success" { @@ -306,15 +312,15 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi // running verdict left for heads that cannot be judged is replaced; a verdict key deleted is judged again on its delete. for _, row := range []struct { name string - pulls []delivery.CandidatePull + pulls []meshcatalog.CandidatesByGroupValuePullsItem running bool // a running verdict is left under the key first deleted bool // the key is deleted after its verdict judged int want string // the verdict's state, and for a failure the repository it names }{ - {name: "a member the checker cannot claim", pulls: []delivery.CandidatePull{pullOf(repoA, 9, head2), pullOf("novox/other", 2, head2)}, want: "novox/other"}, - {name: "a running verdict left for heads that cannot be judged", pulls: []delivery.CandidatePull{newRepoPullOf(unknown, 5, head2)}, running: true, want: unknown}, - {name: "a verdict key deleted", pulls: []delivery.CandidatePull{pullOf(repoA, 5, head2)}, deleted: true, judged: 2, want: delivery.VerdictPass}, + {name: "a member the checker cannot claim", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 9, head2), pullOf("novox/other", 2, head2)}, want: "novox/other"}, + {name: "a running verdict left for heads that cannot be judged", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{newRepoPullOf(unknown, 5, head2)}, running: true, want: unknown}, + {name: "a verdict key deleted", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 5, head2)}, deleted: true, judged: 2, want: meshdeliverycheckerclient.VerdictPass}, } { t.Run(row.name, func(t *testing.T) { j := passJudge() @@ -322,8 +328,8 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi cand := candidateOf(t, "feat/z", row.pulls...) key := keyOf(t, "feat/z", cand) if row.running { - group, heads, _ := delivery.ParseCheckerVerdictKey(key) - raw, _ := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, State: delivery.VerdictRunning}) + group, heads, _ := meshdeliverycheckerclient.ParseVerdictKey(key) + raw, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group, Heads: heads, State: meshdeliverycheckerclient.VerdictRunning}) _, _ = r.verdicts.Create(ctx, key, raw) } r.ch.Candidate(ctx, "feat%2Fz", cand) @@ -334,8 +340,8 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi r.ch.Wait() } v := r.verdict(t, key) - if row.want == delivery.VerdictPass { - if v.State != delivery.VerdictPass || j.count() != row.judged { + if row.want == meshdeliverycheckerclient.VerdictPass { + if v.State != meshdeliverycheckerclient.VerdictPass || j.count() != row.judged { t.Errorf("verdict %s after %d judgings, want a pass after %d", v.State, j.count(), row.judged) } return @@ -344,7 +350,7 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi t.Errorf("verdict %+v after %d judgings, want a failure naming %s and none judged", v, j.count(), row.want) } for _, p := range row.pulls { - if s := owedState(t, r, p.Repository, p.Number, p.Head.Head); s != "error" { + if s := owedState(t, r, p.Repository, int(p.Number), p.Head); s != "error" { t.Errorf("%s#%d owes %q, want error", p.Repository, p.Number, s) } } @@ -391,7 +397,7 @@ func TestATrunkMoveRewritesTheSameVerdictKeyAndTheHeadsStatus(t *testing.T) { // r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() _, rev1, _, _ := r.verdicts.Get(ctx, key) - owedKey, _ := delivery.CheckerOwedKey(repoA, 7, head1) + owedKey, _ := CheckerOwedKey(repoA, 7, head1) _, orev1, _, _ := r.owed.Get(ctx, owedKey) if !r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) { t.Fatal("the trunks did not change") @@ -427,12 +433,12 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T var seen []string var r *rig j := &judgeFunc{f: func(ctx context.Context, c *Case, _ int) (Outcome, error) { - key, _ := delivery.CheckerOwedKey(repoA, 7, head1) + key, _ := CheckerOwedKey(repoA, 7, head1) raw, _, _, _ := r.owed.Get(ctx, key) - o, _ := delivery.DecodeCheckerOwed(raw) + o, _ := DecodeCheckerOwed(raw) seen = append(seen, o.State) - c.series(repoA+"#7", []string{"web/pkg series 0.1 → 0.2"}, []delivery.SeriesChange{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}) - return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "m/a.TestX"}}, + c.series(repoA+"#7", []string{"web/pkg series 0.1 → 0.2"}, []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}) + return Outcome{Verdict: meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "m/a.TestX"}}, OutOfStep: map[string][]string{repoA + "#7": {"go/seats/x/contract.go"}}}, nil }} r = newRig(t, j) @@ -440,19 +446,19 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T r.ch.Says = says r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1))) r.ch.Wait() - key, _ := delivery.CheckerOwedKey(repoA, 7, head1) + key, _ := CheckerOwedKey(repoA, 7, head1) raw, _, _, _ := r.owed.Get(ctx, key) - o, err := delivery.DecodeCheckerOwed(raw) + o, err := DecodeCheckerOwed(raw) if err != nil { t.Fatal(err) } - if len(seen) != 1 || seen[0] != "pending" || o.State != "failure" || o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Target, "https://view.invalid/checks/") { + if len(seen) != 1 || seen[0] != "pending" || o.State != "failure" || o.Context != meshdeliveryclient.StatusContextCheck || !strings.HasPrefix(o.Target, "https://view.invalid/checks/") { t.Errorf("while running %v, at the end %+v", seen, o) } // DLV-C2 (design 26 *Out of step*): the check's end says one verdict event for the pull request, naming its out-of-step // files; the running status says none. if len(says.said) != 1 || says.said[0].Repository != repoA || says.said[0].Pull != 7 || says.said[0].Head != head1 || - says.said[0].Verdict != delivery.VerdictFail || strings.Join(says.said[0].OutOfStep, ",") != "go/seats/x/contract.go" || + says.said[0].Verdict != meshdeliverycheckerclient.VerdictFail || strings.Join(says.said[0].OutOfStep, ",") != "go/seats/x/contract.go" || // The join (A2j finding 2): and its series changes, as the seat's event names them. len(says.said[0].Series) != 1 || says.said[0].Series[0] != (meshdeliverychecker.VerdictHolderEventSeriesItem{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}) { t.Errorf("verdict events %+v", says.said) @@ -480,14 +486,14 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) { r := newRig(t, j) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) live := keyOf(t, "feat/x", cand) - dead, _ := delivery.CheckerVerdictKey("feat/gone", "0123456789ab") + dead, _ := meshdeliverycheckerclient.VerdictKey("feat/gone", "0123456789ab") for _, k := range []string{live, dead} { - group, heads, _ := delivery.ParseCheckerVerdictKey(k) - raw, _ := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, State: delivery.VerdictRunning}) + group, heads, _ := meshdeliverycheckerclient.ParseVerdictKey(k) + raw, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group, Heads: heads, State: meshdeliverycheckerclient.VerdictRunning}) _, _ = r.verdicts.Create(ctx, k, raw) } r.ch.init() - var c delivery.Candidate + var c meshcatalog.CandidatesByGroupValue _ = json.Unmarshal(cand, &c) r.ch.known["feat%2Fx"] = c if err := r.ch.Sweep(ctx); err != nil { @@ -496,7 +502,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) { if v := r.verdict(t, dead); !IsEnded(v) || !strings.Contains(v.Failed.Words, "closed at start") { t.Errorf("the dead key = %+v", v) } - if v := r.verdict(t, live); v.State != delivery.VerdictRunning { + if v := r.verdict(t, live); v.State != meshdeliverycheckerclient.VerdictRunning { t.Errorf("the live key = %+v, want it left for the check to take over", v) } @@ -519,7 +525,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) { r.ch.Candidate(ctx, "feat%2Fx", alone) <-started r.ch.Candidate(ctx, "feat%2Fx", both) - var bothC delivery.Candidate + var bothC meshcatalog.CandidatesByGroupValue _ = json.Unmarshal(both, &bothC) select { case err := <-ended: @@ -533,10 +539,10 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) { if v := r.verdict(t, live); !IsEnded(v) || v.Failed.Words != EndedWords+"superseded by "+GateHeads(bothC) { t.Errorf("the superseded verdict = %+v", v.Failed) } - if v := r.verdict(t, keyOf(t, "feat/x", both)); v.State != delivery.VerdictPass { + if v := r.verdict(t, keyOf(t, "feat/x", both)); v.State != meshdeliverycheckerclient.VerdictPass { t.Errorf("the group's verdict = %s", v.State) } - okey, _ := delivery.CheckerOwedKey(repoA, 7, head1) + okey, _ := CheckerOwedKey(repoA, 7, head1) if raw, _, _, _ := r.owed.Get(ctx, okey); !strings.Contains(string(raw), `"success"`) { t.Errorf("the head's status = %s, want the group's success, never the ended check's", raw) } @@ -554,7 +560,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) { // The member leaves again: the ended heads are held again and checked whole. r.ch.Candidate(ctx, "feat%2Fx", alone) r.ch.Wait() - if v := r.verdict(t, live); v.State != delivery.VerdictPass || j.count() != 3 { + if v := r.verdict(t, live); v.State != meshdeliverycheckerclient.VerdictPass || j.count() != 3 { t.Errorf("the heads held again = %s after %d judgings, want a pass of a third", v.State, j.count()) } @@ -575,7 +581,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) { if _, _, found, _ := r.verdicts.Get(ctx, keyOf(t, "feat/y", sdk)); found || j.count() != 1 { t.Errorf("the partial set has a verdict (%v) or the judge ran %d times, want the whole set alone", found, j.count()) } - if v := r.verdict(t, keyOf(t, "feat/y", whole)); v.State != delivery.VerdictPass { + if v := r.verdict(t, keyOf(t, "feat/y", whole)); v.State != meshdeliverycheckerclient.VerdictPass { t.Errorf("the whole set = %s", v.State) } } @@ -588,7 +594,7 @@ func TestTheForgeBridgeIsToldOnceForEachChangeOfAStatus(t *testing.T) { // 1683 cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() - key, _ := delivery.CheckerOwedKey(repoA, 7, head1) + key, _ := CheckerOwedKey(repoA, 7, head1) want := []string{EventOwedWritten + " " + key, EventOwedWritten + " " + key} // pending, then success if len(told) != 2 || told[0] != want[0] || told[1] != want[1] { t.Fatalf("told %v, want the status said when pending and when success", told) @@ -609,8 +615,8 @@ func TestACandidateOutsideTheMeshIsJudgedNothingAndOwesNothing(t *testing.T) { var logged []string r.ch.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) } const theirs = "someone/own-app" - okey, _ := delivery.CheckerOwedKey(theirs, 55, head1) - before, _ := delivery.EncodeCheckerOwed(delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "error", Text: "cannot be judged"}) + okey, _ := CheckerOwedKey(theirs, 55, head1) + before, _ := EncodeCheckerOwed(meshdeliverychecker.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "error", Text: "cannot be judged"}) if _, err := r.owed.Create(ctx, okey, before); err != nil { t.Fatal(err) } @@ -629,7 +635,7 @@ func TestACandidateOutsideTheMeshIsJudgedNothingAndOwesNothing(t *testing.T) { if n := countWith(logged, "outside the mesh"); n != 1 { t.Errorf("said %d times in the log, want once: %q", n, logged) } - ckey, _ := delivery.CandidatesKey("feat/theirs") + ckey, _ := meshcatalogclient.CandidatesKey("feat/theirs") st, err := Reader{CandidateState: fakeState{ckey: cand}.read(), VerdictState: fakeState(r.verdicts.vals).read(), Trunks: func() (map[string]string, error) { return r.trunks.Heads(), nil }}.Status() if err != nil { diff --git a/modules/mesh-delivery-checker/internal/check/composition.go b/modules/mesh-delivery-checker/internal/check/composition.go index 0501844b..9acc7e50 100644 --- a/modules/mesh-delivery-checker/internal/check/composition.go +++ b/modules/mesh-delivery-checker/internal/check/composition.go @@ -3,26 +3,40 @@ package check import ( "strings" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" ) // withComposition is v with the check's composition taken in: a failed one fails the check, naming its machine. -func withComposition(v *delivery.Verdict, comp delivery.CompositionVerdict) { - v.Composition = comp.Composition() +func withComposition(v *meshdeliverychecker.VerdictsByGroupHeadsValue, comp meshwants.CompositionsByGroupHeadsValue) { + v.Composition = compositionOf(comp) + if v.Took == nil { + v.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{} + } v.Took.Composition = comp.Took - if comp.State == delivery.CompositionPass && len(comp.Warnings) > 0 && v.Composition != nil { + if comp.State == meshdeliverycheckerclient.CompositionPass && len(comp.Warnings) > 0 && v.Composition != nil { v.Composition.Words = strings.Join(comp.Warnings, "; ") // passed with a warning (ADR 0593 §9) } - if comp.State == delivery.CompositionFail { + if comp.State == meshdeliverycheckerclient.CompositionFail { machine := comp.Machine if machine == "" { machine = "the composition" } - f := delivery.FailedFirst{Machine: machine, Words: comp.Words} - if v.State == delivery.VerdictFail { + f := FailedFirst{Machine: machine, Words: comp.Words} + if v.State == meshdeliverycheckerclient.VerdictFail { addFailure(v, f) // the tests failed too: the composition is named after them (the operator, 2026-11-02) return } - v.State, v.Failed = delivery.VerdictFail, f + v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &f } } + +// compositionOf is what the checker writes into its verdict from the assigner's composition answer c: nil while c waits (the +// check is still running). +func compositionOf(c meshwants.CompositionsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValueComposition { + if c.State == compositionWaits { + return nil + } + return &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: c.State, Words: c.Words, Sdk: c.Sdk, Machines: c.Machines} +} diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index 9ba4c332..45c9b3e1 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -8,24 +8,28 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" + buildsrec "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // fakeCompositions answers a check's composition from memory, recording the keys asked. type fakeCompositions struct { - v delivery.CompositionVerdict + v meshwants.CompositionsByGroupHeadsValue err error asked []string // waits answers only when its context ends. waits bool } -func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error) { +func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) { f.asked = append(f.asked, c.Key) if f.waits { <-ctx.Done() - return delivery.CompositionVerdict{}, ctx.Err() + return meshwants.CompositionsByGroupHeadsValue{}, ctx.Err() } return f.v, f.err } @@ -35,12 +39,12 @@ func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (delivery.Compo func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) { t.Run("a change to a build module or base names the modules built on it", aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt) b := newBench(t) - f := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "v0.1.14-x", Machines: []string{"g14"}, Took: 40}} + f := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.CompositionPass, Sdk: "v0.1.14-x", Machines: []string{"g14"}, Took: 40}} b.ex.Compositions = f cs := caseOf(t, pullOf(repoA, 7, head1)) cs.FPSet = "fps-of-the-candidate" v := b.judge(t, cs).Verdict - if v.State != delivery.VerdictPass || v.Composition == nil || v.Composition.State != delivery.CompositionPass || v.Composition.SDK != "v0.1.14-x" || v.Took.Composition != 40 { + if v.State != meshdeliverycheckerclient.VerdictPass || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionPass || v.Composition.Sdk != "v0.1.14-x" || v.Took.Composition != 40 { t.Fatalf("verdict = %+v, composition %+v", v, v.Composition) } if len(f.asked) != 1 || f.asked[0] != cs.Key { @@ -49,32 +53,32 @@ func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) { if !compositionMentions(v, "passed") { t.Errorf("the line says %q", compositionWords(v.Composition, v.Took.Composition)) } - if v.Composition.FPSet == "" || v.Composition.FPSet != cs.FPSet { - t.Errorf("the composition names the fingerprint set %q, want the candidate's %q (ADR 0548 §5)", v.Composition.FPSet, cs.FPSet) + if v.Composition.Fpset == "" || v.Composition.Fpset != cs.FPSet { + t.Errorf("the composition names the fingerprint set %q, want the candidate's %q (ADR 0548 §5)", v.Composition.Fpset, cs.FPSet) } // ADR 0548 §5: a carry whose fingerprints did not change keeps the composition it passed and asks the assigner nothing. carry := caseOf(t, pullOf(repoA, 7, head1)) carry.CarriedFrom, carry.Composition = map[string]string{repoA: trunkA2}, v.Composition - if kept := b.judge(t, carry).Verdict; kept.Composition == nil || kept.Composition.SDK != "v0.1.14-x" || len(f.asked) != 1 { + if kept := b.judge(t, carry).Verdict; kept.Composition == nil || kept.Composition.Sdk != "v0.1.14-x" || len(f.asked) != 1 { t.Errorf("the carry's composition is %+v and the assigner was asked %d times; want the kept one and no second ask", kept.Composition, len(f.asked)) } } -func compositionMentions(v delivery.Verdict, word string) bool { +func compositionMentions(v meshdeliverychecker.VerdictsByGroupHeadsValue, word string) bool { return strings.Contains(compositionWords(v.Composition, v.Took.Composition), word) } // 1297/AC2 at the checker: a failed composition fails the check, naming the machine it failed at and its words. func TestAFailedCompositionFailsTheCheck(t *testing.T) { b := newBench(t) - b.ex.Compositions = &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionFail, SDK: "x", + b.ex.Compositions = &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.CompositionFail, Sdk: "x", Machine: "g14", Words: "g14 runs web, which the change removes from its source"}} v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - if v.State != delivery.VerdictFail || v.Failed.Machine != "g14" || v.Composition == nil || v.Composition.State != delivery.CompositionFail { + if v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Machine != "g14" || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionFail { t.Fatalf("verdict = %+v", v) } v.Group, v.Heads = "feat/x", "0123456789ab" // the checker names them when it writes the verdict - if _, err := delivery.EncodeVerdict(v); err != nil { + if _, err := meshdeliverycheckerclient.EncodeVerdict(v); err != nil { t.Errorf("the verdict cannot be written: %v", err) } } @@ -85,11 +89,11 @@ func TestACompositionNotHadIsNotRun(t *testing.T) { f := &fakeCompositions{err: errors.New("the check's bound passed")} b.ex.Compositions = f v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - if v.State != delivery.VerdictPass || v.Composition == nil || v.Composition.State != delivery.CompositionNotRun { + if v.State != meshdeliverycheckerclient.VerdictPass || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun { t.Fatalf("verdict = %+v", v) } b2 := newBench(t) - f2 := &fakeCompositions{v: delivery.CompositionVerdict{State: delivery.CompositionFail, Words: "w", SDK: "x"}} + f2 := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{State: meshdeliverycheckerclient.CompositionFail, Words: "w", Sdk: "x"}} b2.ex.Compositions = f2 cs := caseOf(t, pullOf(repoA, 7, head1)) cs.Try = &TryOptions{} @@ -113,12 +117,12 @@ type fakeEnds struct { early bool } -func (f *fakeEnds) Ended(_ context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) { - f.asked = builds +func (f *fakeEnds) Ended(_ context.Context, asked map[string]string) (map[string]buildsrec.BuilderBuild, error) { + f.asked = asked f.early = f.composed != nil && len(f.composed.asked) > 0 - out := map[string]delivery.BuilderBuild{} - for module, fp := range builds { - if b, ok := f.builds[module+" "+fp]; ok && (b.State == delivery.BuildDone || b.State == delivery.BuildFailed) { + out := map[string]buildsrec.BuilderBuild{} + for module, fp := range asked { + if b, ok := f.builds[module+" "+fp]; ok && (b.State == buildsrec.BuildDone || b.State == buildsrec.BuildFailed) { out[module] = b } } @@ -154,12 +158,12 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { fp := "src2:" + dig("f") goBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "bin", Kind: catalogue.ArtifactBundle, Language: "go"}}} tsBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "app", Kind: catalogue.ArtifactBundle, Language: "typescript"}}} - done := delivery.BuilderBuild{State: delivery.BuildDone} + done := buildsrec.BuilderBuild{State: buildsrec.BuildDone} for _, row := range []struct { name string manifest map[string]catalogue.Manifest - builds map[string]delivery.BuilderBuild // by module, at fp; nil leaves the check without the builds' ends - runs knownRuns // nil: the trunk holds every module + builds map[string]buildsrec.BuilderBuild // by module, at fp; nil leaves the check without the builds' ends + runs knownRuns // nil: the trunk holds every module want []string waited string // the modules waited on, sorted failed string // the failure's test, when the check fails on a build @@ -169,37 +173,37 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { {name: "the Go build module invokes a verb no running build serves", manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild, Invokes: []string{"seat:package-registry.publish", "seat:artifact-store.put"}}, "web": {Module: "web", Build: goBuild}}, - builds: map[string]delivery.BuilderBuild{"mesh-build-go": done}, served: []string{"seat:artifact-store.put"}, + builds: map[string]buildsrec.BuilderBuild{"mesh-build-go": done}, served: []string{"seat:artifact-store.put"}, want: []string{"the changed build module mesh-build-go invokes seat:package-registry.publish, which no running build serves, " + "so the check cannot run it: web, which it builds, are composed from their manifests (ADR 0614 §7)"}, waited: "mesh-build-go"}, {name: "the Go build module changes", manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild}, "web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}}, - builds: map[string]delivery.BuilderBuild{"mesh-build-go": done, "docs": done}, + builds: map[string]buildsrec.BuilderBuild{"mesh-build-go": done, "docs": done}, want: []string{"the build module mesh-build-go: mail, web, which it builds"}, waited: "docs, mesh-build-go"}, {name: "the Node build module changes", manifest: map[string]catalogue.Manifest{"mesh-build-node": {Module: "mesh-build-node", Build: goBuild}, "panel": {Module: "panel", Build: tsBuild}, "web": {Module: "web", Build: goBuild}}, - builds: map[string]delivery.BuilderBuild{"mesh-build-node": done, "web": done}, + builds: map[string]buildsrec.BuilderBuild{"mesh-build-node": done, "web": done}, want: []string{"the build module mesh-build-node: panel, which it builds"}, waited: "mesh-build-node, web"}, {name: "a base changes", manifest: map[string]catalogue.Manifest{"image": {Module: "image"}, "web": {Module: "web", Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: "image", Artifact: "img"}}}}}, - builds: map[string]delivery.BuilderBuild{"image": done}, + builds: map[string]buildsrec.BuilderBuild{"image": done}, want: []string{"alters image, a base of web", "waits on the build of image alone"}, waited: "image"}, {name: "neither changes", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, - builds: map[string]delivery.BuilderBuild{"web": done}, waited: "web"}, + builds: map[string]buildsrec.BuilderBuild{"web": done}, waited: "web"}, {name: "a build fails", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}}, - builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildFailed, Error: "go build: undefined: x"}, "mail": done}, + builds: map[string]buildsrec.BuilderBuild{"web": {State: buildsrec.BuildFailed, Error: "go build: undefined: x"}, "mail": done}, waited: "mail, web", failed: "build: web"}, {name: "the tests fail", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, - builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildBuilding}}, tests: true, + builds: map[string]buildsrec.BuilderBuild{"web": {State: buildsrec.BuildBuilding}}, tests: true, want: []string{"composes at once, without waiting on the builds of web"}}, {name: "a build does not end within the bound", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, - builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildBuilding}}, + builds: map[string]buildsrec.BuilderBuild{"web": {State: buildsrec.BuildBuilding}}, want: []string{"the builds of web did not end within the build-wait-bound"}, waited: "web"}, {name: "a module the trunk does not hold", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "new": {Module: "new", Build: goBuild}}, - builds: map[string]delivery.BuilderBuild{"web": done}, runs: knownRuns{"web"}, + builds: map[string]buildsrec.BuilderBuild{"web": done}, runs: knownRuns{"web"}, want: []string{"new: the trunk holds no record of it yet"}, waited: "web"}, {name: "the builds cannot be heard", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}}, want: []string{"the builds of web cannot be heard"}}, @@ -212,7 +216,7 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { return r, nil } } - comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}} + comps := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.CompositionPass, Sdk: "x", Machines: []string{"g14"}}} b.ex.Compositions = comps var ends *fakeEnds if row.builds != nil { @@ -228,10 +232,10 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { if row.served != nil { b.ex.Serving = fixedServing(row.served) } - read, fps := fixedManifests{}, map[string]delivery.CandidateModule{} + read, fps := fixedManifests{}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{} for module, m := range row.manifest { d := dig(string(rune('a' + len(read)))) - read[d], fps[module] = m, delivery.CandidateModule{FP2: fp, Manifest: d} + read[d], fps[module] = m, meshcatalog.CandidatesByGroupValuePullsItemFpsValue{Fp2: fp, Manifest: d} } b.ex.Read = read p := pullOf(catalogueRepository, 9, head1) @@ -246,12 +250,12 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) { } } switch { - case row.failed != "" && (v.State != delivery.VerdictFail || v.Failed.Test != row.failed || !strings.Contains(v.Failed.Words, "undefined: x") || + case row.failed != "" && (v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Test != row.failed || !strings.Contains(v.Failed.Words, "undefined: x") || !strings.Contains(v.Failed.Words, "the check asks itself again when the build is done")): t.Errorf("%s: the verdict is %s failing %+v, want it failed naming %s, the builder's words and how it is asked again", row.name, v.State, v.Failed, row.failed) - case row.tests && v.State != delivery.VerdictFail: + case row.tests && v.State != meshdeliverycheckerclient.VerdictFail: t.Errorf("%s: %+v, want the tests' failure", row.name, v) - case row.failed == "" && !row.tests && v.State != delivery.VerdictPass: + case row.failed == "" && !row.tests && v.State != meshdeliverycheckerclient.VerdictPass: t.Errorf("%s: %+v", row.name, v) } words := compositionWords(v.Composition, v.Took.Composition) diff --git a/modules/mesh-delivery-checker/internal/check/digestform_test.go b/modules/mesh-delivery-checker/internal/check/digestform_test.go index c06a2f98..0e1a3f2e 100644 --- a/modules/mesh-delivery-checker/internal/check/digestform_test.go +++ b/modules/mesh-delivery-checker/internal/check/digestform_test.go @@ -5,7 +5,8 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // 1930/AC3: given a check ask whose stored end keeps the digest's packages as a list of {package, digest} (issue 1930), @@ -19,17 +20,17 @@ func TestKeptVerdictsAreReadFromTheDigestsListForm(t *testing.T) { t.Run(tc.name, func(t *testing.T) { b := newBench(t) var stored []string - b.asks.Answer = func(a delivery.StepAsk) delivery.Step { + b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { s := b.answer(a) - if tc.old && s.State == delivery.StepPassed { + if tc.old && s.State == buildstep.StepPassed { s.Result = oldDigestForm(t, s.Result) } stored = append(stored, string(s.Result)) return s } first := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - u := first.Levels[delivery.LevelUnit] - if first.State != delivery.VerdictPass || u.Run == 0 || u.Kept != 0 { + u := first.Levels[buildstep.LevelUnit] + if first.State != meshdeliverycheckerclient.VerdictPass || u.Run == 0 || u.Kept != 0 { t.Fatalf("the first check = %+v", first) } if len(stored) == 0 { @@ -41,7 +42,7 @@ func TestKeptVerdictsAreReadFromTheDigestsListForm(t *testing.T) { } b.steps.calls = nil second := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - if s := second.Levels[delivery.LevelUnit]; second.State != delivery.VerdictPass || s.Kept != u.Run || s.Run != 0 { + if s := second.Levels[buildstep.LevelUnit]; second.State != meshdeliverycheckerclient.VerdictPass || s.Kept != u.Run || s.Run != 0 { t.Fatalf("the second check's unit level = %+v, want %d kept and none run", s, u.Run) } if n := len(b.steps.tool("test")); n != 0 { diff --git a/modules/mesh-delivery-checker/internal/check/engines.go b/modules/mesh-delivery-checker/internal/check/engines.go index a5a4529e..8384cf89 100644 --- a/modules/mesh-delivery-checker/internal/check/engines.go +++ b/modules/mesh-delivery-checker/internal/check/engines.go @@ -9,10 +9,15 @@ import ( "sync" "time" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // **A check's composition is the checker's, made from what each node's engine answers** (novox/hq ADR 0593 §7, §9). @@ -35,7 +40,7 @@ type EngineAsker interface { // Sandbox runs a compose job on the Go build seat and answers the composer's verdict. type Sandbox interface { - Compose(ctx context.Context, job delivery.ComposeJob) (composer.Verdict, error) + Compose(ctx context.Context, job buildstep.ComposeJob) (composer.Verdict, error) } // EngineCompositions is the checker's composition of a check (Examiner.Compositions). @@ -54,7 +59,7 @@ type EngineCompositions struct { // Manifests are the indexer's trunk manifest digests by module, and Verdicts its refusals of them: the catalogue the // sandbox composes with, before the case's own manifests are laid over. Manifests TrunkManifests - Verdicts func() map[string]delivery.ManifestVerdict + Verdicts func() map[string]meshcatalog.VerdictsByDigestValue } // composerHead is the node-engine member's head of a case, or empty. @@ -66,16 +71,16 @@ func composerHead(c *Case) string { } // Compose asks every placed node and judges their answers (JudgeEngines). An error is only a case it cannot ask at all. -func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error) { +func (e EngineCompositions) Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) { began := time.Now() - key, err := delivery.CompositionKey(c.Group, c.HeadsHash) + key, err := nodeengineclient.CompositionKey(c.Group, c.Heads) if err != nil { - return delivery.CompositionVerdict{}, err + return meshwants.CompositionsByGroupHeadsValue{}, err } var nodes []string if e.Placed != nil { if nodes, err = e.Placed(); err != nil { - return delivery.CompositionVerdict{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err) + return meshwants.CompositionsByGroupHeadsValue{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err) } } bound := e.Bound @@ -126,7 +131,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp } else if e.Composer != nil && e.Composer() != "" { composerName = e.Composer() } - v := JudgeEngines(c.Group, c.HeadsHash, composerName, nodes, answers, silent) + v := JudgeEngines(c.Group, c.Heads, composerName, nodes, answers, silent) if head != "" && e.Sandbox == nil { v.Warnings = append(v.Warnings, "the group changes the node-engine's composition code and was composed by the running engines") } @@ -138,8 +143,8 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp // answer is refused, naming each node and each module refused with its words (the first refusing node is the machine); // passed otherwise. Every node in nodes without an answer (silent, by node with why, for the journal only) is named // with nec.NoAnswer as a warning. composer names what composed, for the line. Pure. -func JudgeEngines(group, heads, composerName string, nodes []string, answers map[string]ne.ComposeCandidateAnswer, silent map[string]string) delivery.CompositionVerdict { - v := delivery.CompositionVerdict{Group: group, Heads: heads, State: delivery.CompositionPass, SDK: composerName} +func JudgeEngines(group, heads, composerName string, nodes []string, answers map[string]ne.ComposeCandidateAnswer, silent map[string]string) meshwants.CompositionsByGroupHeadsValue { + v := meshwants.CompositionsByGroupHeadsValue{Group: group, Heads: heads, State: meshdeliverycheckerclient.CompositionPass, Sdk: composerName} sorted := append([]string(nil), nodes...) sort.Strings(sorted) var fails []string @@ -177,7 +182,7 @@ func JudgeEngines(group, heads, composerName string, nodes []string, answers map fails = append(fails, "not composed on "+node+": "+strings.Join(words, "; ")) } if len(fails) > 0 { - v.State, v.Words = delivery.CompositionFail, strings.Join(fails, "; ") + v.State, v.Words = meshdeliverycheckerclient.CompositionFail, strings.Join(fails, "; ") } if len(sorted) == 0 { v.Warnings = append(v.Warnings, "no node is placed, so the change was composed on none") @@ -232,7 +237,7 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s if err != nil { return a, err } - job := delivery.ComposeJob{Fetch: map[string]any{"repository": nodeEngineRepo, "commit": head}, Dir: delivery.RootPath, + job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: nodeEngineRepo, Commit: head}, Dir: meshcatalogclient.RootPath, Package: composer.Package, Request: raw, Reader: "the node-engine head of " + c.Group} verdict, err := e.Sandbox.Compose(ctx, job) if err != nil { diff --git a/modules/mesh-delivery-checker/internal/check/engines_test.go b/modules/mesh-delivery-checker/internal/check/engines_test.go index b5072813..d157f802 100644 --- a/modules/mesh-delivery-checker/internal/check/engines_test.go +++ b/modules/mesh-delivery-checker/internal/check/engines_test.go @@ -9,10 +9,15 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "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/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // fakeEngines answers compose-candidate by node from memory; a node it holds no answer for never answers. @@ -45,12 +50,12 @@ func (f *fakeEngines) ComposeCandidate(ctx context.Context, node string, ask ne. // fakeSandbox answers a compose job with a verdict, recording the job. type fakeSandbox struct { mu sync.Mutex - jobs []delivery.ComposeJob + jobs []buildstep.ComposeJob v func(node string) composer.Verdict err error } -func (f *fakeSandbox) Compose(_ context.Context, job delivery.ComposeJob) (composer.Verdict, error) { +func (f *fakeSandbox) Compose(_ context.Context, job buildstep.ComposeJob) (composer.Verdict, error) { var req composer.Request if err := json.Unmarshal(job.Request, &req); err != nil { return composer.Verdict{}, err @@ -80,8 +85,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { Own: ne.Module{Module: nec.Own, State: nec.ModuleComposed, Digest: "sha256:" + strings.Repeat("b", 64)}} } cs := caseOf(t, pullOf(repoA, 7, head1)) - key, _ := delivery.CompositionKey(cs.Group, cs.HeadsHash) - compose := func(e *fakeEngines, nodes ...string) delivery.CompositionVerdict { + key, _ := nodeengineclient.CompositionKey(cs.Group, cs.HeadsHash) + compose := func(e *fakeEngines, nodes ...string) meshwants.CompositionsByGroupHeadsValue { t.Helper() ec := EngineCompositions{Asker: e, Placed: func() ([]string, error) { return nodes, nil }, Bound: 50 * time.Millisecond, Composer: func() string { return strings.Repeat("7", 40) }} @@ -99,16 +104,16 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { {Module: "mail", State: nec.ModuleHeld, Words: "held as today"}} e := &fakeEngines{answers: map[string]ne.ComposeCandidateAnswer{"anchor": composed("anchor"), "laptop": refusing}} v := compose(e, "anchor", "laptop") - if v.State != delivery.CompositionFail || v.Machine != "laptop" || !strings.Contains(v.Words, "not composed on laptop: web: a required value is unset") || + if v.State != meshdeliverycheckerclient.CompositionFail || v.Machine != "laptop" || !strings.Contains(v.Words, "not composed on laptop: web: a required value is unset") || strings.Contains(v.Words, "mail") || len(v.Machines) != 2 { t.Errorf("verdict = %+v", v) } if e.asked["anchor"].Candidate != key || e.asked["laptop"].Input { t.Errorf("asked %+v, want the check's candidate %s and no input", e.asked, key) } - var ver delivery.Verdict + var ver meshdeliverychecker.VerdictsByGroupHeadsValue withComposition(&ver, v) - if ver.State != delivery.VerdictFail || ver.Failed.Machine != "laptop" { + if ver.State != meshdeliverycheckerclient.VerdictFail || ver.Failed.Machine != "laptop" { t.Errorf("the check's verdict = %+v", ver) } }) @@ -122,7 +127,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { if took := time.Since(began); took > 2*time.Second { t.Errorf("the nodes were not asked in parallel within the bound: %s", took) } - if v.State != delivery.CompositionPass || v.SDK != strings.Repeat("7", 40) || len(v.Machines) != 1 { + if v.State != meshdeliverycheckerclient.CompositionPass || v.Sdk != strings.Repeat("7", 40) || len(v.Machines) != 1 { t.Fatalf("verdict = %+v", v) } for _, node := range []string{"ace", "g14", "laptop"} { @@ -130,16 +135,16 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { t.Errorf("%s is not named: %v", node, v.Warnings) } } - var ver delivery.Verdict - ver.State = delivery.VerdictPass + var ver meshdeliverychecker.VerdictsByGroupHeadsValue + ver.State = meshdeliverycheckerclient.VerdictPass withComposition(&ver, v) - if ver.State != delivery.VerdictPass || !strings.Contains(composition(ver.Composition, 0, false), "laptop: "+nec.NoAnswer) { + if ver.State != meshdeliverycheckerclient.VerdictPass || !strings.Contains(composition(ver.Composition, 0, false), "laptop: "+nec.NoAnswer) { t.Errorf("the line = %q", composition(ver.Composition, 0, false)) } }) t.Run("a node-engine member is composed by the composer at its head over each node's input", func(t *testing.T) { - c, err := delivery.DecodeCandidate(candidateOf(t, "feat/x", pullOf(nodeEngineRepo, 3, head2), pullOf(repoA, 7, head1))) + c, err := meshcatalog.DecodeCandidatesByGroupValue(candidateOf(t, "feat/x", pullOf(nodeEngineRepo, 3, head2), pullOf(repoA, 7, head1))) if err != nil { t.Fatal(err) } @@ -163,11 +168,11 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { job := sb.jobs[0] var req composer.Request _ = json.Unmarshal(job.Request, &req) - if job.Fetch["repository"] != nodeEngineRepo || job.Fetch["commit"] != head2 || job.Package != composer.Package || + if job.Fetch.Repository != nodeEngineRepo || job.Fetch.Commit != head2 || job.Package != composer.Package || string(req.Input) != `{"nodes":[{"name":"anchor"}]}` || req.Cases[0].Manifests["web"].Digest == "" { t.Errorf("job = %+v, request %+v", job, req) } - if v.State != delivery.CompositionFail || v.SDK != head2 || !strings.Contains(v.Words, "the new composer refuses it") { + if v.State != meshdeliverycheckerclient.CompositionFail || v.Sdk != head2 || !strings.Contains(v.Words, "the new composer refuses it") { t.Errorf("verdict = %+v", v) } // ADR 0593 §9 (ADR 0601 §4's kind): a node the composer did not judge is named as no answer with its words, by the @@ -185,7 +190,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { } v, err = ec.Compose(context.Background(), host) named := len(v.Warnings) == 1 && strings.Contains(v.Warnings[0], "anchor: "+nec.NoAnswer+" (its node-engine sends no running state for web)") - if err != nil || (v.State == delivery.CompositionPass) != row.pass || named != row.pass { + if err != nil || (v.State == meshdeliverycheckerclient.CompositionPass) != row.pass || named != row.pass { t.Errorf("%+v: verdict %+v, %v", row.m, v, err) } } @@ -193,7 +198,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { // run fails the composition, naming the head, never passing as a silent node. sb.err = errors.New("the build of mesh-compose failed") v, err = ec.Compose(context.Background(), host) - if err != nil || v.State != delivery.CompositionFail || !strings.Contains(v.Words, head2) || + if err != nil || v.State != meshdeliverycheckerclient.CompositionFail || !strings.Contains(v.Words, head2) || !strings.Contains(v.Words, "the build of mesh-compose failed") || len(v.Warnings) != 0 { t.Errorf("a composer that cannot run: %+v, %v", v, err) } @@ -208,7 +213,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { b := newBench(t) b.ex.Compositions = ec v := b.judge(t, cs).Verdict - if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun || !strings.Contains(v.Composition.Words, "no such bucket") { + if v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun || !strings.Contains(v.Composition.Words, "no such bucket") { t.Errorf("the verdict's composition = %+v", v.Composition) } }) diff --git a/modules/mesh-delivery-checker/internal/check/every_failure_test.go b/modules/mesh-delivery-checker/internal/check/every_failure_test.go index e0c03bd1..67638802 100644 --- a/modules/mesh-delivery-checker/internal/check/every_failure_test.go +++ b/modules/mesh-delivery-checker/internal/check/every_failure_test.go @@ -6,23 +6,26 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "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-delivery/sdk/buildstep" ) // failingResult is a level's test result in which the named tests failed, each in its own package. func failingResult(t *testing.T, tests int, failed ...Failure) json.RawMessage { t.Helper() - raw, err := json.Marshal(Tested{Level: delivery.LevelIntegration, Tests: tests, Elapsed: 2, Failed: failed, Passed: len(failed) == 0}) + raw, err := json.Marshal(Tested{Level: buildstep.LevelIntegration, Tests: tests, Elapsed: 2, Failed: failed, Passed: len(failed) == 0}) if err != nil { t.Fatal(err) } return raw } -func judgeLevel(t *testing.T, tests ...json.RawMessage) (*delivery.Verdict, *state) { +func judgeLevel(t *testing.T, tests ...json.RawMessage) (*meshdeliverychecker.VerdictsByGroupHeadsValue, *state) { t.Helper() - st := &state{j: &Examiner{}, c: &Case{}, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}} - v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, delivery.LevelAnswer{Level: delivery.LevelIntegration, Tests: tests}) + st := &state{j: &Examiner{}, c: &Case{}, levels: map[string]*acc{buildstep.LevelUnit: {}, buildstep.LevelIntegration: {}}, nodes: map[string]bool{}} + v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, buildstep.LevelAnswer{Level: buildstep.LevelIntegration, Tests: tests}) return settled(joinFailures(v)), st } @@ -48,29 +51,29 @@ func TestTwoFailingTestsInTwoPackagesAreTwoEntries(t *testing.T) { if len(v.Failures) != 2 || v.Failed.Test != "integration: x/a.TestA" || !strings.Contains(v.Failed.Words, "a_test.go:3: logged") { t.Errorf("%s: verdict %+v", name, v) } - if run := st.levels[delivery.LevelIntegration].run; run != 10 { + if run := st.levels[buildstep.LevelIntegration].run; run != 10 { t.Errorf("%s: %d tests counted, want all 10", name, run) } - if _, err := delivery.EncodeVerdict(delivery.Verdict{Group: "g", Heads: "0123456789ab", State: v.State, Failed: v.Failed, Failures: v.Failures}); err != nil { + if _, err := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "g", Heads: "0123456789ab", State: v.State, Failed: v.Failed, Failures: v.Failures}); err != nil { t.Errorf("%s: the verdict is not written: %v", name, err) } } // Design 26 *Out of step* (DLV-C2): the out-of-step files the compare step names, in each result, are the outcome's for // the pull request, each once and sorted; a level naming none leaves the outcome with none. step := func(files ...string) json.RawMessage { - raw, _ := json.Marshal(Tested{Level: delivery.LevelUnit, Tests: 1, Passed: false, Why: "out of step", OutOfStep: files}) + raw, _ := json.Marshal(Tested{Level: buildstep.LevelUnit, Tests: 1, Passed: false, Why: "out of step", OutOfStep: files}) return raw } _, st := judgeLevel(t, step("go/seats/b/contract.go", "go/seats/a/contract.go"), step("go/seats/a/contract.go")) - if got := st.outcome(delivery.Verdict{State: delivery.VerdictFail}).OutOfStep["novox/x#0"]; strings.Join(got, ",") != "go/seats/a/contract.go,go/seats/b/contract.go" { + if got := st.outcome(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail}).OutOfStep["novox/x#0"]; strings.Join(got, ",") != "go/seats/a/contract.go,go/seats/b/contract.go" { t.Errorf("out of step %v", got) } - if _, st := judgeLevel(t, failingResult(t, 2, a)); st.outcome(delivery.Verdict{State: delivery.VerdictFail}).OutOfStep != nil { + if _, st := judgeLevel(t, failingResult(t, 2, a)); st.outcome(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail}).OutOfStep != nil { t.Errorf("a level naming no out-of-step file has %v", st.outOfStep) } } -// Issue 1921: past delivery.MaxFailures the list names the first fifty and how many more, the status line says how many +// Issue 1921: past meshdeliverycheckerclient.MaxFailures the list names the first fifty and how many more, the status line says how many // failed and names the first, cut so "and N more" stays whole, and each entry's line is cut to MaxFailureLine. func TestFailuresAreCappedWithHowManyMore(t *testing.T) { var fs []Failure @@ -78,7 +81,7 @@ func TestFailuresAreCappedWithHowManyMore(t *testing.T) { fs = append(fs, Failure{Package: fmt.Sprintf("x/p%d", i), Test: "TestT", Cause: strings.Repeat("y", 500)}) } v, _ := judgeLevel(t, failingResult(t, 60, fs...)) - if len(v.Failures) != delivery.MaxFailures+1 || v.Failures[delivery.MaxFailures].Test != "and 10 more" { + if len(v.Failures) != meshdeliverycheckerclient.MaxFailures+1 || v.Failures[meshdeliverycheckerclient.MaxFailures].Test != "and 10 more" { t.Fatalf("%d entries, last %+v", len(v.Failures), v.Failures[len(v.Failures)-1]) } if len(v.Failures[0].Words) > MaxFailureLine { @@ -93,14 +96,14 @@ func TestFailuresAreCappedWithHowManyMore(t *testing.T) { t.Errorf("line %q", line) } // A composition failing after them is counted in the last entry (the operator, 2026-11-02). - withComposition(v, delivery.CompositionVerdict{State: delivery.CompositionFail, Machine: "g14", Words: "route-proxy is defined twice"}) + withComposition(v, meshwants.CompositionsByGroupHeadsValue{State: meshdeliverycheckerclient.CompositionFail, Machine: "g14", Words: "route-proxy is defined twice"}) if n := failedCount(*v); n != 61 || v.Failed.Test != "integration: x/p0.TestT" { t.Errorf("after the composition failedCount = %d, failed %+v", n, v.Failed) } // A verdict that names no list (a conflict, a refused step, a verdict written before issue 1921) is one failed line, as // before. - one := delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "g14", Words: "files in conflict: go.mod\nmore"}} + one := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "g14", Words: "files in conflict: go.mod\nmore"}} if got := failedLines(one); len(got) != 1 || got[0] != "g14: files in conflict: go.mod" { t.Fatalf("%q", got) } diff --git a/modules/mesh-delivery-checker/internal/check/examiner_test.go b/modules/mesh-delivery-checker/internal/check/examiner_test.go index b5d20493..53cf9215 100644 --- a/modules/mesh-delivery-checker/internal/check/examiner_test.go +++ b/modules/mesh-delivery-checker/internal/check/examiner_test.go @@ -20,10 +20,17 @@ import ( gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep" "git.novox.be/novox/mesh-contracts/go/seats/meshconfig" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "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/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // captured reads a result the build module's own code marshalled (testdata/captured/README). @@ -80,10 +87,10 @@ func (f *fakeSteps) tool(name string) []stepCall { return out } -type entries []delivery.NodeToolchain +type entries []buildstep.NodeToolchain -func (e entries) Entries(language string) []delivery.NodeToolchain { - var out []delivery.NodeToolchain +func (e entries) Entries(language string) []buildstep.NodeToolchain { + var out []buildstep.NodeToolchain for _, x := range e { if x.Language == language { out = append(out, x) @@ -104,8 +111,8 @@ func (l langs) BuildModules(repository string) []string { func goEntries(nodes ...string) entries { var e entries for _, n := range nodes { - e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "7"}}) - e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "typescript", Version: "node24", Recipe: "3"}}) + e = append(e, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "7"}}) + e = append(e, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: "typescript", Version: "node24", Recipe: "3"}}) } return e } @@ -140,8 +147,8 @@ var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun // answer runs a check ask as the build module that pulls it does (gobuild.RunAsk), with the fake tools on shanks and the // bench's kept verdicts, and answers the step it writes. -func (b *bench) answer(a delivery.StepAsk) delivery.Step { - module, _ := delivery.BuildModuleOf(a.Language) +func (b *bench) answer(a buildstep.StepAsk) buildstep.Step { + module, _ := buildstep.BuildModuleOf(a.Language) const node = "shanks" tool := func(name string) gobuild.StepFunc { return func(ctx context.Context, args map[string]any) (any, error) { @@ -155,7 +162,7 @@ func (b *bench) answer(a delivery.StepAsk) delivery.Step { steps := gobuild.AskSteps{Fetch: tool("fetch"), Test: tool("test"), Kept: func(ctx context.Context, level, digest string) (int, bool, error) { k, found, err := (Kept{Store: b.kept}).Lookup(ctx, level, digest) - return k.Tests, found, err + return int(k.Tests), found, err }} if module == buildrun.GoBuildModule { steps.Digest = tool("digest") @@ -164,16 +171,16 @@ func (b *bench) answer(a delivery.StepAsk) delivery.Step { if len(a.Needs) > 0 { if b.declines > 0 { b.declines-- - result, _ := json.Marshal(delivery.StepDeclined{Declined: true, Needs: a.Needs, Lacks: a.Needs}) - return delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: "ace", State: delivery.StepRefused, Started: began, Ended: began, + result, _ := json.Marshal(buildstep.StepDeclined{Declined: true, Needs: a.Needs, Lacks: a.Needs}) + return buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: "ace", State: buildstep.StepRefused, Started: began, Ended: began, Reason: "declined", Result: result} } steps.Has = func(string) bool { return true } } out, err := gobuild.RunAsk(context.Background(), steps, a) - step := delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: delivery.StepPassed, Started: began, Ended: began.Add(2 * time.Second)} + step := buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: buildstep.StepPassed, Started: began, Ended: began.Add(2 * time.Second)} if err != nil { - step.State, step.Reason = delivery.StepFailed, err.Error() + step.State, step.Reason = buildstep.StepFailed, err.Error() return step } step.Result, _ = json.Marshal(out) @@ -188,8 +195,8 @@ func newBench(t testing.TB) *bench { b.fetch.Modules, b.fetch.Changed, b.fetch.Trees = []string{"."}, []string{"README.md"}, nil one := captured[Digested](t, "go-digest") b.digest = map[string]Digested{ - delivery.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}}, - delivery.LevelIntegration: {Level: "integration", Packages: map[string]string{"fixture/slow": digB}}, + buildstep.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}}, + buildstep.LevelIntegration: {Level: "integration", Packages: map[string]string{"fixture/slow": digB}}, } _ = one slow := captured[Tested](t, "go-test-slow") @@ -212,10 +219,10 @@ func newBench(t testing.TB) *bench { return b } -func caseOf(t testing.TB, pulls ...delivery.CandidatePull) *Case { +func caseOf(t testing.TB, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) *Case { t.Helper() raw := candidateOf(t, "feat/x", pulls...) - c, err := delivery.DecodeCandidate(raw) + c, err := meshcatalog.DecodeCandidatesByGroupValue(raw) if err != nil { t.Fatal(err) } @@ -257,10 +264,10 @@ func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679 cs := caseOf(t, pullOf(repoA, 7, head1)) out := b.judge(t, cs) v := out.Verdict - if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 2 || v.Levels[delivery.LevelIntegration].Run != 2 { + if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Run != 2 || v.Levels[buildstep.LevelIntegration].Run != 2 { t.Fatalf("verdict = %+v", v) } - if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun { + if v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun { t.Errorf("composition = %+v, want not run", v.Composition) } if len(v.Machines) != 1 || v.Machines[0] != "shanks" { @@ -292,7 +299,7 @@ func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679 out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) asks := b.asks.Asks() if c.fails != "" { - if out.Verdict.State != delivery.VerdictFail || len(asks) != 0 || !strings.Contains(out.Verdict.Failed.Words, c.fails) { + if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || len(asks) != 0 || !strings.Contains(out.Verdict.Failed.Words, c.fails) { t.Errorf("names %v, %v: verdict %+v, %d asks", c.names, c.err, out.Verdict, len(asks)) } } else if len(asks) == 0 || strings.Join(asks[0].Check.NodeNames, ",") != "anchor,laptop" { @@ -311,7 +318,7 @@ func (n namesOf) Names(context.Context) ([]string, error) { return n.names, n.er func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1 b := newBench(t) - b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/slow": digA, "fixture/other": digC}} + b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/slow": digA, "fixture/other": digC}} if _, err := (Kept{Store: b.kept}).Keep(context.Background(), "unit", digC, keptValue("fixture/other", 40)); err != nil { t.Fatal(err) } @@ -320,7 +327,7 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1 if len(tests) != 2 || strings.Join(packagesOf(tests[0]), ",") != "fixture/slow" || tests[0].args["level"] != "unit" { t.Fatalf("test calls = %+v, want the unit level of fixture/slow alone, then the integration level", tests) } - if u := out.Verdict.Levels[delivery.LevelUnit]; u.Run != 2 || u.Kept != 40 { + if u := out.Verdict.Levels[buildstep.LevelUnit]; u.Run != 2 || u.Kept != 40 { t.Errorf("unit = %+v, want 2 run and 40 kept", u) } @@ -340,11 +347,11 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1 } return d, nil } - b.digest[delivery.LevelIntegration] = Digested{Level: "integration", - Needs: []delivery.PackageNeeds{{Package: "fixture/node", Needs: []string{delivery.NeedNodeBuildNode}}}} + b.digest[buildstep.LevelIntegration] = Digested{Level: "integration", + Needs: []buildstep.PackageNeeds{{Package: "fixture/node", Needs: []string{buildstep.NeedNodeBuildNode}}}} out = b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) asks := b.asks.Asks() - if out.Verdict.State != delivery.VerdictPass || len(asks) != 3 || len(asks[0].Needs) != 0 { + if out.Verdict.State != meshdeliverycheckerclient.VerdictPass || len(asks) != 3 || len(asks[0].Needs) != 0 { t.Fatalf("verdict %+v, asks %+v: want a pass of three asks, the first naming no need", out.Verdict, asks) } for _, a := range asks { // ADR 0584: every check ask names the agreed toolchain, so a node off it declines @@ -353,7 +360,7 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1 } } for _, a := range asks[1:] { - if strings.Join(a.Needs, ",") != delivery.NeedNodeBuildNode || strings.Join(a.Check.Levels, ",") != delivery.LevelIntegration || strings.Join(a.Check.Packages, ",") != "fixture/node" { + if strings.Join(a.Needs, ",") != buildstep.NeedNodeBuildNode || strings.Join(a.Check.Levels, ",") != buildstep.LevelIntegration || strings.Join(a.Check.Packages, ",") != "fixture/node" { t.Errorf("a placed ask %+v, want the integration level needing node-build-node", a) } } @@ -362,13 +369,13 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1 t.Errorf("test calls = %+v, want the unit level, the integration level of fixture/slow and fixture/outside, then fixture/node alone", tests) } slow := captured[Tested](t, "go-test-slow") - if i := out.Verdict.Levels[delivery.LevelIntegration]; i.Run != 2*slow.Tests || i.Kept != 0 { + if i := out.Verdict.Levels[buildstep.LevelIntegration]; int(i.Run) != 2*slow.Tests || i.Kept != 0 { t.Errorf("integration = %+v, want %d run and nothing counted kept", i, 2*slow.Tests) } // And where no node has the need, the check fails once the declines outlast the bound, naming the need. b.kept, b.declines = newMemKV(), 1000 b.ex.Kept, b.ex.DeclineBound = Kept{Store: b.kept}, 200*time.Millisecond - if v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict; v.State != delivery.VerdictFail || !strings.Contains(fmt.Sprint(v), "needs a node with node-build-node") { + if v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict; v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(failedOf(v).Words, "needs a node with node-build-node") { t.Errorf("with no node having the need: %+v", v) } } @@ -376,7 +383,7 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1 func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3 b := newBench(t) failing := captured[Tested](t, "go-test-failing") - b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digB}} + b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digB}} b.test = func(c stepCall) (any, error) { if c.args["level"] == "unit" { return failing, nil @@ -385,7 +392,7 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3 } cs := caseOf(t, pullOf(repoA, 7, head1)) first := b.judge(t, cs).Verdict - if first.State != delivery.VerdictFail || first.Failed.Test != "unit: fixture/failing/bad.TestThreeIsFour" { + if first.State != meshdeliverycheckerclient.VerdictFail || first.Failed.Test != "unit: fixture/failing/bad.TestThreeIsFour" { t.Fatalf("first = %+v", first) } if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digB); !found { @@ -402,7 +409,7 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3 t.Fatalf("the re-ask ran %+v, want only fixture/failing/bad", tests) } // The trunk moved and one digest changed: that package runs, the unchanged one is taken. - b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}} + b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}} b.steps.calls = nil b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) tests = b.steps.tool("test") @@ -421,7 +428,7 @@ func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1 cs := caseOf(t, pullOf(repoA, 7, head1)) for i := 0; i < 2; i++ { v := b.judge(t, cs).Verdict - if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Kept != 0 || v.Levels[delivery.LevelUnit].Run != 1 { + if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Kept != 0 || v.Levels[buildstep.LevelUnit].Run != 1 { t.Fatalf("round %d: verdict = %+v", i, v) } } @@ -445,11 +452,11 @@ func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1 func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) { for _, conflicts := range []bool{false, true} { b := newBench(t) - comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionFail, SDK: "x", + comps := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.CompositionFail, Sdk: "x", Machine: "g14", Words: "route-proxy is defined twice"}, waits: conflicts} b.ex.Compositions = comps b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"} - b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}} + b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}} conflict := captured[Fetched](t, "go-fetch-conflict") b.steps.handlers["build_go_fetch"] = func(c stepCall) (any, error) { if conflicts && c.args["repository"] == repoSDK { @@ -467,9 +474,9 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) { return captured[Tested](t, "go-test-slow"), nil } cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)) - done := make(chan delivery.Verdict, 1) + done := make(chan meshdeliverychecker.VerdictsByGroupHeadsValue, 1) go func() { done <- b.judge(t, cs).Verdict }() - var v delivery.Verdict + var v meshdeliverychecker.VerdictsByGroupHeadsValue select { case v = <-done: case <-time.After(5 * time.Second): @@ -477,14 +484,14 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) { } var got []string for _, f := range v.Failures { - got = append(got, firstFailed(delivery.Verdict{Failed: f})) + got = append(got, firstFailed(meshdeliverychecker.VerdictsByGroupHeadsValue{Failed: &FailedFirst{Test: f.Test, Machine: f.Machine, Words: f.Words}})) } tests := []string{"unit: fixture/failing/bad.TestThreeIsFour", "integration: fixture/failing/bad.TestThreeIsFour"} want := append(append(slices.Clone(tests), tests...), "g14: route-proxy is defined twice") // both members fail their tests if conflicts { want = append([]string{"shanks: novox/mesh-sdk#12 does not merge into the trunk: files in conflict: a.go"}, tests...) } - if v.State != delivery.VerdictFail || len(got) != len(want) { + if v.State != meshdeliverycheckerclient.VerdictFail || len(got) != len(want) { t.Fatalf("conflicts %v: verdict %+v: failures\n%s\nwant\n%s", conflicts, v, strings.Join(got, "\n"), strings.Join(want, "\n")) } for i := range want { @@ -494,9 +501,9 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) { } keys, _ := b.prev.Keys(context.Background()) switch { - case !conflicts && (len(keys) != 2 || v.Composition == nil || v.Composition.State != delivery.CompositionFail): + case !conflicts && (len(keys) != 2 || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionFail): t.Errorf("tests alone failed: previews %v, composition %+v, want both previews and the composition read", keys, v.Composition) - case conflicts && (len(comps.asked) != 0 || v.Composition == nil || v.Composition.State != delivery.CompositionNotRun || + case conflicts && (len(comps.asked) != 0 || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun || !strings.Contains(v.Composition.Words, "novox/mesh-sdk#12")): t.Errorf("a member did not merge: asked %v, composition %+v, want not run naming novox/mesh-sdk#12", comps.asked, v.Composition) } @@ -510,7 +517,7 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) { t.Errorf("the unit level ran %v and %d digests, want the changed test's package, then the rest, at both levels", unit, len(b.steps.tool("digest"))) } v.Group, v.Heads = "feat/x", "0123456789ab" - if _, err := delivery.EncodeVerdict(v); err != nil { + if _, err := meshdeliverycheckerclient.EncodeVerdict(v); err != nil { t.Errorf("conflicts %v: the verdict cannot be written: %v", conflicts, err) } if line := Line(v, cs, cs.Pulls[0], nil, nil, nil); !strings.HasPrefix(line, fmt.Sprintf("failed (%d): %s", len(want), want[0][:20])) || @@ -523,9 +530,9 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) { func TestTheChangesOwnPassingTestRunsFirstAndTheRestFollow(t *testing.T) { // 1289/AC3 b := newBench(t) b.fetch.Changed = []string{"modules/m/internal/b/b_test.go", "modules/m/internal/a/a.go"} - b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}} + b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}} out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) - if out.Verdict.State != delivery.VerdictPass { + if out.Verdict.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", out.Verdict) } var unit []string @@ -544,7 +551,7 @@ func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T b.fetch = captured[Fetched](t, "go-fetch-conflict") out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) f := out.Verdict.Failed - if out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: a.go, sub/b.go") { + if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: a.go, sub/b.go") { t.Fatalf("verdict = %+v", out.Verdict) } if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 { @@ -556,7 +563,7 @@ func TestALevelWhoseDigestIsNotNamedRunsWholeAndKeepsNothing(t *testing.T) { // b := newBench(t) b.steps.handlers["build_go_digest"] = func(stepCall) (any, error) { return nil, fmt.Errorf("go list failed") } out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) - if out.Verdict.State != delivery.VerdictPass { + if out.Verdict.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", out.Verdict) } tests := b.steps.tool("test") @@ -580,7 +587,7 @@ func TestALevelOverItsBoundIsNamedWithItsSlowestTestsAndKeepsItsVerdict(t *testi } out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) v := out.Verdict - if v.State != delivery.VerdictPass || strings.Join(v.OverBound, ",") != "unit,integration" { + if v.State != meshdeliverycheckerclient.VerdictPass || strings.Join(v.OverBound, ",") != "unit,integration" { t.Fatalf("verdict = %+v", v) } if len(out.Slowest) == 0 || !strings.Contains(out.Slowest[0], "TestTakesAWhile") { @@ -594,7 +601,7 @@ func TestSkippedTestsAreCountedPerLevel(t *testing.T) { // 1300/AC6, 1251 return testedWith(t, "go-test-slow", func(r *Tested) { r.Level = c.args["level"].(string); r.Skipped = 3 }), nil } v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - if v.Levels[delivery.LevelUnit].Skipped != 3 || v.Levels[delivery.LevelIntegration].Skipped != 3 { + if v.Levels[buildstep.LevelUnit].Skipped != 3 || v.Levels[buildstep.LevelIntegration].Skipped != 3 { t.Errorf("levels = %+v", v.Levels) } } @@ -603,7 +610,7 @@ func TestAGroupIsOneVerdictAndEachMembersTestsReadTheOthersAtTheirHeads(t *testi b := newBench(t) cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2)) v := b.judge(t, cs).Verdict - if v.State != delivery.VerdictPass { + if v.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", v) } fetch := b.steps.tool("fetch") @@ -618,7 +625,7 @@ func TestAGroupIsOneVerdictAndEachMembersTestsReadTheOthersAtTheirHeads(t *testi } found := false for _, r := range forA { - if r.Repository == repoSDK && r.Commit == head2 && r.Ref == "refs/pull/12/head" { + if r.Repository == repoSDK && r.Commit == head2 { found = true } } @@ -637,17 +644,17 @@ const ( func TestACleanMergeWritesItsPreviewWithTheTreesManifestsAndLocks(t *testing.T) { // 1445/AC1 b := newBench(t) - b.fetch.Trees = map[string]MergeTree{ + b.fetch.Trees = map[string]buildstep.ModuleTree{ "modules/avahi": {Tree: "7777777777777777777777777777777777777777", Manifest: manA, Locks: map[string]string{"go.sum": "ff00"}}, "modules/bare": {Tree: "8888888888888888888888888888888888888888"}, // no manifest digest: left out } b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) - key, _ := delivery.PreviewKey(repoA, head1, trunkA) + key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA) raw, _, found, _ := b.prev.Get(context.Background(), key) if !found { t.Fatalf("no preview %s; the bucket holds %v", key, b.prev.vals) } - pv, err := delivery.DecodePreview(raw) + pv, err := meshdeliverycheckerclient.DecodePreview(raw) if err != nil { t.Fatal(err) } @@ -661,12 +668,12 @@ func TestAHeadThatDoesNotMergeWritesAPreviewOfItsConflictsAlone(t *testing.T) { b := newBench(t) b.fetch = captured[Fetched](t, "go-fetch-conflict") b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) - key, _ := delivery.PreviewKey(repoA, head1, trunkA) + key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA) raw, _, found, _ := b.prev.Get(context.Background(), key) if !found { t.Fatalf("no preview for a head that does not merge; the bucket holds %v", b.prev.vals) } - pv, err := delivery.DecodePreview(raw) + pv, err := meshdeliverycheckerclient.DecodePreview(raw) if err != nil || strings.Join(pv.Conflicts, ",") != "a.go,sub/b.go" || pv.Merge != "" || len(pv.Modules) != 0 { t.Errorf("preview = %+v, %v, want the files in conflict and no merge", pv, err) } @@ -677,12 +684,12 @@ func TestAPreviewIsWrittenOnceAndAnExistingKeyStands(t *testing.T) { b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) b.fetch.Merge = "5656565656565656565656565656565656565656" b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) - key, _ := delivery.PreviewKey(repoA, head1, trunkA) + key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA) if b.prev.creates[key] != 1 { t.Errorf("the preview was created %d times, want once", b.prev.creates[key]) } raw, _, _, _ := b.prev.Get(context.Background(), key) - if pv, _ := delivery.DecodePreview(raw); pv.Merge == b.fetch.Merge { + if pv, _ := meshdeliverycheckerclient.DecodePreview(raw); pv.Merge == b.fetch.Merge { t.Error("the second merge commit replaced the first") } } @@ -693,15 +700,15 @@ func TestTheIndexerIsToldOfAPreviewOnceWhenItIsCreated(t *testing.T) { // 1445/A b.ex.Announce = func(_ context.Context, event, key string) { told = append(told, event+" "+key) } b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) - key, _ := delivery.PreviewKey(repoA, head1, trunkA) + key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA) if len(told) != 1 || told[0] != EventPreviewWritten+" "+key { t.Errorf("told %v, want once of %s", told, key) } } -type fakeBuilds map[string]delivery.BuilderBuild +type fakeBuilds map[string]builds.BuilderBuild -func (f fakeBuilds) Build(_ context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) { +func (f fakeBuilds) Build(_ context.Context, module, fp2 string) (builds.BuilderBuild, bool, error) { b, ok := f[module+" "+fp2] return b, ok, nil } @@ -712,13 +719,13 @@ func TestTheOutcomeSaysIfEachCandidatesModuleIsBuiltFailedOrWaiting(t *testing.T fpB := "src2:" + strings.Repeat("b", 64) fpC := "src2:" + strings.Repeat("c", 64) b.ex.Builds = fakeBuilds{ - "avahi " + fpA: {State: delivery.BuildDone}, - "mesh-tools " + fpB: {State: delivery.BuildFailed, Error: "boom"}, + "avahi " + fpA: {State: builds.BuildDone}, + "mesh-tools " + fpB: {State: builds.BuildFailed, Error: "boom"}, // netcheck has no key yet } p := pullOf(repoA, 7, head1) p.Modules = []string{"avahi", "mesh-tools", "netcheck", "nofp"} - p.Fps = map[string]delivery.CandidateModule{"avahi": {FP2: fpA}, "mesh-tools": {FP2: fpB}, "netcheck": {FP2: fpC}, "nofp": {None2: "toolchain not yet reported"}} + p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"avahi": {Fp2: fpA}, "mesh-tools": {Fp2: fpB}, "netcheck": {Fp2: fpC}, "nofp": {None2: "toolchain not yet reported"}} out := b.judge(t, caseOf(t, p)) var got []string for _, n := range out.Builds { @@ -732,7 +739,7 @@ func TestTheOutcomeSaysIfEachCandidatesModuleIsBuiltFailedOrWaiting(t *testing.T // trunksOf are trunk heads of every repository, as the gate's source names them. func trunksOf(t *testing.T, heads map[string]string) *Trunks { t.Helper() - data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads}) + data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: heads}) if err != nil { t.Fatal(err) } @@ -808,8 +815,8 @@ func TestACoreRepositorysPassingPackageIsKeptUnderADigestCoveringItsSiblings(t * // The same siblings again: everything is taken, nothing runs. b.steps.calls = nil v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - if n := len(b.steps.tool("test")); n != 0 || v.Levels[delivery.LevelUnit].Run != 0 || v.Levels[delivery.LevelUnit].Kept == 0 { - t.Fatalf("%d test steps, unit %+v, want none run and the kept verdict taken", n, v.Levels[delivery.LevelUnit]) + if n := len(b.steps.tool("test")); n != 0 || v.Levels[buildstep.LevelUnit].Run != 0 || v.Levels[buildstep.LevelUnit].Kept == 0 { + t.Fatalf("%d test steps, unit %+v, want none run and the kept verdict taken", n, v.Levels[buildstep.LevelUnit]) } // A sibling's commit moves: a new digest, so the package runs again and is kept under it. b.ex.Trunks = trunksOf(t, map[string]string{repoSDK: trunkS2, "novox/mesh-catalog": trunkA}) @@ -917,15 +924,15 @@ func TestNoStepOfACheckCarriesPublish(t *testing.T) { // ADR 0500 §4, 1309 (the func TestAnAskWhoseStepNeverEndsEndsTheCheckLoudly(t *testing.T) { b := newBench(t) // A node took the ask and its step stays running: the bound runs from its start (review round 2 item 1). - b.asks.Answer = func(a delivery.StepAsk) delivery.Step { - return delivery.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: delivery.StepRunning, Started: time.Now()} + b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { + return buildstep.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: buildstep.StepRunning, Started: time.Now()} } b.ex.Asks = buildrun.Asker{Publisher: b.asks, Waiter: b.asks, Bound: 20 * time.Millisecond} out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) - if f := out.Verdict.Failed; out.Verdict.State != delivery.VerdictFail || !strings.Contains(f.Words, "on shanks did not end within") { + if f := out.Verdict.Failed; out.Verdict.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(f.Words, "on shanks did not end within") { t.Fatalf("verdict = %+v", out.Verdict) } - if n := len(b.asks.Asks()); n != 1 || b.asks.Asks()[0].Kind != delivery.AskCheck || b.asks.Asks()[0].Check == nil { + if n := len(b.asks.Asks()); n != 1 || b.asks.Asks()[0].Kind != buildstep.AskCheck || b.asks.Asks()[0].Check == nil { t.Fatalf("asks = %+v, want one check ask", b.asks.Asks()) } } @@ -936,16 +943,20 @@ func TestACheckIsOneAskAndAReJudgeGoesOnTheRecheckStream(t *testing.T) { b := newBench(t) b.judge(t, caseOf(t, pullOf(repoA, 7, head1))) asks := b.asks.Asks() - if len(asks) != 1 || asks[0].Kind != delivery.AskCheck || asks[0].Language != delivery.AskGo || asks[0].Asker != delivery.CheckerModule { + if len(asks) != 1 || asks[0].Kind != buildstep.AskCheck || asks[0].Language != buildstep.AskGo || asks[0].Asker != CheckerModule { t.Fatalf("asks = %+v, want one Go check ask", asks) } - if j := asks[0].Check; j == nil || !j.Kept || strings.Join(j.Levels, ",") != "unit,integration" || j.Fetch["repository"] != repoA { + if j := asks[0].Check; j == nil || !j.Kept || strings.Join(j.Levels, ",") != "unit,integration" || j.Fetch.Repository != repoA { t.Fatalf("the job = %+v", asks[0].Check) } + // B2g: the job carries the toolchain the checker agreed (ADR 0584), with its language, so a node on another declines it. + if tc := asks[0].Check.Toolchain; tc == nil || *tc != (buildstep.ToolchainUsed{Language: "go", Version: "go1.27.1", Recipe: "7"}) { + t.Errorf("the job's toolchain = %+v, want the agreed go toolchain", tc) + } cs := caseOf(t, pullOf(repoA, 7, head1)) cs.Kind = KindTrunk b.judge(t, cs) - if asks := b.asks.Asks(); len(asks) != 2 || asks[1].Kind != delivery.AskRecheck { + if asks := b.asks.Asks(); len(asks) != 2 || asks[1].Kind != buildstep.AskRecheck { t.Fatalf("asks = %+v, want the re-judge on recheck", asks) } } @@ -971,7 +982,7 @@ func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) { default: } b.asks.End(answer(b.asks.Asks()[0])) // a node took it and ran it - if out := <-done; out.Verdict.State != delivery.VerdictPass || len(b.asks.Asks()) != 1 { + if out := <-done; out.Verdict.State != meshdeliverycheckerclient.VerdictPass || len(b.asks.Asks()) != 1 { t.Fatalf("verdict %+v after %d asks, want a pass from the one ask", out.Verdict, len(b.asks.Asks())) } } @@ -981,13 +992,13 @@ func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) { func TestASecondCheckOfTheSameHeadsKeepsWhatTheFirstPassed(t *testing.T) { b := newBench(t) first := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - u := first.Levels[delivery.LevelUnit] - if first.State != delivery.VerdictPass || u.Run == 0 || u.Kept != 0 { + u := first.Levels[buildstep.LevelUnit] + if first.State != meshdeliverycheckerclient.VerdictPass || u.Run == 0 || u.Kept != 0 { t.Fatalf("the first check = %+v", first) } b.steps.calls = nil second := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict - if s := second.Levels[delivery.LevelUnit]; second.State != delivery.VerdictPass || s.Kept != u.Run || s.Run != 0 { + if s := second.Levels[buildstep.LevelUnit]; second.State != meshdeliverycheckerclient.VerdictPass || s.Kept != u.Run || s.Run != 0 { t.Fatalf("the second check's unit level = %+v, want %d kept and none run", s, u.Run) } if n := len(b.steps.tool("test")); n != 0 { @@ -1171,9 +1182,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { if row.seats != "" { b.ex.SeatList = fakeSeatList{read: row.seats == "read"} } - var judged []delivery.ComposeJob + var judged []buildstep.ComposeJob answer := b.asks.Answer - b.asks.Answer = func(a delivery.StepAsk) delivery.Step { + b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { if a.Compose == nil { return answer(a) } @@ -1188,8 +1199,8 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { verdict, _ := json.Marshal(composer.Verdict{V: composer.Version, SDK: "exe:1", Cases: []composer.CaseVerdict{{Name: JudgeCase, Manifests: map[string]composer.ManifestRead{"web": read, "mail": {Accepted: row.untouched == "", Refused: row.untouched}}}, {Name: JudgeTrunk, Manifests: map[string]composer.ManifestRead{"web": {Accepted: true}, "mail": {Accepted: row.trunkMail == "", Refused: row.trunkMail}}}}}) - result, _ := json.Marshal(delivery.ComposeAnswer{Key: "k", Verdict: verdict}) - return delivery.Step{ID: a.ID, State: delivery.StepPassed, Result: result} + result, _ := json.Marshal(buildstep.ComposeAnswer{Key: "k", Verdict: verdict}) + return buildstep.Step{ID: a.ID, State: buildstep.StepPassed, Result: result} } p := pullOf(repoA, 7, head1) if row.ownJudge { @@ -1197,15 +1208,15 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { b.fetch.Changed = []string{JudgeDir + "/judge/judge.go"} } p.Modules = []string{"web"} - p.Fps = map[string]delivery.CandidateModule{"web": {FP2: "src2:" + strings.Repeat("f", 64), Manifest: web}} + p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"web": {Fp2: "src2:" + strings.Repeat("f", 64), Manifest: web}} cs := caseOf(t, p) v := b.judge(t, cs).Verdict if line := Line(v, cs, cs.Pulls[0], nil, nil, nil); row.seriesWords != "" && !strings.Contains(line, row.seriesWords) || row.seriesWords == "" && strings.Contains(line, "series") { t.Errorf("%s: the line %q, want it to name %q", row.name, line, row.seriesWords) } // The join (A2j finding 2): the verdict names each series change as {module, artifact, from, to}. - if want := []delivery.SeriesChange(nil); row.series == "0.2" { - if want = []delivery.SeriesChange{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}; !reflect.DeepEqual(v.Series, want) { + if want := []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem(nil); row.series == "0.2" { + if want = []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}; !reflect.DeepEqual(v.Series, want) { t.Errorf("%s: the verdict's series %+v, want %+v", row.name, v.Series, want) } } else if len(v.Series) != 0 { @@ -1217,18 +1228,18 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { t.Errorf("%s: the case says %v, want the new indexer's manifest said accepted by the trunk's manifest validator", row.name, cs.notes) } if row.seats == "unread" { - if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 { + if v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 { t.Errorf("%s: %+v, judged %d, want a fail saying %q before any judge", row.name, v.Failed, len(judged), row.fails) } continue } - if row.ownJudge && (len(judged) != 2 || judged[0].Fetch["commit"] != head1 || judged[1].Fetch["commit"] != trunk) { + if row.ownJudge && (len(judged) != 2 || judged[0].Fetch.Commit != head1 || judged[1].Fetch.Commit != trunk) { t.Fatalf("%s: judged %+v, want the head's judge and then the trunk's", row.name, judged) } if row.ownJudge { judged = judged[1:] } - if len(judged) != 1 || judged[0].Fetch["repository"] != DeliveryRepository || judged[0].Fetch["commit"] != trunk || + if len(judged) != 1 || judged[0].Fetch.Repository != DeliveryRepository || judged[0].Fetch.Commit != trunk || judged[0].Package != JudgePackage || judged[0].Dir != JudgeDir { t.Fatalf("%s: judged %+v (verdict %+v)", row.name, judged, v) } @@ -1250,9 +1261,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) { req.Packages, req.Input, row.known) } switch { - case row.fails == "" && v.State != delivery.VerdictPass: + case row.fails == "" && v.State != meshdeliverycheckerclient.VerdictPass: t.Errorf("%s: %+v", row.name, v) - case row.fails != "" && (v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)): + case row.fails != "" && (v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)): t.Errorf("%s: %+v, want a fail saying %q", row.name, v.Failed, row.fails) } } diff --git a/modules/mesh-delivery-checker/internal/check/failure_words_test.go b/modules/mesh-delivery-checker/internal/check/failure_words_test.go index 9c01774d..5358d7a2 100644 --- a/modules/mesh-delivery-checker/internal/check/failure_words_test.go +++ b/modules/mesh-delivery-checker/internal/check/failure_words_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" ) // The failure the build module reported for group r4g on 2026-10-23: the test logged a line, then failed with a multi-line @@ -53,7 +53,7 @@ func TestWordsLongerThanTheBoundKeepTheirLastLines(t *testing.T) { // 1901/AC1 if !strings.HasSuffix(v.Failed.Words, "the cause is here") || len(v.Failed.Words) > MaxWords+200 { t.Errorf("%d bytes, ends %q", len(v.Failed.Words), v.Failed.Words[len(v.Failed.Words)-30:]) } - _ = delivery.VerdictFail + _ = meshdeliverycheckerclient.VerdictFail } // An older build module sends no "cause": its answer is still accepted and the verdict's words are its output, as before @@ -85,7 +85,7 @@ func TestAnOlderBuildModulesFailureIsAcceptedWithItsWholeOutput(t *testing.T) { } { t.Run(c.name, func(t *testing.T) { v := levelFailure(&target{module: c.module}, "unit", Tested{By: c.by, Failed: []Failure{{Package: "modules/gitea", Test: "fails", Cause: c.cause, Output: c.output}}}) - if v.State != delivery.VerdictFail { + if v.State != meshdeliverycheckerclient.VerdictFail { t.Errorf("1901/AC8: the verdict is %q, want it failed", v.State) } line := firstFailed(*v) diff --git a/modules/mesh-delivery-checker/internal/check/gate_test.go b/modules/mesh-delivery-checker/internal/check/gate_test.go index 83381544..744f5510 100644 --- a/modules/mesh-delivery-checker/internal/check/gate_test.go +++ b/modules/mesh-delivery-checker/internal/check/gate_test.go @@ -5,7 +5,7 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" ) // What the gate reads of the checker (R4e, lead's interface with dev-r4e-gate): the verdict's heads are the gate's hash over @@ -14,7 +14,7 @@ import ( func owedState(t testing.TB, r *rig, repo string, number int, head string) string { t.Helper() - key, err := delivery.CheckerOwedKey(repo, number, head) + key, err := CheckerOwedKey(repo, number, head) if err != nil { t.Fatal(err) } @@ -22,7 +22,7 @@ func owedState(t testing.TB, r *rig, repo string, number int, head string) strin if !found { return "" } - o, err := delivery.DecodeCheckerOwed(raw) + o, err := DecodeCheckerOwed(raw) if err != nil { t.Fatal(err) } @@ -37,7 +37,7 @@ func TestTheVerdictsHeadsAreTheGatesHashOverLowercaseHeads(t *testing.T) { // ga cand := candidateOf(t, "feat/x", pullOf("Novox/Mesh-Delivery", 7, upper)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() - want, _ := delivery.HeadsHash([]delivery.Head{{Repository: repoA, Number: 7, Head: head1}}) + want, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: repoA, Number: 7, Head: head1}}) if v := r.verdict(t, keyOf(t, "feat/x", cand)); v.Heads != want { t.Errorf("heads = %q, want the gate's hash of the lone pull request, a group of one, %q", v.Heads, want) } @@ -49,7 +49,7 @@ func TestAGroupsVerdictHeadsCoverEveryMember(t *testing.T) { // gate (b): hq's m cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2), pullOf(hqRepo, 3, head2)) r.ch.Candidate(ctx, "feat%2Fx", cand) r.ch.Wait() - want, _ := delivery.HeadsHash([]delivery.Head{{Repository: repoA, Number: 7, Head: head1}, {Repository: repoSDK, Number: 12, Head: head2}, + want, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: repoA, Number: 7, Head: head1}, {Repository: repoSDK, Number: 12, Head: head2}, {Repository: hqRepo, Number: 3, Head: head2}}) if v := r.verdict(t, keyOf(t, "feat/x", cand)); v.Heads != want { t.Errorf("heads = %q, want %q", v.Heads, want) diff --git a/modules/mesh-delivery-checker/internal/check/graph_cycle.go b/modules/mesh-delivery-checker/internal/check/graph_cycle.go index 444f0b1e..411064ef 100644 --- a/modules/mesh-delivery-checker/internal/check/graph_cycle.go +++ b/modules/mesh-delivery-checker/internal/check/graph_cycle.go @@ -7,9 +7,9 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/sdk/composer" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/graph" ) @@ -29,7 +29,7 @@ type ManifestReader interface { // refuseCycle fails the check for a cycle of the change's module graph, naming it; nil when the case touches no manifest, // when nothing reads manifests, or when there is none. A manifest that cannot be read fails the check, said, never passes it. -func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict { +func (st *state) refuseCycle(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue { if st.j.Manifests == nil || st.j.Read == nil || st.c.Try != nil { return nil } diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index e119b636..bccd846f 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -10,9 +10,11 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants" + "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-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // goModule is the build module the Go repositories are judged by. @@ -48,20 +50,20 @@ const ( ) // verdictPtr is a verdict of failure a step of the check ends with. -type verdictPtr = *delivery.Verdict +type verdictPtr = *meshdeliverychecker.VerdictsByGroupHeadsValue func failed(machine, words string) verdictPtr { return fail(machine, words) } // Asks puts a check's ask on its build seat's stream and waits for its step (buildrun.Asker; ADR 0525 §1, §6): the // checker chooses no node, starts no step and holds none; whichever build node pulls the ask runs the check. type Asks interface { - Put(ctx context.Context, a delivery.StepAsk) error - Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, error) + Put(ctx context.Context, a buildstep.StepAsk) error + Wait(ctx context.Context, a buildstep.StepAsk) (buildstep.Step, error) } // ToolchainEntries are the nodes' toolchain entries of a language (buildrun.ToolchainSet). type ToolchainEntries interface { - Entries(language string) []delivery.NodeToolchain + Entries(language string) []buildstep.NodeToolchain } // BuildModules says which build modules judge a repository: the Go one always, the Node one when the repository holds a @@ -71,7 +73,7 @@ type BuildModules interface { } // askLanguage is the build seat language of each build module the checker asks. -var askLanguage = map[string]string{buildrun.GoBuildModule: delivery.AskGo, buildrun.NodeBuildModule: delivery.AskNode} +var askLanguage = map[string]string{buildrun.GoBuildModule: buildstep.AskGo, buildrun.NodeBuildModule: buildstep.AskNode} // Examiner judges a case through the build seats (ADR 0484 clauses 3, 4, 10, 13; ADR 0525 §1, §3): for each pull request // and build module it puts one check ask whose job fetches the head merged onto its trunk head, then runs the unit level @@ -97,9 +99,9 @@ type Examiner struct { // indexer's modules (novox/hq ADR 0573 §2): a check builds against those, or against a member's head for a package // its group publishes (§4), whatever go.mod names. Nil names none, and go.mod's versions are built. Published interface { - LatestPackages() delivery.PackageVersions + LatestPackages() buildstep.PackageVersions } - // KnownPackages names the packages the indexer's packages state knows (its series keys, delivery.SeriesPackage), by the + // KnownPackages names the packages the indexer's packages state knows (its series keys, meshcatalog.PackagesByPackageSeriesValue), by the // name in its own ecosystem, each with the `/` it is built from: the names a manifest's `packages` is // judged against (manifests_judge.go, design 32). Nil, or PackageNames answering nil while the state is not read yet, // hands the judge no names, and it judges none; an empty map is a state read and empty, against which every name fails. @@ -126,7 +128,7 @@ type Examiner struct { ModuleDir(module string) (string, bool) } // Packages names, of a repository, the paths (repository-relative) its TypeScript package artifacts are built from, from - // the indexer's modules (delivery.Module.ArtifactPaths, lead decision L8); nil serves no group package. + // the indexer's modules (meshcatalog.ModulesByModuleValue.ArtifactPaths, lead decision L8); nil serves no group package. Packages interface { PackagePaths(repository string) []string } @@ -169,7 +171,7 @@ type Examiner struct { // CompositionReader makes a check's composition verdict; an error is only a case that cannot be composed at all. type CompositionReader interface { - Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error) + Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) } func (j *Examiner) now() time.Time { @@ -185,6 +187,15 @@ func (j *Examiner) logf(format string, args ...any) { } } +// fetchBeside are the siblings as the fetch is asked to place them. +func fetchBeside(refs []BesideRef) []buildstep.FetchBeside { + out := make([]buildstep.FetchBeside, 0, len(refs)) + for _, r := range refs { + out = append(out, buildstep.FetchBeside{Repository: r.Repository, Commit: r.Commit}) + } + return out +} + // besideLines are the siblings as the digest step echoes them: ` `, sorted. func besideLines(refs []BesideRef) []string { out := make([]string, 0, len(refs)) @@ -203,10 +214,10 @@ type target struct { node string toolchain string // agreed is the toolchain the ask names, so a node off it declines the check (ADR 0584); nil when none is agreed. - agreed *delivery.ToolchainUsed + agreed *buildstep.ToolchainUsed fetched Fetched // fetchArgs are the arguments of the fetch its check ask runs. - fetchArgs map[string]any + fetchArgs buildstep.Fetch // priority are the Go packages (import paths) or the Node module directories whose test files the merge changes. changed []string // beside are the lines ` ` of the siblings the workspace was given, sorted: its tests may read them, @@ -279,7 +290,7 @@ type state struct { // Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot // be judged at all. func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) { - st := &state{j: j, c: c, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}, + st := &state{j: j, c: c, levels: map[string]*acc{buildstep.LevelUnit: {}, buildstep.LevelIntegration: {}}, nodes: map[string]bool{}, beside: map[string]string{}} // Every member and level runs to its end, and every manifest the change touches is judged by the indexer's judge (ADR // 0578 §2), also after a failure: the verdict names every failure, not only the first (the operator, 2026-11-02). @@ -288,12 +299,12 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) { if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx), st.judgeRenames(ctx), st.refuseUnparsed(ctx))); v != nil { return st.finish(ctx, *v), nil } - return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil + return st.finish(ctx, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictPass}), nil } // finish is the outcome of the verdict with the state of the builds the candidate's modules carry and the assigner's // composition of it, read also when the tests failed, so a failed composition is named beside them. -func (st *state) finish(ctx context.Context, v delivery.Verdict) Outcome { +func (st *state) finish(ctx context.Context, v meshdeliverychecker.VerdictsByGroupHeadsValue) Outcome { out := st.outcome(v) st.composed(ctx, &out.Verdict) out.Builds = st.j.buildNotes(ctx, st.c) @@ -303,7 +314,7 @@ func (st *state) finish(ctx context.Context, v delivery.Verdict) Outcome { // composed makes the check's composition from the engines' answers into its verdict (ADR 0593 §7, §9): a failed // composition fails the check, naming the machine it failed at; a node that did not answer is a warning. A try is not // composed (it has no candidate); a composition that cannot be made leaves it not run, said. -func (st *state) composed(ctx context.Context, v *delivery.Verdict) { +func (st *state) composed(ctx context.Context, v *meshdeliverychecker.VerdictsByGroupHeadsValue) { if st.c.Composition != nil && st.c.Try == nil { // A carry whose candidate's fingerprints did not change keeps the composition it passed (ADR 0548 §5). kept := *st.c.Composition @@ -316,7 +327,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { } if len(st.unpreviewed) > 0 { // An engine would only say it cannot read a manifest the indexer will not have: the check does not ask it. - v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "no preview of a merge of " + + v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun, Words: "no preview of a merge of " + strings.Join(st.unpreviewed, ", ") + " (its head does not merge or its fetch did not answer), so the change cannot be composed"} return } @@ -325,41 +336,41 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) { if err != nil { // Said in the verdict, never left as a composition not yet run (review finding 4). st.j.logf("%s: the composition could not be made, so it is said as not run: %v", st.c.Key, err) - v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "not made: " + err.Error()} + v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun, Words: "not made: " + err.Error()} return } comp.Warnings = append(comp.Warnings, words...) // ADR 0603 §10 withComposition(v, comp) - if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun { - v.Composition.FPSet = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5) + if v.Composition != nil && v.Composition.State != meshdeliverycheckerclient.CompositionNotRun { + v.Composition.Fpset = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5) } } // outcome fills the verdict's levels, times, machines and composition from what ran. -func (st *state) outcome(v delivery.Verdict) Outcome { +func (st *state) outcome(v meshdeliverychecker.VerdictsByGroupHeadsValue) Outcome { v.Series = st.c.allSeries() // the series changes, as structures (A2j finding 2) - if v.State == delivery.VerdictPass || len(v.Levels) == 0 { - v.Levels = map[string]delivery.LevelResult{} + if v.State == meshdeliverycheckerclient.VerdictPass || len(v.Levels) == 0 { + v.Levels = map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{} for level, a := range st.levels { - v.Levels[level] = delivery.LevelResult{Run: a.run, Kept: a.kept, Skipped: a.skipped} + v.Levels[level] = meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{Run: int64(a.run), Kept: int64(a.kept), Skipped: int64(a.skipped)} } } ms := func(s float64) int64 { return int64(s * 1000) } - u, i := st.levels[delivery.LevelUnit], st.levels[delivery.LevelIntegration] - v.Took = delivery.Took{Clone: st.clone.Milliseconds(), Unit: ms(u.elapsed), Lint: ms(u.lint), IntegrationSetup: ms(i.setup), IntegrationRun: ms(i.elapsed)} + u, i := st.levels[buildstep.LevelUnit], st.levels[buildstep.LevelIntegration] + v.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{Clone: st.clone.Milliseconds(), Unit: ms(u.elapsed), Lint: ms(u.lint), IntegrationSetup: ms(i.setup), IntegrationRun: ms(i.elapsed)} for node := range st.nodes { v.Machines = append(v.Machines, node) } sort.Strings(v.Machines) - for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { + for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} { if st.levels[level].over { v.OverBound = append(v.OverBound, level) } } // The composition is made from the engines' answers (ADR 0593 §7): until it is, a verdict says it was not run (ADR 0484 clause 6). - v.Composition = &delivery.Composition{State: delivery.CompositionNotRun} + v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun} var slow []string - for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { + for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} { a := st.levels[level] if !a.over { continue @@ -402,7 +413,7 @@ func (st *state) stepOutOfStep(p Pull, files []string) { } // fail is a verdict of failure at a machine. -func fail(machine, words string) *delivery.Verdict { +func fail(machine, words string) *meshdeliverychecker.VerdictsByGroupHeadsValue { v := failure(machine, words) return &v } @@ -412,14 +423,14 @@ func (st *state) levelsOf() []string { if st.c.Try != nil && st.c.Try.Level != "" { return []string{st.c.Try.Level} // a try of one level runs that level alone } - return []string{delivery.LevelUnit, delivery.LevelIntegration} + return []string{buildstep.LevelUnit, buildstep.LevelIntegration} } // judgeAll puts one check ask per pull request and build module the repository needs, the mesh-sdk member first, and // judges each answer. A head that does not merge, a pin that is not allowed or a level that fails is a failure of that // member; every other member is still judged, and the verdict names every failure (the operator, 2026-11-02), unjoined // to its first lines (settled does that). Only a check whose context ended stops. -func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { +func (st *state) judgeAll(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue { // The mesh-sdk member first: whether its merge changes the TypeScript package's paths decides whether the other members' // Node fetches take the group's package (ADR 0500 §5). pulls := slices.Clone(st.c.Pulls) @@ -437,12 +448,12 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { if v := st.tagged(ctx); v != nil { return v } - var groupPackage map[string]any - var failures []*delivery.Verdict + var groupPackage *buildstep.FetchPackage + var failures []*meshdeliverychecker.VerdictsByGroupHeadsValue for _, p := range pulls { if ctx.Err() != nil { if len(failures) == 0 { // an ask that ended with the context said it already - failures = append(failures, fail(delivery.CheckerModule, ctx.Err().Error())) + failures = append(failures, fail(CheckerModule, ctx.Err().Error())) } break } @@ -450,16 +461,16 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { modules := st.j.Modules.BuildModules(p.Repository) for _, bm := range modules { t := &target{pull: p, module: bm} - if tc, none := delivery.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" { + if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" { t.toolchain = tc.Version - t.agreed = &delivery.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe} + t.agreed = &buildstep.ToolchainUsed{Language: buildrun.LanguageOf[bm], Version: tc.Version, Recipe: tc.Recipe} } - args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk} + args := buildstep.Fetch{Repository: p.Repository, Commit: p.Head, Ref: refOf(p.Number), Base: p.Trunk} if bm == buildrun.NodeBuildModule { if np, ok := st.tags.nodePackageFor(p.Repository); ok { - args["package"] = map[string]any{"name": np.Name, "version": np.Version} // the group's tagged version (ADR 0607 §3) + args.Package = &buildstep.FetchPackage{Name: np.Name, Version: np.Version} // the group's tagged version (ADR 0607 §3) } else if groupPackage != nil && p.Repository != SDKRepository { - args["package"] = groupPackage + args.Package = groupPackage } } if bm == buildrun.GoBuildModule { @@ -469,7 +480,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { continue } if len(beside) > 0 { - args["beside"] = beside + args.Beside = fetchBeside(beside) t.beside = besideLines(beside) besideAtTrunk(st.c, beside, st.beside) } @@ -497,7 +508,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict { // TypeScript version is heard, the form the running checker hands while the new build modules are delivered. Its // removal is a subtask of the milestone after this one's delivery (issue 1966). if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && len(st.tags.node) == 0 && st.j.changesPackage(p, merged) { - groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)} + groupPackage = &buildstep.FetchPackage{Repository: p.Repository, Commit: p.Head, Ref: refOf(p.Number)} } } return joinFailures(failures...) @@ -511,17 +522,17 @@ const DefaultDeclineBound = 30 * time.Minute // each level's digest, kept verdicts and tests. When the answer leaves packages unmet, // their tests needing what that node lacks (issue 890), the passes are kept and the levels from the first such one are // asked again of a node that has it (placed). -func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *delivery.Verdict { +func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *meshdeliverychecker.VerdictsByGroupHeadsValue { return st.askNeeding(ctx, t, merged, st.levelsOf(), nil, nil) } // askNeeding is ask with the levels to run, what the node must have and the packages it tests: none for the first ask // of a target; for the placed one the unmet packages' needs and those packages alone, so no package the first ask judged // is run or counted again (ADR 0552 §4). -func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, levels, needs, packages []string) *delivery.Verdict { +func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, levels, needs, packages []string) *meshdeliverychecker.VerdictsByGroupHeadsValue { p := t.pull placed := len(needs) > 0 - job := &delivery.CheckJob{Fetch: t.fetchArgs, Levels: levels, Kept: true, Packages: packages, Toolchain: t.agreed} + job := &buildstep.CheckJob{Fetch: t.fetchArgs, Levels: levels, Kept: true, Packages: packages, Toolchain: t.agreed} if st.j.Nodes != nil { names, err := st.j.Nodes.Names(ctx) if err == nil && len(names) == 0 { @@ -538,16 +549,16 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l if st.c.Try != nil && st.c.Try.Run != "" { job.Run, job.Kept = st.c.Try.Run, false // a try of some tests runs them alone: a partial run proves no package } - kind := delivery.AskCheck + kind := buildstep.AskCheck if st.c.Kind == KindTrunk { - kind = delivery.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4) + kind = buildstep.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4) } - a := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: delivery.CheckerModule, + a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: CheckerModule, At: st.j.now().UTC(), Check: job, Needs: needs} - if a.Language == delivery.AskGo { + if a.Language == buildstep.AskGo { a.Packages = st.packagesOf(p.Repository) } - var step delivery.Step + var step buildstep.Step var firstDecline time.Time for { what := fmt.Sprintf("the %s ask %s of %s#%d", kind, a.ID, p.Repository, p.Number) @@ -563,7 +574,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l if watch := st.c.Watch; watch != nil { id := a.ID watch(id, nil) // put: a try names it at once, not only when it ends (issue 1890) - waiting = buildrun.WithStepSeen(ctx, func(s delivery.Step) { watch(id, &s) }) + waiting = buildrun.WithStepSeen(ctx, func(s buildstep.Step) { watch(id, &s) }) } var err error step, err = st.j.Asks.Wait(waiting, a) @@ -573,7 +584,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l } return fail(t.module, fmt.Sprintf("the check of %s#%d: %v", p.Repository, p.Number, err)) } - d, declined := delivery.DeclinedOf(step) + d, declined := buildstep.DeclinedOf(step) if !declined { break } @@ -598,7 +609,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l st.j.logf("%s#%d: the ask %s was declined on %s, which lacks %s; asked again", p.Repository, p.Number, a.ID, step.Node, strings.Join(d.Lacks, ", ")) a.ID, a.At = buildstep.NewID(), now.UTC() } - var failures []*delivery.Verdict + var failures []*meshdeliverychecker.VerdictsByGroupHeadsValue // The fetch and the digests are what of the ask's time its levels' running did not take (Took.Clone). took := time.Duration(0) if !step.Started.IsZero() && step.Ended.After(step.Started) { @@ -607,15 +618,15 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l t.node = step.Node st.nodes[step.Node] = true st.c.Progress.Set(fmt.Sprintf("judge the check of %s#%d", p.Repository, p.Number), step.Node) - if step.State != delivery.StepPassed { + if step.State != buildstep.StepPassed { return fail(step.Node, fmt.Sprintf("the check of %s#%d %s: %s", p.Repository, p.Number, step.State, step.Reason)) } - var answer delivery.CheckAnswer + var answer buildstep.CheckAnswer if err := json.Unmarshal(step.Result, &answer); err != nil { return fail(step.Node, fmt.Sprintf("the check of %s#%d answered what is not a check: %s", p.Repository, p.Number, step.Result)) } - if err := json.Unmarshal(answer.Fetch, &t.fetched); err != nil || t.fetched.Workspace == "" { - return fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, answer.Fetch)) + if t.fetched = answer.Fetch; t.fetched.Workspace == "" { + return fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, step.Result)) } if len(t.fetched.Conflicts) > 0 { if st.c.Try == nil { @@ -650,7 +661,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l } } st.clone += max(0, took-time.Duration(ran*float64(time.Second))) - if unmet := delivery.UnmetNeeds(answer.Levels); len(unmet) > 0 && !placed { + if unmet := buildstep.UnmetNeeds(answer.Levels); len(unmet) > 0 && !placed { // Every level ran, its passes kept, and some packages were left to a node with what their tests need: the levels // from the first that left one are asked of such a node, which takes the kept passes and tests those (issue 890). var again, packages []string @@ -673,7 +684,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l } // unmetNames names the unmet packages and their needs. -func unmetNames(unmet []delivery.PackageNeeds) string { +func unmetNames(unmet []buildstep.PackageNeeds) string { var out []string for _, u := range unmet { out = append(out, u.Package+" (needs "+strings.Join(u.Needs, ", ")+")") @@ -682,7 +693,7 @@ func unmetNames(unmet []delivery.PackageNeeds) string { } // unmetOf names the unmet packages of every level. -func unmetOf(levels []delivery.LevelAnswer) string { +func unmetOf(levels []buildstep.LevelAnswer) string { var out []string for _, la := range levels { if len(la.Unmet) > 0 { @@ -696,7 +707,7 @@ func unmetOf(levels []delivery.LevelAnswer) string { // of the packages that passed, the kept ones taken are counted, and every test result is read and counted, also after // one that did not pass, so the verdict names every failed test of the level (issue 1921). It answers the level's // running time (its tests' and setup's). -func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer) (*delivery.Verdict, float64) { +func (st *state) level(ctx context.Context, t *target, la buildstep.LevelAnswer) (*meshdeliverychecker.VerdictsByGroupHeadsValue, float64) { level := la.Level a := st.levels[level] if a == nil { @@ -930,18 +941,18 @@ func firstLine(s string) string { const MaxFailureLine = 300 // levelFailure turns a level that did not pass into a verdict: Failed names what failed first with its words whole, -// and Failures every failed test of the level's results with its first line, at most delivery.MaxFailures and an entry +// and Failures every failed test of the level's results with its first line, at most meshdeliverycheckerclient.MaxFailures and an entry // saying how many more (issue 1921). -func levelFailure(t *target, level string, results ...Tested) *delivery.Verdict { - v := delivery.Verdict{State: delivery.VerdictFail} - var all []delivery.FailedFirst +func levelFailure(t *target, level string, results ...Tested) *meshdeliverychecker.VerdictsByGroupHeadsValue { + v := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail} + var all []FailedFirst for _, res := range results { if len(res.Failed) == 0 { why := res.Why if why == "" { why = "the " + level + " level did not pass" } - all = append(all, delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: why}) + all = append(all, FailedFirst{Test: level + ": " + t.pull.Repository, Words: why}) continue } for _, f := range res.Failed { @@ -949,75 +960,76 @@ func levelFailure(t *target, level string, results ...Tested) *delivery.Verdict if t.module == buildrun.NodeBuildModule && f.Package == "" { name = res.Module } - all = append(all, delivery.FailedFirst{Test: level + ": " + name, Words: failureWordsAfter(f, hidWords(t.module, res.By, f))}) + all = append(all, FailedFirst{Test: level + ": " + name, Words: failureWordsAfter(f, hidWords(t.module, res.By, f))}) } } if len(all) == 0 { // no result: a level said failed with nothing to name - all = append(all, delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: "the " + level + " level did not pass"}) + all = append(all, FailedFirst{Test: level + ": " + t.pull.Repository, Words: "the " + level + " level did not pass"}) } - v.Failed = all[0] - v.Failures = all // whole: settled cuts them to their first lines once every failure of the check is joined + first := all[0] + v.Failed = &first + v.Failures = failureItems(all) // whole: settled cuts them to their first lines once every failure of the check is joined return &v } // joinFailures is one failed verdict of every failure of vs, in the order given (a nil one passed), unsettled: Failed is // the first whole, and Failures every failure whole, or none when the one failure is no level's (a conflict, a refused // step: one line, as before issue 1921). Nil when none failed. -func joinFailures(vs ...*delivery.Verdict) *delivery.Verdict { - var out *delivery.Verdict - var all []delivery.FailedFirst +func joinFailures(vs ...*meshdeliverychecker.VerdictsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValue { + var out *meshdeliverychecker.VerdictsByGroupHeadsValue + var all []FailedFirst listed := false for _, v := range vs { if v == nil { continue } if out == nil { - out = &delivery.Verdict{State: delivery.VerdictFail, Failed: v.Failed} + out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: v.Failed} } if len(v.Failures) > 0 { - all, listed = append(all, v.Failures...), true + all, listed = append(all, failedList(v.Failures)...), true continue } - all = append(all, v.Failed) + all = append(all, failedOf(*v)) } if out != nil && (listed || len(all) > 1) { - out.Failures = all + out.Failures = failureItems(all) } return out } // settled is a joined verdict as it is written: each failure of its list cut to its first line, at most -// delivery.MaxFailures and then how many more (failureLines). -func settled(v *delivery.Verdict) *delivery.Verdict { +// meshdeliverycheckerclient.MaxFailures and then how many more (failureLines). +func settled(v *meshdeliverychecker.VerdictsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValue { if v != nil && len(v.Failures) > 0 { - v.Failures = failureLines(v.Failures) + v.Failures = failureItems(failureLines(failedList(v.Failures))) } return v } // addFailure is a settled list of failures with one more at its end: the first failure is put first when the list is -// empty (a verdict of one failure keeps none), and past delivery.MaxFailures the last entry counts one more. -func addFailure(v *delivery.Verdict, f delivery.FailedFirst) { +// empty (a verdict of one failure keeps none), and past meshdeliverycheckerclient.MaxFailures the last entry counts one more. +func addFailure(v *meshdeliverychecker.VerdictsByGroupHeadsValue, f FailedFirst) { if len(v.Failures) == 0 { - v.Failures = failureLines([]delivery.FailedFirst{v.Failed}) + v.Failures = failureItems(failureLines([]FailedFirst{failedOf(*v)})) } - if n := len(v.Failures); n > delivery.MaxFailures { - v.Failures[n-1].Test = fmt.Sprintf("and %d more", failedCount(*v)-delivery.MaxFailures+1) + if n := len(v.Failures); n > meshdeliverycheckerclient.MaxFailures { + v.Failures[n-1].Test = fmt.Sprintf("and %d more", failedCount(*v)-meshdeliverycheckerclient.MaxFailures+1) return } - v.Failures = append(v.Failures, failureLines([]delivery.FailedFirst{f})...) + v.Failures = append(v.Failures, failureItems(failureLines([]FailedFirst{f}))...) } -// failureLines is every failure with the first line of its words, cut to MaxFailureLine, at most delivery.MaxFailures +// failureLines is every failure with the first line of its words, cut to MaxFailureLine, at most meshdeliverycheckerclient.MaxFailures // and then one entry saying how many more failed. -func failureLines(all []delivery.FailedFirst) []delivery.FailedFirst { - n := min(len(all), delivery.MaxFailures) - out := make([]delivery.FailedFirst, 0, n+1) +func failureLines(all []FailedFirst) []FailedFirst { + n := min(len(all), meshdeliverycheckerclient.MaxFailures) + out := make([]FailedFirst, 0, n+1) for _, f := range all[:n] { - out = append(out, delivery.FailedFirst{Test: f.Test, Machine: f.Machine, Words: clipLine(firstLine(f.Words), MaxFailureLine)}) + out = append(out, FailedFirst{Test: f.Test, Machine: f.Machine, Words: clipLine(firstLine(f.Words), MaxFailureLine)}) } if more := len(all) - n; more > 0 { - out = append(out, delivery.FailedFirst{Test: fmt.Sprintf("and %d more", more)}) + out = append(out, FailedFirst{Test: fmt.Sprintf("and %d more", more)}) } return out } diff --git a/modules/mesh-delivery-checker/internal/check/judged_test.go b/modules/mesh-delivery-checker/internal/check/judged_test.go index e915b390..40a8ebaf 100644 --- a/modules/mesh-delivery-checker/internal/check/judged_test.go +++ b/modules/mesh-delivery-checker/internal/check/judged_test.go @@ -5,7 +5,8 @@ import ( "strings" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // The judged_by answer of mesh-build-go's digest and test steps (ADR 0535 §5; the milestone file's interface for issue @@ -84,7 +85,7 @@ func TestAnInstalledModuleWithoutTheRunnerJudgesAsTodayAndTheLineSaysSo(t *testi b := candidateBench(t, false) cs := caseOf(t, pullOf(catalogueRepository, 9, head1)) out := b.judge(t, cs) - if out.Verdict.State != delivery.VerdictPass { + if out.Verdict.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", out.Verdict) } // As today: the digests key kept verdicts, and the levels are asked without the runner the module ignored. @@ -112,7 +113,7 @@ func TestACandidateJudgedCheckTakesNoKeptVerdictAndWritesNone(t *testing.T) { // t.Fatal(err) } out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 9, head1))) - if out.Verdict.State != delivery.VerdictPass { + if out.Verdict.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", out.Verdict) } tests := b.steps.tool("test") @@ -124,7 +125,7 @@ func TestACandidateJudgedCheckTakesNoKeptVerdictAndWritesNone(t *testing.T) { // t.Errorf("the %v level was asked for %v, want it whole", c.args["level"], c.args["packages"]) } } - if u := out.Verdict.Levels[delivery.LevelUnit]; u.Kept != 0 { + if u := out.Verdict.Levels[buildstep.LevelUnit]; u.Kept != 0 { t.Errorf("unit = %+v, want nothing taken", u) } if keys, _ := b.kept.Keys(context.Background()); len(keys) != 1 || keys[0] != "unit."+digA { @@ -148,8 +149,8 @@ func TestTheLineNamesTheCandidateThatJudged(t *testing.T) { // 1898/AC6 } // A long failing line keeps the program it was judged by; only the lesser parts go. v := out.Verdict - v.State = delivery.VerdictFail - v.Failed = delivery.FailedFirst{Test: "unit: git.novox.be/novox/mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go.TestADigestFoldsTheSiblings", Words: strings.Repeat("x", 60)} + v.State = meshdeliverycheckerclient.VerdictFail + v.Failed = &FailedFirst{Test: "unit: git.novox.be/novox/mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go.TestADigestFoldsTheSiblings", Words: strings.Repeat("x", 60)} if l := Line(v, cs, p, out.Builds, out.Slowest, out.Judged); !strings.Contains(l, "judged by the candidate's") { t.Errorf("the long line %q dropped the program", l) } @@ -174,7 +175,7 @@ func TestACandidateThatDoesNotCompileFailsTheCheckWithTheCompilersWords(t *testi b.steps.handlers["build_go_digest"] = func(stepCall) (any, error) { return nil, errorString(refusal) } b.test = func(stepCall) (any, error) { return nil, errorString(refusal) } out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 9, head1))) - if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Words, "undefined: x") { + if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(out.Verdict.Failed.Words, "undefined: x") { t.Fatalf("verdict = %+v, want a failure naming the compile error", out.Verdict) } if n := len(b.steps.tool("test")); n != 1 { @@ -195,9 +196,9 @@ func TestTheStatusTheForgeShowsNamesTheProgramThatJudged(t *testing.T) { // 1898 r := newRig(t, j) r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1))) r.ch.Wait() - key, _ := delivery.CheckerOwedKey(repoA, 7, head1) + key, _ := CheckerOwedKey(repoA, 7, head1) raw, _, _, _ := r.owed.Get(ctx, key) - o, err := delivery.DecodeCheckerOwed(raw) + o, err := DecodeCheckerOwed(raw) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-checker/internal/check/kept.go b/modules/mesh-delivery-checker/internal/check/kept.go index 7f58dd50..5b581634 100644 --- a/modules/mesh-delivery-checker/internal/check/kept.go +++ b/modules/mesh-delivery-checker/internal/check/kept.go @@ -7,7 +7,8 @@ import ( "sort" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" ) // Kept is the checker's bucket of kept verdicts (ADR 0444, ADR 0484 clause 13): a passing verdict per package and level, @@ -17,31 +18,31 @@ import ( type Kept struct{ Store KV } // Lookup is the kept verdict of a level under a digest, or none. -func (k Kept) Lookup(ctx context.Context, level, digest string) (delivery.Kept, bool, error) { - key, err := delivery.KeptKey(level, digest) +func (k Kept) Lookup(ctx context.Context, level, digest string) (meshdeliverychecker.KeptByLevelDigestValue, bool, error) { + key, err := meshdeliverycheckerclient.KeptKey(level, digest) if err != nil { - return delivery.Kept{}, false, err + return meshdeliverychecker.KeptByLevelDigestValue{}, false, err } value, _, found, err := k.Store.Get(ctx, key) if err != nil || !found { - return delivery.Kept{}, false, err + return meshdeliverychecker.KeptByLevelDigestValue{}, false, err } - v, err := delivery.DecodeKept(value) + v, err := meshdeliverycheckerclient.DecodeKept(value) if err != nil { - return delivery.Kept{}, false, fmt.Errorf("%s holds what is not a kept verdict: %w", key, err) + return meshdeliverychecker.KeptByLevelDigestValue{}, false, fmt.Errorf("%s holds what is not a kept verdict: %w", key, err) } return v, true, nil } // Keep writes v under level and digest by a create. It answers false, and writes nothing, when the key exists: the first // value stands. -func (k Kept) Keep(ctx context.Context, level, digest string, v delivery.Kept) (bool, error) { - key, err := delivery.KeptKey(level, digest) +func (k Kept) Keep(ctx context.Context, level, digest string, v meshdeliverychecker.KeptByLevelDigestValue) (bool, error) { + key, err := meshdeliverycheckerclient.KeptKey(level, digest) if err != nil { return false, err } v.Level = level - data, err := delivery.EncodeKept(v) + data, err := meshdeliverycheckerclient.EncodeKept(v) if err != nil { return false, err } @@ -77,8 +78,8 @@ func (k Kept) KeepPasses(ctx context.Context, repository, level, toolchain strin if failedIn(res, p.Package) { continue } - made, err := k.Keep(ctx, level, digest, delivery.Kept{State: delivery.VerdictPass, Repository: repository, Package: p.Package, - Level: level, Tests: p.Tests, Took: int64(p.Elapsed * 1000), Toolchain: toolchain, At: at}) + made, err := k.Keep(ctx, level, digest, meshdeliverychecker.KeptByLevelDigestValue{State: meshdeliverycheckerclient.VerdictPass, Repository: repository, Package: p.Package, + Level: level, Tests: int64(p.Tests), Took: int64(p.Elapsed * 1000), Toolchain: toolchain, At: at.UTC().Format(time.RFC3339)}) if err != nil { return kept, err } diff --git a/modules/mesh-delivery-checker/internal/check/kept_test.go b/modules/mesh-delivery-checker/internal/check/kept_test.go index 8fc9e788..64ba37c7 100644 --- a/modules/mesh-delivery-checker/internal/check/kept_test.go +++ b/modules/mesh-delivery-checker/internal/check/kept_test.go @@ -6,7 +6,9 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "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-delivery/sdk/buildstep" ) var at0 = time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC) @@ -16,23 +18,23 @@ const ( digB = "bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22" ) -func keptValue(pkg string, tests int) delivery.Kept { - return delivery.Kept{State: delivery.VerdictPass, Repository: "novox/mesh-delivery", Package: pkg, Tests: tests, Took: 120, Toolchain: "go1.26.0", At: at0} +func keptValue(pkg string, tests int) meshdeliverychecker.KeptByLevelDigestValue { + return meshdeliverychecker.KeptByLevelDigestValue{State: meshdeliverycheckerclient.VerdictPass, Repository: "novox/mesh-delivery", Package: pkg, Tests: int64(tests), Took: 120, Toolchain: "go1.26.0", At: at0.UTC().Format(time.RFC3339)} } func TestAPassIsKeptByACreateUnderItsLevelAndDigest(t *testing.T) { // 1287/AC1 ctx, st := context.Background(), newMemKV() k := Kept{Store: st} - made, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/a", 3)) + made, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/a", 3)) if err != nil || !made { t.Fatalf("Keep = %v, %v, want made", made, err) } - key, _ := delivery.KeptKey(delivery.LevelUnit, digA) + key, _ := meshdeliverycheckerclient.KeptKey(buildstep.LevelUnit, digA) if st.creates[key] != 1 { t.Fatalf("the key %s was created %d times, want once", key, st.creates[key]) } - got, found, err := k.Lookup(ctx, delivery.LevelUnit, digA) - if err != nil || !found || got.Package != "m/a" || got.Tests != 3 || got.Level != delivery.LevelUnit || got.Toolchain != "go1.26.0" { + got, found, err := k.Lookup(ctx, buildstep.LevelUnit, digA) + if err != nil || !found || got.Package != "m/a" || got.Tests != 3 || got.Level != buildstep.LevelUnit || got.Toolchain != "go1.26.0" { t.Fatalf("Lookup = %+v, %v, %v", got, found, err) } } @@ -40,14 +42,14 @@ func TestAPassIsKeptByACreateUnderItsLevelAndDigest(t *testing.T) { // 1287/AC1 func TestASecondKeepUnderTheSameDigestIsRefusedAndTheFirstStands(t *testing.T) { // 1287/AC2 ctx, st := context.Background(), newMemKV() k := Kept{Store: st} - if made, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/a", 3)); err != nil || !made { + if made, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/a", 3)); err != nil || !made { t.Fatal(made, err) } - made, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/other", 9)) + made, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/other", 9)) if err != nil || made { t.Fatalf("the second Keep = %v, %v, want refused without an error", made, err) } - got, _, _ := k.Lookup(ctx, delivery.LevelUnit, digA) + got, _, _ := k.Lookup(ctx, buildstep.LevelUnit, digA) if got.Package != "m/a" || got.Tests != 3 { t.Errorf("the first value did not stand: %+v", got) } @@ -103,14 +105,14 @@ func TestOnlyTheSkippingPackageOfAResultIsWithheld(t *testing.T) { // issue 890 func TestAKeptValueOfAnotherDigestIsNotRead(t *testing.T) { // 1287/AC7 ctx, st := context.Background(), newMemKV() k := Kept{Store: st} - if _, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/a", 3)); err != nil { + if _, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/a", 3)); err != nil { t.Fatal(err) } // A toolchain change changes the digest, so the new digest finds nothing and the old key is not deleted. - if _, found, _ := k.Lookup(ctx, delivery.LevelUnit, digB); found { + if _, found, _ := k.Lookup(ctx, buildstep.LevelUnit, digB); found { t.Error("a digest that was not kept was found") } - if _, found, _ := k.Lookup(ctx, delivery.LevelIntegration, digA); found { + if _, found, _ := k.Lookup(ctx, buildstep.LevelIntegration, digA); found { t.Error("a kept unit verdict was found at the integration level") } if keys, _ := st.Keys(ctx); len(keys) != 1 { @@ -121,8 +123,8 @@ func TestAKeptValueOfAnotherDigestIsNotRead(t *testing.T) { // 1287/AC7 func TestAKeptVerdictThatIsNotAPassIsRefused(t *testing.T) { k := Kept{Store: newMemKV()} v := keptValue("m/a", 1) - v.State = delivery.VerdictFail - if _, err := k.Keep(context.Background(), delivery.LevelUnit, digA, v); err == nil { + v.State = meshdeliverycheckerclient.VerdictFail + if _, err := k.Keep(context.Background(), buildstep.LevelUnit, digA, v); err == nil { t.Error("a failure was kept") } } diff --git a/modules/mesh-delivery-checker/internal/check/line.go b/modules/mesh-delivery-checker/internal/check/line.go index dc81ac58..6167710a 100644 --- a/modules/mesh-delivery-checker/internal/check/line.go +++ b/modules/mesh-delivery-checker/internal/check/line.go @@ -6,7 +6,9 @@ import ( "strings" "unicode/utf8" - "git.novox.be/novox/mesh-sdk/go/delivery" + "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-delivery/sdk/buildstep" ) // MaxLine is the most characters a status's line has: the forge shows a status's description in a short space, and the full @@ -33,8 +35,8 @@ func short12(s string) string { } // firstFailed is what failed first, in words: a conflict, a refused step or a test, named before anything else. -func firstFailed(v delivery.Verdict) string { - f := v.Failed +func firstFailed(v meshdeliverychecker.VerdictsByGroupHeadsValue) string { + f := failedOf(v) switch { case f.Test != "": if w := firstWordsLine(f.Words); w != "" { @@ -51,7 +53,7 @@ func firstFailed(v delivery.Verdict) string { // failedLines is everything a verdict names as failed, one line each: every failed test with its first line (issue // 1921), or what failed first when the verdict names no list (a conflict, a refused step, an older verdict). -func failedLines(v delivery.Verdict) []string { +func failedLines(v meshdeliverychecker.VerdictsByGroupHeadsValue) []string { if len(v.Failures) == 0 { if f := firstFailed(v); f != "" { return []string{f} @@ -60,7 +62,7 @@ func failedLines(v delivery.Verdict) []string { } lines := make([]string, 0, len(v.Failures)) for _, f := range v.Failures { - lines = append(lines, firstFailed(delivery.Verdict{Failed: f})) + lines = append(lines, firstFailed(meshdeliverychecker.VerdictsByGroupHeadsValue{Failed: &FailedFirst{Machine: f.Machine, Test: f.Test, Words: f.Words}})) } return lines } @@ -68,7 +70,7 @@ func failedLines(v delivery.Verdict) []string { // failedHead is the head of a failed verdict's line: what failed first, and when more did, how many failed and "and N // more" after it (the operator, 2026-11-02), so a line cut to MaxLine still says that more failed. The words of the first // are cut to fit room characters when room is not negative; the count and "and N more" are never cut. -func failedHead(v delivery.Verdict, room int) string { +func failedHead(v meshdeliverychecker.VerdictsByGroupHeadsValue, room int) string { first := firstFailed(v) n := failedCount(v) if n <= 1 { @@ -93,13 +95,13 @@ func clipRunes(s string, n int) string { return string(r[:n-1]) + "…" } -// failedCount is how many failures a verdict names, the ones after delivery.MaxFailures counted too. -func failedCount(v delivery.Verdict) int { +// failedCount is how many failures a verdict names, the ones after meshdeliverycheckerclient.MaxFailures counted too. +func failedCount(v meshdeliverychecker.VerdictsByGroupHeadsValue) int { n := len(v.Failures) - if n > delivery.MaxFailures { + if n > meshdeliverycheckerclient.MaxFailures { var more int if _, err := fmt.Sscanf(v.Failures[n-1].Test, "and %d more", &more); err == nil { - return delivery.MaxFailures + more + return meshdeliverycheckerclient.MaxFailures + more } } return n @@ -150,7 +152,7 @@ func (p pieces) join(drop int) string { return joinSet(parts...) } -func levelWords(name string, r delivery.LevelResult) string { +func levelWords(name string, r meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue) string { s := fmt.Sprintf("%s %d run, %d kept", name, r.Run, r.Kept) if r.Skipped > 0 { s += fmt.Sprintf(", %d skipped", r.Skipped) @@ -160,14 +162,16 @@ func levelWords(name string, r delivery.LevelResult) string { // compositionWords says the composition: not yet run, or once the engines answered (ADR 0593 §7) passed, with the nodes // that gave no answer, or failed, with the time it took and the machines it composed (issue 1300). -func compositionWords(c *delivery.Composition, took int64) string { return composition(c, took, false) } +func compositionWords(c *meshdeliverychecker.VerdictsByGroupHeadsValueComposition, took int64) string { + return composition(c, took, false) +} // composition is compositionWords; short counts the machines and leaves the SDK commit to the report. -func composition(c *delivery.Composition, took int64, short bool) string { - if c != nil && c.State == delivery.CompositionNotRun && c.Words != "" { +func composition(c *meshdeliverychecker.VerdictsByGroupHeadsValueComposition, took int64, short bool) string { + if c != nil && c.State == meshdeliverycheckerclient.CompositionNotRun && c.Words != "" { return "composition not judged: " + c.Words } - if c == nil || c.State == delivery.CompositionNotRun { + if c == nil || c.State == meshdeliverycheckerclient.CompositionNotRun { return "composition not yet run" } in := "" @@ -186,14 +190,14 @@ func composition(c *delivery.Composition, took int64, short bool) string { } } warned := "" - if c.State == delivery.CompositionPass && c.Words != "" { + if c.State == meshdeliverycheckerclient.CompositionPass && c.Words != "" { warned = " (" + c.Words + ")" // a node that did not answer (ADR 0593 §9) } switch { - case c.State == delivery.CompositionPass && short: + case c.State == meshdeliverycheckerclient.CompositionPass && short: return "composition passed" + in + on + warned // what it composed with is in the report - case c.State == delivery.CompositionPass: - return "composition passed" + in + " with " + short12(c.SDK) + on + warned + case c.State == meshdeliverycheckerclient.CompositionPass: + return "composition passed" + in + " with " + short12(c.Sdk) + on + warned } return "composition failed" + in + on + ": " + c.Words } @@ -211,7 +215,7 @@ func buildWords(builds []BuildNote) string { // carriedWords say the commits a verdict was carried from, for the line of pull request p (ADR 0548 §8): its own trunk's // commit as twelve characters, a sibling's with its name before it; only those that moved, sorted. Empty when none did. -func carriedWords(from map[string]string, v delivery.Verdict, cs *Case, p Pull) string { +func carriedWords(from map[string]string, v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull) string { var own string var others []string for r, old := range from { @@ -243,7 +247,7 @@ func carriedWords(from map[string]string, v delivery.Verdict, cs *Case, p Pull) } // parts makes the pieces of the line of a pull request of a case. -func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) pieces { +func parts(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) pieces { var pc pieces pc.judged = judged[pullKey(p)] pc.series = seriesWords(cs, p) @@ -252,7 +256,7 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s } commits := short12(p.Head) + " on " + short12(p.Trunk) switch v.State { - case delivery.VerdictRunning: + case meshdeliverycheckerclient.VerdictRunning: pc.head = "checking " + commits if cs != nil && cs.CarriedFrom != nil { // Out of date and being carried to the new commit (ADR 0548 §6): pending, and saying so. @@ -261,7 +265,7 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s pc.carried = "from " + w } } - case delivery.VerdictPass: + case meshdeliverycheckerclient.VerdictPass: pc.head = "pass " + commits if w := carriedWords(v.CarriedFrom, v, cs, p); w != "" { pc.carried = "carried from " + w // the counts that follow say what the carry ran and kept @@ -271,16 +275,16 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s pc.commits = commits } var counts []string - for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { + for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} { if r, ok := v.Levels[level]; ok { counts = append(counts, levelWords(level, r)) } } pc.counts = strings.Join(counts, " | ") var short []string - for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} { + for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} { if r, ok := v.Levels[level]; ok { - name := map[string]string{delivery.LevelUnit: "unit", delivery.LevelIntegration: "int"}[level] + name := map[string]string{buildstep.LevelUnit: "unit", buildstep.LevelIntegration: "int"}[level] w := fmt.Sprintf("%s %d run %d kept", name, r.Run, r.Kept) if r.Skipped > 0 { w += fmt.Sprintf(" %d skipped", r.Skipped) @@ -289,8 +293,8 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s } } pc.shortCounts = strings.Join(short, " | ") - if v.State != delivery.VerdictRunning { - t := v.Took + if v.State != meshdeliverycheckerclient.VerdictRunning { + t := tookOf(v) var times []string for _, x := range []struct { name string @@ -329,13 +333,13 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s } } pc.builds = buildWords(builds) - if v.State != delivery.VerdictRunning { - pc.composition = compositionWords(v.Composition, v.Took.Composition) - pc.shortComposition = composition(v.Composition, v.Took.Composition, true) + if v.State != meshdeliverycheckerclient.VerdictRunning { + pc.composition = compositionWords(v.Composition, tookOf(v).Composition) + pc.shortComposition = composition(v.Composition, tookOf(v).Composition, true) } if cs != nil && len(cs.Pulls) > 1 { pc.group = fmt.Sprintf("group %s of %d pull requests", cs.Group, len(cs.Pulls)) - if v.State != delivery.VerdictPass { + if v.State != meshdeliverycheckerclient.VerdictPass { pc.group = fmt.Sprintf("%s#%d in group %s", p.Repository, p.Number, cs.Group) } } @@ -343,7 +347,7 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s } // Report is the full line of a pull request's status, with no limit: what the read tools and the full report say. -func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string { +func Report(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string { return parts(v, cs, p, builds, slowest, judged).join(0) } @@ -355,9 +359,9 @@ func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest [] // builds, then the group drop first, then the times are given in their compact form, then dropped, and the line is cut at last. // The program that judged is never dropped: a line naming it gives the counts in their compact form, then drops them and // what follows, and is cut in its first part rather than lose it. -func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string { +func Line(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string { pc := parts(v, cs, p, builds, slowest, judged) - if v.State == delivery.VerdictFail { + if v.State == meshdeliverycheckerclient.VerdictFail { // The head gives way first to the commits and what is never dropped: its count and "and N more" stay whole. kept := joinSet(pc.commits, pc.carried, pc.warning, pc.judged, pc.series) pc.head = failedHead(v, MaxLine-utf8.RuneCountInString(" | "+kept)) @@ -379,7 +383,7 @@ func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []st return string(head[:room]) + "…" + rest } } - if v.State == delivery.VerdictFail { + if v.State == meshdeliverycheckerclient.VerdictFail { // A failure's head, sized above, and the commits fit: the counts and the composition are in the report. if s := joinSet(pc.head, pc.commits, pc.carried, pc.warning, pc.judged, pc.series); utf8.RuneCountInString(s) <= MaxLine { return s diff --git a/modules/mesh-delivery-checker/internal/check/line_test.go b/modules/mesh-delivery-checker/internal/check/line_test.go index 66cbbdb4..0ae89f10 100644 --- a/modules/mesh-delivery-checker/internal/check/line_test.go +++ b/modules/mesh-delivery-checker/internal/check/line_test.go @@ -6,19 +6,22 @@ import ( "testing" "unicode/utf8" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "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/sdk/buildstep" ) -func lineCase(t testing.TB, pulls ...delivery.CandidatePull) (*Case, Pull) { +func lineCase(t testing.TB, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) (*Case, Pull) { cs := caseOf(t, pulls...) return cs, cs.Pulls[0] } -func passVerdict() delivery.Verdict { - return delivery.Verdict{State: delivery.VerdictPass, - Levels: map[string]delivery.LevelResult{delivery.LevelUnit: {Run: 12, Kept: 301}, delivery.LevelIntegration: {Run: 3, Kept: 40}}, - Took: delivery.Took{Queue: 800, Clone: 4100, Unit: 400, IntegrationSetup: 3200, IntegrationRun: 12000}, - Composition: &delivery.Composition{State: delivery.CompositionNotRun}} +func passVerdict() meshdeliverychecker.VerdictsByGroupHeadsValue { + return meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictPass, + Levels: map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{buildstep.LevelUnit: {Run: 12, Kept: 301}, buildstep.LevelIntegration: {Run: 3, Kept: 40}}, + Took: &meshdeliverychecker.VerdictsByGroupHeadsValueTook{Queue: 800, Clone: 4100, Unit: 400, IntegrationSetup: 3200, IntegrationRun: 12000}, + Composition: &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun}} } func TestAPassOpensWithTheVerdictAndSaysTheCommitsTheCountsTheTimesAndThatTheCompositionWasNotRun(t *testing.T) { // 1300/AC1 @@ -44,17 +47,17 @@ func TestAPassOpensWithTheVerdictAndSaysTheCommitsTheCountsTheTimesAndThatTheCom func TestAFailureNamesTheTestTheConflictOrTheRefusedStepFirst(t *testing.T) { // 1300/AC2 cs, p := lineCase(t, pullOf(repoA, 7, head1)) - long := delivery.FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + "TestSomething", Words: strings.Repeat("because ", 40)} + long := FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + "TestSomething", Words: strings.Repeat("because ", 40)} for name, c := range map[string]struct { - v delivery.Verdict + v meshdeliverychecker.VerdictsByGroupHeadsValue want string }{ - "test": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}, "failed: unit: m/a.TestX: want 4"}, - "conflict": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "shanks", Words: "novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}}, "failed: shanks: novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}, - "refused": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "mesh-build-go", Words: "no go build node qualifies: builders disagree"}}, "failed: mesh-build-go: no go build node qualifies"}, + "test": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}, "failed: unit: m/a.TestX: want 4"}, + "conflict": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "shanks", Words: "novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}}, "failed: shanks: novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}, + "refused": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "mesh-build-go", Words: "no go build node qualifies: builders disagree"}}, "failed: mesh-build-go: no go build node qualifies"}, // ADR 0591: a first failure longer than the forge's limit is shortened; "failed (N)" and "and N-1 more" are never cut. - "many, the first too long": {delivery.Verdict{State: delivery.VerdictFail, Failed: long, Failures: []delivery.FailedFirst{long, {Machine: "g14", Words: "route-proxy is defined twice"}, - {Test: "unit: m/b.TestY", Words: "boom"}}}, "failed (3): integration: pkg/pkg/"}, + "many, the first too long": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &long, Failures: failureItems([]FailedFirst{long, {Machine: "g14", Words: "route-proxy is defined twice"}, + {Test: "unit: m/b.TestY", Words: "boom"}})}, "failed (3): integration: pkg/pkg/"}, } { line := Line(c.v, cs, p, nil, nil, nil) if more := len(c.v.Failures) - 1; more > 0 && (!strings.Contains(line, fmt.Sprintf("…; and %d more | 111111111111", more)) || utf8.RuneCountInString(line) > MaxLine) { @@ -79,7 +82,7 @@ func TestALevelOverItsBoundIsNamedWithItsSlowestTestsAndTheVerdictIsNotChanged(t func TestSkippedTestsAreSaidAndNeverAFlatPass(t *testing.T) { // 1300/AC6 cs, p := lineCase(t, pullOf(repoA, 7, head1)) v := passVerdict() - v.Levels[delivery.LevelUnit] = delivery.LevelResult{Run: 12, Kept: 301, Skipped: 2} + v.Levels[buildstep.LevelUnit] = meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{Run: 12, Kept: 301, Skipped: 2} if line := Line(v, cs, p, nil, nil, nil); !strings.Contains(line, "unit 12 run, 301 kept, 2 skipped") { t.Errorf("line = %q", line) } @@ -108,7 +111,7 @@ func TestTheBuildsOfTheTouchedModulesAreSaidAndDroppedFirstWhenTheLineIsTooLong( t.Errorf("line %q is too long", line) } short := passVerdict() - short.Took = delivery.Took{} + short.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{} if l := Line(short, cs, p, builds[:1], nil, nil); !strings.Contains(l, "builds: avahi built") { t.Errorf("a short line keeps its builds: %q", l) } @@ -116,7 +119,7 @@ func TestTheBuildsOfTheTouchedModulesAreSaidAndDroppedFirstWhenTheLineIsTooLong( func TestALineIsNeverLongerThanTheLimit(t *testing.T) { // 1300/AC1 cs, p := lineCase(t, pullOf(repoA, 7, head1)) - v := delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + ".TestSomething", Words: strings.Repeat("because ", 40)}} + v := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + ".TestSomething", Words: strings.Repeat("because ", 40)}} if line := Line(v, cs, p, nil, nil, nil); utf8.RuneCountInString(line) > MaxLine || !strings.HasPrefix(line, "failed: integration: ") { t.Errorf("line = %q (%d)", line, utf8.RuneCountInString(line)) } @@ -124,7 +127,7 @@ func TestALineIsNeverLongerThanTheLimit(t *testing.T) { // 1300/AC1 func TestARunningCheckIsPending(t *testing.T) { cs, p := lineCase(t, pullOf(repoA, 7, head1)) - if line := Line(delivery.Verdict{State: delivery.VerdictRunning}, cs, p, nil, nil, nil); line != "checking 111111111111 on aaaaaaaaaaaa" { + if line := Line(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, cs, p, nil, nil, nil); line != "checking 111111111111 on aaaaaaaaaaaa" { t.Errorf("line = %q", line) } } @@ -132,7 +135,7 @@ func TestARunningCheckIsPending(t *testing.T) { func TestThePassedCompositionIsSaidOnlyWhenItWasRun(t *testing.T) { // ADR 0484 clause 6 cs, p := lineCase(t, pullOf(repoA, 7, head1)) v := passVerdict() - v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "afbe371003a570f088be4a394d83aa7a9458f7ab"} + v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionPass, Sdk: "afbe371003a570f088be4a394d83aa7a9458f7ab"} if r := Report(v, cs, p, nil, nil, nil); !strings.Contains(r, "composition passed with afbe371003a5") { t.Errorf("report = %q", r) } @@ -146,7 +149,7 @@ func TestTheCompositionsTimeAndMachinesAreSaidOnceTheAssignerReportsThem(t *test cs, p := lineCase(t, pullOf(repoA, 7, head1)) v := passVerdict() v.Took.Composition = 2400 - v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "afbe371003a570f088be4a394d83aa7a9458f7ab", Machines: []string{"g14", "ace", "novox", "shanks"}} + v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionPass, Sdk: "afbe371003a570f088be4a394d83aa7a9458f7ab", Machines: []string{"g14", "ace", "novox", "shanks"}} r := Report(v, cs, p, nil, nil, nil) for _, want := range []string{"composition passed in 2.4s with afbe371003a5 on 4 machines: ace, g14, novox, shanks"} { if !strings.Contains(r, want) { @@ -156,8 +159,8 @@ func TestTheCompositionsTimeAndMachinesAreSaidOnceTheAssignerReportsThem(t *test if l := Line(v, cs, p, nil, nil, nil); !strings.Contains(l, "composition passed in 2.4s") || !strings.Contains(l, "on 4 machines") || utf8.RuneCountInString(l) > MaxLine { t.Errorf("line = %q", l) } - v.State, v.Failed = delivery.VerdictFail, delivery.FailedFirst{Machine: "novox", Words: "composition refused"} - v.Composition = &delivery.Composition{State: delivery.CompositionFail, Words: "novox: two seats claim dns", Machines: []string{"novox"}} + v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &FailedFirst{Machine: "novox", Words: "composition refused"} + v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionFail, Words: "novox: two seats claim dns", Machines: []string{"novox"}} if r := Report(v, cs, p, nil, nil, nil); !strings.Contains(r, "composition failed in 2.4s on novox: novox: two seats claim dns") { t.Errorf("failed composition report = %q", r) } @@ -169,7 +172,7 @@ func TestTheLineSaysTheLintTime(t *testing.T) { test := b.test b.test = func(c stepCall) (any, error) { r, err := test(c) - if res, ok := r.(Tested); ok && res.Level == delivery.LevelUnit { + if res, ok := r.(Tested); ok && res.Level == buildstep.LevelUnit { res.Lint = 1.5 return res, err } diff --git a/modules/mesh-delivery-checker/internal/check/lostask.go b/modules/mesh-delivery-checker/internal/check/lostask.go index bc1db982..8eccda34 100644 --- a/modules/mesh-delivery-checker/internal/check/lostask.go +++ b/modules/mesh-delivery-checker/internal/check/lostask.go @@ -3,9 +3,10 @@ package check import ( "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-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/delivery" + sdkrun "git.novox.be/novox/mesh-delivery/sdk/buildrun" ) // **An ask the build seat gave up on ends its step** (novox/hq issue 734): mesh-sdk buildrun.LostAskWaiter hands the @@ -22,18 +23,18 @@ const LostAskTries = 3 // IsLostAsk says whether a verdict failed because its ask judged nothing: the build seat gave it up (buildrun.LostWords), or // the build module that ran its step was replaced under it, and its next process ended the step it left running -// (buildstep.RestartReason; a delivery of the build module, as on 2026-10-28). Neither is the change's failure: the check +// (sdkrun.RestartReason; a delivery of the build module, as on 2026-10-28). Neither is the change's failure: the check // is asked again, as for a given-up ask. -func IsLostAsk(v delivery.Verdict) bool { - return v.State == delivery.VerdictFail && (strings.Contains(v.Failed.Words, buildrun.LostWords) || - strings.Contains(v.Failed.Words, buildstep.RestartReason)) +func IsLostAsk(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State == meshdeliverycheckerclient.VerdictFail && (strings.Contains(failedOf(v).Words, buildrun.LostWords) || + strings.Contains(failedOf(v).Words, sdkrun.RestartReason)) } // IsNotTaken says whether a verdict failed because no build node took its ask within the bound (buildrun.NotTakenWords). -func IsNotTaken(v delivery.Verdict) bool { - return v.State == delivery.VerdictFail && strings.Contains(v.Failed.Words, buildrun.NotTakenWords) +func IsNotTaken(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { + return v.State == meshdeliverycheckerclient.VerdictFail && strings.Contains(failedOf(v).Words, buildrun.NotTakenWords) } // HearingBucket is the checker's state hearing: whether it hears DEAD_LETTERS (buildrun.Hearing under // buildrun.HearingKey), which checker_status says (dead_letters). -const HearingBucket = delivery.CheckerModule + "_" + buildrun.HearingState +const HearingBucket = CheckerModule + "_" + buildrun.HearingState diff --git a/modules/mesh-delivery-checker/internal/check/lostask_test.go b/modules/mesh-delivery-checker/internal/check/lostask_test.go index 3c547e46..4fb61cf9 100644 --- a/modules/mesh-delivery-checker/internal/check/lostask_test.go +++ b/modules/mesh-delivery-checker/internal/check/lostask_test.go @@ -8,9 +8,11 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/delivery" + sdkrun "git.novox.be/novox/mesh-delivery/sdk/buildrun" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // losingAsks is the build seat over a bus that gives up the first `lose` asks put (DEAD_LETTERS keeps them, as on @@ -18,7 +20,7 @@ import ( // gives up (an ask no node takes). type losingAsks struct { *buildrun.FakeAsks - answer func(delivery.StepAsk) delivery.Step + answer func(buildstep.StepAsk) buildstep.Step dead *buildrun.DeadLetters never bool @@ -47,7 +49,7 @@ func (l *losingAsks) PublishAsk(ctx context.Context, subject, id string, data [] } go l.dead.Heard(g) default: - a, err := delivery.DecodeStepAsk(data) + a, err := buildstep.DecodeStepAsk(data) if err != nil { return err } @@ -146,32 +148,33 @@ func TestAnAskGivenUpOrNeverTakenEndsItsTryAsErrorAndItsHeadIsAskedAnew(t *testi } // troubled is the build seat as a check meets it when its step's end does not reach its wait: the first `restarts` asks' -// steps are ended by the build module's next process (buildstep.RestartReason), and the first `deaf` asks' watches hear +// steps are ended by the build module's next process (sdkrun.RestartReason), and the first `deaf` asks' watches hear // nothing though their steps end (the lost check of 2026-10-28). Its steps state is read through AskSteps and Steps. type troubled struct { *buildrun.FakeAsks - answer func(delivery.StepAsk) delivery.Step + answer func(buildstep.StepAsk) buildstep.Step mu sync.Mutex restarts, deaf int deafIDs map[string]bool - steps map[string]delivery.Step + steps map[string]buildstep.Step } func (tr *troubled) PublishAsk(ctx context.Context, subject, id string, data []byte) error { if err := tr.FakeAsks.PublishAsk(ctx, subject, id, data); err != nil { return err } - a, err := delivery.DecodeStepAsk(data) + a, err := buildstep.DecodeStepAsk(data) if err != nil { return err } step := tr.answer(a) - step.What = buildstep.AskWhat(a) + read, _ := buildstep.AskAs[buildstep.Ask](a) + step.What = sdkrun.AskWhat(read) tr.mu.Lock() if tr.restarts > 0 { tr.restarts-- - step.State, step.Reason, step.Result = delivery.StepFailed, buildstep.RestartReason, nil + step.State, step.Reason, step.Result = buildstep.StepFailed, sdkrun.RestartReason, nil } deaf := tr.deaf > 0 if deaf { @@ -186,21 +189,21 @@ func (tr *troubled) PublishAsk(ctx context.Context, subject, id string, data []b return nil } -func (tr *troubled) WatchAsk(ctx context.Context, module, askID string) (<-chan delivery.Step, error) { +func (tr *troubled) WatchAsk(ctx context.Context, module, askID string) (<-chan buildstep.Step, error) { tr.mu.Lock() deaf := tr.deafIDs[askID] tr.mu.Unlock() if deaf { - return make(chan delivery.Step), nil // open, and silent for good + return make(chan buildstep.Step), nil // open, and silent for good } return tr.FakeAsks.WatchAsk(ctx, module, askID) } -func (tr *troubled) AskSteps(_ context.Context, _, askID string) ([]delivery.Step, error) { +func (tr *troubled) AskSteps(_ context.Context, _, askID string) ([]buildstep.Step, error) { tr.mu.Lock() defer tr.mu.Unlock() if s, ok := tr.steps[askID]; ok { - return []delivery.Step{s}, nil + return []buildstep.Step{s}, nil } return nil, nil } @@ -212,9 +215,9 @@ func (tr *troubled) Steps(running bool) StateRead { vals := map[string]json.RawMessage{} for id, s := range tr.steps { if running { - s.State, s.Ended, s.Started = delivery.StepRunning, time.Time{}, time.Now() + s.State, s.Ended, s.Started = buildstep.StepRunning, time.Time{}, time.Now() } - raw, _ := delivery.EncodeStep(s) + raw, _ := buildstep.EncodeStep(s) vals[s.Node+"."+id] = raw } return StateRead{Keys: func() ([]string, error) { @@ -262,13 +265,13 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) { state string asks int }{ - {name: "given up once", lose: 1, state: delivery.VerdictPass, asks: 2}, - {name: "given up every time", lose: LostAskTries, state: delivery.VerdictFail, asks: LostAskTries}, - {name: "its build module replaced under it", restarts: 1, state: delivery.VerdictPass, asks: 2}, - {name: "replaced under every ask", restarts: LostAskTries, state: delivery.VerdictFail, asks: LostAskTries}, - {name: "its watch deaf, a step of its build module ends", deaf: 1, how: "nudge", state: delivery.VerdictPass, asks: 1}, - {name: "its watch deaf, asked again by hand", deaf: 1, how: "hand", state: delivery.VerdictPass, asks: 2}, - {name: "its watch deaf, asked again by hand after its build module replaced it", deaf: 1, restarts: 1, how: "hand", state: delivery.VerdictPass, asks: 2}, + {name: "given up once", lose: 1, state: meshdeliverycheckerclient.VerdictPass, asks: 2}, + {name: "given up every time", lose: LostAskTries, state: meshdeliverycheckerclient.VerdictFail, asks: LostAskTries}, + {name: "its build module replaced under it", restarts: 1, state: meshdeliverycheckerclient.VerdictPass, asks: 2}, + {name: "replaced under every ask", restarts: LostAskTries, state: meshdeliverycheckerclient.VerdictFail, asks: LostAskTries}, + {name: "its watch deaf, a step of its build module ends", deaf: 1, how: "nudge", state: meshdeliverycheckerclient.VerdictPass, asks: 1}, + {name: "its watch deaf, asked again by hand", deaf: 1, how: "hand", state: meshdeliverycheckerclient.VerdictPass, asks: 2}, + {name: "its watch deaf, asked again by hand after its build module replaced it", deaf: 1, restarts: 1, how: "hand", state: meshdeliverycheckerclient.VerdictPass, asks: 2}, } { t.Run(row.name, func(t *testing.T) { b := newBench(t) @@ -280,7 +283,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) { asked = func() int { return len(l.Asks()) } } else { tr = &troubled{FakeAsks: &buildrun.FakeAsks{}, answer: b.answer, restarts: row.restarts, deaf: row.deaf, - deafIDs: map[string]bool{}, steps: map[string]delivery.Step{}} + deafIDs: map[string]bool{}, steps: map[string]buildstep.Step{}} heard = &buildrun.HeardEnds{Inner: tr, Read: tr} b.ex.Asks = buildrun.Asker{Publisher: tr, Waiter: heard, Bound: 5 * time.Second} asked = func() int { return len(tr.Asks()) } @@ -295,7 +298,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) { for deadline := time.Now().Add(10 * time.Second); heard.Waiting() == 0 && time.Now().Before(deadline); { time.Sleep(2 * time.Millisecond) } - if v := r.verdict(t, key); v.State != delivery.VerdictRunning { + if v := r.verdict(t, key); v.State != meshdeliverycheckerclient.VerdictRunning { t.Fatalf("a check whose step ended unheard = %s, want running until it is read", v.State) } } @@ -305,7 +308,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) { heard.Nudge(buildrun.GoBuildModule) case "hand": cands := newMemKV() - ckey, _ := delivery.CandidatesKey("feat/x") + ckey, _ := meshcatalogclient.CandidatesKey("feat/x") _, _ = cands.Create(ctx, ckey, cand) none := StateRead{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, error) { return nil, nil }} rd := Reader{CandidateState: kvRead(cands), VerdictState: kvRead(r.verdicts), AskState: kvRead(asksKept), @@ -336,7 +339,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) { if _, _, kept, _ := asksKept.Get(ctx, key); kept { t.Errorf("the asks of the ended check are still kept under %s", key) } - if row.state == delivery.VerdictFail && (!IsLostAsk(v) || !IsNodesFailure(v)) { + if row.state == meshdeliverycheckerclient.VerdictFail && (!IsLostAsk(v) || !IsNodesFailure(v)) { t.Errorf("a check that judged nothing every time = %q, want lost and judged again when the nodes change", v.Failed.Words) } if row.how == "hand" { diff --git a/modules/mesh-delivery-checker/internal/check/manifests_judge.go b/modules/mesh-delivery-checker/internal/check/manifests_judge.go index 41b6b21a..703c71b0 100644 --- a/modules/mesh-delivery-checker/internal/check/manifests_judge.go +++ b/modules/mesh-delivery-checker/internal/check/manifests_judge.go @@ -9,10 +9,11 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-delivery/sdk/composer" - "git.novox.be/novox/mesh-sdk/go/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/packagename" ) @@ -20,7 +21,7 @@ import ( // A change's manifests are judged by the indexer's judge (novox/hq ADR 0578 §2): the checker asks the Go build seat to // build the judge's command (mesh-delivery's modules/mesh-delivery-indexer, cmd/mesh-judge) at mesh-delivery's trunk, or // at the group's mesh-delivery member head when that member changes the judge, and to run it in its sandbox over the -// catalogue as the change leaves it, exactly as the composer is run (delivery.ComposeJob, ADR 0482 §4). A manifest the +// catalogue as the change leaves it, exactly as the composer is run (buildstep.ComposeJob, ADR 0482 §4). A manifest the // change touches that the judge refuses fails the check, naming the manifest and each problem. The checker links no // judge of its own. @@ -106,7 +107,7 @@ func judgeRequest(trunk map[string]string, pulls []Pull) (req composer.Request, // judgeManifests runs the indexer's judge over the case's manifests and fails the check for each touched manifest it // refuses. A case that touches no manifest, or an examiner that reads no trunk manifests, judges nothing; a judge that // cannot be asked or does not answer fails the check, said, never passes it. -func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { +func (st *state) judgeManifests(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue { if st.j.Manifests == nil || st.c.Try != nil { return nil } @@ -195,14 +196,14 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict { // runJudge asks the Go build seat to build the judge at the commit and run it over the request, and reads its change and // trunk cases; a failed verdict when it cannot be asked or does not answer. -func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string) (change, trunk composer.CaseVerdict, v *delivery.Verdict) { +func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string) (change, trunk composer.CaseVerdict, v *meshdeliverychecker.VerdictsByGroupHeadsValue) { raw, err := json.Marshal(req) if err != nil { return change, trunk, fail(goModule, err.Error()) } - job := delivery.ComposeJob{Fetch: map[string]any{"repository": DeliveryRepository, "commit": commit}, Dir: JudgeDir, + job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: DeliveryRepository, Commit: commit}, Dir: JudgeDir, Package: JudgePackage, Request: raw, Reader: "the indexer's judge at " + at} - a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule, + a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: buildstep.AskCheck, Language: buildstep.AskGo, Asker: CheckerModule, At: st.j.now().UTC(), Compose: &job} st.c.Progress.Set(fmt.Sprintf("the judge ask %s of the manifests of %s", a.ID, strings.Join(touched, ", ")), "") st.c.Asked(a.ID) @@ -231,7 +232,7 @@ const CatalogSeat = "mesh-catalog" // as well. The verdicts read are the trunk's manifest validator's: its change case, where refused, held (not accepted // with no reason) or with a field set aside (a field the running manifest validator does not know waits, ADR 0601 §3) // each fail it, naming the module and the problems. -func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Request, touched []string, cv composer.CaseVerdict, judgedAt string) *delivery.Verdict { +func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Request, touched []string, cv composer.CaseVerdict, judgedAt string) *meshdeliverychecker.VerdictsByGroupHeadsValue { if st.j.Read == nil { return nil } @@ -258,7 +259,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques if trunk == "" { return fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known") } - var v *delivery.Verdict + var v *meshdeliverychecker.VerdictsByGroupHeadsValue judgedAt = "mesh-delivery's trunk " + trunk if cv, _, v = st.runJudge(ctx, req, touched, trunk, judgedAt); v != nil { return v @@ -285,11 +286,11 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques } // judgeAnswer reads the judge's change and trunk cases from its step, or why it cannot. -func judgeAnswer(step delivery.Step) (change, trunk composer.CaseVerdict, why string) { - if step.State != delivery.StepPassed { +func judgeAnswer(step buildstep.Step) (change, trunk composer.CaseVerdict, why string) { + if step.State != buildstep.StepPassed { return change, trunk, fmt.Sprintf("its step %s: %s", step.State, step.Reason) } - var a delivery.ComposeAnswer + var a buildstep.ComposeAnswer if err := json.Unmarshal(step.Result, &a); err != nil { return change, trunk, "its step's answer cannot be read: " + err.Error() } @@ -400,7 +401,7 @@ func (st *state) knownPackages(ctx context.Context) []string { } dirs := []string{d.dir} if d.dir == "" { - dirs = []string{"modules/" + d.module, delivery.RootPath} + dirs = []string{"modules/" + d.module, meshcatalogclient.RootPath} } var why []string for _, dir := range dirs { diff --git a/modules/mesh-delivery-checker/internal/check/names.go b/modules/mesh-delivery-checker/internal/check/names.go new file mode 100644 index 00000000..fbc015d7 --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/names.go @@ -0,0 +1,90 @@ +package check + +import ( + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs" + "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" + checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" +) + +// The holders and buckets the checker names. No contract names a holder (novox/hq ADR 0625 §4, ADR 0634 §1), so these are its +// own constants, each a state of a seat (the seat's state name beside the holder's module name) and each declared in its +// manifest (module.json: its own states, its `reads`); a unit row checks them against the manifest (TestTheNamesAreTheManifests). +const ( + // CheckerModule is this part's module; its states verdicts, kept, owed and previews are the seat mesh-delivery-checker's. + CheckerModule = "mesh-delivery-checker" + CheckerVerdictsBucket = CheckerModule + "_" + checker.StateVerdicts + CheckerKeptBucket = CheckerModule + "_" + checker.StateKept + CheckerOwedBucket = CheckerModule + "_" + checker.StateOwed + PreviewsBucket = CheckerModule + "_" + checker.StatePreviews + + // GateModule holds the seat mesh-delivery's states source and turns, which the checker reads (reads: mesh-delivery-gate.source, + // mesh-delivery-gate.turns). + GateModule = "mesh-delivery-gate" + SourceBucket = GateModule + "_" + meshdelivery.StateSource + TurnsBucket = GateModule + "_" + meshdelivery.StateTurns + + // IndexerModule holds the seat mesh-catalog's states, which the checker follows by their views (consumes: + // mesh-delivery-indexer.views.*). + IndexerModule = meshcatalogclient.Module + CandidatesBucket = IndexerModule + "_" + meshcatalog.StateCandidates + ModulesBucket = IndexerModule + "_" + meshcatalog.StateModules + ManifestsBucket = IndexerModule + "_" + meshcatalog.StateManifests + IndexerPredefinedState = meshcatalog.StatePredefined + + // WatchmanModule holds the seat mesh-watchdogs' presence (reads: mesh-delivery-watchman.presence). + WatchmanModule = "mesh-delivery-watchman" + PresenceBucket = WatchmanModule + "_" + meshwatchdogs.StatePresence + + // HaveBucket is the node-engine's state have (reads: node-engine.have); a node's own key ends in HaveNodeWord. + HaveBucket = "node-engine_" + nodeengine.StateHave + HaveNodeWord = "node" + + // compositionWaits is the state of the assigner's composition answer that waits for what it composes from. + compositionWaits = "waits" +) + +// FailedFirst names what failed: a test (its package and name) or a machine, with the words of the failure. It is the shape of +// the verdict's `failed` and of each entry of its `failures` (the seat's two types of it have the same fields), so the check +// judges with one and converts at the verdict's edge (failureItems, failedList). +type FailedFirst = checker.VerdictsByGroupHeadsValueFailed + +// failedOf is the first failure a verdict names: the zero value of a verdict that names none. +func failedOf(v checker.VerdictsByGroupHeadsValue) FailedFirst { + if v.Failed == nil { + return FailedFirst{} + } + return *v.Failed +} + +// tookOf is the time of each part of a check a verdict names: the zero value of a verdict that names none. +func tookOf(v checker.VerdictsByGroupHeadsValue) checker.VerdictsByGroupHeadsValueTook { + if v.Took == nil { + return checker.VerdictsByGroupHeadsValueTook{} + } + return *v.Took +} + +func failureItems(list []FailedFirst) []checker.VerdictsByGroupHeadsValueFailuresItem { + if list == nil { + return nil + } + out := make([]checker.VerdictsByGroupHeadsValueFailuresItem, len(list)) + for i, f := range list { + out[i] = checker.VerdictsByGroupHeadsValueFailuresItem(f) + } + return out +} + +func failedList(items []checker.VerdictsByGroupHeadsValueFailuresItem) []FailedFirst { + if items == nil { + return nil + } + out := make([]FailedFirst, len(items)) + for i, f := range items { + out[i] = FailedFirst(f) + } + return out +} diff --git a/modules/mesh-delivery-checker/internal/check/owed.go b/modules/mesh-delivery-checker/internal/check/owed.go new file mode 100644 index 00000000..6849e26f --- /dev/null +++ b/modules/mesh-delivery-checker/internal/check/owed.go @@ -0,0 +1,49 @@ +package check + +import ( + "fmt" + + "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/contracts/go/seats/meshdeliverychecker" +) + +// The checker's owed state holds the one act it owes, the status mesh/check on a pull request at a head: the gate's owed act +// with the context mesh/check. Its value has the shape of the gate's (the seat mesh-delivery's owed), so its rules and its +// codec are the mesh-delivery client's; only the checker reads it, so the checker's own narrowing to its one act lives here. + +// CheckerOwedAct is the act the checker owes. +const CheckerOwedAct = meshdeliveryclient.OwedStatus + "=" + meshdeliveryclient.StatusContextCheck + +// CheckerOwedKey is the key, in the checker's owed state, of the status the checker owes on a pull request at a head. +func CheckerOwedKey(repository string, number int, head string) (string, error) { + return meshdeliveryclient.OwedKey(repository, number, head, CheckerOwedAct) +} + +func checkCheckerOwed(o meshdeliverychecker.OwedByRepositoryNumberHeadActValue) error { + if o.Act != meshdeliveryclient.OwedStatus || o.Context != meshdeliveryclient.StatusContextCheck { + return fmt.Errorf("the checker owes only a status with the context %s, not the act %q with the context %q", meshdeliveryclient.StatusContextCheck, o.Act, o.Context) + } + return nil +} + +// EncodeCheckerOwed is o as the gate's owed value, refused for any act but the checker's status. +func EncodeCheckerOwed(o meshdeliverychecker.OwedByRepositoryNumberHeadActValue) ([]byte, error) { + if err := checkCheckerOwed(o); err != nil { + return nil, err + } + return meshdeliveryclient.EncodeOwed(meshdelivery.OwedByRepositoryNumberHeadActValue(o)) +} + +// DecodeCheckerOwed reads an owed act of the checker's state. +func DecodeCheckerOwed(data []byte) (meshdeliverychecker.OwedByRepositoryNumberHeadActValue, error) { + g, err := meshdeliveryclient.DecodeOwed(data) + if err != nil { + return meshdeliverychecker.OwedByRepositoryNumberHeadActValue{}, err + } + o := meshdeliverychecker.OwedByRepositoryNumberHeadActValue(g) + if err := checkCheckerOwed(o); err != nil { + return meshdeliverychecker.OwedByRepositoryNumberHeadActValue{}, err + } + return o, nil +} diff --git a/modules/mesh-delivery-checker/internal/check/pins.go b/modules/mesh-delivery-checker/internal/check/pins.go index dd64c493..1ec0015d 100644 --- a/modules/mesh-delivery-checker/internal/check/pins.go +++ b/modules/mesh-delivery-checker/internal/check/pins.go @@ -5,11 +5,12 @@ import ( "errors" "maps" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" + "git.novox.be/novox/mesh-delivery/sdk/trunk" ) // **A check builds against the group's own package heads, not against what a member names** (novox/hq ADR 0573 §2, §4; -// issue 1585). Every Go check ask carries the version of each mesh package to build against (delivery.StepAsk.Packages): +// issue 1585). Every Go check ask carries the version of each mesh package to build against (buildstep.StepAsk.Packages): // the head of the group's member that publishes it, else its latest published version as the indexer holds it. The build // module writes them into the workspace, whatever version or commit a member's go.mod names, so no member is refused // for naming no version of the group's package, an older head of it, a merge commit or a commit on no pull request: that @@ -37,8 +38,8 @@ type Forge interface { // also a package new in the group, which has no published version yet), else that member's head (mesh-sdk's Go module // whenever mesh-sdk is a member). A package of repository itself comes with the tree the check fetched, and is left out. // Nil for none. -func (st *state) packagesOf(repository string) delivery.PackageVersions { - versions := delivery.PackageVersions{} +func (st *state) packagesOf(repository string) buildstep.PackageVersions { + versions := buildstep.PackageVersions{} if st.j.Published != nil { maps.Copy(versions, st.j.Published.LatestPackages()) } @@ -51,8 +52,8 @@ func (st *state) packagesOf(repository string) delivery.PackageVersions { versions[pkg] = "" } for pkg := range versions { - repo, _, ok := delivery.PackageRepository(pkg) - if !ok || delivery.SameRepository(repo, repository) { + repo, _, ok := trunk.PackageRepository(pkg) + if !ok || trunk.SameRepository(repo, repository) { delete(versions, pkg) continue } diff --git a/modules/mesh-delivery-checker/internal/check/pins_test.go b/modules/mesh-delivery-checker/internal/check/pins_test.go index 890eb6d8..cfaec3a0 100644 --- a/modules/mesh-delivery-checker/internal/check/pins_test.go +++ b/modules/mesh-delivery-checker/internal/check/pins_test.go @@ -11,21 +11,22 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/buildstep" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // published is the latest published version of each mesh package, as the indexer's modules say (Examiner.Published). -type published delivery.PackageVersions +type published buildstep.PackageVersions -func (p published) LatestPackages() delivery.PackageVersions { - return maps.Clone(delivery.PackageVersions(p)) +func (p published) LatestPackages() buildstep.PackageVersions { + return maps.Clone(buildstep.PackageVersions(p)) } -// packagesOfFetch is the package versions the fetch of repository was handed (delivery.StepAsk.Packages, issue 1585). -func packagesOfFetch(t *testing.T, b *bench, repository string) delivery.PackageVersions { +// packagesOfFetch is the package versions the fetch of repository was handed (buildstep.StepAsk.Packages, issue 1585). +func packagesOfFetch(t *testing.T, b *bench, repository string) buildstep.PackageVersions { t.Helper() for _, f := range b.steps.tool("fetch") { if f.args["repository"] == repository { @@ -50,7 +51,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes b.fetch.Modules = []string{"modules/m"} b.ex.Published = published{SDKModulePath: trunkS, contract: trunkA} v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict - if v.State != delivery.VerdictPass { + if v.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", v) } if got := packagesOfFetch(t, b, repoA); len(got) != 1 || got[SDKModulePath] != head2 { @@ -61,7 +62,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes } b = newBench(t) b.ex.Published = published{SDKModulePath: trunkS, contract: trunkA} - if v := b.judge(t, caseOf(t, pullOf("novox/mesh-catalog", 7, head1))).Verdict; v.State != delivery.VerdictPass { + if v := b.judge(t, caseOf(t, pullOf("novox/mesh-catalog", 7, head1))).Verdict; v.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("a pull request alone: verdict = %+v", v) } if got := packagesOfFetch(t, b, "novox/mesh-catalog"); len(got) != 2 || got[SDKModulePath] != trunkS || got[contract] != trunkA { @@ -76,27 +77,27 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes {Name: "go", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}}, {Name: "go-gopackageregistry", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go/seats/gopackageregistry"}}}}} digest, contractsDigest := strings.Repeat("c", 64), strings.Repeat("d", 64) - sdkPull := func() delivery.CandidatePull { + sdkPull := func() meshcatalog.CandidatesByGroupValuePullsItem { p := pullOf(repoSDK, 12, head2) p.Modules = []string{"mesh-sdk"} - p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64), Manifest: digest}} + p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-sdk": {Fp2: "src2:" + strings.Repeat("f", 64), Manifest: digest}} return p } - unpreviewed := func() delivery.CandidatePull { // a package new in the group: no preview carries its manifest yet + unpreviewed := func() meshcatalog.CandidatesByGroupValuePullsItem { // a package new in the group: no preview carries its manifest yet p := sdkPull() - p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64)}} + p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-sdk": {Fp2: "src2:" + strings.Repeat("f", 64)}} return p } - contractsPull := func() delivery.CandidatePull { + contractsPull := func() meshcatalog.CandidatesByGroupValuePullsItem { p := pullOf("novox/mesh-catalog", 4, strings.Repeat("3", 40)) p.Modules = []string{"mesh-contracts"} - p.Fps = map[string]delivery.CandidateModule{"mesh-contracts": {FP2: "src2:" + strings.Repeat("e", 64), Manifest: contractsDigest}} + p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-contracts": {Fp2: "src2:" + strings.Repeat("e", 64), Manifest: contractsDigest}} return p } const sdkGo, contractsGo, seatGo = SDKModulePath, "git.novox.be/novox/mesh-catalog/go", "git.novox.be/novox/mesh-catalog/go/seats/gopackageregistry" for _, row := range []struct { name string - pulls []delivery.CandidatePull + pulls []meshcatalog.CandidatesByGroupValuePullsItem published []string // the seats that publish the members' heads wants int fails string @@ -107,19 +108,19 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes // noModule: the Go seat's published names no Go module path (ruling 1 on A2d's finding 1: never worked out). noModule bool }{ - {name: "a group", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()}, + {name: "a group", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), sdkPull()}, published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}, node: map[string]any{"name": "lib", "version": "0.1.15-feat-x.1"}}, - {name: "a package new in the group, read at its head", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), unpreviewed()}, + {name: "a package new in the group, read at its head", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), unpreviewed()}, published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}}, - {name: "one head, two Go packages (as mesh-contracts' head)", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), contractsPull()}, + {name: "one head, two Go packages (as mesh-contracts' head)", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), contractsPull()}, published: []string{"go-package-registry"}, wants: 2, built: map[string]string{contractsGo: "v0.1.15-feat-x.1", seatGo: "v0.1.3-feat-x.1"}}, - {name: "a lone pull request", pulls: []delivery.CandidatePull{sdkPull()}}, - {name: "a Go published naming no module", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()}, + {name: "a lone pull request", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{sdkPull()}}, + {name: "a Go published naming no module", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), sdkPull()}, published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, noModule: true, fails: "no published of the tagged version of go of mesh-sdk (novox/mesh-sdk#12) on go-package-registry within 50ms"}, - {name: "a package never published", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()}, + {name: "a package never published", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), sdkPull()}, published: []string{"go-package-registry"}, wants: 2, fails: "no published of the tagged version of lib of mesh-sdk (novox/mesh-sdk#12) on npm-package-registry within 50ms"}, } { @@ -131,7 +132,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes b.ex.Forge = headFiles{repoSDK + " module.json " + head2: string(raw)} tags := &recordedTags{heard: &PublishedHeard{}, publish: row.published, noModule: row.noModule} b.ex.Tags, b.ex.PublishBound = tags, 50*time.Millisecond - b.asks.Answer = func(a delivery.StepAsk) delivery.Step { + b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { if len(tags.wanted) != row.wants { t.Errorf("%s: a member was asked before every tag-wanted was said: %d of %d", row.name, len(tags.wanted), row.wants) } @@ -147,9 +148,9 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes } } switch { - case row.fails == "" && v.State != delivery.VerdictPass: + case row.fails == "" && v.State != meshdeliverycheckerclient.VerdictPass: t.Errorf("%s: verdict = %+v", row.name, v) - case row.fails != "" && (v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)): + case row.fails != "" && (v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)): t.Errorf("%s: verdict = %+v, want it to fail saying %q", row.name, v, row.fails) case row.fails != "" && len(b.steps.tool("fetch")) != 0: t.Errorf("%s: a member was built though a package was not published", row.name) @@ -247,7 +248,7 @@ func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR t.Errorf("the siblings named for the verdict = %v, want the node-engine at %s", out.Beside, hostRuns[:12]) } got := besideOf(t, b, SDKRepository) - if c := got[catalogue]; c.Commit != head2 || c.Ref != "refs/pull/12/head" { + if c := got[catalogue]; c.Commit != head2 { t.Errorf("the catalogue beside mesh-sdk in a group = %+v, want the member's head %s", c, head2) } if h := got[host]; h.Commit != hostRuns { @@ -258,7 +259,7 @@ func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR b = newBench(t) b.ex.Trunks = trunksOf(t, map[string]string{SDKRepository: trunkS, catalogue: trunkA, host: trunkA2, networking: trunkA2}) b.judge(t, caseOf(t, pullOf(host, 8, head1), pullOf(networking, 3, head2))) - if n := besideOf(t, b, host)[networking]; n.Commit != head2 || n.Ref != "refs/pull/3/head" { + if n := besideOf(t, b, host)[networking]; n.Commit != head2 { t.Errorf("mesh-networking beside the node-engine in a group = %+v, want the member's head %s", n, head2) } // Alone, mesh-sdk's tests read the catalogue's trunk, and the verdict names it. @@ -327,7 +328,7 @@ func TestACatalogueChangeHasTheCatalogueBesideItAtItsTrunk(t *testing.T) { func TestAMeshLabHeadGetsACheck(t *testing.T) { const lab = "novox/mesh-lab" raw := candidateOf(t, "fix/x", pullOf(lab, 3, head1)) - c, err := delivery.DecodeCandidate(raw) + c, err := meshcatalog.DecodeCandidatesByGroupValue(raw) if err != nil { t.Fatal(err) } @@ -336,7 +337,7 @@ func TestAMeshLabHeadGetsACheck(t *testing.T) { t.Fatalf("the case = %+v, want mesh-lab#3 judged", cs) } b := newBench(t) - if v := b.judge(t, cs).Verdict; v.State != delivery.VerdictPass { + if v := b.judge(t, cs).Verdict; v.State != meshdeliverycheckerclient.VerdictPass { t.Fatalf("verdict = %+v", v) } if fetched := b.steps.tool("fetch"); len(fetched) == 0 || fetched[0].args["repository"] != lab { diff --git a/modules/mesh-delivery-checker/internal/check/preview.go b/modules/mesh-delivery-checker/internal/check/preview.go index a3b185aa..c8897276 100644 --- a/modules/mesh-delivery-checker/internal/check/preview.go +++ b/modules/mesh-delivery-checker/internal/check/preview.go @@ -7,7 +7,8 @@ import ( "regexp" "slices" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" ) var digest64 = regexp.MustCompile(`^[0-9a-f]{64}$`) @@ -19,10 +20,10 @@ var digest64 = regexp.MustCompile(`^[0-9a-f]{64}$`) // indexer gives its module no fingerprint and says why. A head that does not merge has a preview of the files in conflict // alone, so the indexer says the candidate's modules have no fingerprint because it does not merge (issue 1445). It // answers false when no fetch was made. -func PreviewOf(p Pull, fetched []Fetched) (delivery.Preview, bool) { +func PreviewOf(p Pull, fetched []Fetched) (meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue, bool) { var merge string var conflicts []string - mods := map[string]delivery.PreviewModule{} + mods := map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{} for _, f := range fetched { if len(f.Conflicts) > 0 { conflicts = append(conflicts, f.Conflicts...) @@ -30,7 +31,7 @@ func PreviewOf(p Pull, fetched []Fetched) (delivery.Preview, bool) { } if len(conflicts) > 0 { slices.Sort(conflicts) - return delivery.Preview{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Conflicts: slices.Compact(conflicts)}, true + return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Conflicts: slices.Compact(conflicts)}, true } for _, f := range fetched { if f.Merge == "" { @@ -43,13 +44,13 @@ func PreviewOf(p Pull, fetched []Fetched) (delivery.Preview, bool) { if t.Tree == "" || !digest64.MatchString(t.Manifest) { continue } - mods[dir] = delivery.PreviewModule{Tree: t.Tree, Manifest: t.Manifest, Locks: t.Locks, Paths: t.Paths, Packages: t.Packages} + mods[dir] = meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{Tree: t.Tree, Manifest: t.Manifest, Locks: t.Locks, Paths: t.Paths, Packages: t.Packages} } } if merge == "" { - return delivery.Preview{}, false + return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}, false } - pv := delivery.Preview{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Merge: merge} + pv := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Merge: merge} if len(mods) > 0 { pv.Modules = mods } @@ -67,10 +68,10 @@ func (j *Examiner) writePreview(ctx context.Context, p Pull, fetched []Fetched) if !ok { return } - key, err := delivery.PreviewKey(p.Repository, p.Head, p.Trunk) + key, err := meshdeliverycheckerclient.PreviewKey(p.Repository, p.Head, p.Trunk) if err == nil { var data []byte - if data, err = delivery.EncodePreview(pv); err == nil { + if data, err = meshdeliverycheckerclient.EncodePreview(pv); err == nil { _, err = j.Previews.Create(ctx, key, data) if errors.Is(err, ErrConflict) { return diff --git a/modules/mesh-delivery-checker/internal/check/rank_test.go b/modules/mesh-delivery-checker/internal/check/rank_test.go index e436dd47..14705018 100644 --- a/modules/mesh-delivery-checker/internal/check/rank_test.go +++ b/modules/mesh-delivery-checker/internal/check/rank_test.go @@ -6,7 +6,10 @@ import ( "sync" "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/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" ) // The checker's ranked start of checks (ADR 0536 §4, issue 1897): a freed place goes to a head never judged (or asked again @@ -47,7 +50,7 @@ func TestCheckerStatusNamesTheWaitingChecksByKind(t *testing.T) { // 1897/AC4 cands := fakeState{} put := func(n int, head string) { cand := candidateOf(t, group(n), pullOf(repoA, n, head)) - ckey, _ := delivery.CandidatesKey(group(n)) + ckey, _ := meshcatalogclient.CandidatesKey(group(n)) cands[ckey] = cand r.ch.Candidate(ctx, group(n), cand) } @@ -58,7 +61,7 @@ func TestCheckerStatusNamesTheWaitingChecksByKind(t *testing.T) { // 1897/AC4 put(4, head1) // judged on the new trunk: stands r.ch.Wait() cand3 := candidateOf(t, group(3), pullOf(repoA, 3, head1)) // never judged - ckey3, _ := delivery.CandidatesKey(group(3)) + ckey3, _ := meshcatalogclient.CandidatesKey(group(3)) cands[ckey3] = cand3 verdicts := fakeState{} for k, v := range r.verdicts.vals { @@ -80,10 +83,10 @@ func TestCheckerStatusNamesTheWaitingChecksByKind(t *testing.T) { // 1897/AC4 // is still the one the mesh runs, though the node-engine's trunk has moved on (ADR 0548 §6). func TestCheckerStatusReadsTheNodeEngineAtItsRunningCommit(t *testing.T) { // 568/21 cand := candidateOf(t, group(1), pullOf(repoA, 1, head1)) - ckey, _ := delivery.CandidatesKey(group(1)) + ckey, _ := meshcatalogclient.CandidatesKey(group(1)) vkey := keyOf(t, group(1), cand) - dc, _ := delivery.DecodeCandidate(cand) - judged, err := delivery.EncodeVerdict(delivery.Verdict{Group: group(1), Heads: dc.HeadsHash, Trunk: map[string]string{repoA: trunkA}, Beside: map[string]string{nodeEngineRepo: "run111"}, State: delivery.VerdictPass, Levels: passing().Levels}) + dc, _ := meshcatalog.DecodeCandidatesByGroupValue(cand) + judged, err := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group(1), Heads: dc.Heads, Trunk: map[string]string{repoA: trunkA}, Beside: map[string]string{nodeEngineRepo: "run111"}, State: meshdeliverycheckerclient.VerdictPass, Levels: passing().Levels}) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-checker/internal/check/read.go b/modules/mesh-delivery-checker/internal/check/read.go index 1fc0efbe..6871c7ce 100644 --- a/modules/mesh-delivery-checker/internal/check/read.go +++ b/modules/mesh-delivery-checker/internal/check/read.go @@ -8,8 +8,13 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // The read tools (novox/hq ADR 0484 clauses 8 and 13, issue 1686): what the checker stored, as stored, with the line of the @@ -68,28 +73,28 @@ type VerdictRead struct { Note string `json:"note,omitempty"` } -func (r Reader) candidate(group string) (delivery.Candidate, bool) { - key, err := delivery.CandidatesKey(group) +func (r Reader) candidate(group string) (meshcatalog.CandidatesByGroupValue, bool) { + key, err := meshcatalogclient.CandidatesKey(group) if err != nil || r.CandidateState.Get == nil { - return delivery.Candidate{}, false + return meshcatalog.CandidatesByGroupValue{}, false } raw, err := r.CandidateState.Get(key) if err != nil || len(raw) == 0 { - return delivery.Candidate{}, false + return meshcatalog.CandidatesByGroupValue{}, false } - c, err := delivery.DecodeCandidate(raw) + c, err := meshcatalog.DecodeCandidatesByGroupValue(raw) return c, err == nil } func (r Reader) read(key string, raw json.RawMessage) (VerdictRead, error) { out := VerdictRead{Key: key, Verdict: raw} - v, err := delivery.DecodeVerdict(raw) + v, err := meshdeliverycheckerclient.DecodeVerdict(raw) if err != nil { return out, fmt.Errorf("%s holds what is not a verdict: %w", key, err) } out.Failed = failedLines(v) c, ok := r.candidate(v.Group) - if !ok || (c.HeadsHash != v.Heads && GateHeads(c) != v.Heads) { + if !ok || (c.Heads != v.Heads && GateHeads(c) != v.Heads) { out.Note = "the candidate no longer holds these heads" return out, nil } @@ -98,7 +103,7 @@ func (r Reader) read(key string, raw json.RawMessage) (VerdictRead, error) { if cs == nil { if IsUntrunked(v) { for _, p := range leftOf(c, v.Trunk) { - out.Reports[pullKey(p)] = v.Failed.Words + out.Reports[pullKey(p)] = failedOf(v).Words } } return out, nil @@ -126,7 +131,7 @@ func (r Reader) Verdicts(group string) (out []VerdictRead, cut string, err error var matching []string for _, k := range keys { if group != "" { - if g, _, err := delivery.ParseCheckerVerdictKey(k); err != nil || g != group { + if g, _, err := meshdeliverycheckerclient.ParseVerdictKey(k); err != nil || g != group { continue } } @@ -169,14 +174,14 @@ func (r Reader) Pull(repository string, number int) (VerdictRead, error) { // The candidates are read in batches, each in parallel and taken in key order, so the read stops at the batch that holds it. for from := 0; from < len(keys); from += ReadWorkers * 4 { batch := keys[from:min(from+ReadWorkers*4, len(keys))] - cands := make([]delivery.Candidate, len(batch)) + cands := make([]meshcatalog.CandidatesByGroupValue, len(batch)) ok := make([]bool, len(batch)) _ = parallel(len(batch), func(i int) error { raw, err := r.CandidateState.Get(batch[i]) if err != nil || len(raw) == 0 { return nil } - c, err := delivery.DecodeCandidate(raw) + c, err := meshcatalog.DecodeCandidatesByGroupValue(raw) cands[i], ok[i] = c, err == nil return nil }) @@ -185,10 +190,10 @@ func (r Reader) Pull(repository string, number int) (VerdictRead, error) { continue } for _, p := range c.Pulls { - if p.Repository != repository || p.Number != number { + if p.Repository != repository || int(p.Number) != number { continue } - vkey, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash) + vkey, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads) if err != nil { return VerdictRead{}, err } @@ -215,7 +220,7 @@ func (r Reader) Kept(level, digest string) (map[string]json.RawMessage, string, return nil, "", fmt.Errorf("the kept state cannot be read here") } if digest != "" { - key, err := delivery.KeptKey(level, digest) + key, err := meshdeliverycheckerclient.KeptKey(level, digest) if err != nil { return nil, "", err } @@ -235,7 +240,7 @@ func (r Reader) Kept(level, digest string) (map[string]json.RawMessage, string, sort.Strings(keys) var matching []string for _, k := range keys { - if l, _, err := delivery.ParseKeptKey(k); err == nil && (level == "" || l == level) { + if l, _, err := meshdeliverycheckerclient.ParseKeptKey(k); err == nil && (level == "" || l == level) { matching = append(matching, k) } } @@ -280,7 +285,7 @@ func (r Reader) Previews(repository, head string) ([]PreviewRead, []string, stri parts := map[string]part{} var matching, bad []string for _, k := range keys { - repo, h, trunk, err := delivery.ParsePreviewKey(k) + repo, h, trunk, err := meshdeliverycheckerclient.ParsePreviewKey(k) if err != nil { bad = append(bad, k) continue @@ -398,7 +403,7 @@ func (r Reader) turns(st *Status) *Turns { } // heldBy says the turn that holds a candidate's check off, or "". -func heldBy(t *Turns, c delivery.Candidate, heads map[string]string) string { +func heldBy(t *Turns, c meshcatalog.CandidatesByGroupValue, heads map[string]string) string { cs := CaseOf(c, heads) if t == nil || cs == nil { return "" @@ -451,7 +456,7 @@ func (r Reader) Status() (Status, error) { st.Notes = append(st.Notes, fmt.Sprintf("read the first %d candidates of %d", MaxReads, total)) } type judged struct { - c delivery.Candidate + c meshcatalog.CandidatesByGroupValue raw json.RawMessage ok bool } @@ -461,11 +466,11 @@ func (r Reader) Status() (Status, error) { if err != nil || len(raw) == 0 { return nil } - c, err := delivery.DecodeCandidate(raw) + c, err := meshcatalog.DecodeCandidatesByGroupValue(raw) if err != nil { return nil } - vkey, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash) + vkey, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads) if err != nil { return nil } @@ -501,10 +506,10 @@ func (r Reader) Status() (Status, error) { } continue } - v, err := delivery.DecodeVerdict(raw) + v, err := meshdeliverycheckerclient.DecodeVerdict(raw) switch { case err != nil: - case v.State == delivery.VerdictRunning: + case v.State == meshdeliverycheckerclient.VerdictRunning: if why := read.lostWhy(c); why != "" { st.Lost[c.Group] = why } @@ -545,7 +550,7 @@ type stepsRead struct { type moduleStep struct { module string - step delivery.Step + step buildstep.Step } // readSteps reads the newest MaxReads keys of each build module's steps state. @@ -579,14 +584,14 @@ func (r Reader) readSteps() stepsRead { if cut != "" { out.notes = append(out.notes, "the steps of "+m+": read the newest "+fmt.Sprint(MaxReads)+" of its keys; a step older than those is not shown") } - steps := make([]delivery.Step, len(keys)) + steps := make([]buildstep.Step, len(keys)) decoded := make([]bool, len(keys)) _ = parallel(len(keys), func(i int) error { raw, err := sr.Get(keys[i]) if err != nil || len(raw) == 0 { return nil } - step, err := delivery.DecodeStep(raw) + step, err := buildstep.DecodeStep(raw) steps[i], decoded[i] = step, err == nil return nil }) @@ -596,7 +601,7 @@ func (r Reader) readSteps() stepsRead { } step := steps[i] out.all = append(out.all, moduleStep{module: m, step: step}) - if step.State != delivery.StepRunning { + if step.State != buildstep.StepRunning { continue } rs := RunningStep{Module: m, Node: step.Node, ID: step.ID, Tool: step.Tool, What: step.What} @@ -615,9 +620,9 @@ func (r Reader) readSteps() stepsRead { // asksOf says whether a step is an ask of one of the candidate's pull requests: its What names `@` // (buildstep.AskWhat). -func asksOf(c delivery.Candidate, what string) bool { +func asksOf(c meshcatalog.CandidatesByGroupValue, what string) bool { for _, p := range c.Pulls { - if p.Repository != "" && strings.Contains(what, p.Repository+"@"+short12(p.Head.Head)) { + if p.Repository != "" && strings.Contains(what, p.Repository+"@"+short12(p.Head)) { return true } } @@ -626,16 +631,16 @@ func asksOf(c delivery.Candidate, what string) bool { // lostWhy says why a candidate whose verdict is running has a check that nothing will judge, or "": no step of its pull // requests can still run, and its newest one ended. -func (s stepsRead) lostWhy(c delivery.Candidate) string { +func (s stepsRead) lostWhy(c meshcatalog.CandidatesByGroupValue) string { for _, rs := range s.running { if asksOf(c, rs.What) { return "" } } - var last *delivery.Step + var last *buildstep.Step for i := range s.all { st := s.all[i].step - if st.State != delivery.StepRunning && asksOf(c, st.What) && (last == nil || st.Ended.After(last.Ended)) { + if st.State != buildstep.StepRunning && asksOf(c, st.What) && (last == nil || st.Ended.After(last.Ended)) { last = &s.all[i].step } } @@ -653,9 +658,9 @@ const DefaultStepBound = 30 * time.Minute // leftWhy says why a key in the running state runs no step any more, or "" when it can still be running: its build module // is not on its node or is not running there (the node-engine's have), or it started longer ago than the step bound, when // the checker stopped waiting for it. -func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound time.Duration) string { +func (r Reader) leftWhy(module string, step buildstep.Step, now time.Time, bound time.Duration) string { if r.HaveState.Get != nil { - key, err := delivery.HaveKey(step.Node, module) + key, err := nodeengineclient.HaveKey(step.Node, module) if err == nil { raw, err := r.HaveState.Get(key) switch { @@ -663,7 +668,7 @@ func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound case len(raw) == 0: return fmt.Sprintf("%s is not on %s", module, step.Node) default: - if h, err := delivery.DecodeHave(raw); err == nil && h.Phase != delivery.PhaseRunning { + if h, err := nodeengineclient.DecodeHave(raw); err == nil && h.Phase != nodeengineclient.PhaseRunning { return fmt.Sprintf("%s on %s is %s", module, step.Node, h.Phase) } } @@ -680,17 +685,17 @@ func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound // buildsNotEnded are the builder's builds of the candidate's changed modules that are neither done nor failed, as // Status.BuildsNotEnded says them; empty when none, or when the builder's builds are not readable here. -func (r Reader) buildsNotEnded(c delivery.Candidate) string { +func (r Reader) buildsNotEnded(c meshcatalog.CandidatesByGroupValue) string { if r.BuildState.Get == nil { return "" } var out []string for _, p := range c.Pulls { for module, cm := range p.Fps { - if cm.FP2 == "" || cm.Manifest == "" { + if cm.Fp2 == "" || cm.Manifest == "" { continue } - key, err := delivery.BuilderBuildKey(module, cm.FP2) + key, err := builds.BuilderBuildKey(module, cm.Fp2) if err != nil { continue } @@ -700,8 +705,8 @@ func (r Reader) buildsNotEnded(c delivery.Candidate) string { } state := "not begun" if len(raw) > 0 { - b, err := delivery.DecodeBuilderBuild(raw) - if err != nil || b.State == delivery.BuildDone || b.State == delivery.BuildFailed { + b, err := builds.DecodeBuilderBuild(raw) + if err != nil || b.State == builds.BuildDone || b.State == builds.BuildFailed { continue } state = b.State diff --git a/modules/mesh-delivery-checker/internal/check/read_test.go b/modules/mesh-delivery-checker/internal/check/read_test.go index 033ba635..ec037edb 100644 --- a/modules/mesh-delivery-checker/internal/check/read_test.go +++ b/modules/mesh-delivery-checker/internal/check/read_test.go @@ -9,7 +9,14 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient" + "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" + buildsrec "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) type fakeState map[string][]byte @@ -40,7 +47,7 @@ func readerOf(t *testing.T) (Reader, string) { cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1), pullOf(hqRepo, 3, head2)) r.ch.Candidate(context.Background(), "feat%2Fx", cand) r.ch.Wait() - ckey, _ := delivery.CandidatesKey("feat/x") + ckey, _ := meshcatalogclient.CandidatesKey("feat/x") verdicts, kept := fakeState{}, fakeState{} for k, v := range r.verdicts.vals { verdicts[k] = v @@ -58,7 +65,7 @@ func TestTheVerdictToolsAnswerTheStoredVerdictWithTheFullReportOfEachPullRequest if err != nil || len(vs) != 1 || vs[0].Key != key { t.Fatalf("verdicts = %+v, %v", vs, err) } - if _, err := delivery.DecodeVerdict(vs[0].Verdict); err != nil { + if _, err := meshdeliverycheckerclient.DecodeVerdict(vs[0].Verdict); err != nil { t.Errorf("the verdict is not as stored: %v", err) } if got := vs[0].Reports["novox/mesh-delivery#7"]; !strings.HasPrefix(got, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(got, "composition not yet run") { @@ -68,9 +75,9 @@ func TestTheVerdictToolsAnswerTheStoredVerdictWithTheFullReportOfEachPullRequest t.Errorf("another group's verdicts = %+v", only) } // A failed verdict is answered with every failure it names, one line each (the operator, 2026-11-02). - raw, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.VerdictFail, - Failed: delivery.FailedFirst{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom\nmore"}, - Failures: []delivery.FailedFirst{{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom"}, {Machine: "g14", Words: "route-proxy is defined twice"}}}) + raw, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.VerdictFail, + Failed: &FailedFirst{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom\nmore"}, + Failures: failureItems([]FailedFirst{{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom"}, {Machine: "g14", Words: "route-proxy is defined twice"}})}) if got, _ := rd.read(key, raw); strings.Join(got.Failed, " | ") != "unit: m/a.TestA: a_test.go:3: boom | g14: route-proxy is defined twice" { t.Errorf("failed = %q", got.Failed) } @@ -104,7 +111,7 @@ func TestTheKeptToolAnswersTheKeysAsStoredByLevelAndByDigest(t *testing.T) { // t.Fatalf("one kept = %v, %v", one, err) } for _, raw := range one { - if k, err := delivery.DecodeKept(raw); err != nil || k.Package != "m/a" { + if k, err := meshdeliverycheckerclient.DecodeKept(raw); err != nil || k.Package != "m/a" { t.Errorf("kept value = %+v, %v", k, err) } } @@ -118,12 +125,12 @@ func TestTheKeptToolAnswersTheKeysAsStoredByLevelAndByDigest(t *testing.T) { // // 1680 live acceptance: checker_previews shows the preview key of an open pull request, as stored, narrowed by repository and head. func TestThePreviewsToolShowsTheStoredPreviewOfAPullRequest(t *testing.T) { - pv := delivery.Preview{Repository: repoA, Head: head1, Trunk: head2, Merge: "m1", Modules: map[string]delivery.PreviewModule{}} - data, err := delivery.EncodePreview(pv) + pv := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: repoA, Head: head1, Trunk: head2, Merge: "m1", Modules: map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{}} + data, err := meshdeliverycheckerclient.EncodePreview(pv) if err != nil { t.Fatal(err) } - key, _ := delivery.PreviewKey(repoA, head1, head2) + key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, head2) rd := Reader{PreviewState: fakeState{key: data, "junk": []byte("{}")}.read()} got, bad, _, err := rd.Previews(repoA, head1[:8]) if err != nil || len(got) != 1 || got[0].Key != key || got[0].Head != head1 || got[0].Trunk != head2 || string(got[0].Preview) != string(data) { @@ -143,29 +150,29 @@ func TestThePreviewsToolShowsTheStoredPreviewOfAPullRequest(t *testing.T) { // checker_status reads the steps in flight by node and the candidates without a verdict; since R4g nothing holds the checker // and it has no capacity of its own (ADR 0525 §4, §7; 1685 closed by the hold's deletion). func TestTheStatusToolReadsRunningSteps(t *testing.T) { - running, _ := delivery.EncodeStep(delivery.Step{ID: "abc1", Tool: "build_go_test", Node: "shanks", State: delivery.StepRunning, Started: time.Date(2026, 10, 22, 15, 0, 0, 0, time.UTC)}) - passed, _ := delivery.EncodeStep(delivery.Step{ID: "abc2", Tool: "build_go_test", Node: "ace", State: delivery.StepPassed}) + running, _ := buildstep.EncodeStep(buildstep.Step{ID: "abc1", Tool: "build_go_test", Node: "shanks", State: buildstep.StepRunning, Started: time.Date(2026, 10, 22, 15, 0, 0, 0, time.UTC)}) + passed, _ := buildstep.EncodeStep(buildstep.Step{ID: "abc2", Tool: "build_go_test", Node: "ace", State: buildstep.StepPassed}) cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) - ckey, _ := delivery.CandidatesKey("feat/x") + ckey, _ := meshcatalogclient.CandidatesKey("feat/x") // ADR 0603 §10: feat/y's check runs and waits on the builds of its changed modules: web building, mail not begun; api // is done and named nowhere. fp, man := "src2:"+strings.Repeat("f", 64), strings.Repeat("e", 64) py := pullOf(repoA, 8, head2) - py.Modules, py.Fps = []string{"api", "mail", "web"}, map[string]delivery.CandidateModule{"web": {FP2: fp, Manifest: man}, "mail": {FP2: fp, Manifest: man}, "api": {FP2: fp, Manifest: man}} + py.Modules, py.Fps = []string{"api", "mail", "web"}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"web": {Fp2: fp, Manifest: man}, "mail": {Fp2: fp, Manifest: man}, "api": {Fp2: fp, Manifest: man}} candY := candidateOf(t, "feat/y", py) - ykey, _ := delivery.CandidatesKey("feat/y") - runningV, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/y", Heads: "0123456789ab", State: delivery.VerdictRunning}) + ykey, _ := meshcatalogclient.CandidatesKey("feat/y") + runningV, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/y", Heads: "0123456789ab", State: meshdeliverycheckerclient.VerdictRunning}) builds := fakeState{} - for module, state := range map[string]string{"web": delivery.BuildBuilding, "api": delivery.BuildDone} { - k, _ := delivery.BuilderBuildKey(module, fp) - builds[k], _ = json.Marshal(delivery.BuilderBuild{V: 1, State: state}) + for module, state := range map[string]string{"web": buildsrec.BuildBuilding, "api": buildsrec.BuildDone} { + k, _ := buildsrec.BuilderBuildKey(module, fp) + builds[k], _ = json.Marshal(buildsrec.BuilderBuild{V: 1, State: state}) } rd := Reader{ StepStates: map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()}, CandidateState: fakeState{ckey: cand, ykey: candY}.read(), VerdictState: fakeState{keyOf(t, "feat/y", candY): runningV}.read(), BuildState: builds.read(), - HaveState: fakeState{"shanks.mesh-build-go": haveOf(t, delivery.PhaseRunning)}.read(), + HaveState: fakeState{"shanks.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(), HearingState: fakeState{"dead-letters": []byte(`{"heard":false,"streams":["SEAT_NODE_BUILD_GO"],` + `"why":"DEAD_LETTERS is not heard, so an ask a build seat gives up on is waited for until its bound: SEAT_NODE_BUILD_GO: permissions violation",` + `"at":"2026-10-26T13:00:00Z"}`)}.read(), @@ -195,9 +202,9 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) { } } -func haveOf(t *testing.T, phase delivery.Phase) []byte { +func haveOf(t *testing.T, phase string) []byte { t.Helper() - raw, err := delivery.EncodeHave(delivery.Have{Phase: phase, Health: delivery.HealthOK}) + raw, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Phase: phase, Health: nodeengineclient.HealthOK}) if err != nil { t.Fatal(err) } @@ -206,7 +213,7 @@ func haveOf(t *testing.T, phase delivery.Phase) []byte { func stepOf(t *testing.T, id, node string, started time.Time) []byte { t.Helper() - raw, err := delivery.EncodeStep(delivery.Step{ID: id, Tool: "build_go_fetch", Node: node, State: delivery.StepRunning, Started: started}) + raw, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_fetch", Node: node, State: buildstep.StepRunning, Started: started}) if err != nil { t.Fatal(err) } @@ -228,8 +235,8 @@ func TestTheStatusCountsOnlyStepsThatCanStillRunAndSaysWhyTheOthersAreLeft(t *te }.read(), }, HaveState: fakeState{ - "g14.mesh-build-go": haveOf(t, delivery.PhaseRunning), - "shanks.mesh-build-go": haveOf(t, delivery.PhaseFailed), + "g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning), + "shanks.mesh-build-go": haveOf(t, nodeengineclient.PhaseFailed), }.read(), StepBound: 30 * time.Minute, Now: func() time.Time { return now }, @@ -258,7 +265,7 @@ func TestTheStatusCountsOnlyStepsThatCanStillRunAndSaysWhyTheOthersAreLeft(t *te // 1887/A10: a group of hq's pull requests alone is judged by the checker since R4g, so without a verdict it is unjudged. func TestTheStatusCountsAnHQOnlyGroupAsUnjudged(t *testing.T) { cand := candidateOf(t, "pb-x", pullOf(hqRepo, 3, head2)) - ckey, _ := delivery.CandidatesKey("pb-x") + ckey, _ := meshcatalogclient.CandidatesKey("pb-x") st, err := Reader{CandidateState: fakeState{ckey: cand}.read(), VerdictState: fakeState{}.read()}.Status() if err != nil { t.Fatal(err) @@ -268,7 +275,7 @@ func TestTheStatusCountsAnHQOnlyGroupAsUnjudged(t *testing.T) { } // ADR 0548 §3: a candidate whose check an approved merge's turn holds off is named with the turn. held := candidateOf(t, "feat/x", pullOf(repoA, 7, head1)) - hkey, _ := delivery.CandidatesKey("feat/x") + hkey, _ := meshcatalogclient.CandidatesKey("feat/x") st, err = Reader{CandidateState: fakeState{hkey: held}.read(), VerdictState: fakeState{}.read(), Trunks: func() (map[string]string, error) { return map[string]string{repoA: trunkA}, nil }, TurnState: fakeState{turnKey(t, repoA): turnOf(t, repoA, "fix/other", "0123456789ab")}.read()}.Status() @@ -311,13 +318,13 @@ func TestReadToolsReadOnlyWhatTheyNeedInParallelAndSayWhenCut(t *testing.T) { // kept := fakeState{} for i := 0; i < 1800; i++ { level := map[bool]string{true: "unit", false: "integration"}[i%9 != 0] - k, _ := delivery.KeptKey(level, fmt.Sprintf("%064x", i)) + k, _ := meshdeliverycheckerclient.KeptKey(level, fmt.Sprintf("%064x", i)) kept[k] = []byte(`{"v":1}`) } previews := fakeState{} for i := 0; i < 1500; i++ { repo := map[bool]string{true: repoA, false: repoSDK}[i < 300] - k, _ := delivery.PreviewKey(repo, fmt.Sprintf("%08x%032x", i, 0), trunkA) + k, _ := meshdeliverycheckerclient.PreviewKey(repo, fmt.Sprintf("%08x%032x", i, 0), trunkA) previews[k] = []byte(`{"v":1}`) } for _, row := range []struct { @@ -372,15 +379,15 @@ func TestReadToolsReadOnlyWhatTheyNeedInParallelAndSayWhenCut(t *testing.T) { // steps := fakeState{} for i := 0; i < 1500; i++ { id := fmt.Sprintf("mvo5%04dabcdef", i) - step := delivery.Step{ID: id, Tool: "t", Node: "g14", State: delivery.StepPassed} + step := buildstep.Step{ID: id, Tool: "t", Node: "g14", State: buildstep.StepPassed} if i == 1499 { - step.State, step.Started = delivery.StepRunning, now.Add(-time.Minute) + step.State, step.Started = buildstep.StepRunning, now.Add(-time.Minute) } - raw, _ := delivery.EncodeStep(step) + raw, _ := buildstep.EncodeStep(step) steps["g14."+id] = raw } sb := &slowBucket{vals: steps} - st, err := Reader{StepStates: map[string]StateRead{"mesh-build-go": sb.read()}, HaveState: fakeState{"g14.mesh-build-go": haveOf(t, delivery.PhaseRunning)}.read(), + st, err := Reader{StepStates: map[string]StateRead{"mesh-build-go": sb.read()}, HaveState: fakeState{"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(), Now: func() time.Time { return now }}.Status() if err != nil || sb.reads.Load() != MaxReads || len(st.Running) != 1 || sb.maxFlight.Load() < 2 { t.Errorf("status: %d reads (want %d), running %+v, in flight %d, %v", sb.reads.Load(), MaxReads, st.Running, sb.maxFlight.Load(), err) diff --git a/modules/mesh-delivery-checker/internal/check/renames.go b/modules/mesh-delivery-checker/internal/check/renames.go index 0d8bfc67..dd2525db 100644 --- a/modules/mesh-delivery-checker/internal/check/renames.go +++ b/modules/mesh-delivery-checker/internal/check/renames.go @@ -9,8 +9,9 @@ import ( "strings" "git.novox.be/novox/mesh-contracts/go/seats/meshconfig" + "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/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // A config value renamed (design 32 *A config value renamed*, DLV-C5): the checker asks the seat mesh-config's verb @@ -52,15 +53,15 @@ func renamesOf(was, now catalogue.Manifest) renameChange { } // judgeRenames fails the check for a rename the seat refuses, a window a change would drop, or an ask that cannot be made. -func (st *state) judgeRenames(ctx context.Context) *delivery.Verdict { +func (st *state) judgeRenames(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue { if st.c.Try != nil { return nil } - var out *delivery.Verdict + var out *meshdeliverychecker.VerdictsByGroupHeadsValue failed := func(words string) { - f := delivery.FailedFirst{Machine: meshconfig.Seat, Words: words} + f := FailedFirst{Machine: meshconfig.Seat, Words: words} if out == nil { - out = &delivery.Verdict{State: delivery.VerdictFail, Failed: f} + out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &f} return } addFailure(out, f) diff --git a/modules/mesh-delivery-checker/internal/check/results.go b/modules/mesh-delivery-checker/internal/check/results.go index 218aa4db..67b5ec91 100644 --- a/modules/mesh-delivery-checker/internal/check/results.go +++ b/modules/mesh-delivery-checker/internal/check/results.go @@ -1,11 +1,7 @@ package check import ( - "encoding/json" - "strings" - - "git.novox.be/novox/mesh-sdk/go/buildstep" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // The shapes of what the build modules' steps answer, as their code writes them (mesh-catalog modules/mesh-build-go @@ -13,52 +9,10 @@ import ( // cmd/mesh-build-node check.go TestResult, fetch.go FetchResult) with the fields the fetch step adds for a merge (ADR 0484 // clause 3). Only the fields the checker reads are named; the rest is ignored. -// MergeTree is what a fetch that merged says of one module directory: the tree sha, the digest of its manifest, the -// hashes of its lock files by name and the objects of its named paths. A bare string is the tree alone. -type MergeTree struct { - Tree string `json:"tree"` - Manifest string `json:"manifest,omitempty"` - Locks map[string]string `json:"locks,omitempty"` - // Paths are, of a module whose every artifact names its paths, each named path's object; Tree is then their named - // tree (mesh-sdk buildstep.ModuleTree, novox/hq ADR 0499 clause 2). - Paths map[string]string `json:"paths,omitempty"` - // Packages are the mesh packages the directory's go.sum names (mesh-sdk buildstep.ModuleTree, novox/hq ADR 0573 §2). - Packages []string `json:"packages,omitempty"` -} - -// UnmarshalJSON takes a string as the tree alone, or an object. -func (t *MergeTree) UnmarshalJSON(data []byte) error { - if s := strings.TrimSpace(string(data)); strings.HasPrefix(s, `"`) { - return json.Unmarshal(data, &t.Tree) - } - type plain MergeTree - return json.Unmarshal(data, (*plain)(t)) -} - -// Fetched is the answer of a build module's fetch step. -type Fetched struct { - Workspace string `json:"workspace"` - Commit string `json:"commit"` - Modules []string `json:"modules"` - // What a fetch with a base adds: the trunk commit, the merge commit, the files in conflict (a conflict is an answer), - // the paths the merge changes and the trees of the module directories. - Base string `json:"base,omitempty"` - Merge string `json:"merge,omitempty"` - Conflicts []string `json:"conflicts,omitempty"` - Changed []string `json:"changed,omitempty"` - Trees map[string]MergeTree `json:"trees,omitempty"` - // Programs are the changed files whose first bytes are an executable binary, read by the build module in its workspace - // (issue 1577); nil for a build module that does not report it, a pointer to an empty list for one that found none. - Programs *[]ProgramFile `json:"programs,omitempty"` -} - -// ProgramFile is a changed file that is a compiled program, and its format (ELF, Mach-O or PE). -type ProgramFile struct { - Path string `json:"path"` - Kind string `json:"kind"` - // Commit is set for a program a commit of the branch added and a later one removed: it stays in history once merged. - Commit string `json:"commit,omitempty"` -} +// Fetched is the answer of a build module's fetch step, whole: the Delivery SDK's FetchAnswer, which a check answer keeps +// (B2g; the build seats' part FetchAnswer): the workspace, the merge preview with its conflicts, changed paths, programs and +// trees. A conflict is an answer. +type Fetched = buildstep.FetchAnswer // GoPackage is one package of a Go test result. type GoPackage struct { @@ -128,5 +82,5 @@ type Digested struct { // JudgedBy is the program that ran the digest, as Tested.JudgedBy says. JudgedBy string `json:"judged_by,omitempty"` // Needs are the packages whose tests need something of the node at this level (issue 890). - Needs []delivery.PackageNeeds `json:"needs,omitempty"` + Needs []buildstep.PackageNeeds `json:"needs,omitempty"` } diff --git a/modules/mesh-delivery-checker/internal/check/seatdefs.go b/modules/mesh-delivery-checker/internal/check/seatdefs.go index 4669fd27..bda77725 100644 --- a/modules/mesh-delivery-checker/internal/check/seatdefs.go +++ b/modules/mesh-delivery-checker/internal/check/seatdefs.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" ) // A pull request whose head's seat definition does not parse fails its check, naming the seat and where it lives (design @@ -25,15 +26,15 @@ type SeatDefinitions interface { // refuseUnparsed fails the check of each pull request with a seat definition that does not parse; a state that cannot be // read fails it too, saying so. -func (st *state) refuseUnparsed(ctx context.Context) *delivery.Verdict { +func (st *state) refuseUnparsed(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue { if st.j.SeatDefinitions == nil || st.c.Try != nil { return nil } - var out *delivery.Verdict + var out *meshdeliverychecker.VerdictsByGroupHeadsValue failed := func(words string) { - f := delivery.FailedFirst{Machine: SeatDefinitionsMachine, Words: words} + f := FailedFirst{Machine: SeatDefinitionsMachine, Words: words} if out == nil { - out = &delivery.Verdict{State: delivery.VerdictFail, Failed: f} + out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &f} return } addFailure(out, f) diff --git a/modules/mesh-delivery-checker/internal/check/series.go b/modules/mesh-delivery-checker/internal/check/series.go index 6574b700..1d61bb60 100644 --- a/modules/mesh-delivery-checker/internal/check/series.go +++ b/modules/mesh-delivery-checker/internal/check/series.go @@ -6,8 +6,8 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // A change of a package series is named in the verdict, so the review of the change sees it (design 32 *A package, its @@ -19,7 +19,7 @@ import ( func (st *state) seriesChanges(ctx context.Context) { for _, p := range st.c.Pulls { var words []string - var changes []delivery.SeriesChange + var changes []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem for _, module := range p.Modules { was, now, ok, err := st.manifestPair(ctx, p, module) if err != nil { @@ -72,13 +72,13 @@ func (st *state) manifestPair(ctx context.Context, p Pull, module string) (was, } // seriesLine is a series change as the status line names it: `/ series → `. -func seriesLine(c delivery.SeriesChange) string { +func seriesLine(c meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) string { return fmt.Sprintf("%s/%s series %s → %s", c.Module, c.Artifact, c.From, c.To) } // seriesMoved are the artifacts of module whose series the head's manifest changes from the trunk's, by artifact. An // artifact new on the head is no series change (the lead's ruling): it has no series on the trunk. -func seriesMoved(module string, was, now catalogue.Manifest) []delivery.SeriesChange { +func seriesMoved(module string, was, now catalogue.Manifest) []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem { if was.Build == nil || now.Build == nil { return nil } @@ -86,37 +86,39 @@ func seriesMoved(module string, was, now catalogue.Manifest) []delivery.SeriesCh for _, a := range was.Build.Artifacts { before[a.Name] = a.SeriesOf() } - var out []delivery.SeriesChange + var out []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem for _, a := range now.Build.Artifacts { if old, ok := before[a.Name]; ok && old != a.SeriesOf() { - out = append(out, delivery.SeriesChange{Module: module, Artifact: a.Name, From: old, To: a.SeriesOf()}) + out = append(out, meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{Module: module, Artifact: a.Name, From: old, To: a.SeriesOf()}) } } - slices.SortFunc(out, func(a, b delivery.SeriesChange) int { return strings.Compare(a.Artifact, b.Artifact) }) + slices.SortFunc(out, func(a, b meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) int { + return strings.Compare(a.Artifact, b.Artifact) + }) return out } // series keeps, of a pull request (pullKey), the words of the series changes it makes and the changes themselves. -func (c *Case) series(key string, words []string, changes []delivery.SeriesChange) { +func (c *Case) series(key string, words []string, changes []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) { c.notesMu.Lock() defer c.notesMu.Unlock() if c.seriesOf == nil { - c.seriesOf, c.seriesChanges = map[string][]string{}, map[string][]delivery.SeriesChange{} + c.seriesOf, c.seriesChanges = map[string][]string{}, map[string][]meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{} } c.seriesOf[key], c.seriesChanges[key] = words, changes } // SeriesChanges are the series changes a pull request (pullKey) makes, as the verdict and its event name them. -func (c *Case) SeriesChanges(key string) []delivery.SeriesChange { +func (c *Case) SeriesChanges(key string) []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem { c.notesMu.Lock() defer c.notesMu.Unlock() return slices.Clone(c.seriesChanges[key]) } // allSeries are the series changes of every pull request of the case, each module's artifact once, by module and -// artifact: what the verdict names (delivery.Verdict.Series). -func (c *Case) allSeries() []delivery.SeriesChange { - var out []delivery.SeriesChange +// artifact: what the verdict names (meshdeliverychecker.VerdictsByGroupHeadsValue.Series). +func (c *Case) allSeries() []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem { + var out []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem seen := map[string]bool{} for _, p := range c.Pulls { for _, s := range c.SeriesChanges(pullKey(p)) { @@ -126,7 +128,7 @@ func (c *Case) allSeries() []delivery.SeriesChange { } } } - slices.SortFunc(out, func(a, b delivery.SeriesChange) int { + slices.SortFunc(out, func(a, b meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) int { return strings.Compare(a.Module+"/"+a.Artifact, b.Module+"/"+b.Artifact) }) return out diff --git a/modules/mesh-delivery-checker/internal/check/tagwanted.go b/modules/mesh-delivery-checker/internal/check/tagwanted.go index bc0595c7..4dd1a301 100644 --- a/modules/mesh-delivery-checker/internal/check/tagwanted.go +++ b/modules/mesh-delivery-checker/internal/check/tagwanted.go @@ -10,9 +10,10 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // EventTagWanted is the checker's event that asks the builder for a group member's tagged versions (design 51 *Tagged @@ -158,11 +159,11 @@ func (st *state) manifestAtHead(ctx context.Context, p Pull, module string) (m c } } if dirs == nil { - dirs = []string{"modules/" + module, delivery.RootPath} + dirs = []string{"modules/" + module, meshcatalogclient.RootPath} } for _, d := range dirs { file := "module.json" - if t := strings.Trim(d, "/"); t != "" && t != delivery.RootPath { + if t := strings.Trim(d, "/"); t != "" && t != meshcatalogclient.RootPath { file = t + "/module.json" } text, err := st.j.Forge.File(ctx, p.Repository, file, p.Head) @@ -248,7 +249,7 @@ func (st *state) declaredPackages(ctx context.Context) ([]declaredPackage, error // packages by module path in Go's form (StepAsk.Packages), and the TypeScript packages a member's Node fetch takes from the // registry, by the repository that publishes them. type groupVersions struct { - goPackages delivery.PackageVersions + goPackages buildstep.PackageVersions node map[string]NodePackage } @@ -286,7 +287,7 @@ func (g *groupVersions) took(d declaredPackage, p TaggedPublished) { switch d.language { case "go": if g.goPackages == nil { - g.goPackages = delivery.PackageVersions{} + g.goPackages = buildstep.PackageVersions{} } g.goPackages[p.Module] = goVersion(p.Version) // Heard took no Go published without its module case "typescript": @@ -300,13 +301,13 @@ func (g *groupVersions) took(d declaredPackage, p TaggedPublished) { // tagged asks a group's tagged versions before any member is built (design 51 *Tagged versions*): one tag-wanted per // package a member declares, then each package's published awaited within the publish bound. A lone pull request and a // try ask nothing. A package not published within the bound fails the check naming it. -func (st *state) tagged(ctx context.Context) *delivery.Verdict { +func (st *state) tagged(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue { if st.j.Tags == nil || st.c.Try != nil || len(st.c.Pulls) < 2 { return nil } decl, err := st.declaredPackages(ctx) if err != nil { - return fail(delivery.CheckerModule, err.Error()) + return fail(CheckerModule, err.Error()) } if len(decl) == 0 { return nil @@ -316,7 +317,7 @@ func (st *state) tagged(ctx context.Context) *delivery.Verdict { for _, d := range decl { t := TagWanted{Repository: d.pull.Repository, Branch: st.c.Group, Head: d.pull.Head, Module: d.module, Package: d.name, Pull: int64(d.pull.Number)} if err := st.j.Tags.Want(ctx, t); err != nil { - return fail(delivery.CheckerModule, fmt.Sprintf("the tagged version of %s of %s (%s) cannot be asked: %v", d.name, d.module, pullKey(d.pull), err)) + return fail(CheckerModule, fmt.Sprintf("the tagged version of %s of %s (%s) cannot be asked: %v", d.name, d.module, pullKey(d.pull), err)) } names = append(names, d.module+"/"+d.name) } @@ -341,9 +342,9 @@ func (st *state) tagged(ctx context.Context) *delivery.Verdict { } if len(late) > 0 { if ctx.Err() != nil { - return fail(delivery.CheckerModule, ctx.Err().Error()) + return fail(CheckerModule, ctx.Err().Error()) } - return fail(delivery.CheckerModule, fmt.Sprintf("no published of the tagged version of %s within %s, so no member is built (design 51 *Tagged versions*)", strings.Join(late, "; "), bound)) + return fail(CheckerModule, fmt.Sprintf("no published of the tagged version of %s within %s, so no member is built (design 51 *Tagged versions*)", strings.Join(late, "; "), bound)) } return nil } diff --git a/modules/mesh-delivery-checker/internal/check/try.go b/modules/mesh-delivery-checker/internal/check/try.go index 990cf686..d0155e5d 100644 --- a/modules/mesh-delivery-checker/internal/check/try.go +++ b/modules/mesh-delivery-checker/internal/check/try.go @@ -13,8 +13,11 @@ import ( "sync" "time" + "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-delivery/modules/mesh-delivery-builder/buildrun" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" + "git.novox.be/novox/mesh-sdk/go/keys" ) // A try is a pushed head judged by the checker as a pull request's head is, before its pull request is opened (ADR 0501 @@ -29,7 +32,7 @@ const ( TryEndTool = "checker_try_end" // TriesState is the checker's state of tries: the bucket mesh-delivery-checker_tries, keys try-. TriesState = "tries" - TriesBucket = delivery.CheckerModule + "_" + TriesState + TriesBucket = CheckerModule + "_" + TriesState // MaxTries and MaxTriesWaiting are checked when a try is asked, mesh-wide: a new try is refused while MaxTries tries run // on a build node or MaxTriesWaiting wait for one (queued, or their asks not yet taken by a node). It is no bound on // what runs later: tries let in while waiting may later all run at once, and each node's build capacity bounds what @@ -78,11 +81,11 @@ type Try struct { Said string `json:"said,omitempty"` // Took is the time from its start to its end, as Go writes a duration. Took string `json:"took,omitempty"` - // Failed is every test that failed, each with its first line, at most delivery.MaxFailures and then how many more + // Failed is every test that failed, each with its first line, at most meshdeliverycheckerclient.MaxFailures and then how many more // (issue 1921); or what else failed first (a conflict, a refused step). - Failed []string `json:"failed,omitempty"` - Levels map[string]delivery.LevelResult `json:"levels,omitempty"` - Machines []string `json:"machines,omitempty"` + Failed []string `json:"failed,omitempty"` + Levels map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue `json:"levels,omitempty"` + Machines []string `json:"machines,omitempty"` // Asks are the ids of the asks the try put on the build seats' streams, each ending on the step key . of its // build module: the delivery view counts those steps as a try's, not a candidate's check (build_usage). Asks []string `json:"asks,omitempty"` @@ -122,7 +125,7 @@ type tryWriter struct { } // heard takes an ask put (step nil) or a value of its step key in, and writes the record when it changed. -func (w *tryWriter) heard(askID string, step *delivery.Step) { +func (w *tryWriter) heard(askID string, step *buildstep.Step) { w.mu.Lock() defer w.mu.Unlock() if w.stopped { @@ -180,7 +183,7 @@ func (t Try) HoldsANode() bool { if len(t.Steps) == 0 { return true } - return slices.ContainsFunc(t.Steps, func(s TryStep) bool { return s.State == delivery.StepRunning }) + return slices.ContainsFunc(t.Steps, func(s TryStep) bool { return s.State == buildstep.StepRunning }) } // Over says whether the try came to its end. @@ -199,7 +202,7 @@ func DecodeTry(raw []byte) (Try, error) { } func encodeTry(t Try) ([]byte, error) { - t.V = delivery.Version + t.V = keys.Version return json.Marshal(t) } @@ -232,7 +235,7 @@ func AskTry(store TryStore, trunks map[string]string, a TryAsk, now time.Time) ( return "", fmt.Errorf("%q is not a repository, /", a.Repository) case !tryHeadOK.MatchString(head): return "", fmt.Errorf("the head %q is not a full commit, 40 or 64 hex digits", a.Head) - case a.Level != "" && a.Level != delivery.LevelUnit && a.Level != delivery.LevelIntegration: + case a.Level != "" && a.Level != buildstep.LevelUnit && a.Level != buildstep.LevelIntegration: return "", fmt.Errorf("the level %q is neither unit nor integration", a.Level) case a.Tests != "" && !tryTestsOK.MatchString(a.Tests): return "", fmt.Errorf("the tests %q are not a go test -run pattern", a.Tests) @@ -530,9 +533,9 @@ func (tr *Tryer) run(ctx context.Context, key string) { // Not the change's: its ask was given up or never taken (issue 734), or a part of the mesh was not heard. t.State = TryError if IsLostAsk(v) || IsNotTaken(v) { - t.Failed = []string{v.Failed.Words} + t.Failed = []string{failedOf(v).Words} } - case v.State == delivery.VerdictPass: + case v.State == meshdeliverycheckerclient.VerdictPass: t.State = TryPass default: t.State = TryFail diff --git a/modules/mesh-delivery-checker/internal/check/try_test.go b/modules/mesh-delivery-checker/internal/check/try_test.go index be2ea659..0a2856ef 100644 --- a/modules/mesh-delivery-checker/internal/check/try_test.go +++ b/modules/mesh-delivery-checker/internal/check/try_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" ) // memTries is the tries state as the tools see it, over a memKV. @@ -72,7 +72,7 @@ func TestATryIsAskedQueuedOncePerHeadAndAtMostFourAtOnce(t *testing.T) { // ADR // their asks not taken) are bounded apart, MaxTries and MaxTriesWaiting; the refusal names both counts. onNode := func(i int) Try { return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning, - Steps: []TryStep{{Ask: "a", State: delivery.StepRunning}}} + Steps: []TryStep{{Ask: "a", State: buildstep.StepRunning}}} } waitingFor := func(i int) Try { return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning, @@ -133,7 +133,7 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { / tr.Put(ctx, id, raw) tr.Wait() got := readTry(t, kv, id) - if got.State != TryPass || got.Took == "" || got.Head != head1 || got.Trunk != trunkA || got.Said == "" || got.Levels[delivery.LevelUnit].Run != 2 { + if got.State != TryPass || got.Took == "" || got.Head != head1 || got.Trunk != trunkA || got.Said == "" || got.Levels[buildstep.LevelUnit].Run != 2 { t.Fatalf("try = %+v", got) } // Issue 1890: while it runs, the try names its ask as soon as it is put, and then where its step stands; the end @@ -142,17 +142,17 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { / written.running[2].Steps[0].Node != "shanks" { t.Errorf("the running try's writes = %+v, want its ask named while it ran, then its node", written.running) } - if len(got.Steps) != 1 || got.Steps[0].Node != "shanks" || got.Steps[0].State != delivery.StepPassed { + if len(got.Steps) != 1 || got.Steps[0].Node != "shanks" || got.Steps[0].State != buildstep.StepPassed { t.Errorf("the ended try's steps = %+v", got.Steps) } // The try names the asks it put, so the delivery view counts their steps as a try's (build_usage). if len(got.Asks) != 1 { t.Errorf("asks = %v, want the one check ask the try put", got.Asks) - } else if _, err := delivery.AskStepKey("shanks", got.Asks[0]); err != nil { + } else if _, err := buildstep.AskStepKey("shanks", got.Asks[0]); err != nil { t.Errorf("the try names %q, which is no ask id: %v", got.Asks[0], err) } fetch := b.steps.tool("fetch") - if len(fetch) != 1 || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != "" { + if len(fetch) != 1 || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != nil { t.Errorf("fetch = %+v, want the head merged onto the trunk, with no pull request's ref", fetch) } if keys, _ := b.prev.Keys(ctx); len(keys) != 0 { @@ -170,7 +170,7 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { / func TestATryOfOneLevelAndSomeTestsRunsThemAloneAndKeepsNothing(t *testing.T) { ctx := context.Background() kv := newMemKV() - id, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1, Level: delivery.LevelIntegration, Tests: "TestBus|TestStore"}, time.Now()) + id, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1, Level: buildstep.LevelIntegration, Tests: "TestBus|TestStore"}, time.Now()) if err != nil { t.Fatal(err) } @@ -180,7 +180,7 @@ func TestATryOfOneLevelAndSomeTestsRunsThemAloneAndKeepsNothing(t *testing.T) { tr.Put(ctx, id, raw) tr.Wait() tests := b.steps.tool("test") - if len(tests) != 1 || tests[0].args["level"] != delivery.LevelIntegration || tests[0].args["run"] != "TestBus|TestStore" { + if len(tests) != 1 || tests[0].args["level"] != buildstep.LevelIntegration || tests[0].args["run"] != "TestBus|TestStore" { t.Fatalf("tests = %+v, want the integration level alone with the run pattern", tests) } if n := len(b.steps.tool("digest")); n != 0 { diff --git a/modules/mesh-delivery-checker/internal/check/turns.go b/modules/mesh-delivery-checker/internal/check/turns.go index 4873e333..cb8da5e0 100644 --- a/modules/mesh-delivery-checker/internal/check/turns.go +++ b/modules/mesh-delivery-checker/internal/check/turns.go @@ -5,7 +5,8 @@ import ( "strings" "sync" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" ) // Turns holds the gate's merge turns (novox/hq ADR 0548 §3), by repository, as a watch of the gate's turns bucket puts them @@ -13,26 +14,26 @@ import ( // approved merge goes before a check not yet started. A bucket not there is read as empty. type Turns struct { mu sync.Mutex - byRepo map[string]delivery.Turn + byRepo map[string]meshdelivery.TurnsByRepositoryValue } // Put takes a value of the turns bucket in, or its delete, and answers whether what the checker holds changed. A value that // is not a turn is dropped as a delete would be: a turn the checker cannot read holds nothing. func (t *Turns) Put(key string, value []byte, deleted bool) bool { - repository, err := delivery.ParseTurnsKey(key) + repository, err := meshdeliveryclient.ParseTurnsKey(key) if err != nil { return false } t.mu.Lock() defer t.mu.Unlock() if t.byRepo == nil { - t.byRepo = map[string]delivery.Turn{} + t.byRepo = map[string]meshdelivery.TurnsByRepositoryValue{} } cur, had := t.byRepo[repository] if !deleted { - if turn, err := delivery.DecodeTurn(value); err == nil { + if turn, err := meshdeliveryclient.DecodeTurn(value); err == nil { t.byRepo[repository] = turn - return !had || !delivery.SameTurn(cur, turn) + return !had || !meshdeliveryclient.SameTurn(cur, turn) } } delete(t.byRepo, repository) @@ -41,9 +42,9 @@ func (t *Turns) Put(key string, value []byte, deleted bool) bool { // Holding names the first of the repositories (by name) a turn holds off the check of the heads, and the delivery that // waits first in it; false when none does. -func (t *Turns) Holding(repositories []string, heads string) (string, delivery.TurnWait, bool) { +func (t *Turns) Holding(repositories []string, heads string) (string, meshdelivery.TurnsByRepositoryValueWaitingItem, bool) { if t == nil { - return "", delivery.TurnWait{}, false + return "", meshdelivery.TurnsByRepositoryValueWaitingItem{}, false } sorted := make([]string, 0, len(repositories)) for _, r := range repositories { @@ -54,7 +55,7 @@ func (t *Turns) Holding(repositories []string, heads string) (string, delivery.T defer t.mu.Unlock() for _, r := range sorted { turn, ok := t.byRepo[r] - if !ok || !delivery.TurnHolds(turn, heads) { + if !ok || !meshdeliveryclient.TurnHolds(turn, heads) { continue } for _, w := range turn.Waiting { @@ -63,14 +64,14 @@ func (t *Turns) Holding(repositories []string, heads string) (string, delivery.T } } } - return "", delivery.TurnWait{}, false + return "", meshdelivery.TurnsByRepositoryValueWaitingItem{}, false } // All is every turn held now, by repository (a copy). -func (t *Turns) All() map[string]delivery.Turn { +func (t *Turns) All() map[string]meshdelivery.TurnsByRepositoryValue { t.mu.Lock() defer t.mu.Unlock() - out := make(map[string]delivery.Turn, len(t.byRepo)) + out := make(map[string]meshdelivery.TurnsByRepositoryValue, len(t.byRepo)) for r, turn := range t.byRepo { out[r] = turn } diff --git a/modules/mesh-delivery-checker/internal/check/verdictevent.go b/modules/mesh-delivery-checker/internal/check/verdictevent.go index 12434b3e..fe558a2a 100644 --- a/modules/mesh-delivery-checker/internal/check/verdictevent.go +++ b/modules/mesh-delivery-checker/internal/check/verdictevent.go @@ -3,8 +3,8 @@ package check import ( "context" - "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/clients/meshdeliverycheckerclient" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" ) // EventVerdict is the checker's event that says a pull request's check ended, with the generated files its steps found out @@ -23,8 +23,8 @@ type VerdictSayer interface { // sayVerdict says, for each pull request of a check that ended in pass or fail, one verdict event naming its head and its // out-of-step files. A try owes no pull request anything and says none (ADR 0501 §5). A publish that fails is said in the // log: the verdict and the status are written already, and the next check of the heads says it again. -func (ch *Checker) sayVerdict(ctx context.Context, cs *Case, v delivery.Verdict, out Outcome) { - if ch.Says == nil || cs.Try != nil || (v.State != delivery.VerdictPass && v.State != delivery.VerdictFail) { +func (ch *Checker) sayVerdict(ctx context.Context, cs *Case, v meshdeliverychecker.VerdictsByGroupHeadsValue, out Outcome) { + if ch.Says == nil || cs.Try != nil || (v.State != meshdeliverycheckerclient.VerdictPass && v.State != meshdeliverycheckerclient.VerdictFail) { return } for _, p := range cs.Pulls { diff --git a/modules/mesh-delivery-checker/manifest_test.go b/modules/mesh-delivery-checker/manifest_test.go index 6fe40f36..30e5f4a7 100644 --- a/modules/mesh-delivery-checker/manifest_test.go +++ b/modules/mesh-delivery-checker/manifest_test.go @@ -10,6 +10,8 @@ import ( "slices" "strings" "testing" + + "mesh-delivery-checker/internal/check" ) // The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521, @@ -101,7 +103,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 if m.Module != "mesh-delivery-checker" { t.Errorf("module = %q", m.Module) } - // delivery.CheckerKeptHistory, CheckerOwedHistory, PreviewsHistory, CheckerVerdictsHistory; the names follow the SDK's buckets. + // The histories of the checker's four seat states (checked against internal/check names below); the names follow the seat mesh-delivery-checker. if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}}; !reflect.DeepEqual(m.State, want) { t.Errorf("state = %+v, want %+v", m.State, want) } @@ -165,6 +167,28 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1 t.Errorf("the manifest declares %q: the checker has none", k) } } + // D-11 (ADR 0634 §1): the holders and buckets internal/check names, no contract naming a holder, are the manifest's: a state + // the checker reads is a `reads` (.) and a state of the indexer it follows a `consumes` of its views family. + for bucket, read := range map[string]string{check.SourceBucket: "mesh-delivery-gate.source", check.TurnsBucket: "mesh-delivery-gate.turns", + check.PresenceBucket: "mesh-delivery-watchman.presence", check.HaveBucket: "node-engine.have"} { + if strings.Replace(bucket, "_", ".", 1) != read || !slices.Contains(m.Reads, read) { + t.Errorf("the bucket %s is not the declared read %s", bucket, read) + } + } + for bucket, view := range map[string]string{check.CandidatesBucket: "candidates", check.ModulesBucket: "modules", check.ManifestsBucket: "manifests"} { + if bucket != check.IndexerModule+"_"+view || !slices.Contains(m.Consumes, check.IndexerModule+".views."+view) { + t.Errorf("the bucket %s is not a followed view of the indexer's %s", bucket, view) + } + } + if !slices.Contains(m.Consumes, check.IndexerModule+".views."+check.IndexerPredefinedState) { + t.Errorf("the indexer's state %s is followed, and not declared", check.IndexerPredefinedState) + } + for bucket, history := range map[string]int{check.CheckerVerdictsBucket: 5, check.CheckerKeptBucket: 1, check.CheckerOwedBucket: 5, check.PreviewsBucket: 5} { + own := strings.TrimPrefix(bucket, check.CheckerModule+"_") + if own == bucket || !slices.ContainsFunc(m.State, func(st state) bool { return st.Name == own && st.History == history }) { + t.Errorf("the bucket %s is not a state of the manifest at history %d", bucket, history) + } + } } func TestManifestProcess(t *testing.T) { // 1678/AC1