From 4e4601a1eb90150bb8d9d08ced48235a2a0ad93f Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 00:05:01 +0100 Subject: [PATCH] Take-in and forge contracts live with their owners: the indexer's takein package, the forge bridge's forgemsg package; mesh-sdk pinned at main (issue 1585, ADR 0633) The operator, 2026-11-04: logic one module owns lives inside that module as a package of its own, so the take-in step changes no mesh-sdk. TakeIn, its keys and event names, PendingWait and PendingAnswer move to modules/mesh-delivery-indexer/takein (its merges a takein.Merge of its own); Forge whole, with its group fields, ForgeGroupEnd and GroupMerge, moves to modules/mesh-delivery-forge-bridge/forgemsg. Both are nested Go modules that their readers take by replace, as buildrun is. Every importer moves in this commit: the indexer, the bridge, the watchman and the view. Forge's private checks (the repository name, the version) are copied small; mesh-sdk main still holds an unused Forge until its own fold removes it. The indexer, bridge, watchman, view and checker pin mesh-sdk 5a38a12 (origin/main), no feat/take-in commit. The bridge's integration bus now grants the state end-asks that bridge_group_end_again added: without it every test of the package failed on a permissions violation. Test budget: two new test functions (TestForge, TestTakeInEvents) and the two the join added are paid by merging four pairs in internal/bridge; the repository stays at 806 unit tests. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn --- modules/mesh-delivery-checker/go.mod | 2 +- modules/mesh-delivery-checker/go.sum | 4 +- .../mesh-delivery-forge-bridge/gate_test.go | 9 +- .../cmd/mesh-delivery-forge-bridge/groups.go | 23 +- .../mesh-delivery-forge-bridge/groups_test.go | 7 +- .../integration_test.go | 33 +- .../cmd/mesh-delivery-forge-bridge/main.go | 3 +- .../mesh-delivery-forge-bridge/main_test.go | 6 +- .../cmd/mesh-delivery-forge-bridge/open.go | 6 +- .../mesh-delivery-forge-bridge/open_test.go | 22 +- .../forgemsg/forge.go | 413 ++++++++++++++++++ .../forgemsg/forge_test.go | 193 ++++++++ .../forgemsg/go.mod | 5 + .../forgemsg/go.sum | 14 + .../forgemsg/group.go | 78 ++++ modules/mesh-delivery-forge-bridge/go.mod | 5 +- modules/mesh-delivery-forge-bridge/go.sum | 4 +- .../internal/bridge/acts.go | 5 +- .../internal/bridge/acts_test.go | 31 +- .../internal/bridge/bridge.go | 50 +-- .../internal/bridge/bridge_test.go | 104 +++-- .../internal/bridge/files_test.go | 14 +- .../internal/bridge/paths_test.go | 4 +- .../mesh-delivery-indexer/integration_test.go | 72 +-- .../cmd/mesh-delivery-indexer/main.go | 11 +- .../cmd/mesh-delivery-indexer/main_test.go | 11 +- .../cmd/mesh-delivery-indexer/named.go | 11 +- .../packages_integration_test.go | 15 +- .../cmd/mesh-delivery-indexer/verdicts.go | 9 +- modules/mesh-delivery-indexer/go.mod | 12 +- modules/mesh-delivery-indexer/go.sum | 4 +- .../index/candidate_manifests_test.go | 7 +- .../internal/index/candidates.go | 30 +- .../internal/index/control_records.go | 9 +- .../internal/index/control_records_test.go | 3 +- .../internal/index/index_test.go | 23 +- .../internal/index/manifests.go | 3 +- .../internal/index/manifests_test.go | 7 +- .../internal/index/modules.go | 17 +- .../internal/index/modules_test.go | 25 +- .../internal/index/moves.go | 17 +- .../internal/index/order.go | 3 +- .../internal/index/own.go | 3 +- .../internal/index/pending.go | 96 ++-- .../internal/index/pending_test.go | 48 +- .../internal/index/pending_tools.go | 4 +- .../internal/index/previews_test.go | 3 +- .../internal/index/registered.go | 5 +- .../internal/index/taken.go | 7 +- .../internal/index/toolchains_test.go | 5 +- .../internal/index/verdicts_test.go | 3 +- .../mesh-delivery-indexer/manifest_test.go | 3 +- modules/mesh-delivery-indexer/takein/go.mod | 5 + modules/mesh-delivery-indexer/takein/go.sum | 14 + .../mesh-delivery-indexer/takein/takein.go | 251 +++++++++++ .../takein/takein_test.go | 95 ++++ .../cmd/mesh-delivery-view-tools/main.go | 7 +- .../tiers_rows_test.go | 11 +- modules/mesh-delivery-view/go.mod | 9 +- modules/mesh-delivery-view/go.sum | 4 +- .../mesh-delivery-view/internal/view/read.go | 3 +- .../mesh-delivery-view/internal/view/tiers.go | 3 +- .../cmd/mesh-delivery-watchman/main.go | 5 +- modules/mesh-delivery-watchman/go.mod | 5 +- modules/mesh-delivery-watchman/go.sum | 6 +- .../internal/watch/findings_test.go | 43 +- .../internal/watch/takein.go | 10 +- 67 files changed, 1547 insertions(+), 425 deletions(-) create mode 100644 modules/mesh-delivery-forge-bridge/forgemsg/forge.go create mode 100644 modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go create mode 100644 modules/mesh-delivery-forge-bridge/forgemsg/go.mod create mode 100644 modules/mesh-delivery-forge-bridge/forgemsg/go.sum create mode 100644 modules/mesh-delivery-forge-bridge/forgemsg/group.go create mode 100644 modules/mesh-delivery-indexer/takein/go.mod create mode 100644 modules/mesh-delivery-indexer/takein/go.sum create mode 100644 modules/mesh-delivery-indexer/takein/takein.go create mode 100644 modules/mesh-delivery-indexer/takein/takein_test.go diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index bf775228..8c2c7fe6 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 7ff42872..46655fec 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go index 608685e4..fae4fb07 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/gate_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -120,8 +121,8 @@ func TestARefusedActIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1341/AC if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.refused" { t.Fatalf("published on %v", pub.subjects) } - m, err := delivery.DecodeForge(pub.data[0]) - if err != nil || m.Kind != delivery.ForgeRefused || m.Act != "comment.1" || m.Number != 12 || m.Head != headA || m.Words != "the pull request is locked" { + m, err := forgemsg.DecodeForge(pub.data[0]) + if err != nil || m.Kind != forgemsg.ForgeRefused || m.Act != "comment.1" || m.Number != 12 || m.Head != headA || m.Words != "the pull request is locked" { t.Errorf("message %+v err %v", m, err) } f.refuse = nil @@ -188,8 +189,8 @@ func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/ if len(f.calls) != 1 || len(pub.data) != 1 { t.Fatalf("calls %d messages %d, want one each", len(f.calls), len(pub.data)) } - m, err := delivery.DecodeForge(pub.data[0]) - if err != nil || m.Kind != delivery.ForgeRefused || m.Repository != "novox/mesh-delivery" || m.Number != 12 || m.Head != headA || + m, err := forgemsg.DecodeForge(pub.data[0]) + if err != nil || m.Kind != forgemsg.ForgeRefused || m.Repository != "novox/mesh-delivery" || m.Number != 12 || m.Head != headA || m.Act != "merge" || m.Words != "novox/mesh-delivery#12 is not merged: its head moved" { t.Errorf("message %+v err %v", m, err) } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go index 824a6a7b..f00c6688 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups.go @@ -10,6 +10,7 @@ import ( "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" groupkeys "mesh-delivery-forge-bridge/internal/groups" @@ -58,7 +59,7 @@ func kvMerges(kv jetstream.KeyValue) mergesRead { // groupOf is the gate's merge of the group a merged message's branch names, when the message's pull request is one of // its approved members and the group has more than one: a lone pull request is no group. -func (g *grouper) groupOf(ctx context.Context, f delivery.Forge) (delivery.Merge, uint64, bool) { +func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (delivery.Merge, uint64, bool) { if g == nil || g.merges == nil || f.Branch == "" { return delivery.Merge{}, 0, false } @@ -110,9 +111,9 @@ func membersOf(m delivery.Merge) []delivery.Head { // member sets the group and its approved members on a merged message's body (ADR 0633 §4); any other body, and a // merge of no group, is left as it is. -func (g *grouper) member(ctx context.Context, data []byte) ([]byte, delivery.Forge, bool) { - f, err := delivery.DecodeForge(data) - if err != nil || f.Kind != delivery.ForgeMerged { +func (g *grouper) member(ctx context.Context, data []byte) ([]byte, forgemsg.Forge, bool) { + f, err := forgemsg.DecodeForge(data) + if err != nil || f.Kind != forgemsg.ForgeMerged { return data, f, false } m, _, ok := g.groupOf(ctx, f) @@ -120,7 +121,7 @@ func (g *grouper) member(ctx context.Context, data []byte) ([]byte, delivery.For return data, f, false } f.Group, f.Members = m.Group, membersOf(m) - out, err := delivery.EncodeForge(f) + out, err := forgemsg.EncodeForge(f) if err != nil { g.logger.Printf("%s#%d: its merged message cannot name the group %q, written without: %v", f.Repository, f.Number, m.Group, err) return data, f, false @@ -129,7 +130,7 @@ func (g *grouper) member(ctx context.Context, data []byte) ([]byte, delivery.For } // published keeps that a member's merged message is written, and says the group's end when it was the last. -func (g *grouper) published(ctx context.Context, f delivery.Forge) { +func (g *grouper) published(ctx context.Context, f forgemsg.Forge) { if err := g.done.Put(ctx, groupkeys.PublishedKey(f.Repository, f.Number, f.Merge), 1); err != nil { g.logger.Printf("%s#%d: that its merged message is written is not kept, so the group's end waits for the next act: %v", f.Repository, f.Number, err) return @@ -163,7 +164,7 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, return false } } - end := delivery.ForgeGroupEnd{Group: m.Group} + end := forgemsg.ForgeGroupEnd{Group: m.Group} for _, member := range membersOf(m) { at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return delivery.SameRepository(p.Repository, member.Repository) && p.Number == member.Number @@ -174,14 +175,14 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, if _, ok, err := g.done.Get(ctx, groupkeys.PublishedKey(member.Repository, member.Number, m.Pulls[at].Commit)); err != nil || !ok { return false // its merged message is not written yet: said when it is (published) } - end.Members = append(end.Members, delivery.GroupMerge{Repository: member.Repository, Number: member.Number, Merge: m.Pulls[at].Commit}) + end.Members = append(end.Members, forgemsg.GroupMerge{Repository: member.Repository, Number: member.Number, Merge: m.Pulls[at].Commit}) } // The members in the order the gate merged them, so the last commit names the end. - slices.SortStableFunc(end.Members, func(a, b delivery.GroupMerge) int { + slices.SortStableFunc(end.Members, func(a, b forgemsg.GroupMerge) int { return slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == a.Merge }) - slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == b.Merge }) }) - data, err := delivery.EncodeForgeGroupEnd(end) + data, err := forgemsg.EncodeForgeGroupEnd(end) if err != nil { g.logger.Printf("the end of the group %q cannot be written: %v", m.Group, err) return false @@ -190,7 +191,7 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, if again != "" { id += "." + again } - if err := g.send(ctx, forgeSubject(delivery.ForgeGroupEndEvent), data, id); err != nil { + if err := g.send(ctx, forgeSubject(forgemsg.ForgeGroupEndEvent), data, id); err != nil { g.logger.Printf("the end of the group %q is not written on FORGE, written at the gate's next write or by bridge_group_end_again: %v", m.Group, err) return false } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go index 0f792c32..17c57b22 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/groups_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" groupkeys "mesh-delivery-forge-bridge/internal/groups" @@ -50,7 +51,7 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1 return v, 7, ok, nil }} mergedOf := func(repo string, number int, head, commit string) []byte { - data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/take-in", Number: number, Head: head, Merge: commit, Root: "r"}) + data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/take-in", Number: number, Head: head, Merge: commit, Root: "r"}) if err != nil { t.Fatal(err) } @@ -63,7 +64,7 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1 // The first member: its message names the group and both members; the group is not ended. gate["feat/take-in"] = merge(sdk(delivery.MergeMerged, c1, ""), dlv(delivery.MergeMerging, "")) data, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1)) - if back, err := delivery.DecodeForge(data); !ok || err != nil || back.Group != "feat/take-in" || len(back.Members) != 2 { + if back, err := forgemsg.DecodeForge(data); !ok || err != nil || back.Group != "feat/take-in" || len(back.Members) != 2 { t.Fatalf("a member's message: %+v %v", back, err) } g.published(ctx, f) @@ -82,7 +83,7 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1 if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.group.merged" || pub.ids[0] != "group-end.feat/take-in."+c2 { t.Fatalf("ends %v ids %v, want one forge.group.merged with id group-end.feat/take-in.%s", pub.subjects, pub.ids, c2) } - end, err := delivery.DecodeForgeGroupEnd(pub.data[0]) + end, err := forgemsg.DecodeForgeGroupEnd(pub.data[0]) if err != nil || len(end.Members) != 2 || end.Members[0].Merge != c1 || end.Members[1].Merge != c2 { t.Errorf("the end = %+v %v, want both members in their merge order", end, err) } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go index 84644d6f..b45ca285 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/integration_test.go @@ -20,6 +20,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -36,7 +37,7 @@ import ( // // The lists are what mesh-controller's broker.PermissionsFor answers (internal/broker/nats.go:577) for a module principal // with the manifest of module.json: consumes the three gitea.pull events, invokes gitea.gitea_open_pulls, -// gitea.gitea_commit_trees and gitea.gitea_get_file, state heads, and the stream forge; `**` is written `>` on the wire (modstreams.go +// gitea.gitea_commit_trees and gitea.gitea_get_file, state heads, performed, inputs and end-asks (ADR 0633 §5's hand end), and the stream forge; `**` is written `>` on the wire (modstreams.go // WirePattern). The consumer's durable is _ on EVENTS (nats.go consumerDurable). The controller's own // user keeps all rights, as it has. const busConf = ` @@ -83,6 +84,12 @@ accounts { "$JS.FC.KV_mesh-delivery-forge-bridge_inputs.>" "$KV.mesh-delivery-forge-bridge_performed.>" "$KV.mesh-delivery-forge-bridge_inputs.>" + "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-forge-bridge_end-asks.>" + "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-forge-bridge_end-asks.>" + "$JS.API.DIRECT.GET.KV_mesh-delivery-forge-bridge_end-asks.>" + "$JS.API.STREAM.INFO.KV_mesh-delivery-forge-bridge_end-asks" + "$JS.FC.KV_mesh-delivery-forge-bridge_end-asks.>" + "$KV.mesh-delivery-forge-bridge_end-asks.>" "$JS.API.CONSUMER.CREATE.KV_mesh-delivery-gate_merges.>" "$JS.API.CONSUMER.DELETE.KV_mesh-delivery-gate_merges.>" "$JS.API.DIRECT.GET.KV_mesh-delivery-gate_merges.>" @@ -321,13 +328,13 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2 if err != nil { t.Fatal(err) } - f, err := delivery.DecodeForge(raw.Data) + f, err := forgemsg.DecodeForge(raw.Data) if err != nil { t.Fatal(err) } - want := delivery.Forge{V: delivery.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, + want := forgemsg.Forge{V: delivery.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: "updated", Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}} - if !delivery.SameForge(f, want) { + if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } @@ -416,16 +423,16 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5 if err != nil { t.Fatal(err) } - f, err := delivery.DecodeForge(raw.Data) + f, err := forgemsg.DecodeForge(raw.Data) if err != nil { t.Fatal(err) } - want := delivery.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, + want := forgemsg.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: merge, Root: "r0", Trees: map[string]string{"": "t0", "modules/a": "t1"}, Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)}, // No mesh package: only the indexer computes what a fingerprint takes (ADR 0593 §2). Locks: map[string]map[string]string{"modules/a": {"go.sum": hex.EncodeToString(sha256Sum(sum))}}} - if !delivery.SameForge(f, want) { + if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } select { @@ -478,7 +485,7 @@ func TestBridgeSaysTheOpenPullsAtStart(t *testing.T) { // 1237/AC1 if err != nil { t.Fatal(err) } - if f, err := delivery.DecodeForge(raw.Data); err != nil || f.Action != "updated" || f.DirsKnown || f.Branch != "feat/x" { + if f, err := forgemsg.DecodeForge(raw.Data); err != nil || f.Action != "updated" || f.DirsKnown || f.Branch != "feat/x" { t.Errorf("%s: %+v err %v", repo, f, err) } } @@ -548,20 +555,20 @@ func TestBridgeReadsTheForgeAtStartAndAfterAReconnectAndHearsNoOlderEvent(t *tes t.Fatalf("FORGE holds %d messages (%v), want %d", info.State.Msgs, err, want) } } - named := func(seq uint64) []delivery.OpenPull { + named := func(seq uint64) []forgemsg.OpenPull { t.Helper() raw, err := forge.GetMsg(b.ctx, seq) if err != nil || raw.Subject != openSubject { t.Fatalf("message %d: %v %v, want the open message", seq, raw, err) } - f, err := delivery.DecodeForge(raw.Data) + f, err := forgemsg.DecodeForge(raw.Data) if err != nil { t.Fatal(err) } return f.Open } - bothOpen := []delivery.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}} - same := func(got []delivery.OpenPull) bool { + bothOpen := []forgemsg.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}} + same := func(got []forgemsg.OpenPull) bool { return len(got) == 2 && got[0] == bothOpen[0] && got[1] == bothOpen[1] } @@ -713,7 +720,7 @@ func forgeMerged(t *testing.T, b *bed) []string { if err != nil { continue } - if f, err := delivery.DecodeForge(raw.Data); err == nil && f.Kind == delivery.ForgeMerged { + if f, err := forgemsg.DecodeForge(raw.Data); err == nil && f.Kind == forgemsg.ForgeMerged { out = append(out, f.Repository) } } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go index 26331490..25e4b970 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main.go @@ -21,6 +21,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-forge-bridge/internal/bridge" @@ -95,7 +96,7 @@ func handleGrouped(ctx context.Context, m delivered, pub publisher, trees bridge if id := m.Headers().Get("x-event-id"); id != "" { opts = append(opts, jetstream.WithMsgID(id)) } - var member delivery.Forge + var member forgemsg.Forge grouped := false if g != nil { msg.Data, member, grouped = g.member(ctx, msg.Data) diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main_test.go index 6234a90d..ede623fd 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/main_test.go @@ -18,7 +18,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/modules/mesh-delivery-forge-bridge/forgemsg" "mesh-delivery-forge-bridge/internal/bridge" ) @@ -85,7 +85,7 @@ func TestReadableEventIsWrittenOnForgeAndAcked(t *testing.T) { // 1235/AC2 (unit if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull" { t.Fatalf("published on %v", pub.subjects) } - f, err := delivery.DecodeForge(pub.data[0]) + f, err := forgemsg.DecodeForge(pub.data[0]) if err != nil || f.Number != 12 || f.Branch != "feat/x" || f.Head != head || f.Action != "updated" || !f.DirsKnown { t.Errorf("message %+v err %v", f, err) } @@ -172,7 +172,7 @@ func TestMergeCallsTheToolOnceAndWritesTheMergedMessage(t *testing.T) { // 1236/ if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.merged" { t.Fatalf("published on %v", pub.subjects) } - f, err := delivery.DecodeForge(pub.data[0]) + f, err := forgemsg.DecodeForge(pub.data[0]) if err != nil || f.Root != "r0" || f.Trees[""] != "t0" || f.Trees["modules/a"] != "t1" || f.Merge == "" { t.Errorf("message %+v err %v", f, err) } diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open.go index f09913f9..a93543dd 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open.go @@ -10,7 +10,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" "mesh-delivery-forge-bridge/internal/bridge" ) @@ -41,7 +41,7 @@ func (r *recorder) Publish(ctx context.Context, subject string, payload []byte, if err != nil { return ack, err } - f, derr := delivery.DecodeForge(payload) + f, derr := forgemsg.DecodeForge(payload) if derr != nil { return ack, nil } @@ -101,7 +101,7 @@ func syncOpen(ctx context.Context, call caller, pub publisher, heads headStore, } var differing []bridge.Message for _, m := range msgs { - f, err := delivery.DecodeForge(m.Data) + f, err := forgemsg.DecodeForge(m.Data) if err != nil { differing = append(differing, m) continue diff --git a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open_test.go b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open_test.go index c1009b3b..a9caade0 100644 --- a/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open_test.go +++ b/modules/mesh-delivery-forge-bridge/cmd/mesh-delivery-forge-bridge/open_test.go @@ -13,7 +13,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" ) // openAnswer is gitea_open_pulls's answer as mesh-catalog modules/gitea/client.ts:369-381 builds its entries inside {pulls}. @@ -43,7 +43,7 @@ func TestSyncOpenWritesOneUpdatedMessagePerOpenPull(t *testing.T) { // 1237/AC1, } want := []string{"mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull", "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-catalog.pull"} for i, w := range want { - f, err := delivery.DecodeForge(pub.data[i]) + f, err := forgemsg.DecodeForge(pub.data[i]) if pub.subjects[i] != w || err != nil || f.Action != "updated" || f.DirsKnown || f.Branch != "feat/x" { t.Errorf("message %d on %s = %+v err %v", i, pub.subjects[i], f, err) } @@ -112,8 +112,8 @@ func TestSyncOpenWritesPullsForNewHeadsThenOneOpenMessageNamingAll(t *testing.T) if pub.subjects[1] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.open" { t.Fatalf("last message on %s, want the open message after the pull messages", pub.subjects[1]) } - f, err := delivery.DecodeForge(pub.data[1]) - want := []delivery.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}} + f, err := forgemsg.DecodeForge(pub.data[1]) + want := []forgemsg.OpenPull{{Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}} if err != nil || len(f.Open) != 2 || f.Open[0] != want[0] || f.Open[1] != want[1] { t.Errorf("open message = %+v err %v, want it to name %v, the pull request whose head did not differ among them", f, err, want) } @@ -140,10 +140,10 @@ func TestWhatIsWrittenOnFORGEIsKeptInTheBucket(t *testing.T) { // 1279/AC6 pub := &fakePub{} heads := newHeads() r := &recorder{pub: pub, heads: heads, logger: log.New(&bytes.Buffer{}, "", 0)} - write := func(f delivery.Forge) { + write := func(f forgemsg.Forge) { t.Helper() - ev, _ := delivery.ForgeEvent(f.Repository, f.Kind) - raw, err := delivery.EncodeForge(f) + ev, _ := forgemsg.ForgeEvent(f.Repository, f.Kind) + raw, err := forgemsg.EncodeForge(f) if err != nil { t.Fatal(err) } @@ -151,19 +151,19 @@ func TestWhatIsWrittenOnFORGEIsKeptInTheBucket(t *testing.T) { // 1279/AC6 t.Fatal(err) } } - p := delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullUpdated} + p := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated} write(p) if heads.get("novox.mesh-delivery.12") != head { t.Fatalf("bucket %v", heads.vals) } - p.Action = delivery.PullClosed + p.Action = forgemsg.PullClosed write(p) if _, kept := heads.vals["novox.mesh-delivery.12"]; kept { t.Errorf("a closed pull request is still kept: %v", heads.vals) } - p.Action = delivery.PullUpdated + p.Action = forgemsg.PullUpdated write(p) - write(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}}) + write(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}}) if _, kept := heads.vals["novox.mesh-delivery.12"]; kept { t.Errorf("a merged pull request is still kept: %v", heads.vals) } diff --git a/modules/mesh-delivery-forge-bridge/forgemsg/forge.go b/modules/mesh-delivery-forge-bridge/forgemsg/forge.go new file mode 100644 index 00000000..16f632dd --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/forgemsg/forge.go @@ -0,0 +1,413 @@ +// Package forgemsg is the forge bridge's contract on its own stream `forge` (novox/hq ADR 0633 §4): its messages (Forge, +// one per pull request, merge, refusal or list of open pull requests) and a delivery group's end (ForgeGroupEnd). The +// forge bridge writes them; the indexer and the gate read them. It is the forge bridge's own package: one module owns +// these types, so they live with it and not in mesh-sdk (the operator, 2026-11-04). +package forgemsg + +import ( + "encoding/json" + "fmt" + "reflect" + "regexp" + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// Version is the version of every value this package writes and the only one it reads: mesh-sdk's delivery values'. +const Version = delivery.Version + +// ForgeStream is the forge bridge's stream as its manifest declares it; the broker derives the real names. +const ForgeStream = "forge" + +// The kinds of forge message, the last part of a forge event's name. +const ( + ForgePull = "pull" + ForgeMerged = "merged" + ForgeRefused = "refused" + ForgeOpen = "open" +) + +// OpenPull names an open pull request into the trunk: the repository as `/` and the number. +type OpenPull struct { + Repository string `json:"repository"` + Number int `json:"number"` +} + +// OpenEvent is the name of an open message: `forge.open`. It names no repository: it says every open pull request of +// every repository the forge bridge reads (novox/hq ADR 0443 §5). +func OpenEvent() string { return "forge." + ForgeOpen } + +// The actions of a pull message. +const ( + PullOpened = "opened" + PullUpdated = "updated" + PullClosed = "closed" +) + +// checkRepository refuses a repository that is not `/`: exactly one slash, each part a valid name. +func checkRepository(repository string) error { + owner, name, ok := strings.Cut(repository, "/") + if !ok || strings.Contains(name, "/") { + return fmt.Errorf("delivery: the repository %q is not /, with exactly one slash", repository) + } + if err := checkName("repository owner", owner); err != nil { + return fmt.Errorf("delivery: the repository %q: %w", repository, err) + } + if err := checkName("repository name", name); err != nil { + return fmt.Errorf("delivery: the repository %q: %w", repository, err) + } + if strings.Contains(owner, "/") { + return fmt.Errorf("delivery: the repository %q is not /, with exactly one slash", repository) + } + return nil +} + +func checkForgeKind(kind string) error { + switch kind { + case ForgePull, ForgeMerged, ForgeRefused, ForgeOpen: + return nil + } + return fmt.Errorf("delivery: the forge kind %q is not pull, merged, refused or open", kind) +} + +// ForgeEvent is the name of a forge message of the given kind: `forge...`. +func ForgeEvent(repository, kind string) (string, error) { + if err := checkForgeKind(kind); err != nil { + return "", err + } + if kind == ForgeOpen { + return "", fmt.Errorf("delivery: an open message names no repository, its event is %s", OpenEvent()) + } + if err := checkRepository(repository); err != nil { + return "", err + } + owner, name, _ := strings.Cut(repository, "/") + return "forge." + owner + "." + name + "." + kind, nil +} + +// ParseForgeEvent is the inverse of ForgeEvent. +func ParseForgeEvent(event string) (repository, kind string, err error) { + if event == OpenEvent() { + return "", ForgeOpen, nil + } + parts := strings.Split(event, ".") + if len(parts) != 4 || parts[0] != "forge" { + return "", "", fmt.Errorf("delivery: %q is not a forge event, forge...", event) + } + repository = parts[1] + "/" + parts[2] + if _, err := ForgeEvent(repository, parts[3]); err != nil { + return "", "", fmt.Errorf("delivery: %q is not a forge event: %w", event, err) + } + return repository, parts[3], nil +} + +// Forge is a message of the forge bridge's stream, trimmed to repository, branch, pull request and head. A pull +// message says Action; a merged message carries the merge commit, the tree of every directory holding a +// module.json and the repository's root tree; a refused message names the owed act it answers and the forge's own +// words. A pull message may also carry the module directories its head changes (Dirs, in the order the forge said) and +// DirsKnown, true when the forge could say them: known and empty is DirsKnown with no Dirs, not known is neither +// (the forge's module_dirs and module_dirs_said, mesh-catalog modules/gitea/trunk.ts). Other messages carry neither. +type Forge struct { + V int `json:"v"` + Kind string `json:"kind"` + Repository string `json:"repository"` + Branch string `json:"branch,omitempty"` + Number int `json:"number,omitempty"` + Head string `json:"head,omitempty"` + Action string `json:"action,omitempty"` + Dirs []string `json:"dirs,omitempty"` + DirsKnown bool `json:"dirs-known,omitempty"` + Merge string `json:"merge,omitempty"` + Trees map[string]string `json:"trees,omitempty"` + Root string `json:"root,omitempty"` + // Manifests are the module.json bytes, exact, of each module directory: of the merge's trees on a merged message, + // of Dirs at the head on a pull message. Locks are, of each such directory, its lock files' hex sha256 by path + // relative to it (novox/hq ADR 0465 clause 1). Older writers send neither. + Manifests map[string]string `json:"manifests,omitempty"` + Locks map[string]map[string]string `json:"locks,omitempty"` + // Paths are, of each module directory whose manifest names paths on every artifact, each named path's git object + // id at the same commit as its manifest, by path relative to the directory (novox/hq ADR 0499 clause 3). The + // indexer names the paths; a message without them gives such a module none. Older writers send none. + Paths map[string]map[string]string `json:"paths,omitempty"` + // Packages are, of each module directory, the mesh packages its lock file names (MeshPackagesOf; novox/hq ADR 0573 + // §2): the indexer keeps them in the module's inputs and takes each one's latest published version into its + // fingerprint. Absent for a directory that uses none, and from an older writer. + Packages map[string][]string `json:"packages,omitempty"` + Act string `json:"act,omitempty"` + Words string `json:"words,omitempty"` + Open []OpenPull `json:"open,omitempty"` + // RetiredPlacements is the placement files a bridge before ADR 0567 carried (ADR 0549 §2, ADR 0559 §2): placement is + // the seat mesh-placement's state now and no repository holds it (ADR 0567 §11), so a message carrying them is + // refused, loudly, rather than read with them dropped. + RetiredPlacements json.RawMessage `json:"placements,omitempty"` + RetiredRead bool `json:"placements-read,omitempty"` + // Predefined is the catalogue's predefined.json, exact, at the merge commit of a merged message (novox/hq ADR 0565 + // §2): what the indexer keeps as its state `predefined`. PredefinedRead says the bridge read the merge and it holds + // no predefined.json. Carried on a merged message only; an older bridge sends neither, and the indexer keeps what it + // held. + Predefined json.RawMessage `json:"predefined,omitempty"` + PredefinedRead bool `json:"predefined-read,omitempty"` + // Group is the delivery group a merged message's pull request was merged in, and Members every approved member of + // that group with its approved head, this one included (novox/hq ADR 0633 §4): the forge bridge sets both from the + // gate's merges state, and the indexer gathers the group's merges until the group's end (ForgeGroupEnd). A merge + // with no group carries neither, and an older bridge sends neither; an older reader ignores both. + Group string `json:"group,omitempty"` + Members []delivery.Head `json:"members,omitempty"` +} + +// checkGroup refuses a group on anything but a merged message, a group without its members or members without a +// group, a member that is not a repository's pull request at a head, a member named twice, and members that leave out +// the message's own pull request. +func (f Forge) checkGroup() error { + if f.Group == "" && len(f.Members) == 0 { + return nil + } + if f.Kind != ForgeMerged { + return fmt.Errorf("delivery: only a merged message names its delivery group, not a %s message", f.Kind) + } + if f.Group == "" || len(f.Members) == 0 { + return fmt.Errorf("delivery: a merged message names its group and the group's members together, or neither") + } + self := false + seen := map[string]bool{} + for _, m := range f.Members { + if err := checkRepository(m.Repository); err != nil { + return err + } + if m.Number <= 0 || m.Head == "" { + return fmt.Errorf("delivery: the member %s of the group %q needs its pull request number and head", m.Repository, f.Group) + } + k := fmt.Sprintf("%s#%d", m.Repository, m.Number) + if seen[k] { + return fmt.Errorf("delivery: the group %q names its member %s twice", f.Group, k) + } + seen[k] = true + self = self || (delivery.SameRepository(m.Repository, f.Repository) && m.Number == f.Number) + } + if !self { + return fmt.Errorf("delivery: the members of the group %q leave out the merged pull request %s#%d", f.Group, f.Repository, f.Number) + } + return nil +} + +// checkPredefined refuses the predefined list on anything but a merged message, a list that is not one JSON object, and +// a list said both present and absent. +func (f Forge) checkPredefined() error { + if len(f.Predefined) == 0 && !f.PredefinedRead { + return nil + } + if f.Kind != ForgeMerged { + return fmt.Errorf("delivery: only a merged message carries the predefined list, not a %s message", f.Kind) + } + if len(f.Predefined) > 0 && f.PredefinedRead { + return fmt.Errorf("delivery: a merged message carries the predefined list and says the merge holds none (predefined-read)") + } + var list map[string]json.RawMessage + if len(f.Predefined) > 0 && json.Unmarshal(f.Predefined, &list) != nil { + return fmt.Errorf("delivery: the predefined list a merged message carries is not one JSON object") + } + return nil +} + +func (f Forge) check() error { + if err := checkForgeKind(f.Kind); err != nil { + return err + } + if f.Kind == ForgeOpen { + return f.checkOpen() + } + if len(f.Open) > 0 { + return fmt.Errorf("delivery: only an open message names open pull requests") + } + if len(f.RetiredPlacements) > 0 || f.RetiredRead { + return fmt.Errorf("delivery: a forge message carries placement files, which no repository holds since ADR 0567: " + + "placement is the seat mesh-placement's state") + } + if err := f.checkPredefined(); err != nil { + return err + } + if err := f.checkGroup(); err != nil { + return err + } + if err := checkRepository(f.Repository); err != nil { + return err + } + switch f.Kind { + case ForgePull: + if f.Number <= 0 { + return fmt.Errorf("delivery: a pull message needs its pull request number") + } + if f.Head == "" { + return fmt.Errorf("delivery: a pull message needs its head") + } + switch f.Action { + case PullOpened, PullUpdated, PullClosed: + default: + return fmt.Errorf("delivery: the pull action %q is not opened, updated or closed", f.Action) + } + if len(f.Dirs) > 0 && !f.DirsKnown { + return fmt.Errorf("delivery: a pull message with module directories must say it knows them (dirs-known)") + } + dirs := map[string]bool{} + for _, d := range f.Dirs { + dirs[d] = true + } + if err := f.checkInputs(func(d string) bool { return dirs[d] }, "its module directories"); err != nil { + return err + } + case ForgeMerged: + if len(f.Dirs) > 0 || f.DirsKnown { + return fmt.Errorf("delivery: a merged message carries no module directories") + } + if f.Number <= 0 { + return fmt.Errorf("delivery: a merged message needs its pull request number") + } + if f.Head == "" { + return fmt.Errorf("delivery: a merged message needs its head") + } + if f.Merge == "" { + return fmt.Errorf("delivery: a merged message needs its merge commit") + } + if f.Root == "" { + return fmt.Errorf("delivery: a merged message needs the repository's root tree") + } + // No trees is a merge that leaves the repository with no module.json anywhere (a documents repository, or one + // whose last module was removed): it is written all the same, so the indexer takes the merge in and drops the + // repository's modules (novox/hq issue 1126). An older writer always sent at least one tree, so none is unambiguous; + // such a message carries no manifests, locks or paths (checkInputs below: each names a directory of its trees). + for dir, tree := range f.Trees { + if tree == "" { + return fmt.Errorf("delivery: a merged message's tree of the directory %q is empty", dir) + } + } + if err := f.checkInputs(func(d string) bool { _, ok := f.Trees[d]; return ok }, "its trees"); err != nil { + return err + } + case ForgeRefused: + if len(f.Dirs) > 0 || f.DirsKnown { + return fmt.Errorf("delivery: a refused message carries no module directories") + } + if len(f.Manifests) > 0 || len(f.Locks) > 0 || len(f.Paths) > 0 { + return fmt.Errorf("delivery: a refused message carries no manifests, lock hashes or named paths") + } + if f.Act == "" { + return fmt.Errorf("delivery: a refused message needs the act it answers") + } + if f.Words == "" { + return fmt.Errorf("delivery: a refused message needs the forge's words") + } + } + return nil +} + +// checkInputs refuses a manifest or a lock hash of a directory that known does not know (of is what it knows them +// by), an empty manifest and an empty lock path or hash, naming the directory. +func (f Forge) checkInputs(known func(dir string) bool, of string) error { + for dir, m := range f.Manifests { + if !known(dir) { + return fmt.Errorf("delivery: the manifest of the directory %q is not of %s", dir, of) + } + if m == "" { + return fmt.Errorf("delivery: the manifest of the directory %q is empty", dir) + } + } + for dir, locks := range f.Locks { + if !known(dir) { + return fmt.Errorf("delivery: the lock hashes of the directory %q are not of %s", dir, of) + } + for path, hash := range locks { + if path == "" || hash == "" { + return fmt.Errorf("delivery: the lock %q of the directory %q has an empty path or hash", path, dir) + } + } + } + for dir, paths := range f.Paths { + if !known(dir) { + return fmt.Errorf("delivery: the named paths of the directory %q are not of %s", dir, of) + } + for path, object := range paths { + if path == "" || object == "" { + return fmt.Errorf("delivery: the named path %q of the directory %q has an empty path or object", path, dir) + } + } + } + return nil +} + +// checkOpen refuses an open message that names a repository, a branch, a head or anything of the other kinds, or +// an open pull request that is not `/` and a number. +func (f Forge) checkOpen() error { + if f.Repository != "" || f.Branch != "" || f.Number != 0 || f.Head != "" || f.Action != "" || len(f.Dirs) > 0 || f.DirsKnown || + f.Merge != "" || len(f.Trees) > 0 || len(f.Manifests) > 0 || len(f.Locks) > 0 || len(f.Paths) > 0 || f.Root != "" || f.Act != "" || f.Words != "" || f.Group != "" || len(f.Members) > 0 { + return fmt.Errorf("delivery: an open message carries only the open pull requests it names") + } + for _, p := range f.Open { + if err := checkRepository(p.Repository); err != nil { + return err + } + if p.Number <= 0 { + return fmt.Errorf("delivery: the open pull request of %s has no number", p.Repository) + } + } + return nil +} + +// EncodeForge is f as JSON at Version; it refuses a message missing what its kind needs. +func EncodeForge(f Forge) ([]byte, error) { + if err := f.check(); err != nil { + return nil, err + } + f.V = Version + return json.Marshal(f) +} + +// DecodeForge reads a forge message; it refuses an unknown version (ErrUnknownVersion) and what EncodeForge refuses. +func DecodeForge(data []byte) (Forge, error) { + var f Forge + if err := decode(data, &f); err != nil { + return Forge{}, err + } + if err := f.check(); err != nil { + return Forge{}, err + } + return f, nil +} + +// SameForge says whether two forge messages are the same value. +func SameForge(a, b Forge) bool { return reflect.DeepEqual(a, b) } + +var nameOK = regexp.MustCompile(`^[-/_=a-zA-Z0-9]+$`) + +// checkName refuses a name that is empty or holds a dot, a wildcard, whitespace or a character a bus key refuses +// (mesh-sdk delivery's rule for a name). +func checkName(kind, name string) error { + if name == "" { + return fmt.Errorf("delivery: the %s name is empty", kind) + } + if !nameOK.MatchString(name) { + return fmt.Errorf("delivery: the %s name %q holds a dot, a wildcard, whitespace or a character a key refuses", kind, name) + } + return nil +} + +// decode reads a value of Version, refusing another or none with delivery.ErrUnknownVersion, as mesh-sdk delivery's +// values are read. +func decode(data []byte, into any) error { + var probe struct { + V *int `json:"v"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return fmt.Errorf("delivery: value is not JSON: %w", err) + } + if probe.V == nil { + return fmt.Errorf("%w: no version in the value, this reader reads %d", delivery.ErrUnknownVersion, Version) + } + if *probe.V != Version { + return fmt.Errorf("%w: the value is version %d, this reader reads %d", delivery.ErrUnknownVersion, *probe.V, Version) + } + if err := json.Unmarshal(data, into); err != nil { + return fmt.Errorf("delivery: value does not fit its shape: %w", err) + } + return nil +} diff --git a/modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go b/modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go new file mode 100644 index 00000000..1ee2382a --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/forgemsg/forge_test.go @@ -0,0 +1,193 @@ +package forgemsg + +import ( + "encoding/json" + "errors" + "strings" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +const oddManifest = "{\n\t\"module\": \"a\",\n \"z\": 1, \"a\": [ \"x\",\"y\" ],\n \"why\": \"é — & \\n\"\n}\n\n" + +func sampleForges() []Forge { + return []Forge{ + {Kind: ForgePull, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 36, Head: "aaaa", Action: "opened"}, + {Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Merge: "cccc", + Trees: map[string]string{"modules/a": "t1"}, Root: "t0"}, + {Kind: ForgeRefused, Repository: "novox/mesh-sdk", Act: "novox/mesh-sdk.36.aaaa.view", Words: "403 forbidden"}, + {Kind: ForgeOpen, Open: []OpenPull{{Repository: "novox/mesh-sdk", Number: 36}, {Repository: "novox/mesh-control", Number: 7}}}, + // 1585/T1: a group member's merged message names its group and every approved member (ADR 0633 §4). + {Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 37, Head: "dddd", Merge: "eeee", Root: "t0", + Group: "feat/take-in", Members: []delivery.Head{{Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}, {Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}}}, + } +} + +func mergedWithInputs() Forge { + return Forge{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa", Merge: "cccc", Root: "t0", + Trees: map[string]string{"a": "t1", "b": "t2"}, + Manifests: map[string]string{"a": oddManifest}, + Locks: map[string]map[string]string{"a": {"go.sum": strings.Repeat("ab", 32)}}} +} + +// TestForge holds the forge bridge's contract, moved whole from mesh-sdk go/delivery with its tests (1200/AC1, AC3, +// AC4; 1279/AC5; 1442/AC1-AC4; 1585/T1). +func TestForge(t *testing.T) { + t.Run("events", func(t *testing.T) { + ev, err := ForgeEvent("novox/mesh-sdk", ForgeMerged) + if err != nil || ev != "forge.novox.mesh-sdk.merged" { + t.Fatalf("forge event = %q, %v", ev, err) + } + if repo, kind, err := ParseForgeEvent(ev); err != nil || repo != "novox/mesh-sdk" || kind != ForgeMerged { + t.Fatalf("parse forge event = %q %q %v", repo, kind, err) + } + for _, bad := range []struct{ repo, kind string }{ + {"", "pull"}, {"novox", "pull"}, {"a/b/c", "pull"}, {"/b", "pull"}, {"a/", "pull"}, + {"a.b/c", "pull"}, {"a/b c", "pull"}, {"a/b", "closed"}, {"a/b", ""}, {"a/b", ForgeOpen}, + } { + if _, err := ForgeEvent(bad.repo, bad.kind); err == nil { + t.Errorf("forge event %q %q accepted", bad.repo, bad.kind) + } + } + for _, bad := range []string{"", "forge.a.b", "forge.a.b.closed", "x.a.b.pull", "forge.a.b.pull.x", "forge..b.pull", ForgeGroupEndEvent} { + if _, _, err := ParseForgeEvent(bad); err == nil { + t.Errorf("parse forge event %q accepted", bad) + } + } + if repo, kind, err := ParseForgeEvent(OpenEvent()); OpenEvent() != "forge.open" || err != nil || repo != "" || kind != ForgeOpen { + t.Errorf("open event %q = %q %q %v", OpenEvent(), repo, kind, err) + } + }) + t.Run("round trip", func(t *testing.T) { + fs := append(sampleForges(), mergedWithInputs()) + for _, f := range fs { + data, err := EncodeForge(f) + if err != nil { + t.Fatalf("%s: %v", f.Kind, err) + } + var probe map[string]any + if err := json.Unmarshal(data, &probe); err != nil || probe["v"] != float64(1) { + t.Fatalf("%s: json %s does not carry v:1", f.Kind, data) + } + f.V = Version + if got, err := DecodeForge(data); err != nil || !SameForge(got, f) { + t.Errorf("forge %s round trip: %+v %v", f.Kind, got, err) + } + } + // An empty list is an open message too: nothing is open. + if data, err := EncodeForge(Forge{Kind: ForgeOpen, Open: []OpenPull{}}); err != nil { + t.Errorf("empty open: %v", err) + } else if f, err := DecodeForge(data); err != nil || f.Kind != ForgeOpen || len(f.Open) != 0 { + t.Errorf("empty open round trip: %+v %v", f, err) + } + ge := ForgeGroupEnd{Group: "feat/take-in", Members: []GroupMerge{{Repository: "novox/mesh-sdk", Number: 37, Merge: "eeee"}}} + data, err := EncodeForgeGroupEnd(ge) + ge.V = Version + if got, err2 := DecodeForgeGroupEnd(data); err != nil || err2 != nil || !SameForgeGroupEnd(got, ge) { + t.Errorf("group end round trip: %+v %v %v", got, err, err2) + } + // An older bridge's message reads, and is written back byte for byte. + old := `{"v":1,"kind":"merged","repository":"novox/mesh-sdk","number":36,"head":"aaaa","merge":"cccc","trees":{"a":"t1"},"root":"t0"}` + if f, err := DecodeForge([]byte(old)); err != nil || f.Manifests != nil { + t.Fatalf("%+v, %v", f, err) + } else if data, err := EncodeForge(f); err != nil || string(data) != old { + t.Errorf("%s, %v, want exactly %s", data, err, old) + } + // A merge leaving the repository with no module.json is a message too (issue 1126). + f := sampleForges()[1] + f.Trees = nil + if data, err := EncodeForge(f); err != nil { + t.Errorf("a merged message with no trees is refused: %v", err) + } else if got, err := DecodeForge(data); err != nil || len(got.Trees) != 0 { + t.Errorf("a merged message with no trees comes back as %+v, %v", got, err) + } + for name, dec := range map[string]func([]byte) error{ + "forge": func(b []byte) error { _, err := DecodeForge(b); return err }, + "group end": func(b []byte) error { _, err := DecodeForgeGroupEnd(b); return err }, + } { + for _, in := range []string{`{"group":"g"}`, `{"v":2,"group":"g"}`} { + if err := dec([]byte(in)); !errors.Is(err, delivery.ErrUnknownVersion) { + t.Errorf("%s decode of %s = %v, want delivery.ErrUnknownVersion", name, in, err) + } + } + } + }) + t.Run("refused", func(t *testing.T) { + at := func(i int, mut func(*Forge)) Forge { f := sampleForges()[i]; mut(&f); return f } + in := func(mut func(*Forge)) Forge { f := mergedWithInputs(); mut(&f); return f } + pullIn := func(mut func(*Forge)) Forge { + f := Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 1, Head: "h", Action: PullOpened, + Dirs: []string{"a"}, DirsKnown: true, Manifests: map[string]string{"a": oddManifest}, + Locks: map[string]map[string]string{"a": {"go.sum": strings.Repeat("0", 64)}}} + mut(&f) + return f + } + for _, c := range []struct { + name, want string + f Forge + }{ + {"merged without merge commit", "merge", at(1, func(f *Forge) { f.Merge = "" })}, + {"merged without root", "root", at(1, func(f *Forge) { f.Root = "" })}, + {"merged with a manifest and no trees", "modules/a", at(1, func(f *Forge) { f.Trees = nil; f.Manifests = map[string]string{"modules/a": "{}"} })}, + {"merged with an empty tree", "modules/a", at(1, func(f *Forge) { f.Trees = map[string]string{"modules/a": ""} })}, + {"pull with an unknown action", "reopened", at(0, func(f *Forge) { f.Action = "reopened" })}, + {"pull without head", "head", at(0, func(f *Forge) { f.Head = "" })}, + {"refused without words", "words", at(2, func(f *Forge) { f.Words = "" })}, + {"refused without act", "act", at(2, func(f *Forge) { f.Act = "" })}, + {"unknown kind", "closed", Forge{Kind: "closed", Repository: "a/b"}}, + {"bad repository", "nope", Forge{Kind: ForgePull, Repository: "nope", Number: 1, Head: "h", Action: "opened"}}, + {"open with a repository", "only the open", Forge{Kind: ForgeOpen, Repository: "a/b", Open: []OpenPull{{Repository: "a/b", Number: 1}}}}, + {"open with a bad entry", "", Forge{Kind: ForgeOpen, Open: []OpenPull{{Repository: "a", Number: 1}}}}, + {"open with a zero number", "no number", Forge{Kind: ForgeOpen, Open: []OpenPull{{Repository: "a/b"}}}}, + {"pull naming open pull requests", "only an open message", Forge{Kind: ForgePull, Repository: "a/b", Number: 1, Head: "h", Action: "updated", Open: []OpenPull{{Repository: "a/b", Number: 1}}}}, + {"manifest outside trees", `"zz"`, in(func(f *Forge) { f.Manifests["zz"] = "{}" })}, + {"locks outside trees", `"yy"`, in(func(f *Forge) { f.Locks["yy"] = map[string]string{"go.sum": "h"} })}, + {"empty manifest", `"b"`, in(func(f *Forge) { f.Manifests["b"] = "" })}, + {"empty lock hash", `"a"`, in(func(f *Forge) { f.Locks["a"]["go.sum"] = "" })}, + {"named paths outside trees", `"other"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"other": {"src": "o"}} })}, + {"a named path with no object", `"src"`, in(func(f *Forge) { f.Paths = map[string]map[string]string{"a": {"src": ""}} })}, + {"placement files", "mesh-placement", in(func(f *Forge) { f.RetiredPlacements = []byte(`{"anchor":"{}"}`) })}, + {"placements read", "mesh-placement", in(func(f *Forge) { f.RetiredRead = true })}, + {"predefined not an object", "one JSON object", in(func(f *Forge) { f.Predefined = []byte(`["domain"]`) })}, + {"predefined both", "says the merge holds none", in(func(f *Forge) { f.Predefined = []byte(`{}`); f.PredefinedRead = true })}, + {"pull with the predefined list", "only a merged message", Forge{Kind: ForgePull, Repository: "novox/mesh-sdk", Number: 1, Head: "h", Action: PullOpened, PredefinedRead: true}}, + {"pull manifest outside dirs", `"b"`, pullIn(func(f *Forge) { f.Manifests["b"] = "{}" })}, + {"pull locks outside dirs", `"c"`, pullIn(func(f *Forge) { f.Locks["c"] = map[string]string{"go.sum": "h"} })}, + {"refused with manifests", "no manifests", Forge{Kind: ForgeRefused, Repository: "a/b", Act: "x", Words: "no", Manifests: map[string]string{"a": "{}"}}}, + {"open with locks", "only the open", Forge{Kind: ForgeOpen, Locks: map[string]map[string]string{"a": {"go.sum": "h"}}}}, + // 1585/T1: a group only on a merged message, with its members, the message's own pull request among them. + {"pull with a group", "only a merged message names its delivery group", at(0, func(f *Forge) { + f.Group, f.Members = "g", []delivery.Head{{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}} + })}, + {"merged with a group and no members", "together", at(4, func(f *Forge) { f.Members = nil })}, + {"merged with members and no group", "together", at(4, func(f *Forge) { f.Group = "" })}, + {"merged with members leaving itself out", "leave out", at(4, func(f *Forge) { f.Members = f.Members[1:] })}, + {"merged with a member twice", "twice", at(4, func(f *Forge) { f.Members = append(f.Members, f.Members[0]) })}, + {"open with a group", "only the open pull requests", Forge{Kind: ForgeOpen, Group: "g"}}, + } { + if _, err := EncodeForge(c.f); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.want) + } + } + // A member's repository in another case still names the merged pull request (10c91fba). + f := sampleForges()[4] + f.Members[0].Repository = "Novox/Mesh-SDK" + if _, err := EncodeForge(f); err != nil { + t.Errorf("a member in another case: %v", err) + } + for _, c := range []struct { + name, want string + e ForgeGroupEnd + }{ + {"group end without members", "no member", ForgeGroupEnd{Group: "g"}}, + {"group end without a group", "names its group", ForgeGroupEnd{Members: []GroupMerge{{Repository: "a/b", Number: 1, Merge: "m"}}}}, + {"group end with a member without its merge", "merge commit", ForgeGroupEnd{Group: "g", Members: []GroupMerge{{Repository: "a/b", Number: 1}}}}, + {"group end with a member twice", "twice", ForgeGroupEnd{Group: "g", Members: []GroupMerge{{Repository: "a/b", Number: 1, Merge: "m"}, {Repository: "a/b", Number: 1, Merge: "m"}}}}, + } { + if _, err := EncodeForgeGroupEnd(c.e); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.want) + } + } + }) +} diff --git a/modules/mesh-delivery-forge-bridge/forgemsg/go.mod b/modules/mesh-delivery-forge-bridge/forgemsg/go.mod new file mode 100644 index 00000000..f54fcd87 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/forgemsg/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg + +go 1.26.0 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 diff --git a/modules/mesh-delivery-forge-bridge/forgemsg/go.sum b/modules/mesh-delivery-forge-bridge/forgemsg/go.sum new file mode 100644 index 00000000..5e18d943 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/forgemsg/go.sum @@ -0,0 +1,14 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/modules/mesh-delivery-forge-bridge/forgemsg/group.go b/modules/mesh-delivery-forge-bridge/forgemsg/group.go new file mode 100644 index 00000000..a36702c5 --- /dev/null +++ b/modules/mesh-delivery-forge-bridge/forgemsg/group.go @@ -0,0 +1,78 @@ +package forgemsg + +import ( + "encoding/json" + "fmt" + "reflect" +) + +// ForgeGroupEndEvent is the name of a delivery group's end on the forge bridge's stream (novox/hq ADR 0633 §4): the +// bridge publishes it once every approved member of the group is merged, after the last member's merged message. It +// has three parts, so no repository's forge event (`forge...`) is ever this name. +const ForgeGroupEndEvent = "forge.group.merged" + +// GroupMerge is one merged member of a delivery group: the repository as `/`, its pull request and the +// merge commit the forge made of it. +type GroupMerge struct { + Repository string `json:"repository"` + Number int `json:"number"` + Merge string `json:"merge"` +} + +// ForgeGroupEnd is the body of ForgeGroupEndEvent: the group and every member merged in it, with its merge commit. +// The indexer takes the members it holds in as one source revision once it hears it (ADR 0633 §4). +type ForgeGroupEnd struct { + V int `json:"v"` + Group string `json:"group"` + Members []GroupMerge `json:"members"` +} + +func (e ForgeGroupEnd) check() error { + if e.Group == "" { + return fmt.Errorf("delivery: a group's end names its group") + } + if len(e.Members) == 0 { + return fmt.Errorf("delivery: the end of the group %q names no member merged in it", e.Group) + } + seen := map[string]bool{} + for _, m := range e.Members { + if err := checkRepository(m.Repository); err != nil { + return err + } + if m.Number <= 0 || m.Merge == "" { + return fmt.Errorf("delivery: the member %s of the group %q's end needs its pull request number and merge commit", m.Repository, e.Group) + } + k := fmt.Sprintf("%s#%d", m.Repository, m.Number) + if seen[k] { + return fmt.Errorf("delivery: the end of the group %q names its member %s twice", e.Group, k) + } + seen[k] = true + } + return nil +} + +// EncodeForgeGroupEnd is e as JSON at Version; it refuses an end with no group or no member, and a member without its +// pull request or merge commit. +func EncodeForgeGroupEnd(e ForgeGroupEnd) ([]byte, error) { + if err := e.check(); err != nil { + return nil, err + } + e.V = Version + return json.Marshal(e) +} + +// DecodeForgeGroupEnd reads a group's end; it refuses an unknown version (ErrUnknownVersion) and what +// EncodeForgeGroupEnd refuses. +func DecodeForgeGroupEnd(data []byte) (ForgeGroupEnd, error) { + var e ForgeGroupEnd + if err := decode(data, &e); err != nil { + return ForgeGroupEnd{}, err + } + if err := e.check(); err != nil { + return ForgeGroupEnd{}, err + } + return e, nil +} + +// SameForgeGroupEnd says whether two group ends are the same value. +func SameForgeGroupEnd(a, b ForgeGroupEnd) bool { return reflect.DeepEqual(a, b) } diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index 16f053bc..1a4b02ee 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,12 +3,13 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 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-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-00010101000000-000000000000 github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op // indirect github.com/google/go-tpm v0.9.8 // indirect github.com/klauspost/compress v1.20.0 // indirect @@ -20,3 +21,5 @@ require ( golang.org/x/sys v0.48.0 // indirect golang.org/x/time v0.15.0 // indirect ) + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg => ./forgemsg diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 7679a5dc..1c3af991 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go b/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go index f35ce988..32aae566 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/acts.go @@ -5,6 +5,7 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -145,11 +146,11 @@ func MergeActs(value []byte) ([]Act, error) { // RefusedMessage is the refused message the bridge writes on FORGE when the forge refuses an act: the act, the pull // request and head it was on, and the forge's own words. func RefusedMessage(a Act, words string) (Message, error) { - event, err := delivery.ForgeEvent(a.Repository, delivery.ForgeRefused) + event, err := forgemsg.ForgeEvent(a.Repository, forgemsg.ForgeRefused) if err != nil { return Message{}, err } - body, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeRefused, Repository: a.Repository, Number: a.Number, + body, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeRefused, Repository: a.Repository, Number: a.Number, Head: a.Head, Act: a.Name, Words: words}) if err != nil { return Message{}, err diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go index 4883012f..26fa4f41 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/acts_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -111,18 +112,12 @@ func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) { } } -func TestOwedActOfADottedRepositoryIsUnescaped(t *testing.T) { - key, v := owedValue(t, "novox/my.repo", 3, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "x"}) - a, err := OwedAct(key, v) - if err != nil { - t.Fatal(err) - } - if a.Call.Args["repo"] != "my.repo" || a.Repository != "novox/my.repo" { - t.Errorf("act = %+v", a) - } -} - func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) { + // A dotted repository's key is unescaped, and its act names the repository whole. + dk, dv := owedValue(t, "novox/my.repo", 3, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "x"}) + if a, err := OwedAct(dk, dv); err != nil || a.Call.Args["repo"] != "my.repo" || a.Repository != "novox/my.repo" { + t.Errorf("a dotted repository's act = %+v, %v", a, err) + } key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", delivery.Owed{Act: delivery.OwedView, Text: "v"}) v, _ := delivery.EncodeOwed(delivery.Owed{Act: delivery.OwedComment, Text: "v"}) if _, err := OwedAct(key, v); err == nil { @@ -166,13 +161,11 @@ func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { / if acts[0].Name != "merge" || acts[0].Number != 12 || acts[0].Head != headA { t.Errorf("act = %+v", acts[0]) } -} - -func TestMergeActsOfAMergeWithoutApprovedHeadsAreRefused(t *testing.T) { - v := mergeValue(t, delivery.Merge{Group: "g", Ask: "a", Pulls: []delivery.MergePull{ + // A merge with no approved heads is refused: the forge's tool would refuse it with nothing to say which. + v = mergeValue(t, delivery.Merge{Group: "g", Ask: "a", Pulls: []delivery.MergePull{ {Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging}}}) if _, err := MergeActs(v); err == nil { - t.Error("a merge with no approved heads was taken: the forge's tool would refuse it with nothing to say which") + t.Error("a merge with no approved heads was taken") } } @@ -185,13 +178,13 @@ func TestRefusedMessageNamesTheActAndTheForgesWords(t *testing.T) { // 1341/AC3, if m.Event != "forge.novox.mesh-delivery.refused" { t.Errorf("event = %q", m.Event) } - f, err := delivery.DecodeForge(m.Data) + f, err := forgemsg.DecodeForge(m.Data) if err != nil { t.Fatal(err) } - want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA, + want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA, Act: "merge", Words: "novox/mesh-delivery#12 is not merged: its head moved"} - if !delivery.SameForge(f, want) { + if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v", f) } } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go index 774bc6b1..bfc80440 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge.go @@ -15,7 +15,7 @@ import ( "strings" "sync" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" ) // ErrUnreadable is wrapped by every error that says an event cannot be turned into a message; it is never @@ -110,11 +110,11 @@ func ConvertKept(subject string, data []byte, trees Trees, files Files, kept Kep var action string switch subject { case UpdatedSubject: - action = delivery.PullUpdated + action = forgemsg.PullUpdated case ClosedSubject: - action = delivery.PullClosed + action = forgemsg.PullClosed case MergedSubject: - action = delivery.PullClosed // unless it merged into the trunk, below + action = forgemsg.PullClosed // unless it merged into the trunk, below default: return Message{}, fmt.Errorf("%w: %s", ErrNotHandled, subject) } @@ -136,29 +136,29 @@ func ConvertKept(subject string, data []byte, trees Trees, files Files, kept Kep return merged(subject, p, trees, files, kept) } said := "" - if action == delivery.PullUpdated && p.Base != "" && p.Base != Trunk { + if action == forgemsg.PullUpdated && p.Base != "" && p.Base != Trunk { // The checker judges pull requests into main only, and source.main names main's head only (novox/hq ADR 0484 §11): a // pull request into another base is no candidate, as it is none at the start (OpenPulls) and at its merge. It is // written closed, so a candidate an older head made is taken out, and said (issue 1422). - action = delivery.PullClosed + action = forgemsg.PullClosed said = fmt.Sprintf("%s/%s#%d at %s is into %s, not %s: written closed, so it is no candidate; the checker judges pull requests into %s only (novox/hq ADR 0484 §11)", p.Owner, p.Repo, p.Number, p.HeadSHA[:12], p.Base, Trunk, Trunk) } - f := delivery.Forge{Kind: delivery.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number, + f := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number, Head: p.HeadSHA, Action: action} // The directories are the forge's word only when it said it could read them (module_dirs_said). - if action == delivery.PullUpdated && p.ModuleDirsSaid { + if action == forgemsg.PullUpdated && p.ModuleDirsSaid { f.Dirs, f.DirsKnown = p.ModuleDirs, true var err error if f.Manifests, f.Locks, _, err = inputsRead(p.Owner, p.Repo, p.HeadSHA, p.ModuleDirs, files); err != nil { return Message{}, fmt.Errorf("%w: %s: the manifests of %s/%s at the head %s: %v", ErrRetry, subject, p.Owner, p.Repo, p.HeadSHA, err) } } - event, err := delivery.ForgeEvent(f.Repository, delivery.ForgePull) + event, err := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgePull) if err != nil { return Message{}, unreadable(subject, "%v", err) } - body, err := delivery.EncodeForge(f) + body, err := forgemsg.EncodeForge(f) if err != nil { return Message{}, unreadable(subject, "%v", err) } @@ -313,11 +313,11 @@ func merged(subject string, p pull, trees Trees, files Files, kept Kept) (Messag if len(locks) == 0 { locks = nil } - event, err := delivery.ForgeEvent(repository, delivery.ForgeMerged) + event, err := forgemsg.ForgeEvent(repository, forgemsg.ForgeMerged) if err != nil { return Message{}, unreadable(subject, "%v", err) } - f := delivery.Forge{Kind: delivery.ForgeMerged, Repository: repository, Branch: p.Head, Number: p.Number, + f := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: repository, Branch: p.Head, Number: p.Number, Head: p.HeadSHA, Merge: p.MergeSHA, Root: root, Trees: dirs, Manifests: manifests, Locks: locks} if repository == CatalogRepository { // The catalogue's predefined list at the merge (novox/hq ADR 0565 §2): the indexer keeps it for mesh-config. @@ -331,7 +331,7 @@ func merged(subject string, p pull, trees Trees, files Files, kept Kept) (Messag f.Predefined = json.RawMessage(raw) } } - body, err := delivery.EncodeForge(f) + body, err := forgemsg.EncodeForge(f) if err != nil { return Message{}, unreadable(subject, "%v", err) } @@ -373,12 +373,12 @@ func OpenPulls(answer []byte) (msgs []Message, skipped []string, err error) { skipped = append(skipped, name+": it names no owner, repo, number, branch or 40-hex head") continue } - f := delivery.Forge{Kind: delivery.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number, - Head: p.HeadSHA, Action: delivery.PullUpdated} - event, err := delivery.ForgeEvent(f.Repository, delivery.ForgePull) + f := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: p.Owner + "/" + p.Repo, Branch: p.Head, Number: p.Number, + Head: p.HeadSHA, Action: forgemsg.PullUpdated} + event, err := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgePull) var body []byte if err == nil { - body, err = delivery.EncodeForge(f) + body, err = forgemsg.EncodeForge(f) } if err != nil { skipped = append(skipped, name+": "+err.Error()) @@ -392,12 +392,12 @@ func OpenPulls(answer []byte) (msgs []Message, skipped []string, err error) { // Track says what a message the bridge wrote changes in the head it keeps for each pull request (the bucket heads, // novox/hq ADR 0443 §5): the key `..`, and the head to keep under it, or "" to take the key away // (a closed pull request, a merged one). ok is false for a message that keeps no head. -func Track(f delivery.Forge) (key, head string, ok bool) { +func Track(f forgemsg.Forge) (key, head string, ok bool) { key = strings.ReplaceAll(f.Repository, "/", ".") + "." + strconv.Itoa(f.Number) switch { - case f.Kind == delivery.ForgePull && f.Action != delivery.PullClosed: + case f.Kind == forgemsg.ForgePull && f.Action != forgemsg.PullClosed: return key, f.Head, true - case f.Kind == delivery.ForgePull || f.Kind == delivery.ForgeMerged: + case f.Kind == forgemsg.ForgePull || f.Kind == forgemsg.ForgeMerged: return key, "", true } return "", "", false @@ -413,22 +413,22 @@ func OpenMessage(answer []byte) (Message, error) { if err := json.Unmarshal(answer, &a); err != nil || a.Pulls == nil { return Message{}, fmt.Errorf("the answer of gitea_open_pulls is not {pulls: [...]}: %v", err) } - open := []delivery.OpenPull{} + open := []forgemsg.OpenPull{} for _, p := range *a.Pulls { if p.Base != Trunk || p.Owner == "" || p.Repo == "" || p.Number <= 0 { continue } - open = append(open, delivery.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number}) + open = append(open, forgemsg.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number}) } - slices.SortFunc(open, func(a, b delivery.OpenPull) int { + slices.SortFunc(open, func(a, b forgemsg.OpenPull) int { if c := strings.Compare(a.Repository, b.Repository); c != 0 { return c } return a.Number - b.Number }) - body, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeOpen, Open: open}) + body, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeOpen, Open: open}) if err != nil { return Message{}, fmt.Errorf("the open pull requests cannot be said: %w", err) } - return Message{Event: delivery.OpenEvent(), Data: body}, nil + return Message{Event: forgemsg.OpenEvent(), Data: body}, nil } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go index 3bd1116f..68f5a0c1 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/bridge_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -54,13 +55,13 @@ func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion; if msg.Event != "forge.novox.mesh-delivery.pull" { t.Errorf("event = %q", msg.Event) } - f, err := delivery.DecodeForge(msg.Data) + f, err := forgemsg.DecodeForge(msg.Data) if err != nil { t.Fatal(err) } - want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, - Head: head, Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}} - if !delivery.SameForge(f, want) { + want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + Head: head, Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}} + if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } } @@ -70,7 +71,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 if err != nil { t.Fatal(err) } - f, _ := delivery.DecodeForge(msg.Data) + f, _ := forgemsg.DecodeForge(msg.Data) if f.DirsKnown || len(f.Dirs) != 0 { t.Errorf("dirs = %v known = %v, want not known", f.Dirs, f.DirsKnown) } @@ -79,7 +80,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 if err != nil { t.Fatal(err) } - f, _ = delivery.DecodeForge(msg.Data) + f, _ = forgemsg.DecodeForge(msg.Data) if !f.DirsKnown || len(f.Dirs) != 0 { t.Errorf("dirs = %v known = %v, want known and empty", f.Dirs, f.DirsKnown) } @@ -88,7 +89,7 @@ func TestDirsNotSaidAreNotKnown(t *testing.T) { // 1235/AC3 if err != nil { t.Fatal(err) } - if f, _ = delivery.DecodeForge(msg.Data); f.DirsKnown || len(f.Dirs) != 0 { + if f, _ = forgemsg.DecodeForge(msg.Data); f.DirsKnown || len(f.Dirs) != 0 { t.Errorf("dirs %v known %v without module_dirs_said", f.Dirs, f.DirsKnown) } } @@ -101,13 +102,13 @@ func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4 if msg.Event != "forge.novox.mesh-delivery.pull" { t.Errorf("event = %q", msg.Event) } - f, err := delivery.DecodeForge(msg.Data) + f, err := forgemsg.DecodeForge(msg.Data) if err != nil { t.Fatal(err) } - want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, - Head: head, Action: delivery.PullClosed} - if !delivery.SameForge(f, want) { + want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + Head: head, Action: forgemsg.PullClosed} + if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } // Issue 1422, ADR 0484 §11: a head pushed to a pull request into another base is written closed, read nothing for, and said; @@ -116,12 +117,12 @@ func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4 if err != nil { t.Fatal(err) } - if f, err = delivery.DecodeForge(msg.Data); err != nil || !delivery.SameForge(f, want) || !strings.Contains(msg.Said, "is into nox-mesh, not main") { + if f, err = forgemsg.DecodeForge(msg.Data); err != nil || !forgemsg.SameForge(f, want) || !strings.Contains(msg.Said, "is into nox-mesh, not main") { t.Errorf("an update into nox-mesh is %+v (%v), said %q; want it closed and said", f, err, msg.Said) } for _, base := range []any{"main", nil} { msg, err = Convert(updatedSubject, updated(map[string]any{"base": base}), nil, anyFiles) - if f, _ = delivery.DecodeForge(msg.Data); err != nil || f.Action != delivery.PullUpdated || msg.Said != "" { + if f, _ = forgemsg.DecodeForge(msg.Data); err != nil || f.Action != forgemsg.PullUpdated || msg.Said != "" { t.Errorf("an update with base %v is %+v (%v), said %q; want it updated", base, f, err, msg.Said) } } @@ -170,12 +171,18 @@ func mustDrop(body []byte, key string) []byte { return b } -func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235 +func TestOtherSubjectsAreNotHandled(t *testing.T) { // 1235, 1236 for _, s := range []string{"mesh.mod.gitea.event.something.else", "mesh.mod.other.event.pull.updated"} { if _, err := Convert(s, updated(nil), nil, anyFiles); !errors.Is(err, ErrNotHandled) { t.Errorf("%s: err = %v, want ErrNotHandled", s, err) } } + // 1236: a merged event without a full merge commit is unreadable, and the forge is not asked. + var calls []call + _, err := Convert(mergedSubject, mergedBody(map[string]any{"merge_commit_sha": "abc"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil), anyFiles) + if !errors.Is(err, ErrUnreadable) || len(calls) != 0 { + t.Errorf("merged without a merge commit: err = %v calls = %v", err, calls) + } } const merge = "89abcdef0123456789abcdef0123456789abcdef" @@ -222,13 +229,13 @@ func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1 if msg.Event != "forge.novox.mesh-delivery.merged" { t.Errorf("event = %q", msg.Event) } - f, err := delivery.DecodeForge(msg.Data) + f, err := forgemsg.DecodeForge(msg.Data) if err != nil { t.Fatal(err) } - want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: merge, Root: "r0", Trees: answer, Manifests: map[string]string{"modules/a": "{}", "modules/b": "{}"}} - if !delivery.SameForge(f, want) { + if !forgemsg.SameForge(f, want) { t.Errorf("message = %+v, want %+v", f, want) } if len(calls) != 1 || calls[0] != (call{"novox", "mesh-delivery", merge}) { @@ -242,13 +249,13 @@ func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2 if err != nil { t.Fatal(err) } - f, err := delivery.DecodeForge(msg.Data) + f, err := forgemsg.DecodeForge(msg.Data) if err != nil { t.Fatal(err) } - want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, - Head: head, Action: delivery.PullClosed} - if msg.Event != "forge.novox.mesh-delivery.pull" || !delivery.SameForge(f, want) { + want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + Head: head, Action: forgemsg.PullClosed} + if msg.Event != "forge.novox.mesh-delivery.pull" || !forgemsg.SameForge(f, want) { t.Errorf("event %q message %+v, want a closed pull message", msg.Event, f) } if len(calls) != 0 { @@ -277,22 +284,14 @@ func TestNoModuleJSONAnywhereWritesAMergedMessageWithNoTrees(t *testing.T) { if err != nil { t.Fatal(err) } - f, err := delivery.DecodeForge(msg.Data) - want := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + f, err := forgemsg.DecodeForge(msg.Data) + want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: merge, Root: "r0"} - if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !delivery.SameForge(f, want) || !strings.Contains(msg.Said, "no module.json") { + if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !forgemsg.SameForge(f, want) || !strings.Contains(msg.Said, "no module.json") { t.Errorf("event %q message %+v said %q, %v; want a merged message with no trees", msg.Event, f, msg.Said, err) } } -func TestMergedWithoutAMergeCommitIsUnreadable(t *testing.T) { // 1236 - var calls []call - _, err := Convert(mergedSubject, mergedBody(map[string]any{"merge_commit_sha": "abc"}), treesOf(&calls, "r0", map[string]string{"a": "t"}, nil), anyFiles) - if !errors.Is(err, ErrUnreadable) || len(calls) != 0 { - t.Errorf("err = %v calls = %v", err, calls) - } -} - // openAnswer is gitea_open_pulls's answer as mesh-catalog modules/gitea/client.ts:369-381 builds each entry // (owner, repo, number, base, head, head_sha, html_url, required, statuses, checks, labels) inside {pulls}. func openAnswer(pulls ...map[string]any) []byte { @@ -311,12 +310,12 @@ func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/ if err != nil || len(skipped) != 0 || len(msgs) != 2 { t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err) } - for i, want := range []delivery.Forge{ - {V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: delivery.PullUpdated}, - {V: delivery.Version, Kind: delivery.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: delivery.PullUpdated}, + for i, want := range []forgemsg.Forge{ + {V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated}, + {V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated}, } { - f, err := delivery.DecodeForge(msgs[i].Data) - if err != nil || !delivery.SameForge(f, want) { + f, err := forgemsg.DecodeForge(msgs[i].Data) + if err != nil || !forgemsg.SameForge(f, want) { t.Errorf("message %d = %+v err %v, want %+v", i, f, err, want) } if f.DirsKnown { @@ -328,14 +327,11 @@ func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/ } } -func TestOpenPullIntoAnotherBranchGetsNoMessage(t *testing.T) { // 1237/AC4 - msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "feat/y", "feat/x", head))) - if err != nil || len(msgs) != 0 || len(skipped) != 0 { - t.Errorf("msgs %d skipped %v err %v, want none", len(msgs), skipped, err) +func TestOpenPullsUnreadableEntryIsNamedAndTheRestGo(t *testing.T) { // 1237, 1237/AC4 + // 1237/AC4: an open pull request into another branch gets no message. + if msgs, skipped, err := OpenPulls(openAnswer(openEntry("mesh-delivery", 12, "feat/y", "feat/x", head))); err != nil || len(msgs) != 0 || len(skipped) != 0 { + t.Errorf("into another branch: msgs %d skipped %v err %v, want none", len(msgs), skipped, err) } -} - -func TestOpenPullsUnreadableEntryIsNamedAndTheRestGo(t *testing.T) { // 1237 msgs, skipped, err := OpenPulls(openAnswer(openEntry("a", 1, "main", "b", "short"), openEntry("mesh-delivery", 12, "main", "feat/x", head))) if err != nil || len(msgs) != 1 || len(skipped) != 1 || !strings.Contains(skipped[0], "novox/a#1") { t.Errorf("msgs %d skipped %v err %v", len(msgs), skipped, err) @@ -355,15 +351,15 @@ func TestOpenMessageNamesEveryOpenPullIntoTheTrunk(t *testing.T) { // 1279/AC6 if err != nil || m.Event != "forge.open" { t.Fatalf("message %+v err %v", m, err) } - f, err := delivery.DecodeForge(m.Data) + f, err := forgemsg.DecodeForge(m.Data) // Every one into the trunk, whether its head reads or not (it is open); none into another branch; in a fixed order. - want := []delivery.OpenPull{{Repository: "novox/a", Number: 1}, {Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}} - if err != nil || f.Kind != delivery.ForgeOpen || len(f.Open) != 3 || f.Open[0] != want[0] || f.Open[1] != want[1] || f.Open[2] != want[2] { + want := []forgemsg.OpenPull{{Repository: "novox/a", Number: 1}, {Repository: "novox/mesh-catalog", Number: 300}, {Repository: "novox/mesh-delivery", Number: 12}} + if err != nil || f.Kind != forgemsg.ForgeOpen || len(f.Open) != 3 || f.Open[0] != want[0] || f.Open[1] != want[1] || f.Open[2] != want[2] { t.Errorf("open message = %+v err %v, want %v", f, err, want) } // Nothing open is a message too: the indexer then drops every candidate. m, err = OpenMessage([]byte(`{"pulls":[]}`)) - if f, derr := delivery.DecodeForge(m.Data); err != nil || derr != nil || f.Kind != delivery.ForgeOpen || len(f.Open) != 0 { + if f, derr := forgemsg.DecodeForge(m.Data); err != nil || derr != nil || f.Kind != forgemsg.ForgeOpen || len(f.Open) != 0 { t.Errorf("empty answer: %+v %v %v", f, err, derr) } for _, bad := range []string{`{"pulls":null}`, `{}`, `x`} { @@ -374,24 +370,24 @@ func TestOpenMessageNamesEveryOpenPullIntoTheTrunk(t *testing.T) { // 1279/AC6 } func TestTrackSaysWhichHeadOfWhichPullRequestWasWritten(t *testing.T) { // 1279/AC6 - pullMsg := func(action string) delivery.Forge { - return delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: action} + pullMsg := func(action string) forgemsg.Forge { + return forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: action} } - for _, a := range []string{delivery.PullOpened, delivery.PullUpdated} { + for _, a := range []string{forgemsg.PullOpened, forgemsg.PullUpdated} { if key, h, ok := Track(pullMsg(a)); key != "novox.mesh-delivery.12" || h != head || !ok { t.Errorf("%s: Track = %q %q %v, want the head kept under a bucket key", a, key, h, ok) } } // A closed pull request and a merged one are no longer kept: their key goes. - if key, h, ok := Track(pullMsg(delivery.PullClosed)); key != "novox.mesh-delivery.12" || h != "" || !ok { + if key, h, ok := Track(pullMsg(forgemsg.PullClosed)); key != "novox.mesh-delivery.12" || h != "" || !ok { t.Errorf("closed: Track = %q %q %v", key, h, ok) } - merged := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}} + merged := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Merge: "m", Root: "r", Trees: map[string]string{"": "t"}} if key, h, ok := Track(merged); key != "novox.mesh-delivery.12" || h != "" || !ok { t.Errorf("merged: Track = %q %q %v", key, h, ok) } // The open message and the rest hold no head. - if _, _, ok := Track(delivery.Forge{Kind: delivery.ForgeOpen}); ok { + if _, _, ok := Track(forgemsg.Forge{Kind: forgemsg.ForgeOpen}); ok { t.Error("an open message was tracked") } } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go index ce1f5458..602b3e7b 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/files_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" ) // The bytes the forge answers in these tests are two real files of this repository, read from the working tree: the @@ -69,7 +69,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1 if err != nil { t.Fatal(err) } - f, err := delivery.DecodeForge(msg.Data) + f, err := forgemsg.DecodeForge(msg.Data) if err != nil { t.Fatal(err) } @@ -90,7 +90,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1 if err != nil { t.Fatal(err) } - if f, err = delivery.DecodeForge(msg.Data); err != nil { + if f, err = forgemsg.DecodeForge(msg.Data); err != nil { t.Fatal(err) } if len(f.RetiredPlacements) > 0 || f.RetiredRead || strings.Contains(string(msg.Data), `"placements`) { @@ -108,7 +108,7 @@ func TestMergedCarriesManifestsAndLocks(t *testing.T) { // 1448/AC1 } forge.have[PredefinedFile] = []byte(`{"font-size":{"type":"integer","value":13}}`) msg, _ = Convert(mergedSubject, mergedBody(map[string]any{"repo": "mesh-catalog"}), treesOf(&calls, "r0", map[string]string{"modules/a": "t1"}, nil), forge.get) - if f, err = delivery.DecodeForge(msg.Data); err != nil || string(f.Predefined) != string(forge.have[PredefinedFile]) || f.PredefinedRead { + if f, err = forgemsg.DecodeForge(msg.Data); err != nil || string(f.Predefined) != string(forge.have[PredefinedFile]) || f.PredefinedRead { t.Errorf("predefined = %s (%v)", f.Predefined, err) } forge.calls = nil @@ -133,7 +133,7 @@ func TestPullWithKnownDirsCarriesTheSameAtTheHead(t *testing.T) { // 1448/AC2 if err != nil { t.Fatal(err) } - f, _ := delivery.DecodeForge(msg.Data) + f, _ := forgemsg.DecodeForge(msg.Data) if !reflect.DeepEqual(f.Manifests, map[string]string{"modules/a": string(manifest)}) || len(f.Locks) != 1 || len(f.Packages) != 0 { t.Errorf("manifests %v locks %v packages %v", f.Manifests, f.Locks, f.Packages) } @@ -146,7 +146,7 @@ func TestPullWithKnownDirsCarriesTheSameAtTheHead(t *testing.T) { // 1448/AC2 if err != nil { t.Fatal(err) } - f, _ = delivery.DecodeForge(msg.Data) + f, _ = forgemsg.DecodeForge(msg.Data) if f.Manifests != nil || f.Locks != nil || len(forge.calls) != 0 { t.Errorf("manifests %v locks %v calls %v", f.Manifests, f.Locks, forge.calls) } @@ -175,7 +175,7 @@ func TestAForgeErrorOrAMissingManifestIsRetryAndALockNotFoundIsLeftOut(t *testin if err != nil { t.Fatal(err) } - if f, _ := delivery.DecodeForge(msg.Data); f.Locks != nil { + if f, _ := forgemsg.DecodeForge(msg.Data); f.Locks != nil { t.Errorf("locks = %v, want none for lock files the forge does not have", f.Locks) } } diff --git a/modules/mesh-delivery-forge-bridge/internal/bridge/paths_test.go b/modules/mesh-delivery-forge-bridge/internal/bridge/paths_test.go index 57c06bc4..c4ddac59 100644 --- a/modules/mesh-delivery-forge-bridge/internal/bridge/paths_test.go +++ b/modules/mesh-delivery-forge-bridge/internal/bridge/paths_test.go @@ -4,7 +4,7 @@ import ( "os" "testing" - "git.novox.be/novox/mesh-sdk/go/delivery" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" ) // ADR 0495 clause 1 with the lead's decision L8 of R4e: the forge bridge is no reader of a manifest's `paths`. A merge of a @@ -26,7 +26,7 @@ func TestAMergedMessageCarriesNoNamedPathsTheBridgeDerived(t *testing.T) { if err != nil { t.Fatal(err) } - f, err := delivery.DecodeForge(msg.Data) + f, err := forgemsg.DecodeForge(msg.Data) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go index e8b23054..db89bb59 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/integration_test.go @@ -19,6 +19,8 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -102,15 +104,15 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 } }) h1, h2 := "0123456789abcdef0123456789abcdef01234567", "89abcdef0123456789abcdef0123456789abcdef" - for _, f := range []delivery.Forge{ - {Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1, Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true}, - {Kind: delivery.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: delivery.PullUpdated, Dirs: []string{"modules/c"}, DirsKnown: true}, + for _, f := range []forgemsg.Forge{ + {Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1, Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true}, + {Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated, Dirs: []string{"modules/c"}, DirsKnown: true}, } { - data, err := delivery.EncodeForge(f) + data, err := forgemsg.EncodeForge(f) if err != nil { t.Fatal(err) } - event, _ := delivery.ForgeEvent(f.Repository, delivery.ForgePull) + event, _ := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgePull) if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+event, data); err != nil { t.Fatal(err) } @@ -143,43 +145,43 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 t.Fatal(err) } const ix = delivery.IndexerModule - m1 := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1, + m1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: h1, Merge: "1111111111111111111111111111111111111111", Root: "r0", Trees: map[string]string{"modules/" + ix: "tree-indexer-2", "modules/a": "tree-a-2"}, Manifests: map[string]string{"modules/" + ix: string(manifest), "modules/a": `{"module":"a","version":"1"}`}, Locks: map[string]map[string]string{"modules/" + ix: {"go.sum": "3b5028f0c1e9bde04dd766abd63eb5931bc516bae695cf46f00abcd8510b0381"}}} - merged := func(f delivery.Forge) uint64 { + merged := func(f forgemsg.Forge) uint64 { t.Helper() - data, err := delivery.EncodeForge(f) + data, err := forgemsg.EncodeForge(f) if err != nil { t.Fatal(err) } - event, _ := delivery.ForgeEvent(f.Repository, delivery.ForgeMerged) + event, _ := forgemsg.ForgeEvent(f.Repository, forgemsg.ForgeMerged) ack, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+event, data) if err != nil { t.Fatal(err) } return ack.Sequence } - m1Key := delivery.PendingKey(merged(m1)) + m1Key := takein.PendingKey(merged(m1)) takeIns, err := js.Stream(ctx, "TAKE_IN") if err != nil { t.Fatal(err) } // said is the last take-in event under kind and key, within 20 s. - said := func(kind, key string) (delivery.TakeIn, bool) { - event, _ := delivery.TakeInEvent(kind, key) + said := func(kind, key string) (takein.TakeIn, bool) { + event, _ := takein.TakeInEvent(kind, key) for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { if msg, err := takeIns.GetLastMsgForSubject(ctx, process.ModuleStreamSubject(ix, event)); err == nil { - if ti, err := delivery.DecodeTakeIn(msg.Data); err == nil { + if ti, err := takein.DecodeTakeIn(msg.Data); err == nil { return ti, true } } } - return delivery.TakeIn{}, false + return takein.TakeIn{}, false } - held, ok := said(delivery.TakeInPending, m1Key) - if !ok || held.What != delivery.TakeInRevision || held.WaitsFor == "" || held.WaitsFor == first || len(held.Merges) != 1 { + held, ok := said(takein.TakeInPending, m1Key) + if !ok || held.What != takein.TakeInRevision || held.WaitsFor == "" || held.WaitsFor == first || len(held.Merges) != 1 { t.Fatalf("take-in.pending.%s = %+v (said %v) within 20 s, want the revision waiting for another build than %s", m1Key, held, ok, first) } named := held.WaitsFor @@ -226,14 +228,14 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 // A group's first member arrives meanwhile; its end is still to come (the knowledge base's remark: it stays across the restart). members := []delivery.Head{{Repository: "novox/mesh-catalog", Number: 40, Head: "4040404040404040404040404040404040404040"}, {Repository: "novox/mesh-controller", Number: 41, Head: "4141414141414141414141414141414141414141"}} - g1 := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/g", Number: 40, Head: members[0].Head, + g1 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/g", Number: 40, Head: members[0].Head, Merge: "4000000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/c": "tree-c-2"}, Manifests: map[string]string{"modules/c": `{"module":"c","version":"1"}`}, Group: "feat/g", Members: members} - g2 := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-controller", Branch: "feat/g", Number: 41, Head: members[1].Head, + g2 := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-controller", Branch: "feat/g", Number: 41, Head: members[1].Head, Merge: "4100000000000000000000000000000000000000", Root: "r0", Trees: map[string]string{"modules/k": "tree-k-1"}, Manifests: map[string]string{"modules/k": `{"module":"k","version":"1"}`}, Group: "feat/g", Members: members} - g1Key, groupKey := delivery.PendingKey(merged(g1)), delivery.GroupTakeInKey("feat/g") - if gathered, ok := said(delivery.TakeInPending, groupKey); !ok || gathered.What != delivery.TakeInGroup || len(gathered.Merges) != 1 { + g1Key, groupKey := takein.PendingKey(merged(g1)), takein.GroupTakeInKey("feat/g") + if gathered, ok := said(takein.TakeInPending, groupKey); !ok || gathered.What != takein.TakeInGroup || len(gathered.Merges) != 1 { t.Fatalf("take-in.pending.%s = %+v (said %v) within 20 s, want the group with its first member", groupKey, gathered, ok) } // indexer_pending, the tool over the same state, names both waits. @@ -241,29 +243,29 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 if err != nil { t.Fatal(err) } - waits := func() []delivery.PendingWait { + waits := func() []takein.PendingWait { t.Helper() answer, err := index.PendingTools(kvStore{pendingKV}, nil, time.Now)[0].Run(map[string]any{}) if err != nil { t.Fatal(err) } - return answer.(delivery.PendingAnswer).Waits + return answer.(takein.PendingAnswer).Waits } - var w []delivery.PendingWait + var w []takein.PendingWait for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { if w = waits(); len(w) == 2 && w[0].WaitsFor == named && w[1].Said != "" { break } } if len(w) != 2 || w[0].Key != m1Key || w[0].WaitsFor != named || !slices.Contains(w[0].Holds, "a") || !slices.Contains(w[0].Holds, ix) || - w[1].Key != groupKey || w[1].What != delivery.TakeInGroup || len(w[1].Merges) != 1 || !strings.HasSuffix(w[1].Said, "pending."+groupKey) { + w[1].Key != groupKey || w[1].What != takein.TakeInGroup || len(w[1].Merges) != 1 || !strings.HasSuffix(w[1].Said, "pending."+groupKey) { t.Fatalf("indexer_pending = %+v, want the revision %s waiting for %s holding a and the indexer, then the group gathered", w, m1Key, named) } // The named indexer starts (the node-engine now runs that build): it takes the revision in, and leaves the group. running.Store(&named) restart() - if taken, ok := said(delivery.TakeInTaken, m1Key); !ok || len(taken.Merges) != 1 || taken.Merges[0].Merge != m1.Merge { + if taken, ok := said(takein.TakeInTaken, m1Key); !ok || len(taken.Merges) != 1 || taken.Merges[0].Merge != m1.Merge { t.Fatalf("take-in.taken.%s = %+v (said %v) within 20 s of the start of %s", m1Key, taken, ok, named) } for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) { @@ -286,15 +288,15 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 // I8b: the group's second member and its end: one take-in of both, said under the group's key. merged(g2) - end, err := delivery.EncodeForgeGroupEnd(delivery.ForgeGroupEnd{Group: "feat/g", Members: []delivery.GroupMerge{ + end, err := forgemsg.EncodeForgeGroupEnd(forgemsg.ForgeGroupEnd{Group: "feat/g", Members: []forgemsg.GroupMerge{ {Repository: g1.Repository, Number: g1.Number, Merge: g1.Merge}, {Repository: g2.Repository, Number: g2.Number, Merge: g2.Merge}}}) if err != nil { t.Fatal(err) } - if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+delivery.ForgeGroupEndEvent, end); err != nil { + if _, err := js.Publish(ctx, "mesh.stream.mesh-delivery-forge-bridge.event."+forgemsg.ForgeGroupEndEvent, end); err != nil { t.Fatal(err) } - if taken, ok := said(delivery.TakeInTaken, groupKey); !ok || taken.What != delivery.TakeInGroup || len(taken.Merges) != 2 { + if taken, ok := said(takein.TakeInTaken, groupKey); !ok || taken.What != takein.TakeInGroup || len(taken.Merges) != 2 { t.Fatalf("take-in.taken.%s = %+v (said %v) within 20 s of the group's end, want both members", groupKey, taken, ok) } var hasC, hasK bool @@ -307,7 +309,7 @@ func TestIndexerWhole(t *testing.T) { // 1238/AC2, AC10; 1585/T14, T15 t.Errorf("after the group's end: modules.c %v, modules.k %v, indexer_pending %+v: want both taken in and nothing waiting", hasC, hasK, w) } // One take-in: neither member was said as a revision of its own. - single, _ := delivery.TakeInEvent(delivery.TakeInPending, g1Key) + single, _ := takein.TakeInEvent(takein.TakeInPending, g1Key) if _, err := takeIns.GetLastMsgForSubject(ctx, process.ModuleStreamSubject(ix, single)); err == nil { t.Errorf("the group's first member %s was said pending as a revision of its own", g1Key) } @@ -443,7 +445,7 @@ func TestIndexerWritesModulesAtAMerge(t *testing.T) { // 1239/AC9; I1, I3: the m builds.end(t, "novox/mesh-controller", "build-agent", "modules/build-agent") }) const merge = "89abcdef0123456789abcdef0123456789abcdef" - data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, + data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: "0123456789abcdef0123456789abcdef01234567", Merge: merge, Root: "r0", Trees: map[string]string{"modules/a": "newtree"}, Manifests: map[string]string{"modules/a": `{"module":"a","version":"1"}`}}) if err != nil { @@ -497,7 +499,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15 t.Fatal(err) } }) - forge := delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, + forge := forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, Head: "0123456789abcdef0123456789abcdef01234567", Merge: "89abcdef0123456789abcdef0123456789abcdef", Root: "r0", Trees: map[string]string{"modules/avahi": "newtree"}, Manifests: map[string]string{"modules/avahi": string(manifest)}, @@ -505,7 +507,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15 for name, raw := range judged { forge.Manifests["modules/"+name], forge.Trees["modules/"+name] = raw, "tree-"+name } - data, err := delivery.EncodeForge(forge) + data, err := forgemsg.EncodeForge(forge) if err != nil { t.Fatal(err) } @@ -599,7 +601,7 @@ func TestIndexerWritesAGoModuleAgainWhenTheBuildModuleReportsItsToolchain(t *tes js, ctx, _, modules := startIndexer(t, func(ctx context.Context, builds buildLog) { builds.end(t, "novox/mesh-catalog", "avahi", "modules/avahi") }) - data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, + data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, Head: "0123456789abcdef0123456789abcdef01234567", Merge: "89abcdef0123456789abcdef0123456789abcdef", Root: "r0", Trees: map[string]string{"modules/avahi": "newtree"}, Manifests: map[string]string{"modules/avahi": string(manifest)}, @@ -686,7 +688,7 @@ func TestIndexerGivesACandidateItsFingerprintWhenTheCheckerCreatesThePreviewsBuc t.Fatal(err) } }) - pull, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, Head: head, Action: delivery.PullUpdated, + pull, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 301, Head: head, Action: forgemsg.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true, Manifests: map[string]string{"modules/avahi": string(manifest)}}) if err != nil { t.Fatal(err) @@ -856,7 +858,7 @@ func TestIndexerCountsOnlyThePlacedBuildNodesToolchains(t *testing.T) { place(ctx, t, "n1", "mesh-build-go") place(ctx, t, "n2") }) - data, err := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 302, + data, err := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 302, Head: "0123456789abcdef0123456789abcdef01234567", Merge: "89abcdef0123456789abcdef0123456789abcdef", Root: "r0", Trees: map[string]string{"modules/avahi": "newtree"}, Manifests: map[string]string{"modules/avahi": string(manifest)}, diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index ccdbe7ac..ce9d794b 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -25,6 +25,7 @@ import ( "github.com/nats-io/nats.go/jetstream" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -157,14 +158,14 @@ type delivered interface { // handleGroupEnd takes a group's end in (index.EndGroup): a body that does not read is terminated and said; an end // whose members are not all held yet, or while the build this program runs is not known, is left for redelivery. func handleGroupEnd(ctx context.Context, m delivered, ix *index.Indexer, h *index.Heard, logger *log.Logger) { - end, err := delivery.DecodeForgeGroupEnd(m.Data()) + end, err := forgemsg.DecodeForgeGroupEnd(m.Data()) if err != nil { logger.Printf("%s: not a group's end, dropped: %v", m.Subject(), err) h.Outcome, h.Words = index.HeardDropped, err.Error() _ = m.Term() return } - h.Kind, h.Branch = delivery.ForgeGroupEndEvent, end.Group + h.Kind, h.Branch = forgemsg.ForgeGroupEndEvent, end.Group applied, words, err := ix.EndGroup(ctx, end) h.Written, h.Late = applied.Written, applied.Late if err != nil { @@ -197,11 +198,11 @@ func handle(ctx context.Context, m delivered, ix *index.Indexer, heard index.Sto } }() // A delivery group's end (novox/hq ADR 0633 §4) has a body of its own: told apart by its name, before the decode. - if strings.HasSuffix(m.Subject(), "."+delivery.ForgeGroupEndEvent) { + if strings.HasSuffix(m.Subject(), "."+forgemsg.ForgeGroupEndEvent) { handleGroupEnd(ctx, m, ix, &h, logger) return } - f, err := delivery.DecodeForge(m.Data()) + f, err := forgemsg.DecodeForge(m.Data()) if err != nil { logger.Printf("%s: not a forge message, dropped: %v", m.Subject(), err) h.Outcome, h.Words = index.HeardDropped, err.Error() @@ -213,7 +214,7 @@ func handle(ctx context.Context, m delivered, ix *index.Indexer, heard index.Sto h.Commit = f.Merge } var applied index.Applied - if f.Kind == delivery.ForgeMerged && h.Sequence > 0 { + if f.Kind == forgemsg.ForgeMerged && h.Sequence > 0 { // A merge is taken in only by the indexer its revision names (ADR 0633 §1): kept in pending until then. applied, h.Words, err = ix.Arrive(ctx, h.Sequence, f) } else { diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main_test.go index f42d7972..c4c3672c 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main_test.go @@ -11,6 +11,7 @@ import ( "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-indexer/internal/index" @@ -73,8 +74,8 @@ const head = "0123456789abcdef0123456789abcdef01234567" func TestAMessageIsIndexedAndAcked(t *testing.T) { // 1238/AC2 (unit part) st := &memStore{vals: map[string][]byte{}} ix := &index.Indexer{Store: st, Builds: index.NewBuilds()} - data, _ := delivery.EncodeForge(delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, - Action: delivery.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true}) + data, _ := forgemsg.EncodeForge(forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, + Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true}) m := &fakeMsg{subject: "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull", data: data} heard := &memStore{vals: map[string][]byte{}} handle(context.Background(), m, ix, heard, log.New(&bytes.Buffer{}, "", 0)) @@ -84,7 +85,7 @@ func TestAMessageIsIndexedAndAcked(t *testing.T) { // 1238/AC2 (unit part) } // Issue 1126 note 76: what the indexer heard of FORGE is readable (indexer_heard): subject, sequence, deliveries, time, // repository, commit and what it did. - if h := heardOf(t, heard); h.Subject != m.subject || h.Sequence != 41 || h.Delivered != 2 || h.Published.Hour() != 13 || h.Kind != delivery.ForgePull || + if h := heardOf(t, heard); h.Subject != m.subject || h.Sequence != 41 || h.Delivered != 2 || h.Published.Hour() != 13 || h.Kind != forgemsg.ForgePull || h.Repository != "novox/mesh-delivery" || h.Number != 12 || h.Commit != head || h.Outcome != index.HeardApplied || h.Handled.IsZero() { t.Errorf("heard = %+v", h) } @@ -111,12 +112,12 @@ func TestAMessageThatIsNoForgeMessageIsTerminatedAndLogged(t *testing.T) { // 12 }{{`{"v":1,"group":"feat/x","members":[{"repository":"novox/mesh-sdk","number":3,"merge":"` + head + `"}]}`, 0, ""}, {`{"v":1,"group":"feat/x"}`, 1, "not a group's end"}} { logs.Reset() - m := &fakeMsg{subject: "mesh.stream.mesh-delivery-forge-bridge.event." + delivery.ForgeGroupEndEvent, data: []byte(c.data)} + m := &fakeMsg{subject: "mesh.stream.mesh-delivery-forge-bridge.event." + forgemsg.ForgeGroupEndEvent, data: []byte(c.data)} handle(context.Background(), m, &index.Indexer{Store: st, Builds: index.NewBuilds()}, heard, log.New(&logs, "", 0)) if m.termd != c.termd || m.acked+m.termd != 1 || !strings.Contains(logs.String(), c.logged) { t.Errorf("group end %s: terminated %d acked %d log %q", c.data, m.termd, m.acked, logs.String()) } - if h := heardOf(t, heard); h.Kind != delivery.ForgeGroupEndEvent && c.termd == 0 { + if h := heardOf(t, heard); h.Kind != forgemsg.ForgeGroupEndEvent && c.termd == 0 { t.Errorf("heard of a group's end = %+v", h) } } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go index f10cac87..8d60e4b9 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named.go @@ -9,6 +9,7 @@ import ( "path" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-indexer/internal/fingerprint" @@ -18,7 +19,7 @@ import ( // commitTreesTool is the forge's tool for a commit's trees (mesh-catalog modules/gitea): with `paths`, by module directory // (the root spelled ""), the paths relative to it, it answers {root, trees, paths} with paths by directory then path, each // path's git object id; a path not at the commit fails the call naming it (the interface agreed in R4e; mesh-sdk -// delivery.Forge.Paths; novox/hq ADR 0499 clause 3). +// forgemsg.Forge.Paths; novox/hq ADR 0499 clause 3). const commitTreesTool = "gitea.gitea_commit_trees" // toolCaller calls a module's tool, `.`, as the indexer's own account (mesh-sdk process.Conn.Call). @@ -258,8 +259,8 @@ const ( ) // openVia asks the forge which pull requests into the trunk are open now (gitea_open_pulls), every repository's. -func openVia(call toolCaller) func(ctx context.Context) ([]delivery.OpenPull, error) { - return func(context.Context) ([]delivery.OpenPull, error) { +func openVia(call toolCaller) func(ctx context.Context) ([]forgemsg.OpenPull, error) { + return func(context.Context) ([]forgemsg.OpenPull, error) { raw, err := call(openPullsTool, map[string]any{}) if err != nil { return nil, err @@ -275,10 +276,10 @@ func openVia(call toolCaller) func(ctx context.Context) ([]delivery.OpenPull, er if err := json.Unmarshal(raw, &o); err != nil || o.Pulls == nil { return nil, fmt.Errorf("the answer of %s is not {pulls}: %v", openPullsTool, err) } - out := []delivery.OpenPull{} + out := []forgemsg.OpenPull{} for _, p := range *o.Pulls { if p.Base == trunkBranch && p.Number > 0 { - out = append(out, delivery.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number}) + out = append(out, forgemsg.OpenPull{Repository: p.Owner + "/" + p.Repo, Number: p.Number}) } } return out, nil diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go index 069be8be..45012efa 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/packages_integration_test.go @@ -13,6 +13,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/buscompose" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -71,10 +72,10 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T) } serveForge(t, js.Conn(), listing, goSumContent) }) - merged := func(repo string, number int, merge string, f delivery.Forge) { + merged := func(repo string, number int, merge string, f forgemsg.Forge) { t.Helper() - f.Kind, f.Repository, f.Branch, f.Number, f.Head, f.Merge = delivery.ForgeMerged, repo, "feat/x", number, "0123456789abcdef0123456789abcdef01234567", merge - data, err := delivery.EncodeForge(f) + f.Kind, f.Repository, f.Branch, f.Number, f.Head, f.Merge = forgemsg.ForgeMerged, repo, "feat/x", number, "0123456789abcdef0123456789abcdef01234567", merge + data, err := forgemsg.EncodeForge(f) if err != nil { t.Fatal(err) } @@ -101,10 +102,10 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T) "tsconfig.json": "5555555555555555555555555555555555555555"}} } // mesh-sdk at m1, then the catalogue's two modules: their go.sum names mesh-sdk, read by the indexer itself. - merged("novox/mesh-sdk", 70, m1, delivery.Forge{Root: sdkRoot1, Trees: map[string]string{"": sdkRoot1}, + merged("novox/mesh-sdk", 70, m1, forgemsg.Forge{Root: sdkRoot1, Trees: map[string]string{"": sdkRoot1}, Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("6666666666666666666666666666666666666666")}) module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m1 }) - merged("novox/mesh-catalog", 301, catalogMerge, delivery.Forge{Root: "r0", + merged("novox/mesh-catalog", 301, catalogMerge, forgemsg.Forge{Root: "r0", Trees: map[string]string{"modules/importer": "t1", "modules/plain": "t2"}, Manifests: map[string]string{"modules/importer": named("importer"), "modules/plain": named("plain")}, Locks: map[string]map[string]string{"modules/importer": {"go.sum": goSumHash}, "modules/plain": {"go.sum": goSumHash}}}) @@ -140,11 +141,11 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T) // over its named tree, is not written (issue 1758, ADR 0499 clause 6; the operator, 2026-10-25: a module's value moves // only with the module). const goOnly = "4444444444444444444444444444444444444444" - merged("novox/mesh-sdk", 72, goOnly, delivery.Forge{Root: "cccccccccccccccccccccccccccccccccccccccc", + merged("novox/mesh-sdk", 72, goOnly, forgemsg.Forge{Root: "cccccccccccccccccccccccccccccccccccccccc", Trees: map[string]string{"": "cccccccccccccccccccccccccccccccccccccccc"}, Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("6666666666666666666666666666666666666666")}) // mesh-sdk merges a change of buildstep alone (its version moves: src changed too). - merged("novox/mesh-sdk", 71, m2, delivery.Forge{Root: sdk2, Trees: map[string]string{"": sdk2}, + merged("novox/mesh-sdk", 71, m2, forgemsg.Forge{Root: sdk2, Trees: map[string]string{"": sdk2}, Manifests: map[string]string{"": string(sdkManifest)}, Paths: sdkObjects("7777777777777777777777777777777777777777")}) sdkAt2 := module("mesh-sdk", func(m delivery.Module) bool { return m.Commit == m2 }) if sdkAt2.Inputs2 == nil || !strings.HasPrefix(sdkAt2.Inputs2.Tree, delivery.NamedTreePrefix) { diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/verdicts.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/verdicts.go index 495cb684..3526751a 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/verdicts.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/verdicts.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" "github.com/nats-io/nats.go" @@ -65,13 +66,13 @@ func sayAccepted(js jetstream.JetStream, cred process.Account, logf func(string, // sayTakeIn publishes a take-in event on the indexer's own stream take-in (novox/hq ADR 0633 §5): // `mesh.stream..event.take-in..`. A publish that fails is the caller's to say. -func sayTakeIn(js jetstream.JetStream, cred process.Account) func(context.Context, delivery.TakeIn) error { - return func(ctx context.Context, t delivery.TakeIn) error { - event, err := delivery.TakeInEvent(t.Kind, t.Key) +func sayTakeIn(js jetstream.JetStream, cred process.Account) func(context.Context, takein.TakeIn) error { + return func(ctx context.Context, t takein.TakeIn) error { + event, err := takein.TakeInEvent(t.Kind, t.Key) if err != nil { return err } - data, err := delivery.EncodeTakeIn(t) + data, err := takein.EncodeTakeIn(t) if err != nil { return err } diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 4c66d058..0f4eee43 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-indexer go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) @@ -24,6 +24,14 @@ require ( replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ../mesh-delivery-builder/buildrun -require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 +require ( + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 +) replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ./takein + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg => ../mesh-delivery-forge-bridge/forgemsg diff --git a/modules/mesh-delivery-indexer/go.sum b/modules/mesh-delivery-indexer/go.sum index 7679a5dc..1c3af991 100644 --- a/modules/mesh-delivery-indexer/go.sum +++ b/modules/mesh-delivery-indexer/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/internal/index/candidate_manifests_test.go b/modules/mesh-delivery-indexer/internal/index/candidate_manifests_test.go index a43f4e99..0a25604d 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidate_manifests_test.go +++ b/modules/mesh-delivery-indexer/internal/index/candidate_manifests_test.go @@ -7,12 +7,13 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) -func manifestPull(t *testing.T, number int, head, manifest string) delivery.Forge { - f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: number, Head: head, - Action: delivery.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true} +func manifestPull(t *testing.T, number int, head, manifest string) forgemsg.Forge { + f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: number, Head: head, + Action: forgemsg.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true} if manifest != "" { f.Manifests = map[string]string{"modules/avahi": manifest} } diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index 75b6ea90..31d23828 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -11,6 +11,8 @@ import ( "sync/atomic" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -98,7 +100,7 @@ type Indexer struct { // ReadOpen asks the forge which pull requests into the trunk are open now (gitea_open_pulls). A catalogue pull message // that finds the candidates of another pull request asks it and deletes those of a pull request no longer open, so a // closed message the indexer missed heals at the next one (issue 1126); nil asks none. - ReadOpen func(ctx context.Context) ([]delivery.OpenPull, error) + ReadOpen func(ctx context.Context) ([]forgemsg.OpenPull, error) mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach source sourceNow @@ -106,7 +108,7 @@ type Indexer struct { toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go) tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go) unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread - moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go) + moving map[string]forgemsg.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go) retrying atomic.Bool // a RetryUnread runs in the background (kickRetry) retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on @@ -125,7 +127,7 @@ type Indexer struct { // takes every merge in at once, as before the take-in step. Pending Store // SayTakeIn publishes a take-in event on the indexer's own stream take-in (ADR 0633 §5); nil says none. - SayTakeIn func(ctx context.Context, t delivery.TakeIn) error + SayTakeIn func(ctx context.Context, t takein.TakeIn) error pendingMu sync.Mutex // one take-in step at a time: a merge, a group's end, the start and a hand act (pending.go) } @@ -139,23 +141,23 @@ func (ix *Indexer) logf(format string, args ...any) { // pull request, a closed one or a merged message takes it out; a group left with none is deleted. A change is written // by compare-and-set on the revision read, and read again on a conflict. A lone pull request is a group of one. A // merged message also writes the fingerprint of each module of its repository (modules.go). -func (ix *Indexer) Apply(ctx context.Context, f delivery.Forge) error { +func (ix *Indexer) Apply(ctx context.Context, f forgemsg.Forge) error { _, err := ix.ApplyMessage(ctx, f) return err } // ApplyMessage is Apply, and answers which modules a merged message wrote and kept (for the indexer's heard state). Each // applied merged message is said in one line: its repository, commit and pull request, and the modules written. -func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied, error) { +func (ix *Indexer) ApplyMessage(ctx context.Context, f forgemsg.Forge) (Applied, error) { ix.kickJudge(ctx) var applied Applied - if f.Kind == delivery.ForgeOpen { + if f.Kind == forgemsg.ForgeOpen { return applied, ix.applyOpen(ctx, f) } - if f.Kind != delivery.ForgePull && f.Kind != delivery.ForgeMerged { + if f.Kind != forgemsg.ForgePull && f.Kind != forgemsg.ForgeMerged { return applied, nil } - if f.Kind == delivery.ForgeMerged && ix.Modules != nil { + if f.Kind == forgemsg.ForgeMerged && ix.Modules != nil { w, err := ix.applyMerged(ctx, f) applied = Applied{Written: w.written, Late: w.late} if err != nil { @@ -184,7 +186,7 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied, } var digests map[string]string var held map[string]delivery.Module - if f.Kind == delivery.ForgePull { + if f.Kind == forgemsg.ForgePull { var err error if digests, err = ix.keepManifests(ctx, f); err != nil { return applied, err @@ -251,8 +253,8 @@ func (ix *Indexer) ApplyMessage(ctx context.Context, f delivery.Forge) (Applied, // applyOpen takes an open message into the candidates (novox/hq ADR 0443 §5): every pull request it does not name is // taken out of its group, and a group left with none is deleted. What the message names is left as it is. A pull request // opened after the bridge read the forge is heard after this message and added again by its own. -func (ix *Indexer) applyOpen(ctx context.Context, f delivery.Forge) error { - named := make(map[delivery.OpenPull]bool, len(f.Open)) +func (ix *Indexer) applyOpen(ctx context.Context, f forgemsg.Forge) error { + named := make(map[forgemsg.OpenPull]bool, len(f.Open)) for _, p := range f.Open { named[p] = true } @@ -270,7 +272,7 @@ func (ix *Indexer) applyOpen(ctx context.Context, f delivery.Forge) error { return nil, false, fmt.Errorf("candidates.%s holds what is not a candidate: %w", key, err) } kept := slices.DeleteFunc(slices.Clone(c.Pulls), func(p delivery.CandidatePull) bool { - return !named[delivery.OpenPull{Repository: p.Repository, Number: p.Number}] + return !named[forgemsg.OpenPull{Repository: p.Repository, Number: p.Number}] }) switch { case len(kept) == len(c.Pulls): @@ -337,14 +339,14 @@ func write(ctx context.Context, st Store, key string, compute func(value []byte, // its manifest and why it has no fingerprint of the new form (NoneNoPreview), and the candidate its FPSet (issue 1451); a // pull request whose head is held again without its directories keeps the Fps it had. digests are the manifests' digests by // directory; held are the modules the modules bucket holds, which name the module of a changed directory (moduleOfDir). -func (ix *Indexer) next(cur *delivery.Candidate, f delivery.Forge, digests map[string]string, held map[string]delivery.Module) *delivery.Candidate { +func (ix *Indexer) next(cur *delivery.Candidate, f forgemsg.Forge, digests map[string]string, held map[string]delivery.Module) *delivery.Candidate { var pulls []delivery.CandidatePull var base uint64 if cur != nil { pulls, base = slices.Clone(cur.Pulls), cur.Base } at := slices.IndexFunc(pulls, func(p delivery.CandidatePull) bool { return p.Repository == f.Repository && p.Number == f.Number }) - if f.Kind == delivery.ForgeMerged || f.Action == delivery.PullClosed { + if f.Kind == forgemsg.ForgeMerged || f.Action == forgemsg.PullClosed { if at < 0 { return cur } diff --git a/modules/mesh-delivery-indexer/internal/index/control_records.go b/modules/mesh-delivery-indexer/internal/index/control_records.go index 0feb4ec3..19941487 100644 --- a/modules/mesh-delivery-indexer/internal/index/control_records.go +++ b/modules/mesh-delivery-indexer/internal/index/control_records.go @@ -10,6 +10,7 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -27,7 +28,7 @@ var ( // writePredefined keeps the predefined list a merged message carries, unless the key names a later merge: a merge older // than the one it names never replaces it (ADR 0348 §7). The same list at the same merge writes nothing (ADR 0289 §1). -func (ix *Indexer) writePredefined(ctx context.Context, f delivery.Forge) error { +func (ix *Indexer) writePredefined(ctx context.Context, f forgemsg.Forge) error { if ix.Predefined == nil || len(f.Predefined) == 0 { return nil } @@ -203,8 +204,8 @@ func (ix *Indexer) dropConfigCandidate(ctx context.Context, repository string, n // configCandidateOfPull is a pull message's part in config-candidates: a head whose manifests the message carries is written, // a closed one deleted. A head held again without its directories leaves the key as it is. -func (ix *Indexer) configCandidateOfPull(ctx context.Context, f delivery.Forge, held map[string]delivery.Module) error { - if f.Action == delivery.PullClosed { +func (ix *Indexer) configCandidateOfPull(ctx context.Context, f forgemsg.Forge, held map[string]delivery.Module) error { + if f.Action == forgemsg.PullClosed { return ix.dropConfigCandidate(ctx, f.Repository, f.Number) } if !f.DirsKnown { @@ -220,7 +221,7 @@ func (ix *Indexer) configCandidateOfPull(ctx context.Context, f delivery.Forge, // configCandidatesAfterMerge writes again every open pull request's key that names a module the merge changed, since what // it adds is judged against the trunk (ADR 0565 §2), from the head's manifests its candidate keeps; and deletes the merged // pull request's. -func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f delivery.Forge, changed []string) error { +func (ix *Indexer) configCandidatesAfterMerge(ctx context.Context, f forgemsg.Forge, changed []string) error { if ix.ConfigCandidates == nil { return nil } diff --git a/modules/mesh-delivery-indexer/internal/index/control_records_test.go b/modules/mesh-delivery-indexer/internal/index/control_records_test.go index 119a7fe8..358d802b 100644 --- a/modules/mesh-delivery-indexer/internal/index/control_records_test.go +++ b/modules/mesh-delivery-indexer/internal/index/control_records_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -77,7 +78,7 @@ func TestTheControlRecords(t *testing.T) { t.Errorf("a head declaring nothing new = %+v, want an empty set", c) } closed := pull(catalogueRepo, 7, "feat/y", strings.Repeat("d", 40)) - closed.Action = delivery.PullClosed + closed.Action = forgemsg.PullClosed if err := ix.Apply(ctx, closed); err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-indexer/internal/index/index_test.go b/modules/mesh-delivery-indexer/internal/index/index_test.go index b584aa67..a29adba7 100644 --- a/modules/mesh-delivery-indexer/internal/index/index_test.go +++ b/modules/mesh-delivery-indexer/internal/index/index_test.go @@ -5,6 +5,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -91,9 +92,9 @@ func buildRecord(t *testing.T, repo, module, path, at string) []byte { return logged(t, "build-"+module, module, repo, path, "", "", at) } -func pull(repo string, number int, branch, head string, dirs ...string) delivery.Forge { - return delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: repo, Branch: branch, Number: number, Head: head, - Action: delivery.PullUpdated, Dirs: dirs, DirsKnown: dirs != nil} +func pull(repo string, number int, branch, head string, dirs ...string) forgemsg.Forge { + return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: repo, Branch: branch, Number: number, Head: head, + Action: forgemsg.PullUpdated, Dirs: dirs, DirsKnown: dirs != nil} } func newIndexer(t *testing.T) (*Indexer, *memStore, *strings.Builder) { @@ -212,13 +213,13 @@ func TestClosedAndMergedTakeAPullOutAndTheLastDeletesTheKey(t *testing.T) { // 1 ctx := context.Background() _ = ix.Apply(ctx, pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a")) _ = ix.Apply(ctx, pull("novox/mesh-catalog", 300, "feat/x", h2, "modules/c")) - gone := func(repo string, n int, head string) delivery.Forge { + gone := func(repo string, n int, head string) forgemsg.Forge { if how == "closed" { f := pull(repo, n, "feat/x", head) - f.Action = delivery.PullClosed + f.Action = forgemsg.PullClosed return f } - return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/x", Number: n, Head: head, Merge: h3, Root: "r", Trees: map[string]string{"a": "t"}} + return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: n, Head: head, Merge: h3, Root: "r", Trees: map[string]string{"a": "t"}} } if err := ix.Apply(ctx, gone("novox/mesh-delivery", 12, h1)); err != nil { t.Fatal(err) @@ -330,8 +331,8 @@ func TestAnOddBranchNameKeysEscaped(t *testing.T) { } } -func open(pulls ...delivery.OpenPull) delivery.Forge { - return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeOpen, Open: pulls} +func open(pulls ...forgemsg.OpenPull) forgemsg.Forge { + return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeOpen, Open: pulls} } // 1279/AC7 (ADR 0443 §5): on an open message, candidates lose every pull request it does not name, a group left empty is @@ -339,7 +340,7 @@ func open(pulls ...delivery.OpenPull) delivery.Forge { func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/AC7 ix, st, _ := newIndexer(t) ctx := context.Background() - for _, f := range []delivery.Forge{ + for _, f := range []forgemsg.Forge{ pull("novox/mesh-delivery", 12, "feat/x", h1, "modules/a"), pull("novox/mesh-catalog", 300, "feat/x", h2, "modules/c"), pull("novox/mesh-delivery", 13, "feat/y", h2, "modules/a"), pull("novox/mesh-catalog", 280, "feat/old", h3, "modules/c"), @@ -351,7 +352,7 @@ func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/A before, _ := read(t, st, "feat/x") // Open: mesh-delivery#12 only (and a pull request no group holds, which changes nothing). - if err := ix.Apply(ctx, open(delivery.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, delivery.OpenPull{Repository: "novox/mesh-catalog", Number: 999})); err != nil { + if err := ix.Apply(ctx, open(forgemsg.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, forgemsg.OpenPull{Repository: "novox/mesh-catalog", Number: 999})); err != nil { t.Fatal(err) } c, ok := read(t, st, "feat/x") @@ -374,7 +375,7 @@ func TestAnOpenMessageDropsEveryCandidateItDoesNotName(t *testing.T) { // 1279/A } // The same open message again changes nothing (no write), and an empty one drops every group. writes := st.writes - if err := ix.Apply(ctx, open(delivery.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, delivery.OpenPull{Repository: "novox/mesh-catalog", Number: 300})); err != nil { + if err := ix.Apply(ctx, open(forgemsg.OpenPull{Repository: "novox/mesh-delivery", Number: 12}, forgemsg.OpenPull{Repository: "novox/mesh-catalog", Number: 300})); err != nil { t.Fatal(err) } if st.writes != writes { diff --git a/modules/mesh-delivery-indexer/internal/index/manifests.go b/modules/mesh-delivery-indexer/internal/index/manifests.go index 740e9ce7..78340eb7 100644 --- a/modules/mesh-delivery-indexer/internal/index/manifests.go +++ b/modules/mesh-delivery-indexer/internal/index/manifests.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -48,7 +49,7 @@ func (ix *Indexer) keepManifest(ctx context.Context, module, raw string) (string // keepManifests writes the manifest of each directory a message carries, and answers the digest of each by directory. // A manifest that cannot be kept is left out and said in the log, so the module reads as one with no manifest. -func (ix *Indexer) keepManifests(ctx context.Context, f delivery.Forge) (map[string]string, error) { +func (ix *Indexer) keepManifests(ctx context.Context, f forgemsg.Forge) (map[string]string, error) { digests := make(map[string]string, len(f.Manifests)) for dir, raw := range f.Manifests { module := manifestModule(raw) // the name the manifest gives itself: one digest always has one module diff --git a/modules/mesh-delivery-indexer/internal/index/manifests_test.go b/modules/mesh-delivery-indexer/internal/index/manifests_test.go index b9817ec9..132aeed4 100644 --- a/modules/mesh-delivery-indexer/internal/index/manifests_test.go +++ b/modules/mesh-delivery-indexer/internal/index/manifests_test.go @@ -5,6 +5,7 @@ import ( "os" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -37,7 +38,7 @@ func manifestsIndexer(t *testing.T, recs ...[2]any) (*Indexer, *memStore, *memSt return ix, mods, manifests } -func withManifests(f delivery.Forge, manifests map[string]string, locks map[string]map[string]string) delivery.Forge { +func withManifests(f forgemsg.Forge, manifests map[string]string, locks map[string]map[string]string) forgemsg.Forge { f.Manifests, f.Locks = manifests, locks return f } @@ -81,8 +82,8 @@ func sameManifestAppliedAgainIsWrittenOnce(t *testing.T) { // 1449/AC2 if err := ix.Apply(context.Background(), f); err != nil { t.Errorf("a second merge: %v", err) } - pull := delivery.Forge{V: delivery.Version, Kind: delivery.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: 301, Head: h1, - Action: delivery.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true, Manifests: map[string]string{"modules/avahi": avahi}} + pull := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: catalogueRepo, Branch: "feat/x", Number: 301, Head: h1, + Action: forgemsg.PullUpdated, Dirs: []string{"modules/avahi"}, DirsKnown: true, Manifests: map[string]string{"modules/avahi": avahi}} if err := ix.Apply(context.Background(), pull); err != nil { t.Errorf("a pull message: %v", err) } diff --git a/modules/mesh-delivery-indexer/internal/index/modules.go b/modules/mesh-delivery-indexer/internal/index/modules.go index 241bd88e..87c621f0 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules.go +++ b/modules/mesh-delivery-indexer/internal/index/modules.go @@ -7,6 +7,7 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -31,20 +32,20 @@ import ( // // A merge never moves a module backwards (order.go): a merged message older than the repository's taken merge writes and // deletes nothing, and is said; it is asked once, before anything is written. -func (ix *Indexer) applyMerged(ctx context.Context, f delivery.Forge) (*mergeWrite, error) { +func (ix *Indexer) applyMerged(ctx context.Context, f forgemsg.Forge) (*mergeWrite, error) { return ix.applyManifests(ctx, f, true) } // applyManifests is applyMerged; drop says whether the modules of the repository the message holds no manifest for are // deleted. A message that names only some of a repository's modules (a registered module read on its own, registered.go) // drops none. -func (ix *Indexer) applyManifests(ctx context.Context, f delivery.Forge, drop bool) (*mergeWrite, error) { +func (ix *Indexer) applyManifests(ctx context.Context, f forgemsg.Forge, drop bool) (*mergeWrite, error) { return ix.applyMoving(ctx, f, drop, nil) } // applyMoving is applyManifests; moves names the modules keys the message may take from the repository that holds them // (a move handed over, moves.go). Every other module held of another repository is left, said and kept waiting. -func (ix *Indexer) applyMoving(ctx context.Context, f delivery.Forge, drop bool, moves map[string]bool) (*mergeWrite, error) { +func (ix *Indexer) applyMoving(ctx context.Context, f forgemsg.Forge, drop bool, moves map[string]bool) (*mergeWrite, error) { w := newMergeWrite(f) if drop { if err := ix.lateMerge(ctx, w); err != nil || w.late != "" { @@ -180,7 +181,7 @@ func withV(m delivery.Module) delivery.Module { m.V = delivery.Version; return m // namedObjects are, by module directory, the git objects of the paths each manifest of the merge names for every artifact // (catalogue Manifest.NamedPaths): those the message carries, else asked of the forge at the merge commit in one call // (Named). A forge that cannot say gives, for each directory asked, its words, so the module's reason names them. -func (ix *Indexer) namedObjects(ctx context.Context, f delivery.Forge) (map[string]map[string]string, map[string]string) { +func (ix *Indexer) namedObjects(ctx context.Context, f forgemsg.Forge) (map[string]map[string]string, map[string]string) { objects := map[string]map[string]string{} need := map[string][]string{} for dir, raw := range f.Manifests { @@ -220,7 +221,7 @@ func (ix *Indexer) namedObjects(ctx context.Context, f delivery.Forge) (map[stri // held(key, module) says whether the trunk holds it. A module of another repository, or of none known, is never touched. func (ix *Indexer) dropRemoved(ctx context.Context, repository string, held map[string]delivery.Module, trunkHolds func(key string, m delivery.Module) bool) error { var handed []string - elsewhere := map[string]delivery.Forge{} + elsewhere := map[string]forgemsg.Forge{} for _, key := range slices.Sorted(maps.Keys(held)) { cur := held[key] if cur.Repository == "" || !delivery.SameRepository(cur.Repository, repository) || trunkHolds(key, cur) { @@ -354,7 +355,7 @@ func unreadInputs(tree string, locks, paths map[string]string, packages []string // goSumPackages are the version line's packages of the merge's directory dir (ADR 0573 §2, ADR 0593 §2): the mesh Go // modules of other repositories its go.sum names, and those of its own repository its go.mod replaces by a directory // (siblingPackages), which no go.sum line names. A directory with no go.sum is no Go module and has none. -func (ix *Indexer) goSumPackages(ctx context.Context, f delivery.Forge, dir string, cur delivery.Module, has bool) []string { +func (ix *Indexer) goSumPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string { others := ix.otherPackages(ctx, f, dir, cur, has) if f.Locks[dir]["go.sum"] == "" && len(f.Packages[dir]) == 0 { return others @@ -368,7 +369,7 @@ func (ix *Indexer) goSumPackages(ctx context.Context, f delivery.Forge, dir stri // siblingPackages are the mesh Go modules of the merge's own repository the go.mod of dir replaces by a directory, read // from the forge (GoMod); where it cannot be read, the held module's own-repository packages stand, said. -func (ix *Indexer) siblingPackages(ctx context.Context, f delivery.Forge, dir string, cur delivery.Module, has bool) []string { +func (ix *Indexer) siblingPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string { held := func() []string { if !has || cur.Inputs2 == nil { return nil @@ -395,7 +396,7 @@ func (ix *Indexer) siblingPackages(ctx context.Context, f delivery.Forge, dir st // otherPackages are the mesh Go modules of other repositories the go.sum of the merge's directory dir names: those the // message carries (an older bridge), else the held module's while its go.sum's hash is unchanged, else the go.sum read // from the forge (GoSum). -func (ix *Indexer) otherPackages(ctx context.Context, f delivery.Forge, dir string, cur delivery.Module, has bool) []string { +func (ix *Indexer) otherPackages(ctx context.Context, f forgemsg.Forge, dir string, cur delivery.Module, has bool) []string { if carried := f.Packages[dir]; len(carried) > 0 { return fingerprint.OtherPackages(carried, f.Repository) // its own repository's come with its tree } diff --git a/modules/mesh-delivery-indexer/internal/index/modules_test.go b/modules/mesh-delivery-indexer/internal/index/modules_test.go index 294e2ff5..370b0a88 100644 --- a/modules/mesh-delivery-indexer/internal/index/modules_test.go +++ b/modules/mesh-delivery-indexer/internal/index/modules_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -71,12 +72,12 @@ func rec(t *testing.T, id, module, repo, path, ref, state, at, inputs string) [2 // mergedMsg is a merged message of the repository with these module directories' trees, each with a manifest naming its // module, the directory's last element ("r" for the root), as the forge bridge sends it (one manifest per directory that // holds a module.json). -func mergedMsg(repo string, trees map[string]string) delivery.Forge { +func mergedMsg(repo string, trees map[string]string) forgemsg.Forge { manifests := map[string]string{} for dir := range trees { manifests[dir] = `{"module":"` + nameOfDir(dir) + `","version":"1"}` } - return delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests} + return forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repo, Branch: "feat/x", Number: 12, Head: h1, Merge: merge, Root: "r0", Trees: trees, Manifests: manifests} } func nameOfDir(dir string) string { @@ -459,7 +460,7 @@ func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) { gone.Trees, gone.Manifests = nil, nil for _, tc := range []struct { name string - merges []delivery.Forge + merges []forgemsg.Forge repository string // modules.a's repository at the end, "" for none deletedKeyA bool // Issue 1367: a fresh indexer, no wait in memory, the source naming heads; trunk is the module directories the forge @@ -472,20 +473,20 @@ func TestAModuleMovesBetweenRepositoriesOnlyWhenItsTrunkDropsIt(t *testing.T) { stored bool bGoneAtEnd bool }{ - {name: "named while its trunk holds it: left", merges: []delivery.Forge{held, named}, repository: from}, - {name: "named, then dropped by its trunk: handed over in place", merges: []delivery.Forge{held, named, dropped}, repository: to}, - {name: "dropped by its trunk, then named: deleted, then written new", merges: []delivery.Forge{held, dropped, named}, repository: to, deletedKeyA: true}, - {name: "dropped by its trunk with no merge naming it: deleted", merges: []delivery.Forge{held, dropped}, deletedKeyA: true}, + {name: "named while its trunk holds it: left", merges: []forgemsg.Forge{held, named}, repository: from}, + {name: "named, then dropped by its trunk: handed over in place", merges: []forgemsg.Forge{held, named, dropped}, repository: to}, + {name: "dropped by its trunk, then named: deleted, then written new", merges: []forgemsg.Forge{held, dropped, named}, repository: to, deletedKeyA: true}, + {name: "dropped by its trunk with no merge naming it: deleted", merges: []forgemsg.Forge{held, dropped}, deletedKeyA: true}, {name: "no module.json left, another trunk holds it, nothing waits in memory: written from that trunk, never deleted", - merges: []delivery.Forge{held, gone}, heads: true, trunk: []string{"modules/a", "modules/other"}, repository: to, bGoneAtEnd: true}, + merges: []forgemsg.Forge{held, gone}, heads: true, trunk: []string{"modules/a", "modules/other"}, repository: to, bGoneAtEnd: true}, {name: "no module.json left, no other trunk holds it: deleted", - merges: []delivery.Forge{held, gone}, heads: true, deletedKeyA: true, bGoneAtEnd: true}, + merges: []forgemsg.Forge{held, gone}, heads: true, deletedKeyA: true, bGoneAtEnd: true}, {name: "taken in by repository, a head with no module.json: handed over from the other trunk", - merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: from, answer: "handed over", stored: true, repository: to, bGoneAtEnd: true}, + merges: []forgemsg.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: from, answer: "handed over", stored: true, repository: to, bGoneAtEnd: true}, {name: "taken in by repository, a head with modules: answered by name", - merges: []delivery.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: to, answer: "by name", repository: from}, + merges: []forgemsg.Forge{held}, heads: true, trunk: []string{"modules/a"}, takeIn: to, answer: "by name", repository: from}, {name: "taken in by repository, no head named: answered so", - merges: []delivery.Forge{held}, heads: true, takeIn: "novox/nobody", answer: "names no head", repository: from}, + merges: []forgemsg.Forge{held}, heads: true, takeIn: "novox/nobody", answer: "names no head", repository: from}, } { t.Run(tc.name, func(t *testing.T) { mods := &deletesSeen{memStore: newMem()} diff --git a/modules/mesh-delivery-indexer/internal/index/moves.go b/modules/mesh-delivery-indexer/internal/index/moves.go index e192731a..04553296 100644 --- a/modules/mesh-delivery-indexer/internal/index/moves.go +++ b/modules/mesh-delivery-indexer/internal/index/moves.go @@ -7,6 +7,7 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -14,9 +15,9 @@ import ( // holds it at the head the latest source names: the module directories there are asked of the forge (Trunk) and the manifest of // each directory named like the module is read (Read). It answers the merge to hand the module over with, or none: a forge // that cannot say leaves the module to be dropped as it was, said (issue 1367). -func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository string) (delivery.Forge, bool) { +func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository string) (forgemsg.Forge, bool) { if ix.Trunk == nil || ix.Read == nil { - return delivery.Forge{}, false + return forgemsg.Forge{}, false } ix.mu.Lock() heads := maps.Clone(ix.source.heads) @@ -45,7 +46,7 @@ func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository st if read.Trees[dir] == "" || manifestModule(raw) != module { continue } - f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: other, Merge: head, Root: read.Root, + f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: other, Merge: head, Root: read.Root, Trees: map[string]string{dir: read.Trees[dir]}, Manifests: map[string]string{dir: raw}} if l := read.Locks[dir]; len(l) > 0 { f.Locks = map[string]map[string]string{dir: l} @@ -57,7 +58,7 @@ func (ix *Indexer) heldByAnotherTrunk(ctx context.Context, module, repository st return f, true } } - return delivery.Forge{}, false + return forgemsg.Forge{}, false } // nameOfDirectory is the last element of a module directory. @@ -76,8 +77,8 @@ func nameOfDirectory(dir string) string { return dir[strings.LastIndex(dir, "/") // waitToMove keeps, by modules key, the part of a merged message naming one module directory the indexer holds of another // repository. A later merge of the same repository replaces it. -func (ix *Indexer) waitToMove(key string, f delivery.Forge, dir string) { - one := delivery.Forge{V: f.V, Kind: f.Kind, Repository: f.Repository, Branch: f.Branch, Number: f.Number, Merge: f.Merge, Root: f.Root, +func (ix *Indexer) waitToMove(key string, f forgemsg.Forge, dir string) { + one := forgemsg.Forge{V: f.V, Kind: f.Kind, Repository: f.Repository, Branch: f.Branch, Number: f.Number, Merge: f.Merge, Root: f.Root, Trees: map[string]string{dir: f.Trees[dir]}, Manifests: map[string]string{dir: f.Manifests[dir]}} if l := f.Locks[dir]; len(l) > 0 { one.Locks = map[string]map[string]string{dir: l} @@ -91,7 +92,7 @@ func (ix *Indexer) waitToMove(key string, f delivery.Forge, dir string) { ix.mu.Lock() defer ix.mu.Unlock() if ix.moving == nil { - ix.moving = map[string]delivery.Forge{} + ix.moving = map[string]forgemsg.Forge{} } ix.moving[key] = one } @@ -117,7 +118,7 @@ func (ix *Indexer) handOver(ctx context.Context, key string) error { } // handOverFrom writes the module at key from the merge f, which names it, in place. -func (ix *Indexer) handOverFrom(ctx context.Context, key string, f delivery.Forge) error { +func (ix *Indexer) handOverFrom(ctx context.Context, key string, f forgemsg.Forge) error { w, err := ix.applyMoving(ctx, f, false, map[string]bool{key: true}) if err != nil { return fmt.Errorf("modules.%s, moving to %s: %w", key, f.Repository, err) diff --git a/modules/mesh-delivery-indexer/internal/index/order.go b/modules/mesh-delivery-indexer/internal/index/order.go index 5fac94da..221125de 100644 --- a/modules/mesh-delivery-indexer/internal/index/order.go +++ b/modules/mesh-delivery-indexer/internal/index/order.go @@ -7,6 +7,7 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -46,7 +47,7 @@ func (w *mergeWrite) taken(digest string) { w.fresh[digest] = true } -func newMergeWrite(f delivery.Forge) *mergeWrite { +func newMergeWrite(f forgemsg.Forge) *mergeWrite { return &mergeWrite{repository: f.Repository, merge: f.Merge} } diff --git a/modules/mesh-delivery-indexer/internal/index/own.go b/modules/mesh-delivery-indexer/internal/index/own.go index bcb0f81f..626b9cf4 100644 --- a/modules/mesh-delivery-indexer/internal/index/own.go +++ b/modules/mesh-delivery-indexer/internal/index/own.go @@ -7,6 +7,7 @@ import ( "path" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -70,7 +71,7 @@ func moduleAt(held map[string]delivery.Module, repository, dir string) (string, // moduleOfDir is the module of a changed directory of a pull request: the module its manifest in the message names, else // the one the modules bucket holds at that directory of the repository, else the directory's last element (the // catalogue's rule for nearly every manifest). -func moduleOfDir(held map[string]delivery.Module, f delivery.Forge, dir string) string { +func moduleOfDir(held map[string]delivery.Module, f forgemsg.Forge, dir string) string { if name := manifestModule(f.Manifests[dir]); name != "" { return name } diff --git a/modules/mesh-delivery-indexer/internal/index/pending.go b/modules/mesh-delivery-indexer/internal/index/pending.go index 6e660acc..19e91f1b 100644 --- a/modules/mesh-delivery-indexer/internal/index/pending.go +++ b/modules/mesh-delivery-indexer/internal/index/pending.go @@ -10,6 +10,8 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -21,7 +23,7 @@ import ( // build of the indexer writes only what building and moving that indexer needs, and the rest stays pending for it. const ( // PendingState is the indexer's state of held merges: the bucket mesh-delivery-indexer_pending, keyed - // delivery.PendingKey, so the key order is the merge order. An old `state` entry (the indexer is foundation, ADR + // takein.PendingKey, so the key order is the merge order. An old `state` entry (the indexer is foundation, ADR // 0595 §11), history 1, reached by the indexer alone. PendingState = "pending" PendingBucket = delivery.IndexerModule + "_" + PendingState @@ -66,23 +68,23 @@ type PendingEntry struct { Key string Revision uint64 Value Pending - Forge delivery.Forge + Forge forgemsg.Forge } // DecodePending reads one pending value and its merged message. -func DecodePending(data []byte) (Pending, delivery.Forge, error) { +func DecodePending(data []byte) (Pending, forgemsg.Forge, error) { var p Pending if err := json.Unmarshal(data, &p); err != nil { - return p, delivery.Forge{}, fmt.Errorf("not a pending value: %w", err) + return p, forgemsg.Forge{}, fmt.Errorf("not a pending value: %w", err) } if p.V != 1 { - return p, delivery.Forge{}, fmt.Errorf("a pending value of version %d, not 1", p.V) + return p, forgemsg.Forge{}, fmt.Errorf("a pending value of version %d, not 1", p.V) } - f, err := delivery.DecodeForge(p.Forge) + f, err := forgemsg.DecodeForge(p.Forge) if err != nil { return p, f, fmt.Errorf("its merged message: %w", err) } - if f.Kind != delivery.ForgeMerged { + if f.Kind != forgemsg.ForgeMerged { return p, f, fmt.Errorf("a pending value holds a %s message, not a merged one", f.Kind) } return p, f, nil @@ -143,7 +145,7 @@ func WaitsOf(entries []PendingEntry) Waits { w.Groups[e.Value.Group] = append(w.Groups[e.Value.Group], e) for _, m := range e.Forge.Members { if _, ok := blocked[strings.ToLower(m.Repository)]; !ok { - blocked[strings.ToLower(m.Repository)] = delivery.GroupTakeInKey(e.Value.Group) + blocked[strings.ToLower(m.Repository)] = takein.GroupTakeInKey(e.Value.Group) } } continue @@ -158,10 +160,10 @@ func WaitsOf(entries []PendingEntry) Waits { } // mergesOf are the merges of entries, oldest first. -func mergesOf(entries []PendingEntry) []delivery.GroupMerge { - out := make([]delivery.GroupMerge, len(entries)) +func mergesOf(entries []PendingEntry) []takein.Merge { + out := make([]takein.Merge, len(entries)) for i, e := range entries { - out[i] = delivery.GroupMerge{Repository: e.Forge.Repository, Number: e.Forge.Number, Merge: e.Forge.Merge} + out[i] = takein.Merge{Repository: e.Forge.Repository, Number: e.Forge.Number, Merge: e.Forge.Merge} } return out } @@ -177,8 +179,8 @@ func sinceOf(entries []PendingEntry) time.Time { } // groupWait is the take-in body of a group being gathered. -func groupWait(kind, group string, held []PendingEntry) delivery.TakeIn { - t := delivery.TakeIn{Kind: kind, Key: delivery.GroupTakeInKey(group), What: delivery.TakeInGroup, Group: group, +func groupWait(kind, group string, held []PendingEntry) takein.TakeIn { + t := takein.TakeIn{Kind: kind, Key: takein.GroupTakeInKey(group), What: takein.TakeInGroup, Group: group, Merges: mergesOf(held), Since: sinceOf(held)} if len(held) > 0 { t.Members = held[len(held)-1].Forge.Members @@ -187,8 +189,8 @@ func groupWait(kind, group string, held []PendingEntry) delivery.TakeIn { } // revisionWait is the take-in body of a revision: its key is its oldest merge's. -func revisionWait(kind string, ready []PendingEntry, waitsFor string) delivery.TakeIn { - return delivery.TakeIn{Kind: kind, Key: ready[0].Key, What: delivery.TakeInRevision, Merges: mergesOf(ready), +func revisionWait(kind string, ready []PendingEntry, waitsFor string) takein.TakeIn { + return takein.TakeIn{Kind: kind, Key: ready[0].Key, What: takein.TakeInRevision, Merges: mergesOf(ready), WaitsFor: waitsFor, Since: sinceOf(ready)} } @@ -196,7 +198,7 @@ func revisionWait(kind string, ready []PendingEntry, waitsFor string) delivery.T // (issue 1790), and until then every merge is taken in at once, so the build this program runs is not needed. A read // that fails otherwise counts as on the bus: the step that follows answers the error. func (ix *Indexer) pendingOnBus(ctx context.Context) bool { - _, _, _, err := ix.Pending.Get(ctx, delivery.PendingKey(0)) + _, _, _, err := ix.Pending.Get(ctx, takein.PendingKey(0)) return !errors.Is(err, process.ErrNotYet) } @@ -216,7 +218,7 @@ func (ix *Indexer) ownBuildNow(ctx context.Context) string { // sayTakeIn publishes a take-in event and keeps what it said on the entries, so indexer_pending reads the last event of // each wait from the state. A publish that fails is said in the log: the next evaluation says it again. -func (ix *Indexer) sayTakeIn(ctx context.Context, t delivery.TakeIn) bool { +func (ix *Indexer) sayTakeIn(ctx context.Context, t takein.TakeIn) bool { if ix.SayTakeIn == nil { return true } @@ -229,7 +231,7 @@ func (ix *Indexer) sayTakeIn(ctx context.Context, t delivery.TakeIn) bool { // markSaid writes on each entry the wait it was said under, by compare-and-set; a lost one is left (its next // evaluation says it again). -func (ix *Indexer) markSaid(ctx context.Context, entries []PendingEntry, t *delivery.TakeIn, change func(*Pending)) { +func (ix *Indexer) markSaid(ctx context.Context, entries []PendingEntry, t *takein.TakeIn, change func(*Pending)) { for _, e := range entries { p := e.Value if t != nil { @@ -253,7 +255,7 @@ func (ix *Indexer) markSaid(ctx context.Context, entries []PendingEntry, t *deli // holds is evaluated (Evaluate): taken in whole when its revision names the build this program runs, or held for the // indexer it names. It answers what was taken in, and a line saying what waits. A message is never written while the // build this program runs is not known (ErrOwnBuildUnknown). -func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Applied, string, error) { +func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f forgemsg.Forge) (Applied, string, error) { if ix.Pending == nil || ix.Modules == nil || !ix.pendingOnBus(ctx) { // The state this delivery brings is not on the bus yet (issue 1790): the merge is taken in at once, as before, // whether or not the build this program runs is known. @@ -266,11 +268,11 @@ func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Ap } ix.pendingMu.Lock() defer ix.pendingMu.Unlock() - raw, err := delivery.EncodeForge(f) + raw, err := forgemsg.EncodeForge(f) if err != nil { return Applied{}, "", fmt.Errorf("%w: %v", ErrUnreadable, err) } - key := delivery.PendingKey(seq) + key := takein.PendingKey(seq) data, err := encodePending(Pending{Forge: raw, Group: f.Group, Since: ix.now(), LastWriter: own}) if err != nil { return Applied{}, "", err @@ -290,7 +292,7 @@ func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Ap return Applied{}, "", err } if held := WaitsOf(entries).Groups[f.Group]; len(held) > 0 { - t := groupWait(delivery.TakeInPending, f.Group, held) + t := groupWait(takein.TakeInPending, f.Group, held) if ix.sayTakeIn(ctx, t) { ix.markSaid(ctx, held, &t, nil) } @@ -304,7 +306,7 @@ func (ix *Indexer) Arrive(ctx context.Context, seq uint64, f delivery.Forge) (Ap // one revision. A member named and neither held nor taken in yet answers an error, so the end is delivered again once // the member arrived. An end of a group taken in already (every member's merge at or behind its repository's taken // merge) changes nothing and is said: it is never delivered again for ever. -func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Applied, string, error) { +func (ix *Indexer) EndGroup(ctx context.Context, end forgemsg.ForgeGroupEnd) (Applied, string, error) { if ix.Pending == nil || !ix.pendingOnBus(ctx) { return Applied{}, "no pending state: the group's merges were taken in one by one", nil } @@ -327,7 +329,7 @@ func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Ap held = append(held, e) } } - isHeld := func(m delivery.GroupMerge) bool { + isHeld := func(m forgemsg.GroupMerge) bool { return slices.ContainsFunc(held, func(e PendingEntry) bool { return delivery.SameRepository(e.Forge.Repository, m.Repository) && e.Forge.Number == m.Number }) @@ -357,7 +359,7 @@ func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Ap } // The group's wait ends first (take-in.taken under its group key); a revision it then is, pending for another // indexer, is said pending under its oldest merge's key by the evaluation that follows. - t := groupWait(delivery.TakeInTaken, end.Group, held) + t := groupWait(takein.TakeInTaken, end.Group, held) said := &t if !ix.sayTakeIn(ctx, t) { said = nil // said again by the take-in or the hold that follows (saidWaits) @@ -368,7 +370,7 @@ func (ix *Indexer) EndGroup(ctx context.Context, end delivery.ForgeGroupEnd) (Ap } // takenAlready says whether a group member's merge is at or behind its repository's taken merge. -func (ix *Indexer) takenAlready(ctx context.Context, m delivery.GroupMerge) (bool, error) { +func (ix *Indexer) takenAlready(ctx context.Context, m forgemsg.GroupMerge) (bool, error) { if ix.Taken == nil { return false, nil } @@ -622,21 +624,21 @@ func byWords(by string) string { } // saidWaits are the taken events of every wait the entries were said under that is not ended yet. -func saidWaits(entries []PendingEntry) []delivery.TakeIn { +func saidWaits(entries []PendingEntry) []takein.TakeIn { by := map[string][]PendingEntry{} for _, e := range entries { - if e.Value.Said != "" && e.Value.SaidKind == delivery.TakeInPending { + if e.Value.Said != "" && e.Value.SaidKind == takein.TakeInPending { by[e.Value.Said] = append(by[e.Value.Said], e) } } - var out []delivery.TakeIn + var out []takein.TakeIn for _, key := range slices.Sorted(maps.Keys(by)) { held := by[key] if strings.HasPrefix(key, "g.") { - out = append(out, groupWait(delivery.TakeInTaken, held[0].Value.Group, held)) + out = append(out, groupWait(takein.TakeInTaken, held[0].Value.Group, held)) continue } - t := revisionWait(delivery.TakeInTaken, held, held[0].Value.WaitsFor) + t := revisionWait(takein.TakeInTaken, held, held[0].Value.WaitsFor) t.Key = key out = append(out, t) } @@ -666,18 +668,18 @@ func (ix *Indexer) holdFor(ctx context.Context, ready []PendingEntry, names, own } } } - t := revisionWait(delivery.TakeInPending, ready, names) + t := revisionWait(takein.TakeInPending, ready, names) var ended, unmarked []PendingEntry said, changed := false, false for _, e := range ready { switch { - case e.Value.Said == t.Key && e.Value.SaidKind == delivery.TakeInPending: + case e.Value.Said == t.Key && e.Value.SaidKind == takein.TakeInPending: said = true changed = changed || e.Value.WaitsFor != names if e.Value.WaitsFor != names { unmarked = append(unmarked, e) } - case e.Value.Said != "" && e.Value.SaidKind == delivery.TakeInPending: + case e.Value.Said != "" && e.Value.SaidKind == takein.TakeInPending: ended = append(ended, e) unmarked = append(unmarked, e) default: @@ -722,28 +724,28 @@ func copyKey(ctx context.Context, from *overlayStore, to Store, key string) erro // PendingWaits is what indexer_pending answers of the pending state's entries (ADR 0633 §5): the ready merges as one // revision, each group being gathered, and the merges behind a group, each with the modules it holds back and the // last take-in event said of it. -func PendingWaits(entries []PendingEntry) []delivery.PendingWait { +func PendingWaits(entries []PendingEntry) []takein.PendingWait { w := WaitsOf(entries) - var out []delivery.PendingWait + var out []takein.PendingWait if len(w.Ready) > 0 { - out = append(out, waitOf(w.Ready, delivery.TakeInRevision, "")) + out = append(out, waitOf(w.Ready, takein.TakeInRevision, "")) } for _, g := range slices.Sorted(maps.Keys(w.Groups)) { - out = append(out, waitOf(w.Groups[g], delivery.TakeInGroup, "")) + out = append(out, waitOf(w.Groups[g], takein.TakeInGroup, "")) } for _, g := range slices.Sorted(maps.Keys(w.Behind)) { - out = append(out, waitOf(w.Behind[g], delivery.TakeInRevision, g)) + out = append(out, waitOf(w.Behind[g], takein.TakeInRevision, g)) } - slices.SortStableFunc(out, func(a, b delivery.PendingWait) int { return a.Since.Compare(b.Since) }) + slices.SortStableFunc(out, func(a, b takein.PendingWait) int { return a.Since.Compare(b.Since) }) return out } -func waitOf(entries []PendingEntry, what, behind string) delivery.PendingWait { - pw := delivery.PendingWait{Key: entries[0].Key, What: what, Merges: mergesOf(entries), Behind: behind, Since: sinceOf(entries), +func waitOf(entries []PendingEntry, what, behind string) takein.PendingWait { + pw := takein.PendingWait{Key: entries[0].Key, What: what, Merges: mergesOf(entries), Behind: behind, Since: sinceOf(entries), WaitsFor: entries[len(entries)-1].Value.WaitsFor, Holds: []string{}} - if what == delivery.TakeInGroup { + if what == takein.TakeInGroup { pw.Group = entries[0].Value.Group - pw.Key = delivery.GroupTakeInKey(pw.Group) + pw.Key = takein.GroupTakeInKey(pw.Group) pw.Members = entries[len(entries)-1].Forge.Members pw.WaitsFor = "" } @@ -755,7 +757,7 @@ func waitOf(entries []PendingEntry, what, behind string) delivery.PendingWait { } } if e.Value.Said != "" && e.Value.SaidAt.After(pw.SaidAt) { - pw.Said, pw.SaidAt = delivery.TakeInStream+"."+e.Value.SaidKind+"."+e.Value.Said, e.Value.SaidAt + pw.Said, pw.SaidAt = takein.TakeInStream+"."+e.Value.SaidKind+"."+e.Value.Said, e.Value.SaidAt } } slices.Sort(pw.Holds) @@ -785,7 +787,7 @@ func (ix *Indexer) TakeInPendingByHand(ctx context.Context, key, by, why string) switch { case strings.HasPrefix(key, "g."): for g, held := range w.Groups { - if delivery.GroupTakeInKey(g) == key { + if takein.GroupTakeInKey(g) == key { take = held } } @@ -838,7 +840,7 @@ func (ix *Indexer) DropPending(ctx context.Context, key, by, why string) TakenIn return r } ix.logf("take-in: %s (%s#%d at %s) dropped by hand, asked by %s: %s; nothing of it is taken in", key, e.Forge.Repository, e.Forge.Number, e.Forge.Merge, by, why) - if e.Value.Said != "" && e.Value.SaidKind == delivery.TakeInPending { + if e.Value.Said != "" && e.Value.SaidKind == takein.TakeInPending { var same []PendingEntry for _, o := range entries { if o.Key != key && o.Value.Said == e.Value.Said { diff --git a/modules/mesh-delivery-indexer/internal/index/pending_test.go b/modules/mesh-delivery-indexer/internal/index/pending_test.go index cca2d25e..51d7157f 100644 --- a/modules/mesh-delivery-indexer/internal/index/pending_test.go +++ b/modules/mesh-delivery-indexer/internal/index/pending_test.go @@ -9,6 +9,8 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -19,8 +21,8 @@ type takeInBed struct { ix *Indexer mods *memStore pending *memStore - said []delivery.TakeIn - base delivery.Forge + said []takein.TakeIn + base forgemsg.Forge logs *strings.Builder } @@ -36,7 +38,7 @@ func newTakeInBed(t *testing.T) *takeInBed { b := &takeInBed{ix: ix, mods: mods, pending: newMem(), base: f, logs: &strings.Builder{}} ix.Verdicts = newMem() ix.Pending = b.pending - ix.SayTakeIn = func(_ context.Context, t delivery.TakeIn) error { b.said = append(b.said, t); return nil } + ix.SayTakeIn = func(_ context.Context, t takein.TakeIn) error { b.said = append(b.said, t); return nil } logf := ix.Logf ix.Logf = func(format string, args ...any) { b.logs.WriteString(format + "\n") @@ -65,7 +67,7 @@ func (b *takeInBed) commit(t *testing.T, module string) string { } // merge is a catalogue merge at commit c whose trees of avahi and the indexer are given ("" keeps the base's). -func (b *takeInBed) merge(number int, c, avahi, indexer string) delivery.Forge { +func (b *takeInBed) merge(number int, c, avahi, indexer string) forgemsg.Forge { f := b.base f.Number, f.Merge, f.Head = number, c, c f.Trees = map[string]string{} @@ -81,7 +83,7 @@ func (b *takeInBed) merge(number int, c, avahi, indexer string) delivery.Forge { return f } -func (b *takeInBed) arrive(t *testing.T, seq uint64, f delivery.Forge) { +func (b *takeInBed) arrive(t *testing.T, seq uint64, f forgemsg.Forge) { t.Helper() if _, _, err := b.ix.Arrive(context.Background(), seq, f); err != nil { t.Fatalf("arrive %d: %v", seq, err) @@ -144,7 +146,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) if n := len(b.ix.Verdicts.(*memStore).vals); n > verdicts+1 { t.Errorf("verdicts %d, want at most %d: only the new indexer manifest's", n, verdicts+1) } - if want := []string{delivery.PendingKey(2)}; !reflect.DeepEqual(b.keys(), want) || !slices.Equal(b.events(), []string{"pending " + delivery.PendingKey(2)}) || b.said[0].WaitsFor != named { + if want := []string{takein.PendingKey(2)}; !reflect.DeepEqual(b.keys(), want) || !slices.Equal(b.events(), []string{"pending " + takein.PendingKey(2)}) || b.said[0].WaitsFor != named { t.Fatalf("pending %v, said %+v: want m.2 pending for %s", b.keys(), b.said, named) } // T5: the merge delivered again writes no second key and says nothing again. @@ -162,7 +164,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) if len(b.keys()) != 0 || b.fp(t, delivery.IndexerModule) != own || b.commit(t, "avahi") != sha('e') { t.Fatalf("revert: pending %v, indexer %s, avahi at %s: want all taken in", b.keys(), b.fp(t, delivery.IndexerModule), b.commit(t, "avahi")) } - if want := []string{"pending " + delivery.PendingKey(2), "taken " + delivery.PendingKey(2)}; !slices.Equal(b.events(), want) { + if want := []string{"pending " + takein.PendingKey(2), "taken " + takein.PendingKey(2)}; !slices.Equal(b.events(), want) { t.Errorf("said %v, want %v", b.events(), want) } }) @@ -192,7 +194,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) if _, _, err := next.TakeInAtStart(ctx, func(context.Context) bool { return true }); err != nil { t.Fatal(err) } - if asked < 2 || !reflect.DeepEqual(b.keys(), []string{delivery.PendingKey(5)}) || b.commit(t, "avahi") != sha('c') { + if asked < 2 || !reflect.DeepEqual(b.keys(), []string{takein.PendingKey(5)}) || b.commit(t, "avahi") != sha('c') { t.Errorf("start: asked %d, pending %v, avahi at %s: want the revision taken in, the group left", asked, b.keys(), b.commit(t, "avahi")) } }) @@ -204,7 +206,7 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) b.ix.Contains = func(_ context.Context, _, commit, in string) (bool, error) { return strings.IndexByte(order, commit[0]) <= strings.IndexByte(order, in[0]), nil } - member := func(repo string, number int, c string) delivery.Forge { + member := func(repo string, number int, c string) forgemsg.Forge { f := b.merge(number, c, "", "") f.Group = "feat/take-in" f.Members = []delivery.Head{{Repository: catalogueRepo, Number: 40, Head: sha('1')}, {Repository: "novox/mesh-sdk", Number: 41, Head: sha('2')}} @@ -223,10 +225,10 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) alone.Repository, alone.Trees, alone.Manifests, alone.Locks = "novox/hq", map[string]string{}, nil, nil b.arrive(t, 2, alone) b.arrive(t, 3, b.merge(51, sha('6'), "tree-avahi-9", "")) - if !reflect.DeepEqual(b.keys(), []string{delivery.PendingKey(1), delivery.PendingKey(3)}) { + if !reflect.DeepEqual(b.keys(), []string{takein.PendingKey(1), takein.PendingKey(3)}) { t.Fatalf("pending %v: want the member and the merge behind it", b.keys()) } - end := delivery.ForgeGroupEnd{Group: "feat/take-in", Members: []delivery.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('7')}, {Repository: "novox/mesh-sdk", Number: 41, Merge: sha('b')}}} + end := forgemsg.ForgeGroupEnd{Group: "feat/take-in", Members: []forgemsg.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('7')}, {Repository: "novox/mesh-sdk", Number: 41, Merge: sha('b')}}} if _, _, err := b.ix.EndGroup(ctx, end); err == nil { t.Fatal("an end before its last member is taken in") } @@ -250,10 +252,10 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) g.Group, g.Trees = "feat/two", b.merge(0, "", "", "tree-indexer-7").Trees g.Members = g.Members[:1] b.arrive(t, 9, g) - if _, _, err := b.ix.EndGroup(ctx, delivery.ForgeGroupEnd{Group: "feat/two", Members: []delivery.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('c')}}}); err != nil { + if _, _, err := b.ix.EndGroup(ctx, forgemsg.ForgeGroupEnd{Group: "feat/two", Members: []forgemsg.GroupMerge{{Repository: catalogueRepo, Number: 40, Merge: sha('c')}}}); err != nil { t.Fatal(err) } - if want := []string{"pending g.feat/two", "taken g.feat/two", "pending " + delivery.PendingKey(9)}; !slices.Equal(b.events(), want) { + if want := []string{"pending g.feat/two", "taken g.feat/two", "pending " + takein.PendingKey(9)}; !slices.Equal(b.events(), want) { t.Errorf("a group naming another indexer: said %v, want %v", b.events(), want) } }) @@ -266,12 +268,12 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) b.ix.ownMu.Unlock() // The other indexer claims the merge between this one's read and its claim. b.pending.beforeWrite = func() { - v := b.pending.vals[delivery.PendingKey(2)] + v := b.pending.vals[takein.PendingKey(2)] var p Pending _ = json.Unmarshal(v, &p) p.Taking = "the other indexer" data, _ := json.Marshal(p) - b.pending.put(delivery.PendingKey(2), data) + b.pending.put(takein.PendingKey(2), data) } if _, _, err := b.ix.TakeInAtStart(ctx, func(context.Context) bool { return false }); err != nil { t.Fatal(err) @@ -284,32 +286,32 @@ func TestTakeIn(t *testing.T) { // 1585/T3-T13 (ADR 0633) b := newTakeInBed(t) b.arrive(t, 2, b.merge(21, sha('c'), "tree-avahi-3", "tree-indexer-2")) b.arrive(t, 3, b.merge(22, sha('d'), "tree-avahi-4", "tree-indexer-2")) - var answer delivery.PendingAnswer + var answer takein.PendingAnswer for _, tool := range PendingTools(b.pending, nil, nil) { if tool.Name == PendingTool { out, err := tool.Run(map[string]any{}) if err != nil { t.Fatal(err) } - answer = out.(delivery.PendingAnswer) + answer = out.(takein.PendingAnswer) } } - if len(answer.Waits) != 1 || answer.Waits[0].Key != delivery.PendingKey(2) || !slices.Equal(answer.Waits[0].Holds, []string{"avahi", delivery.IndexerModule}) || - answer.Waits[0].Said != "take-in.pending."+delivery.PendingKey(2) || len(answer.Waits[0].Keys) != 2 { + if len(answer.Waits) != 1 || answer.Waits[0].Key != takein.PendingKey(2) || !slices.Equal(answer.Waits[0].Holds, []string{"avahi", delivery.IndexerModule}) || + answer.Waits[0].Said != "take-in.pending."+takein.PendingKey(2) || len(answer.Waits[0].Keys) != 2 { t.Fatalf("indexer_pending = %+v", answer) } b.said = nil - if r := b.ix.DropPending(ctx, delivery.PendingKey(2), "lead@shanks", "replaced by m.3"); !r.Stored { + if r := b.ix.DropPending(ctx, takein.PendingKey(2), "lead@shanks", "replaced by m.3"); !r.Stored { t.Fatalf("drop: %+v", r) } - if len(b.said) < 2 || !b.said[0].Dropped || b.said[0].Key != delivery.PendingKey(2) || b.said[0].By != "lead@shanks" || b.said[1].Key != delivery.PendingKey(3) { + if len(b.said) < 2 || !b.said[0].Dropped || b.said[0].Key != takein.PendingKey(2) || b.said[0].By != "lead@shanks" || b.said[1].Key != takein.PendingKey(3) { t.Fatalf("drop said %+v: want m.2 taken (dropped, by) and m.3 pending", b.said) } b.said = nil - if r := b.ix.TakeInPendingByHand(ctx, delivery.PendingKey(3), "lead@shanks", "the named indexer can never run"); !r.Stored { + if r := b.ix.TakeInPendingByHand(ctx, takein.PendingKey(3), "lead@shanks", "the named indexer can never run"); !r.Stored { t.Fatalf("hand take-in: %+v", r) } - if len(b.keys()) != 0 || b.commit(t, "avahi") != sha('d') || len(b.said) != 1 || b.said[0].Kind != delivery.TakeInTaken || b.said[0].By != "lead@shanks" { + if len(b.keys()) != 0 || b.commit(t, "avahi") != sha('d') || len(b.said) != 1 || b.said[0].Kind != takein.TakeInTaken || b.said[0].By != "lead@shanks" { t.Errorf("hand take-in: pending %v, avahi at %s, said %+v", b.keys(), b.commit(t, "avahi"), b.said) } }) diff --git a/modules/mesh-delivery-indexer/internal/index/pending_tools.go b/modules/mesh-delivery-indexer/internal/index/pending_tools.go index 33c08f4c..bf3ab9b7 100644 --- a/modules/mesh-delivery-indexer/internal/index/pending_tools.go +++ b/modules/mesh-delivery-indexer/internal/index/pending_tools.go @@ -5,8 +5,8 @@ import ( "fmt" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" stdio "git.novox.be/novox/mesh-sdk/go" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // PendingTools are the indexer's tools of what waits for a take-in (novox/hq ADR 0633 §5): indexer_pending reads the @@ -46,7 +46,7 @@ func PendingTools(pending Store, ask func(key string, value []byte) (TakeInAsk, if err != nil { return nil, err } - return delivery.PendingAnswer{Waits: PendingWaits(entries), Unread: unread}, nil + return takein.PendingAnswer{Waits: PendingWaits(entries), Unread: unread}, nil }}, {Name: TakeInPendingTool, Description: "Take a wait in by hand with the running indexer and its judge (ADR 0633 §5): the revision under its m. key, " + diff --git a/modules/mesh-delivery-indexer/internal/index/previews_test.go b/modules/mesh-delivery-indexer/internal/index/previews_test.go index 8d25c915..282ad860 100644 --- a/modules/mesh-delivery-indexer/internal/index/previews_test.go +++ b/modules/mesh-delivery-indexer/internal/index/previews_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-indexer/internal/fingerprint" @@ -38,7 +39,7 @@ func avahiOnTools(t *testing.T) string { } // avahiPull is the pull message of the catalogue's pull request 301 touching avahi, with its manifest. -func avahiPull(t *testing.T, head string) delivery.Forge { +func avahiPull(t *testing.T, head string) forgemsg.Forge { t.Helper() f := manifestPull(t, 301, head, avahiOnTools(t)) f.Manifests = map[string]string{"modules/avahi": avahiOnTools(t)} diff --git a/modules/mesh-delivery-indexer/internal/index/registered.go b/modules/mesh-delivery-indexer/internal/index/registered.go index cf0940bb..09bf3d94 100644 --- a/modules/mesh-delivery-indexer/internal/index/registered.go +++ b/modules/mesh-delivery-indexer/internal/index/registered.go @@ -7,6 +7,7 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -125,7 +126,7 @@ func (ix *Indexer) indexAt(ctx context.Context, r Registration, force, reread bo t.why = fmt.Sprintf("the manifest at %q of %s at %s names the module %q", dir, repository, commit, name) return t, nil } - f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: repository, Merge: commit, Root: read.Root, + f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: repository, Merge: commit, Root: read.Root, Trees: map[string]string{dir: read.Trees[dir]}, Manifests: map[string]string{dir: raw}} if l := read.Locks[dir]; len(l) > 0 { @@ -396,7 +397,7 @@ func (ix *Indexer) TakeInRepository(ctx context.Context, repository string) Take out.Answer = "the modules state cannot be read: " + err.Error() return out } - f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: out.Repository, Merge: head} + f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: out.Repository, Merge: head} if _, err := ix.ApplyMessage(ctx, f); err != nil { out.Answer = fmt.Sprintf("the merge of %s at %.12s could not be taken in: %v", repository, head, err) return out diff --git a/modules/mesh-delivery-indexer/internal/index/taken.go b/modules/mesh-delivery-indexer/internal/index/taken.go index 89c02931..a66d7e08 100644 --- a/modules/mesh-delivery-indexer/internal/index/taken.go +++ b/modules/mesh-delivery-indexer/internal/index/taken.go @@ -8,6 +8,7 @@ import ( "slices" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -19,13 +20,13 @@ import ( // say keeps the key as it is and says so. // writeTaken writes the repository's taken key for the merge f carries, unless it names that merge or a later one. -func (ix *Indexer) writeTaken(ctx context.Context, f delivery.Forge) error { +func (ix *Indexer) writeTaken(ctx context.Context, f forgemsg.Forge) error { return ix.writeTakenKnown(ctx, f, "") } // writeTakenKnown is writeTaken where the merge is known not to be older than notOlder (lateMerge asked it, issue 1126): a // key naming that merge is written over without asking the forge again. -func (ix *Indexer) writeTakenKnown(ctx context.Context, f delivery.Forge, notOlder string) error { +func (ix *Indexer) writeTakenKnown(ctx context.Context, f forgemsg.Forge, notOlder string) error { if ix.Taken == nil || f.Merge == "" { return nil } @@ -101,7 +102,7 @@ func (ix *Indexer) FillTaken(ctx context.Context) error { } } for _, repository := range slices.Sorted(maps.Keys(pending)) { - err := ix.writeTaken(ctx, delivery.Forge{Repository: repository, Merge: pending[repository]}) + err := ix.writeTaken(ctx, forgemsg.Forge{Repository: repository, Merge: pending[repository]}) if err == nil { ix.mu.Lock() if ix.untaken[repository] == pending[repository] { diff --git a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go index 435c00c6..4e62b312 100644 --- a/modules/mesh-delivery-indexer/internal/index/toolchains_test.go +++ b/modules/mesh-delivery-indexer/internal/index/toolchains_test.go @@ -10,6 +10,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/process" @@ -91,7 +92,7 @@ func newFormIndexer(t *testing.T, mods ...mod) (*Indexer, *memStore) { } // newFormIndexerUnmerged is the indexer and the catalogue merge of these modules, not yet applied. -func newFormIndexerUnmerged(t *testing.T, mods ...mod) (*Indexer, *memStore, delivery.Forge) { +func newFormIndexerUnmerged(t *testing.T, mods ...mod) (*Indexer, *memStore, forgemsg.Forge) { t.Helper() var recs [][2]any trees, manifests := map[string]string{}, map[string]string{} @@ -468,7 +469,7 @@ func TestAModuleKeepsTheToolchainsItsFingerprintWasComputedWith(t *testing.T) { tools, _ := moduleOf(t, mods, "mesh-tools") sdkMerge := func(commit, tree string) delivery.Module { t.Helper() - f := delivery.Forge{V: delivery.Version, Kind: delivery.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1, + f := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-sdk", Branch: "feat/x", Number: 3, Head: h1, Merge: commit, Root: tree, Trees: map[string]string{"": tree}, Manifests: map[string]string{"": `{"module":"mesh-sdk","version":"1"}`}} if err := ix.Apply(context.Background(), f); err != nil { t.Fatal(err) diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index b8ce4a49..6dd718a7 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -319,7 +320,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { t.Fatalf("the start pass: %+v, this judge's own refusal %+v (%v)", verdict(atStart), verdict(ownRefusal), err) } byPull := stale("tools", `{"module":"tools","version":"8"}`) - pull := delivery.Forge{Kind: delivery.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main", + pull := forgemsg.Forge{Kind: forgemsg.ForgePull, Repository: "novox/mesh-tools", Number: 3, Branch: "main", Manifests: map[string]string{"modules/tools": `{"module":"tools","version":"8"}`}} if _, err := ix.ApplyMessage(ctx, pull); err != nil || verdict(byPull).Refused() || verdict(byPull).Judge != ix.judgeID() { t.Fatalf("a pull carrying a digest an older judge refused: %+v %v", verdict(byPull), err) diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index d8f68d3b..ace5d0ed 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -12,6 +12,7 @@ import ( "strings" "testing" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" @@ -147,7 +148,7 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build } // 1585/T11 (ADR 0633 §1, §5): its one stream, take-in, an old streams entry (the indexer is foundation, ADR 0595 §11), // carries the take-in events. - if want := []streamDecl{{Name: delivery.TakeInStream, Events: []string{"take-in.**"}, MaxAgeDays: 7}}; !reflect.DeepEqual(m.Streams, want) { + if want := []streamDecl{{Name: takein.TakeInStream, Events: []string{"take-in.**"}, MaxAgeDays: 7}}; !reflect.DeepEqual(m.Streams, want) { t.Errorf("streams = %+v, want %+v", m.Streams, want) } // No other bus section, no other seat (ADR 0437 §4). diff --git a/modules/mesh-delivery-indexer/takein/go.mod b/modules/mesh-delivery-indexer/takein/go.mod new file mode 100644 index 00000000..48ab0844 --- /dev/null +++ b/modules/mesh-delivery-indexer/takein/go.mod @@ -0,0 +1,5 @@ +module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein + +go 1.26.0 + +require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 diff --git a/modules/mesh-delivery-indexer/takein/go.sum b/modules/mesh-delivery-indexer/takein/go.sum new file mode 100644 index 00000000..5e18d943 --- /dev/null +++ b/modules/mesh-delivery-indexer/takein/go.sum @@ -0,0 +1,14 @@ +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= +github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= +github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA= +github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k= +github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg= +github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= +golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= +golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= diff --git a/modules/mesh-delivery-indexer/takein/takein.go b/modules/mesh-delivery-indexer/takein/takein.go new file mode 100644 index 00000000..dc53626c --- /dev/null +++ b/modules/mesh-delivery-indexer/takein/takein.go @@ -0,0 +1,251 @@ +// Package takein is the indexer's take-in contract (novox/hq ADR 0633 §1, §5): the events it publishes on its own stream +// `take-in` while a source revision waits for the indexer it names or a delivery group is gathered, their keys, and the +// answer of its tool indexer_pending. The indexer writes them; the watchman and the view read them. It is the indexer's +// own package: one module owns these types, so they live with it and not in mesh-sdk (the operator, 2026-11-04). +package takein + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// Merge is one merge a take-in event or a wait holds: the repository as `/`, its pull request and the +// merge commit the forge made of it. +type Merge struct { + Repository string `json:"repository"` + Number int `json:"number"` + Merge string `json:"merge"` +} + +// checkRepository refuses a repository that is not `/`: one slash, neither part empty. +func checkRepository(repository string) error { + owner, name, ok := strings.Cut(repository, "/") + if !ok || owner == "" || name == "" || strings.Contains(name, "/") { + return fmt.Errorf("delivery: the repository %q is not /, with exactly one slash", repository) + } + return nil +} + +// decode reads a value of delivery.Version, refusing another or none with delivery.ErrUnknownVersion, as mesh-sdk +// delivery's values are read. +func decode(data []byte, into any) error { + var probe struct { + V *int `json:"v"` + } + if err := json.Unmarshal(data, &probe); err != nil { + return fmt.Errorf("delivery: value is not JSON: %w", err) + } + if probe.V == nil || *probe.V != delivery.Version { + return fmt.Errorf("%w: this reader reads %d", delivery.ErrUnknownVersion, delivery.Version) + } + if err := json.Unmarshal(data, into); err != nil { + return fmt.Errorf("delivery: value does not fit its shape: %w", err) + } + return nil +} + +// The take-in events (novox/hq ADR 0633 §1, §5): the indexer publishes them on its own stream, which keeps the last +// message per subject, `take-in..`. A wait starts with `take-in.pending` and ends with `take-in.taken`, +// whoever ended it (the named indexer, a revert, a hand take-in or a drop). The stream's name is the indexer's manifest's; +// these names are its events, relative to it. +const ( + TakeInStream = "take-in" + TakeInPending = "pending" + TakeInTaken = "taken" +) + +// What waits in a take-in event: a source revision for the indexer build it names, or a delivery group for its end. +const ( + TakeInRevision = "revision" + TakeInGroup = "group" +) + +// PendingKey is the key of one held merge in the indexer's pending state, and the key a revision's take-in events +// carry (its oldest merge's): `m.`, so the key order is the merge order. +func PendingKey(seq uint64) string { return fmt.Sprintf("m.%020d", seq) } + +// GroupTakeInKey is the key a group's take-in events carry while it is gathered: `g.`, with every character a +// bus subject cannot carry (a space, `*`, `>`) written `_`. The event's body names the group exactly. +func GroupTakeInKey(group string) string { + return "g." + strings.Map(func(r rune) rune { + if r == '*' || r == '>' || r <= ' ' || r == 0x7f { + return '_' + } + return r + }, group) +} + +// TakeInEvent is the name of a take-in event of the given kind and key: `take-in..`. +func TakeInEvent(kind, key string) (string, error) { + if kind != TakeInPending && kind != TakeInTaken { + return "", fmt.Errorf("delivery: the take-in kind %q is not pending or taken", kind) + } + if err := checkTakeInKey(key); err != nil { + return "", err + } + return TakeInStream + "." + kind + "." + key, nil +} + +// ParseTakeInEvent is the inverse of TakeInEvent. +func ParseTakeInEvent(event string) (kind, key string, err error) { + parts := strings.SplitN(event, ".", 3) + if len(parts) != 3 || parts[0] != TakeInStream { + return "", "", fmt.Errorf("delivery: %q is not a take-in event, take-in..", event) + } + if _, err := TakeInEvent(parts[1], parts[2]); err != nil { + return "", "", fmt.Errorf("delivery: %q is not a take-in event: %w", event, err) + } + return parts[1], parts[2], nil +} + +func checkTakeInKey(key string) error { + if !strings.HasPrefix(key, "m.") && !strings.HasPrefix(key, "g.") { + return fmt.Errorf("delivery: the take-in key %q is neither m. nor g.", key) + } + for _, tok := range strings.Split(key, ".") { + if tok == "" || strings.ContainsAny(tok, "*> \t\r\n") { + return fmt.Errorf("delivery: the take-in key %q is not a bus subject's tokens", key) + } + } + return nil +} + +// TakeIn is the body of a take-in event. Kind is the event's kind and Key its key. What says what waits: a revision +// (Key its oldest merge's PendingKey, WaitsFor the indexer build it names) or a group being gathered (Key its +// GroupTakeInKey, Members every approved member with its head, Merges the members held so far). Merges are the merges +// it holds, oldest first; Since is when the wait began. A `taken` event names the same once the wait ended; By and Why +// name a hand act (a hand take-in, or a drop, Dropped), and are carried by nothing else. +type TakeIn struct { + V int `json:"v"` + Kind string `json:"kind"` + Key string `json:"key"` + What string `json:"what"` + Merges []Merge `json:"merges"` + Group string `json:"group,omitempty"` + Members []delivery.Head `json:"members,omitempty"` + WaitsFor string `json:"waits-for,omitempty"` + Since time.Time `json:"since"` + By string `json:"by,omitempty"` + Why string `json:"why,omitempty"` + Dropped bool `json:"dropped,omitempty"` +} + +func (t TakeIn) check() error { + if _, err := TakeInEvent(t.Kind, t.Key); err != nil { + return err + } + if len(t.Merges) == 0 { + return fmt.Errorf("delivery: the take-in event %s names no merge", t.Key) + } + for _, m := range t.Merges { + if err := checkRepository(m.Repository); err != nil { + return err + } + if m.Merge == "" { + return fmt.Errorf("delivery: the take-in event %s names a merge of %s without its commit", t.Key, m.Repository) + } + } + if t.Since.IsZero() { + return fmt.Errorf("delivery: the take-in event %s does not say since when it waits", t.Key) + } + switch t.What { + case TakeInRevision: + if !strings.HasPrefix(t.Key, "m.") { + return fmt.Errorf("delivery: a revision's take-in key is its oldest merge's m., not %q", t.Key) + } + if t.Kind == TakeInPending && t.WaitsFor == "" { + return fmt.Errorf("delivery: a pending revision %s names the indexer build it waits for", t.Key) + } + case TakeInGroup: + if t.Group == "" { + return fmt.Errorf("delivery: a group's take-in event %s names its group", t.Key) + } + if t.Key != GroupTakeInKey(t.Group) { + return fmt.Errorf("delivery: the take-in key of the group %q is %s, not %s", t.Group, GroupTakeInKey(t.Group), t.Key) + } + if t.WaitsFor != "" { + return fmt.Errorf("delivery: a group being gathered waits for its end, not for an indexer build") + } + default: + return fmt.Errorf("delivery: a take-in event's what %q is not revision or group", t.What) + } + if t.Kind != TakeInTaken && (t.By != "" || t.Why != "" || t.Dropped) { + return fmt.Errorf("delivery: only a taken event names the hand act that ended a wait") + } + if (t.Why != "" || t.Dropped) && t.By == "" { + return fmt.Errorf("delivery: a hand act's take-in event names who acted (by)") + } + return nil +} + +// EncodeTakeIn is t as JSON at Version; it refuses what its kind and what do not carry. +func EncodeTakeIn(t TakeIn) ([]byte, error) { + if err := t.check(); err != nil { + return nil, err + } + t.V = delivery.Version + return json.Marshal(t) +} + +// DecodeTakeIn reads a take-in event's body; it refuses an unknown version (ErrUnknownVersion) and what EncodeTakeIn +// refuses. +func DecodeTakeIn(data []byte) (TakeIn, error) { + var t TakeIn + if err := decode(data, &t); err != nil { + return TakeIn{}, err + } + if err := t.check(); err != nil { + return TakeIn{}, err + } + return t, nil +} + +// SameTakeIn says whether two take-in bodies are the same value. +func SameTakeIn(a, b TakeIn) bool { return reflect.DeepEqual(a, b) } + +// PendingWait is one wait the indexer's tool indexer_pending answers (novox/hq ADR 0633 §5), as the view reads it to +// name a module waiting for its revision's indexer. Key and What are a take-in event's: a revision under its oldest +// merge's key, waiting for the indexer build WaitsFor (empty while it is worked out), or a group being gathered under +// its group key, with Members every approved member and Merges those held so far. A revision that waits behind a +// group being gathered names that group's key in Behind. Keys are the pending keys of its merges, in Merges' order (indexer_drop_pending drops one). Holds are the modules it holds back: every module of its +// merges' trees. Said is the last take-in event said of it (`take-in..`) and SaidAt when. +type PendingWait struct { + Key string `json:"key"` + What string `json:"what"` + Merges []Merge `json:"merges"` + Keys []string `json:"keys"` + Group string `json:"group,omitempty"` + Members []delivery.Head `json:"members,omitempty"` + Behind string `json:"behind,omitempty"` + WaitsFor string `json:"waits-for,omitempty"` + Since time.Time `json:"since"` + Holds []string `json:"holds"` + Said string `json:"said,omitempty"` + SaidAt time.Time `json:"said-at,omitzero"` +} + +// PendingAnswer is indexer_pending's answer: every wait, oldest first, and the pending keys that do not read. +type PendingAnswer struct { + Waits []PendingWait `json:"waits"` + Unread []string `json:"unread,omitempty"` +} + +// DecodePendingAnswer reads indexer_pending's answer, refusing a wait that is not a revision or a group, or names no +// key. +func DecodePendingAnswer(data []byte) (PendingAnswer, error) { + var a PendingAnswer + if err := json.Unmarshal(data, &a); err != nil { + return a, fmt.Errorf("delivery: not an answer of indexer_pending: %w", err) + } + for _, w := range a.Waits { + if w.Key == "" || (w.What != TakeInRevision && w.What != TakeInGroup) { + return a, fmt.Errorf("delivery: indexer_pending answers a wait %q of %q, not a revision or a group", w.Key, w.What) + } + } + return a, nil +} diff --git a/modules/mesh-delivery-indexer/takein/takein_test.go b/modules/mesh-delivery-indexer/takein/takein_test.go new file mode 100644 index 00000000..a37bc4c6 --- /dev/null +++ b/modules/mesh-delivery-indexer/takein/takein_test.go @@ -0,0 +1,95 @@ +package takein + +import ( + "encoding/json" + "errors" + "reflect" + "strings" + "testing" + "time" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// sampleTakeIns are a pending revision, a group being gathered and a hand take-in's taken (1585/T2). +func sampleTakeIns(since time.Time) []TakeIn { + m := []Merge{{Repository: "novox/mesh-delivery", Number: 9, Merge: "eeee"}} + return []TakeIn{ + {Kind: TakeInPending, Key: PendingKey(12), What: TakeInRevision, Merges: m, WaitsFor: "src2:abc", Since: since}, + {Kind: TakeInPending, Key: GroupTakeInKey("feat/take-in"), What: TakeInGroup, Merges: m, Group: "feat/take-in", + Members: []delivery.Head{{Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}, {Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}}, Since: since}, + {Kind: TakeInTaken, Key: PendingKey(12), What: TakeInRevision, Merges: m, WaitsFor: "src2:abc", Since: since, + By: "lead-47@shanks", Why: "the named indexer can never run"}, + } +} + +// TestTakeInEvents holds the indexer's take-in contract, moved from mesh-sdk go/delivery with its tests (1585/T2, T12). +func TestTakeInEvents(t *testing.T) { + // The take-in events, `take-in..`, a group's key with its dots and no subject wildcard. + if PendingKey(42) != "m.00000000000000000042" || GroupTakeInKey("feat/a.b *>") != "g.feat/a.b___" { + t.Errorf("take-in keys %q %q", PendingKey(42), GroupTakeInKey("feat/a.b *>")) + } + for _, c := range []struct{ kind, key string }{{TakeInPending, PendingKey(7)}, {TakeInTaken, GroupTakeInKey("feat/a.b")}} { + ev, err := TakeInEvent(c.kind, c.key) + if err != nil || ev != "take-in."+c.kind+"."+c.key { + t.Fatalf("take-in event = %q, %v", ev, err) + } + if k, key, err := ParseTakeInEvent(ev); err != nil || k != c.kind || key != c.key { + t.Errorf("parse take-in event %q = %q %q %v", ev, k, key, err) + } + } + for _, bad := range []string{"", "take-in.pending", "take-in.held.m.1", "take-in.taken.x.1", "take-in.taken.m..1", "forge.taken.m.1"} { + if _, _, err := ParseTakeInEvent(bad); err == nil { + t.Errorf("parse take-in event %q accepted", bad) + } + } + // The bodies round-trip: a pending revision, a group being gathered, and a hand take-in's taken with its by. + since := time.Date(2026, 11, 4, 23, 0, 0, 0, time.UTC) + for _, ti := range sampleTakeIns(since) { + data, err := EncodeTakeIn(ti) + if err != nil { + t.Fatalf("take-in %s %s: %v", ti.Kind, ti.What, err) + } + ti.V = delivery.Version + if got, err := DecodeTakeIn(data); err != nil || !SameTakeIn(got, ti) { + t.Errorf("take-in %s round trip: %+v %v", ti.Key, got, err) + } + } + for _, in := range []string{`{"key":"m.1"}`, `{"v":2,"key":"m.1"}`} { + if _, err := DecodeTakeIn([]byte(in)); !errors.Is(err, delivery.ErrUnknownVersion) { + t.Errorf("decode of %s = %v, want delivery.ErrUnknownVersion", in, err) + } + } + // A body that does not hold is refused, naming why. + for _, c := range []struct { + name, want string + i int + mut func(*TakeIn) + }{ + {"group take-in without its group", "names its group", 1, func(t *TakeIn) { t.Group = "" }}, + {"group take-in under another group's key", "g.other", 1, func(t *TakeIn) { t.Key = GroupTakeInKey("other") }}, + {"pending revision waiting for no build", "waits for", 0, func(t *TakeIn) { t.WaitsFor = "" }}, + {"pending with a hand act", "only a taken event", 0, func(t *TakeIn) { t.By = "someone@node" }}, + {"hand act without its by", "by", 2, func(t *TakeIn) { t.By = "" }}, + {"take-in without merges", "no merge", 0, func(t *TakeIn) { t.Merges = nil }}, + {"take-in without since", "since", 0, func(t *TakeIn) { t.Since = time.Time{} }}, + {"a merge of no repository", "owner", 0, func(t *TakeIn) { t.Merges = []Merge{{Repository: "nope", Merge: "m"}} }}, + } { + ti := sampleTakeIns(since)[c.i] + c.mut(&ti) + if _, err := EncodeTakeIn(ti); err == nil || !strings.Contains(err.Error(), c.want) { + t.Errorf("%s: %v, want a refusal naming %q", c.name, err, c.want) + } + } + // indexer_pending's answer reads back; a wait of neither kind is refused. + pa := PendingAnswer{Waits: []PendingWait{{Key: PendingKey(12), What: TakeInRevision, Merges: sampleTakeIns(since)[0].Merges, + Keys: []string{PendingKey(12)}, WaitsFor: "src2:abc", Since: since, Holds: []string{"mesh-delivery-indexer"}, Said: "take-in.pending." + PendingKey(12), SaidAt: since}}} + if data, err := json.Marshal(pa); err != nil { + t.Fatal(err) + } else if got, err := DecodePendingAnswer(data); err != nil || !reflect.DeepEqual(got, pa) { + t.Errorf("pending answer round trip: %+v %v", got, err) + } + if _, err := DecodePendingAnswer([]byte(`{"waits":[{"key":"m.1","what":"held"}]}`)); err == nil { + t.Error("a pending answer with a wait of neither kind accepted") + } +} diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go index 834e8c39..bbe5f2bd 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/main.go @@ -12,6 +12,7 @@ import ( "mesh-delivery-view/internal/view" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/graph" @@ -340,10 +341,10 @@ func main() { } // pendingOf asks the indexer's tool indexer_pending (declared in invokes): what it holds back and why (ADR 0633 §5). -func pendingOf() (delivery.PendingAnswer, error) { +func pendingOf() (takein.PendingAnswer, error) { raw, err := stdio.Ask(delivery.IndexerModule+".indexer_pending", map[string]any{}) if err != nil { - return delivery.PendingAnswer{}, err + return takein.PendingAnswer{}, err } - return delivery.DecodePendingAnswer(raw) + return takein.DecodePendingAnswer(raw) } diff --git a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go index c9f8bba5..baa4406a 100644 --- a/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go +++ b/modules/mesh-delivery-view/cmd/mesh-delivery-view-tools/tiers_rows_test.go @@ -9,6 +9,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/graph" @@ -106,10 +107,10 @@ func tiersToolSaysEachModulesWait(t *testing.T) { // wait's since; a held module the revision does not move is named in the first tier with that wait alone, a group's wait // says its group, and a tool that cannot be asked is said and names no module. heldSince := time.Date(2026, 11, 4, 22, 0, 0, 0, time.UTC) - r.Pending = func() (delivery.PendingAnswer, error) { - return delivery.PendingAnswer{Waits: []delivery.PendingWait{ - {Key: "m.00000000000000000042", What: delivery.TakeInRevision, WaitsFor: "src2:abc", Since: heldSince, Holds: []string{"web", "newmod"}}, - {Key: "g.feat", What: delivery.TakeInGroup, Group: "feat", Since: heldSince.Add(time.Hour), Holds: []string{"lib"}}}}, nil + r.Pending = func() (takein.PendingAnswer, error) { + return takein.PendingAnswer{Waits: []takein.PendingWait{ + {Key: "m.00000000000000000042", What: takein.TakeInRevision, WaitsFor: "src2:abc", Since: heldSince, Holds: []string{"web", "newmod"}}, + {Key: "g.feat", What: takein.TakeInGroup, Group: "feat", Since: heldSince.Add(time.Hour), Holds: []string{"lib"}}}}, nil } out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}) held, _ := out.(view.Tiers) @@ -126,7 +127,7 @@ func tiersToolSaysEachModulesWait(t *testing.T) { if nm := held.Tiers[0][len(held.Tiers[0])-1]; nm.Module != "newmod" || len(nm.Waits) != 1 || nm.Waits[0].Edge != view.TakeInEdge { t.Errorf("the first tier = %+v, want newmod, which the revision does not move, named with its take-in wait alone", held.Tiers[0]) } - r.Pending = func() (delivery.PendingAnswer, error) { return delivery.PendingAnswer{}, errors.New("no answer") } + r.Pending = func() (takein.PendingAnswer, error) { return takein.PendingAnswer{}, errors.New("no answer") } if out, err = run(t, r, view.TiersTool, map[string]any{"revision": float64(9)}); err != nil || !strings.Contains(out.(view.Tiers).Pending, "no answer") || strings.Contains(fmt.Sprint(out.(view.Tiers).Tiers), view.TakeInEdge) { t.Errorf("view_tiers with no answer of indexer_pending = %+v, %v", out, err) diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 035f39f7..45992491 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) @@ -21,6 +21,11 @@ require ( golang.org/x/time v0.15.0 // indirect ) -require git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 +require ( + git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 +) replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ../mesh-delivery-indexer/takein diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 7679a5dc..1c3af991 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index 7e7afa6a..3cf91505 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -14,6 +14,7 @@ import ( "sync/atomic" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -59,7 +60,7 @@ type Reader struct { // ProvisionsState the provisioner's keys: what view_tiers reads (tiers.go). Unset reads none, said in the answer. GraphState, SourceState, ProvisionsState StateRead // Pending reads the indexer's waits through its tool indexer_pending (ADR 0633 §5); unset names no module held back. - Pending func() (delivery.PendingAnswer, error) + Pending func() (takein.PendingAnswer, error) // BuildLog reads the builder's log from a time (stdio.Log(delivery.BuildLogRead).Since); unset reads none. BuildLog LogSince } diff --git a/modules/mesh-delivery-view/internal/view/tiers.go b/modules/mesh-delivery-view/internal/view/tiers.go index 59d05bf1..0d09e4f1 100644 --- a/modules/mesh-delivery-view/internal/view/tiers.go +++ b/modules/mesh-delivery-view/internal/view/tiers.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/graph" @@ -227,7 +228,7 @@ func (r Reader) heldBack(out *Tiers) (map[string]TierWait, []string) { } for _, w := range a.Waits { words := "the indexer holds back " + w.Key + ", which waits for the indexer build " + w.WaitsFor - if w.What == delivery.TakeInGroup { + if w.What == takein.TakeInGroup { words = "the indexer gathers the group " + w.Group + " and its end has not come" } else if w.Behind != "" { words = "the indexer holds back " + w.Key + " behind " + w.Behind diff --git a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go index 239ad6e4..9e280190 100644 --- a/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go +++ b/modules/mesh-delivery-watchman/cmd/mesh-delivery-watchman/main.go @@ -22,6 +22,7 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" @@ -379,10 +380,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // The indexer's stream take-in (ADR 0633 §5): a revision or a group it holds back is a wait, ended by take-in.taken. The // durable consumer _ is made by the controller from this module's consumes; the whole stream is heard at // the start, so a wait that began while the watchman was away is read with its own since. - switch tc, err := cred.EventConsumer(ctx, js, catalogue.StreamBusName(delivery.TakeInStream)); { + switch tc, err := cred.EventConsumer(ctx, js, catalogue.StreamBusName(takein.TakeInStream)); { case err != nil: logger.Printf("the consumer %s_%s on %s does not exist yet, so no take-in wait is said until the watchman starts again: %v", - cred.Node, cred.Module, catalogue.StreamBusName(delivery.TakeInStream), err) + cred.Node, cred.Module, catalogue.StreamBusName(takein.TakeInStream), err) default: // The replay has caught up when a message says none is pending after it, or when the consumer had none pending at its // bind (the stream holds nothing it has not acknowledged): the waits past their bound are said then, not before. diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index 33a7e8dc..2ce3eef0 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,12 +3,13 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104221203-10c91fba4a28 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 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-delivery/modules/mesh-delivery-indexer/takein 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 github.com/klauspost/compress v1.20.1 // indirect @@ -22,3 +23,5 @@ require ( ) replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun => ../mesh-delivery-builder/buildrun + +replace git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein => ../mesh-delivery-indexer/takein diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 5928f081..711a3687 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,7 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8 h1:XEbIVImp6yGjA91rp0iZBoDdL5KfamjOuDLwWvqh0hc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103221822-85ba0a9a70d8/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104221203-10c91fba4a28 h1:Zoi+myMvKaRYdM8w/96W6GnoAiTvgwvEF1QPA6HelaU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104221203-10c91fba4a28/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 h1:tK7MzxVm/ECNg1Yblhh1ynVEthbZCg2mSPW0wQZC0b0= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/internal/watch/findings_test.go b/modules/mesh-delivery-watchman/internal/watch/findings_test.go index 0454d76f..8f81f1a1 100644 --- a/modules/mesh-delivery-watchman/internal/watch/findings_test.go +++ b/modules/mesh-delivery-watchman/internal/watch/findings_test.go @@ -6,6 +6,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" "git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/provisions" ) @@ -36,11 +37,11 @@ func TestTheWatchmansFindings(t *testing.T) { }{ {"ADR 0633 §5 (1585/T21): a revision waiting for an indexer build is a finding past 60 minutes from its since, not within; a group past 30", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.TakeInCaughtUp() // a live message, heard after the replay - w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0)) if o.ttls[signalKey("take-in", revKey)] != TakeInRevisionBound || len(o.findings) != 0 { t.Fatalf("a revision's wait: ttls %v, findings %v", o.ttls, o.findings) } - w.TakeIn(takeInEvent(t, delivery.TakeInPending, groupKey), takeInBody(t, delivery.TakeInPending, groupKey, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInPending, groupKey), takeInBody(t, takein.TakeInPending, groupKey, t0)) if o.ttls[signalKey("take-in", groupKey)] != TakeInGroupBound { t.Fatalf("a group's wait: ttls %v", o.ttls) } @@ -53,41 +54,41 @@ func TestTheWatchmansFindings(t *testing.T) { {"ADR 0633 §5 (1585/T21): the take-in.taken of a named indexer, a hand take-in or a drop clears the finding", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.TakeInCaughtUp() // a live message, heard after the replay for _, by := range []string{"", "operator@shanks"} { - w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0)) expire(w, c, signalKey("take-in", revKey), TakeInRevisionBound) if len(o.findings) != 1 { t.Fatalf("findings past the bound: %v", o.findings) } - taken, err := delivery.EncodeTakeIn(delivery.TakeIn{Kind: delivery.TakeInTaken, Key: revKey, What: delivery.TakeInRevision, Since: t0, - Merges: []delivery.GroupMerge{{Repository: "novox/a", Merge: "c0ffee"}}, By: by, Why: map[bool]string{true: "dropped", false: ""}[by != ""], Dropped: by != ""}) + taken, err := takein.EncodeTakeIn(takein.TakeIn{Kind: takein.TakeInTaken, Key: revKey, What: takein.TakeInRevision, Since: t0, + Merges: []takein.Merge{{Repository: "novox/a", Merge: "c0ffee"}}, By: by, Why: map[bool]string{true: "dropped", false: ""}[by != ""], Dropped: by != ""}) if err != nil { t.Fatal(err) } - w.TakeIn(takeInEvent(t, delivery.TakeInTaken, revKey), taken) + w.TakeIn(takeInEvent(t, takein.TakeInTaken, revKey), taken) c.t = t0 } }, map[string]string{}}, {"ADR 0633 §5 (1585/T21): a group's take-in.taken ends its 30 minutes, and the revision then pending under m. starts its own 60", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.TakeInCaughtUp() // a live message, heard after the replay - w.TakeIn(takeInEvent(t, delivery.TakeInPending, groupKey), takeInBody(t, delivery.TakeInPending, groupKey, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInPending, groupKey), takeInBody(t, takein.TakeInPending, groupKey, t0)) expire(w, c, signalKey("take-in", groupKey), 25*time.Minute) - w.TakeIn(takeInEvent(t, delivery.TakeInTaken, groupKey), takeInBody(t, delivery.TakeInTaken, groupKey, t0)) - w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, c.t)) + w.TakeIn(takeInEvent(t, takein.TakeInTaken, groupKey), takeInBody(t, takein.TakeInTaken, groupKey, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, c.t)) if len(o.findings) != 0 || o.ttls[signalKey("take-in", revKey)] != TakeInRevisionBound { t.Fatalf("after the hand-over: findings %v, ttls %v", o.findings, o.ttls) } expire(w, c, signalKey("take-in", revKey), TakeInRevisionBound) }, map[string]string{FindingKey(ScopeDelivery, revKey, KindTakeInWaits): Warning}}, {"ADR 0633 §5 (1585/T21): a pending event heard again at the start, since past its bound, is said once the replay has caught up, not before", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0.Add(-2*time.Hour))) + w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0.Add(-2*time.Hour))) if len(o.findings) != 0 || len(o.pulses) != 0 { t.Fatalf("a replayed old wait before the replay caught up: findings %v, pulses %v", o.findings, o.pulses) } w.TakeInCaughtUp() }, map[string]string{FindingKey(ScopeDelivery, revKey, KindTakeInWaits): Warning}}, {"ADR 0633 §5 (1585/T21): a replayed pending followed by its taken never raises", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { - w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0.Add(-2*time.Hour))) - w.TakeIn(takeInEvent(t, delivery.TakeInTaken, revKey), takeInBody(t, delivery.TakeInTaken, revKey, t0.Add(-2*time.Hour))) + w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0.Add(-2*time.Hour))) + w.TakeIn(takeInEvent(t, takein.TakeInTaken, revKey), takeInBody(t, takein.TakeInTaken, revKey, t0.Add(-2*time.Hour))) w.TakeInCaughtUp() if o.findingWrites != 0 || len(o.pulses) != 0 { t.Fatalf("a wait that ended in the replay wrote %d findings and left pulses %v", o.findingWrites, o.pulses) @@ -95,17 +96,17 @@ func TestTheWatchmansFindings(t *testing.T) { }, map[string]string{}}, {"ADR 0633 §5 (1585/T21): a drop clears the wait, and the merges left under it said pending again with the same since are raised from it", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { w.TakeInCaughtUp() // a live message, heard after the replay - w.TakeIn(takeInEvent(t, delivery.TakeInPending, revKey), takeInBody(t, delivery.TakeInPending, revKey, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInPending, revKey), takeInBody(t, takein.TakeInPending, revKey, t0)) expire(w, c, signalKey("take-in", revKey), TakeInRevisionBound) if len(o.findings) != 1 { t.Fatalf("findings past the bound: %v", o.findings) } - w.TakeIn(takeInEvent(t, delivery.TakeInTaken, revKey), takeInBody(t, delivery.TakeInTaken, revKey, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInTaken, revKey), takeInBody(t, takein.TakeInTaken, revKey, t0)) if len(o.findings) != 0 { t.Fatalf("the drop left %v", o.findings) } next := "m.00000000000000000043" - w.TakeIn(takeInEvent(t, delivery.TakeInPending, next), takeInBody(t, delivery.TakeInPending, next, t0)) + w.TakeIn(takeInEvent(t, takein.TakeInPending, next), takeInBody(t, takein.TakeInPending, next, t0)) }, map[string]string{FindingKey(ScopeDelivery, "m.00000000000000000043", KindTakeInWaits): Warning}}, {"ADR 0578 §1: a module whose value carries a refusal of its manifest is a finding until it is accepted", func(t *testing.T, w *Watchman, o *fakeOut, c *clock) { value := func(verdict string, problems ...string) []byte { @@ -609,7 +610,7 @@ const ( func takeInEvent(t *testing.T, kind, key string) string { t.Helper() - e, err := delivery.TakeInEvent(kind, key) + e, err := takein.TakeInEvent(kind, key) if err != nil { t.Fatal(err) } @@ -619,14 +620,14 @@ func takeInEvent(t *testing.T, kind, key string) string { // takeInBody is a take-in event's body for a key: a group's when it starts with g. func takeInBody(t *testing.T, kind, key string, since time.Time) []byte { t.Helper() - body := delivery.TakeIn{Kind: kind, Key: key, What: delivery.TakeInRevision, Since: since, - Merges: []delivery.GroupMerge{{Repository: "novox/a", Number: 1, Merge: "c0ffee"}}} + body := takein.TakeIn{Kind: kind, Key: key, What: takein.TakeInRevision, Since: since, + Merges: []takein.Merge{{Repository: "novox/a", Number: 1, Merge: "c0ffee"}}} if key[0] == 'g' { - body.What, body.Group = delivery.TakeInGroup, "feat/take-in" - } else if kind == delivery.TakeInPending { + body.What, body.Group = takein.TakeInGroup, "feat/take-in" + } else if kind == takein.TakeInPending { body.WaitsFor = "src2:abc" } - b, err := delivery.EncodeTakeIn(body) + b, err := takein.EncodeTakeIn(body) if err != nil { t.Fatal(err) } diff --git a/modules/mesh-delivery-watchman/internal/watch/takein.go b/modules/mesh-delivery-watchman/internal/watch/takein.go index 9cff07b2..4d3e4276 100644 --- a/modules/mesh-delivery-watchman/internal/watch/takein.go +++ b/modules/mesh-delivery-watchman/internal/watch/takein.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-indexer/takein" ) // KindTakeInWaits is a source revision or a delivery group the indexer holds back and has not taken in within its bound @@ -28,12 +28,12 @@ const takeInRow = "take-in" // a pending one starts the wait for its key, whose bound runs from the event's since, and a taken one ends it. A group's // taken event ends the group's wait only: the revision it becomes is its own key and starts its own, longer wait. func (w *Watchman) TakeIn(event string, body []byte) { - kind, key, err := delivery.ParseTakeInEvent(event) + kind, key, err := takein.ParseTakeInEvent(event) if err != nil { w.say("a take-in event is left: %v", err) return } - t, err := delivery.DecodeTakeIn(body) + t, err := takein.DecodeTakeIn(body) if err != nil { w.say("the body of %s cannot be read, left: %v", event, err) return @@ -41,14 +41,14 @@ func (w *Watchman) TakeIn(event string, body []byte) { w.mu.Lock() defer w.mu.Unlock() finding := FindingKey(ScopeDelivery, key, KindTakeInWaits) - if kind == delivery.TakeInTaken { + if kind == takein.TakeInTaken { delete(w.takeInHeld, key) w.unsignal(takeInRow, key) w.unfind(finding) return } bound, what := TakeInRevisionBound, "a source revision waits for the indexer build "+t.WaitsFor - if t.What == delivery.TakeInGroup { + if t.What == takein.TakeInGroup { bound, what = TakeInGroupBound, "a delivery group is gathered and its end has not come" } var merges []string