Forge bridge: the gate's merges and owed acts are the seat's types and the delivery client's, the version and the repository check the keys' and the trunk's; the holders and buckets it names are its own constants checked against its manifest (D-11; ADR 0634 §1, §2; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 16:13:00 +01:00
parent e000ed0f66
commit 95fabbfcde
21 changed files with 270 additions and 187 deletions
@@ -17,8 +17,10 @@ import (
"sync" "sync"
"time" "time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
stdio "git.novox.be/novox/mesh-sdk/go" stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
@@ -26,6 +28,9 @@ import (
"mesh-delivery-forge-bridge/internal/groups" "mesh-delivery-forge-bridge/internal/groups"
) )
// gateMergesState is the gate's state merges, which the tools read (reads: mesh-delivery-gate.merges in the manifest).
const gateMergesState = "mesh-delivery-gate.merges"
// Tool names. // Tool names.
const ( const (
GroupsTool = "bridge_groups" GroupsTool = "bridge_groups"
@@ -125,18 +130,18 @@ func MergedOf(raws map[string][]byte, repository, merge string, trees bridge.Tre
return nil, fmt.Errorf("%q is not owner/name", repository) return nil, fmt.Errorf("%q is not owner/name", repository)
} }
for _, key := range slices.Sorted(maps.Keys(raws)) { for _, key := range slices.Sorted(maps.Keys(raws)) {
m, err := delivery.DecodeMerge(raws[key]) m, err := meshdeliveryclient.DecodeMerge(raws[key])
if err != nil { if err != nil {
continue continue
} }
at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool {
return delivery.SameRepository(p.Repository, repository) && p.Commit == merge return trunk.SameRepository(p.Repository, repository) && p.Commit == merge
}) })
if at < 0 { if at < 0 {
continue continue
} }
p := m.Pulls[at] p := m.Pulls[at]
msg, err := bridge.MergedMessage(owner, repo, p.Number, m.Group, p.Head.Head, merge, trees, files) msg, err := bridge.MergedMessage(owner, repo, int(p.Number), m.Group, p.Head, merge, trees, files)
if err != nil { if err != nil {
return nil, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err) return nil, fmt.Errorf("the merged message of %s at %s cannot be built: %w", repository, merge, err)
} }
@@ -144,7 +149,7 @@ func MergedOf(raws map[string][]byte, repository, merge string, trees bridge.Tre
if err != nil { if err != nil {
return nil, err return nil, err
} }
if _, approved := m.Approved[delivery.ApprovedKey(f.Repository, f.Number)]; approved && len(m.Approved) >= 2 { if _, approved := m.Approved[meshdeliveryclient.ApprovedKey(f.Repository, f.Number)]; approved && len(m.Approved) >= 2 {
f.Group, f.Members = m.Group, groups.MembersOf(m) f.Group, f.Members = m.Group, groups.MembersOf(m)
} }
out, err := forgemsg.EncodeForge(f) out, err := forgemsg.EncodeForge(f)
@@ -253,7 +258,7 @@ func askCaller(_ context.Context, key string, body any) (json.RawMessage, error)
} }
func main() { func main() {
merges, performed, asks := stdio.State(delivery.GateModule+".merges"), stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState) merges, performed, asks := stdio.State(gateMergesState), stdio.State(groups.PerformedState), stdio.State(groups.EndAsksState)
tools := Tools(askEnd(asks, 25*time.Second), time.Now, tools := Tools(askEnd(asks, 25*time.Second), time.Now,
func() ([]groups.Row, []string, error) { return read(merges, performed, asks) }, func() ([]groups.Row, []string, error) { return read(merges, performed, asks) },
func(repository, merge string) (json.RawMessage, error) { func(repository, merge string) (json.RawMessage, error) {
@@ -8,8 +8,9 @@ import (
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-forge-bridge/internal/bridge" "mesh-delivery-forge-bridge/internal/bridge"
@@ -22,19 +23,19 @@ import (
func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) { func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40) h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40)
c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40) c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40)
enc := func(group string, state2 delivery.MergeState, c string) []byte { enc := func(group string, state2 meshdeliveryclient.MergeState, c string) []byte {
v, err := delivery.EncodeMerge(delivery.Merge{Group: group, Ask: "ask", Approved: map[string]string{"novox/a@1": h1, "novox/b@2": h2}, v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: group, Ask: "ask", Approved: map[string]string{"novox/a@1": h1, "novox/b@2": h2},
Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 1, Head: h1}, State: delivery.MergeMerged, Commit: c1}, Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 1, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1},
{Head: delivery.Head{Repository: "novox/b", Number: 2, Head: h2}, State: state2, Commit: c}}}) {Repository: "novox/b", Number: 2, Head: h2, State: string(state2), Commit: c}}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return v return v
} }
lone, _ := delivery.EncodeMerge(delivery.Merge{Group: "solo", Ask: "ask", Approved: map[string]string{"novox/a@5": h1}, lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "ask", Approved: map[string]string{"novox/a@5": h1},
Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 5, Head: h1}, State: delivery.MergeMerged, Commit: c1}}}) Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}})
merges := map[string][]byte{"ended": enc("ended", delivery.MergeMerged, c2), "ready": enc("ready", delivery.MergeMerged, c2), merges := map[string][]byte{"ended": enc("ended", meshdeliveryclient.MergeMerged, c2), "ready": enc("ready", meshdeliveryclient.MergeMerged, c2),
"gathered": enc("gathered", delivery.MergeMerging, ""), "solo": lone, "bad": []byte("{")} "gathered": enc("gathered", meshdeliveryclient.MergeMerging, ""), "solo": lone, "bad": []byte("{")}
performed := map[string]bool{} performed := map[string]bool{}
for _, c := range []string{c1, c2} { for _, c := range []string{c1, c2} {
performed[groups.PublishedKey("novox/a", 1, c)], performed[groups.PublishedKey("novox/b", 2, c)] = true, true performed[groups.PublishedKey("novox/a", 1, c)], performed[groups.PublishedKey("novox/b", 2, c)] = true, true
@@ -98,7 +99,7 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
if a := all["ready"].LastEndAsk; a == nil || a.By != "lead@shanks" || a.Why != "lost" || !a.Written || a.Answer == "" || all["ended"].LastEndAsk != nil { if a := all["ready"].LastEndAsk; a == nil || a.By != "lead@shanks" || a.Why != "lost" || !a.Written || a.Answer == "" || all["ended"].LastEndAsk != nil {
t.Errorf("ready's last end ask = %+v, ended's %+v: want ready's ask and its answer, none for ended", a, all["ended"].LastEndAsk) t.Errorf("ready's last end ask = %+v, ended's %+v: want ready's ask and its answer, none for ended", a, all["ended"].LastEndAsk)
} }
if r := all["gathered"]; !r.Open || !strings.Contains(r.Words, "gathered") || r.Members[1].State != string(delivery.MergeMerging) { if r := all["gathered"]; !r.Open || !strings.Contains(r.Words, "gathered") || r.Members[1].State != string(meshdeliveryclient.MergeMerging) {
t.Errorf("gathered = %+v", r) t.Errorf("gathered = %+v", r)
} }
if open := rowsOf(map[string]any{"open": true}); len(open) != 2 || open["ended"].Group != "" { if open := rowsOf(map[string]any{"open": true}); len(open) != 2 || open["ended"].Group != "" {
@@ -146,11 +147,11 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
} }
return f return f
} }
lone, _ := delivery.EncodeMerge(delivery.Merge{Group: "solo", Ask: "a", Approved: map[string]string{"novox/a@5": h1}, lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "a", Approved: map[string]string{"novox/a@5": h1},
Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 5, Head: h1}, State: delivery.MergeMerged, Commit: c1}}}) Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}})
group, _ := delivery.EncodeMerge(delivery.Merge{Group: "feat/g", Ask: "a", Approved: map[string]string{"novox/a@6": h1, "novox/b@7": h2}, group, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/g", Ask: "a", Approved: map[string]string{"novox/a@6": h1, "novox/b@7": h2},
Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 6, Head: h1}, State: delivery.MergeMerged, Commit: c1}, Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 6, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1},
{Head: delivery.Head{Repository: "novox/b", Number: 7, Head: h2}, State: delivery.MergeMerging}}}) {Repository: "novox/b", Number: 7, Head: h2, State: string(meshdeliveryclient.MergeMerging)}}})
for _, c := range []struct { for _, c := range []struct {
name string name string
raws map[string][]byte raws map[string][]byte
@@ -162,7 +163,7 @@ func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
{name: "a lone pull request", raws: map[string][]byte{"solo": lone}, repository: "novox/a", merge: c1, want: published("a", 5, h1, "solo", c1)}, {name: "a lone pull request", raws: map[string][]byte{"solo": lone}, repository: "novox/a", merge: c1, want: published("a", 5, h1, "solo", c1)},
{name: "a member of a group", raws: map[string][]byte{"feat~g": group}, repository: "novox/a", merge: c1, want: func() forgemsg.Forge { {name: "a member of a group", raws: map[string][]byte{"feat~g": group}, repository: "novox/a", merge: c1, want: func() forgemsg.Forge {
f := published("a", 6, h1, "feat/g", c1) f := published("a", 6, h1, "feat/g", c1)
f.Group, f.Members = "feat/g", []delivery.Head{{Repository: "novox/a", Number: 6, Head: h1}, {Repository: "novox/b", Number: 7, Head: h2}} f.Group, f.Members = "feat/g", []meshdeliveryclient.Head{{Repository: "novox/a", Number: 6, Head: h1}, {Repository: "novox/b", Number: 7, Head: h2}}
return f return f
}()}, }()},
{name: "a merge the gate does not hold", raws: map[string][]byte{"solo": lone, "bad": []byte("{")}, repository: "novox/a", merge: c2, fail: "hold no merge"}, {name: "a merge the gate does not hold", raws: map[string][]byte{"solo": lone, "bad": []byte("{")}, repository: "novox/a", merge: c2, fail: "hold no merge"},
@@ -9,15 +9,13 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-forge-bridge/internal/bridge" "mesh-delivery-forge-bridge/internal/bridge"
) )
// performedBucket is the module's state bucket performed, `<module>_<name>` as heads is: for each key of the gate's // performedBucket is the module's state bucket performed, `<module>_<name>` as heads is: for each key of the gate's
// buckets the bridge has acted on, the revision it acted at (manifest state entry performed, novox/hq ADR 0437 §8). // buckets the bridge has acted on, the revision it acted at (manifest state entry performed, novox/hq ADR 0437 §8).
// Its keys are `owed.<owed key>` and `merges.<merges key>`; its value is the revision in decimal. // Its keys are `owed.<owed key>` and `merges.<merges key>`; its value is the revision in decimal.
const performedBucket = delivery.ForgeBridgeModule + "_performed" const performedBucket = forgeBridgeModule + "_performed"
// kvRevisions is the bucket performed as the performer's revisions. // kvRevisions is the bucket performed as the performer's revisions.
type kvRevisions struct{ kv jetstream.KeyValue } type kvRevisions struct{ kv jetstream.KeyValue }
@@ -9,8 +9,9 @@ import (
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
) )
const ( const (
@@ -52,13 +53,13 @@ func newPerformer(f *forgeFake) (*performer, *fakePub, memRevisions, *bytes.Buff
} }
// The values are the SDK's own encoding (mesh-sdk go/delivery EncodeOwed, EncodeMerge), the keys its OwedKey. // The values are the SDK's own encoding (mesh-sdk go/delivery EncodeOwed, EncodeMerge), the keys its OwedKey.
func owedKV(t *testing.T, repository string, number int, act string, o delivery.Owed) (string, []byte) { func owedKV(t *testing.T, repository string, number int, act string, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) {
t.Helper() t.Helper()
key, err := delivery.OwedKey(repository, number, headA, act) key, err := meshdeliveryclient.OwedKey(repository, number, headA, act)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
v, err := delivery.EncodeOwed(o) v, err := meshdeliveryclient.EncodeOwed(o)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -66,10 +67,11 @@ func owedKV(t *testing.T, repository string, number int, act string, o delivery.
} }
func TestAnOwedStatusIsSetOnceForARevision(t *testing.T) { // 1341/AC1 func TestAnOwedStatusIsSetOnceForARevision(t *testing.T) { // 1341/AC1
t.Run("the names are the manifest's", theNamesAreTheManifests)
f := &forgeFake{} f := &forgeFake{}
p, _, _, _ := newPerformer(f) p, _, _, _ := newPerformer(f)
key, v := owedKV(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate", key, v := owedKV(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate",
delivery.Owed{Act: delivery.OwedStatus, Context: "mesh/merge-gate", State: "success", Text: "all green", Target: "https://forge.example/p/12"}) seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: "mesh/merge-gate", State: "success", Text: "all green", Target: "https://forge.example/p/12"})
p.owed(context.Background(), key, 7, v) p.owed(context.Background(), key, 7, v)
if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_commit_status" || if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_commit_status" ||
f.calls[0].body != `{"context":"mesh/merge-gate","description":"all green","owner":"novox","repo":"mesh-delivery","sha":"`+headA+`","state":"success","target_url":"https://forge.example/p/12"}` { f.calls[0].body != `{"context":"mesh/merge-gate","description":"all green","owner":"novox","repo":"mesh-delivery","sha":"`+headA+`","state":"success","target_url":"https://forge.example/p/12"}` {
@@ -90,11 +92,11 @@ func TestViewNoteAndCommentEachCallTheirToolOnce(t *testing.T) { // 1341/AC2
p, _, _, _ := newPerformer(f) p, _, _, _ := newPerformer(f)
for _, c := range []struct { for _, c := range []struct {
act string act string
o delivery.Owed o seat.OwedByRepositoryNumberHeadActValue
}{ }{
{"view", delivery.Owed{Act: delivery.OwedView, Text: "the view"}}, {"view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "the view"}},
{"note", delivery.Owed{Act: delivery.OwedNote, Text: "a line", Ref: "mesh-plan"}}, {"note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "a line", Ref: "mesh-plan"}},
{"comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}}, {"comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"}},
} { } {
key, v := owedKV(t, "novox/mesh-delivery", 12, c.act, c.o) key, v := owedKV(t, "novox/mesh-delivery", 12, c.act, c.o)
p.owed(context.Background(), key, 1, v) p.owed(context.Background(), key, 1, v)
@@ -112,7 +114,7 @@ func TestViewNoteAndCommentEachCallTheirToolOnce(t *testing.T) { // 1341/AC2
func TestARefusedActIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1341/AC3 func TestARefusedActIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1341/AC3
f := &forgeFake{refuse: map[string]string{"gitea.gitea_add_comment": "the pull request is locked"}} f := &forgeFake{refuse: map[string]string{"gitea.gitea_add_comment": "the pull request is locked"}}
p, pub, _, _ := newPerformer(f) p, pub, _, _ := newPerformer(f)
key, v := owedKV(t, "novox/mesh-delivery", 12, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}) key, v := owedKV(t, "novox/mesh-delivery", 12, "comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"})
p.owed(context.Background(), key, 3, v) p.owed(context.Background(), key, 3, v)
p.owed(context.Background(), key, 3, v) p.owed(context.Background(), key, 3, v)
if len(f.calls) != 1 { if len(f.calls) != 1 {
@@ -147,28 +149,28 @@ func TestAnOwedValueThatCannotBeReadIsLoggedAndNotRetried(t *testing.T) {
} }
} }
func mergeKV(t *testing.T, m delivery.Merge) []byte { func mergeKV(t *testing.T, m seat.MergesByGroupValue) []byte {
t.Helper() t.Helper()
v, err := delivery.EncodeMerge(m) v, err := meshdeliveryclient.EncodeMerge(m)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return v return v
} }
func twoMembers(a, b delivery.MergeState) delivery.Merge { func twoMembers(a, b meshdeliveryclient.MergeState) seat.MergesByGroupValue {
return delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", return seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab",
Approved: map[string]string{delivery.ApprovedKey("novox/mesh-sdk", 4): headB, delivery.ApprovedKey("novox/mesh-delivery", 12): headA}, Approved: map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA},
Pulls: []delivery.MergePull{ Pulls: []seat.MergesByGroupValuePullsItem{
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: a, Commit: map[bool]string{true: "c1"}[a == delivery.MergeMerged]}, {Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(a), Commit: map[bool]string{true: "c1"}[a == meshdeliveryclient.MergeMerged]},
{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: b, Commit: map[bool]string{true: "c2"}[b == delivery.MergeMerged]}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(b), Commit: map[bool]string{true: "c2"}[b == meshdeliveryclient.MergeMerged]},
}} }}
} }
func TestAMergeRevisionMergesOnlyTheMemberHanded(t *testing.T) { // 1342/AC1 func TestAMergeRevisionMergesOnlyTheMemberHanded(t *testing.T) { // 1342/AC1
f := &forgeFake{} f := &forgeFake{}
p, _, _, _ := newPerformer(f) p, _, _, _ := newPerformer(f)
m := twoMembers(delivery.MergeMerging, delivery.MergeMerged) m := twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerged)
p.merges(context.Background(), "g", 5, mergeKV(t, m)) p.merges(context.Background(), "g", 5, mergeKV(t, m))
want := `{"approved_heads":["novox/mesh-delivery@` + headA + `","novox/mesh-sdk@` + headB + `"],"head":"` + headA + `","number":12,"owner":"novox","repo":"mesh-delivery"}` want := `{"approved_heads":["novox/mesh-delivery@` + headA + `","novox/mesh-sdk@` + headB + `"],"head":"` + headA + `","number":12,"owner":"novox","repo":"mesh-delivery"}`
if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_merge_pull_request" || f.calls[0].body != want { if len(f.calls) != 1 || f.calls[0].key != "gitea.gitea_merge_pull_request" || f.calls[0].body != want {
@@ -183,7 +185,7 @@ func TestAMergeRevisionMergesOnlyTheMemberHanded(t *testing.T) { // 1342/AC1
func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/AC2 func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/AC2
f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "novox/mesh-delivery#12 is not merged: its head moved"}} f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "novox/mesh-delivery#12 is not merged: its head moved"}}
p, pub, _, _ := newPerformer(f) p, pub, _, _ := newPerformer(f)
v := mergeKV(t, twoMembers(delivery.MergeMerging, delivery.MergeMerged)) v := mergeKV(t, twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerged))
p.merges(context.Background(), "g", 5, v) p.merges(context.Background(), "g", 5, v)
p.merges(context.Background(), "g", 5, v) p.merges(context.Background(), "g", 5, v)
if len(f.calls) != 1 || len(pub.data) != 1 { if len(f.calls) != 1 || len(pub.data) != 1 {
@@ -204,7 +206,7 @@ func TestARefusedMergeIsWrittenOnForgeOnceAndNotRetried(t *testing.T) { // 1342/
func TestAMergeWithTwoHandedStopsAtTheFirstRefusal(t *testing.T) { // ADR 0441 §3 func TestAMergeWithTwoHandedStopsAtTheFirstRefusal(t *testing.T) { // ADR 0441 §3
f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "no"}} f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "no"}}
p, pub, _, _ := newPerformer(f) p, pub, _, _ := newPerformer(f)
p.merges(context.Background(), "g", 1, mergeKV(t, twoMembers(delivery.MergeMerging, delivery.MergeMerging))) p.merges(context.Background(), "g", 1, mergeKV(t, twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerging)))
if len(f.calls) != 1 || len(pub.data) != 1 { if len(f.calls) != 1 || len(pub.data) != 1 {
t.Errorf("calls %d messages %d, want the first refusal to stop the pass", len(f.calls), len(pub.data)) t.Errorf("calls %d messages %d, want the first refusal to stop the pass", len(f.calls), len(pub.data))
} }
@@ -8,8 +8,10 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/sdk/trunk"
groupkeys "mesh-delivery-forge-bridge/internal/groups" groupkeys "mesh-delivery-forge-bridge/internal/groups"
) )
@@ -57,29 +59,29 @@ 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 // 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. // 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 forgemsg.Forge) (delivery.Merge, uint64, bool) { func (g *grouper) groupOf(ctx context.Context, f forgemsg.Forge) (seat.MergesByGroupValue, uint64, bool) {
if g == nil || g.merges == nil || f.Branch == "" { if g == nil || g.merges == nil || f.Branch == "" {
return delivery.Merge{}, 0, false return seat.MergesByGroupValue{}, 0, false
} }
key, err := delivery.MergesKey(f.Branch) key, err := meshdeliveryclient.MergesKey(f.Branch)
if err != nil { if err != nil {
return delivery.Merge{}, 0, false return seat.MergesByGroupValue{}, 0, false
} }
value, rev, found, err := g.merges(ctx, key) value, rev, found, err := g.merges(ctx, key)
if err != nil { if err != nil {
g.logger.Printf("%s#%d: the gate's merges.%s cannot be read, so its merged message names no group: %v", f.Repository, f.Number, key, err) g.logger.Printf("%s#%d: the gate's merges.%s cannot be read, so its merged message names no group: %v", f.Repository, f.Number, key, err)
return delivery.Merge{}, 0, false return seat.MergesByGroupValue{}, 0, false
} }
if !found { if !found {
return delivery.Merge{}, 0, false return seat.MergesByGroupValue{}, 0, false
} }
m, err := delivery.DecodeMerge(value) m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil { if err != nil {
g.logger.Printf("%s#%d: the gate's merges.%s is not a merge, so its merged message names no group: %v", f.Repository, f.Number, key, err) g.logger.Printf("%s#%d: the gate's merges.%s is not a merge, so its merged message names no group: %v", f.Repository, f.Number, key, err)
return delivery.Merge{}, 0, false return seat.MergesByGroupValue{}, 0, false
} }
if _, ok := m.Approved[delivery.ApprovedKey(f.Repository, f.Number)]; !ok || len(m.Approved) < 2 { if _, ok := m.Approved[meshdeliveryclient.ApprovedKey(f.Repository, f.Number)]; !ok || len(m.Approved) < 2 {
return delivery.Merge{}, 0, false return seat.MergesByGroupValue{}, 0, false
} }
return m, rev, true return m, rev, true
} }
@@ -110,7 +112,7 @@ func (g *grouper) published(ctx context.Context, f forgemsg.Forge) {
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) 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 return
} }
key, err := delivery.MergesKey(f.Branch) key, err := meshdeliveryclient.MergesKey(f.Branch)
if err != nil { if err != nil {
return return
} }
@@ -130,7 +132,7 @@ func (g *grouper) ended(ctx context.Context, key string, rev uint64, value []byt
} }
func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, again string) bool { func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte, again string) bool {
m, err := delivery.DecodeMerge(value) m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil || len(m.Approved) < 2 { if err != nil || len(m.Approved) < 2 {
return false return false
} }
@@ -141,8 +143,8 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte,
} }
end := forgemsg.ForgeGroupEnd{Group: m.Group} end := forgemsg.ForgeGroupEnd{Group: m.Group}
for _, member := range groupkeys.MembersOf(m) { for _, member := range groupkeys.MembersOf(m) {
at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool {
return delivery.SameRepository(p.Repository, member.Repository) && p.Number == member.Number return trunk.SameRepository(p.Repository, member.Repository) && int(p.Number) == member.Number
}) })
if at < 0 || m.Pulls[at].Commit == "" { if at < 0 || m.Pulls[at].Commit == "" {
return false // not every member is merged (a merge commit is written only for a merged pull): the group stays gathered return false // not every member is merged (a merge commit is written only for a merged pull): the group stays gathered
@@ -154,8 +156,8 @@ func (g *grouper) end(ctx context.Context, key string, rev uint64, value []byte,
} }
// The members in the order the gate merged them, so the last commit names the end. // The members in the order the gate merged them, so the last commit names the end.
slices.SortStableFunc(end.Members, func(a, b forgemsg.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 }) - return slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == a.Merge }) -
slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == b.Merge }) slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == b.Merge })
}) })
data, err := forgemsg.EncodeForgeGroupEnd(end) data, err := forgemsg.EncodeForgeGroupEnd(end)
if err != nil { if err != nil {
@@ -8,8 +8,9 @@ import (
"testing" "testing"
"time" "time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
groupkeys "mesh-delivery-forge-bridge/internal/groups" groupkeys "mesh-delivery-forge-bridge/internal/groups"
) )
@@ -29,19 +30,19 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
ctx := context.Background() ctx := context.Background()
h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40) h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40)
c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40) c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40)
merge := func(states ...delivery.MergePull) []byte { merge := func(states ...seat.MergesByGroupValuePullsItem) []byte {
v, err := delivery.EncodeMerge(delivery.Merge{Group: "feat/take-in", Ask: "ask", Pulls: states, v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Pulls: states,
Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}}) Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return v return v
} }
sdk := func(state delivery.MergeState, commit, words string) delivery.MergePull { sdk := func(state meshdeliveryclient.MergeState, commit, words string) seat.MergesByGroupValuePullsItem {
return delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 37, Head: h1}, State: state, Commit: commit, Words: words} return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 37, Head: h1, State: string(state), Commit: commit, Words: words}
} }
dlv := func(state delivery.MergeState, commit string) delivery.MergePull { dlv := func(state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
return delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 9, Head: h2}, State: state, Commit: commit} return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(state), Commit: commit}
} }
gate := map[string][]byte{} gate := map[string][]byte{}
var logs bytes.Buffer var logs bytes.Buffer
@@ -63,17 +64,17 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
t.Errorf("a lone pull request names the group %q", f.Group) t.Errorf("a lone pull request names the group %q", f.Group)
} }
// The first member: its message names the group and both members; the group is not ended. // 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, "")) gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerging, ""))
data, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1)) data, f, ok := g.member(ctx, mergedOf("novox/mesh-sdk", 37, h1, c1))
if back, err := forgemsg.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) t.Fatalf("a member's message: %+v %v", back, err)
} }
g.published(ctx, f) g.published(ctx, f)
// A refusal of the other member is no end (ADR 0441 §3: the group merges on from it later). // A refusal of the other member is no end (ADR 0441 §3: the group merges on from it later).
gate["feat/take-in"] = merge(sdk(delivery.MergeMerged, c1, ""), delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 9, Head: h2}, State: delivery.MergeRefused, Words: "405"}) gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(meshdeliveryclient.MergeRefused), Words: "405"})
g.ended(ctx, "feat/take-in", 8, gate["feat/take-in"]) g.ended(ctx, "feat/take-in", 8, gate["feat/take-in"])
// Merged on at the gate before its merged message is written: still no end; then its message ends the group. // Merged on at the gate before its merged message is written: still no end; then its message ends the group.
gate["feat/take-in"] = merge(sdk(delivery.MergeMerged, c1, ""), dlv(delivery.MergeMerged, c2)) gate["feat/take-in"] = merge(sdk(meshdeliveryclient.MergeMerged, c1, ""), dlv(meshdeliveryclient.MergeMerged, c2))
g.ended(ctx, "feat/take-in", 9, gate["feat/take-in"]) g.ended(ctx, "feat/take-in", 9, gate["feat/take-in"])
if len(pub.subjects) != 0 { if len(pub.subjects) != 0 {
t.Fatalf("an end before every member's merged message is written: %v", pub.subjects) t.Fatalf("an end before every member's merged message is written: %v", pub.subjects)
@@ -97,12 +98,12 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
} }
// 1585/T19: an ask to say a group's end again is answered by the bridge from the gate's merges, with the ask's time on // 1585/T19: an ask to say a group's end again is answered by the bridge from the gate's merges, with the ask's time on
// the message id; not written while a member is not merged, with the reason; not for a group the gate does not hold. // the message id; not written while a member is not merged, with the reason; not for a group the gate does not hold.
pull := func(repo string, n int, head string, state delivery.MergeState, commit string) delivery.MergePull { pull := func(repo string, n int, head string, state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
return delivery.MergePull{Head: delivery.Head{Repository: repo, Number: n, Head: head}, State: state, Commit: commit} return seat.MergesByGroupValuePullsItem{Repository: repo, Number: int64(n), Head: head, State: string(state), Commit: commit}
} }
askMerge := func(second delivery.MergeState, commit2 string) []byte { askMerge := func(second meshdeliveryclient.MergeState, commit2 string) []byte {
v, err := delivery.EncodeMerge(delivery.Merge{Group: "feat/take-in", Ask: "ask", Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2}, v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/take-in", Ask: "ask", Approved: map[string]string{"novox/mesh-sdk@37": h1, "novox/mesh-delivery@9": h2},
Pulls: []delivery.MergePull{pull("novox/mesh-sdk", 37, h1, delivery.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}}) Pulls: []seat.MergesByGroupValuePullsItem{pull("novox/mesh-sdk", 37, h1, meshdeliveryclient.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -116,9 +117,9 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
written bool written bool
answer string answer string
}{ }{
{"merged and published: written again", map[string][]byte{"feat/take-in": askMerge(delivery.MergeMerged, c2)}, true, true, "written again"}, {"merged and published: written again", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, true, true, "written again"},
{"a member's message not written", map[string][]byte{"feat/take-in": askMerge(delivery.MergeMerged, c2)}, false, false, "not written"}, {"a member's message not written", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerged, c2)}, false, false, "not written"},
{"a member not merged", map[string][]byte{"feat/take-in": askMerge(delivery.MergeMerging, "")}, true, false, "not written"}, {"a member not merged", map[string][]byte{"feat/take-in": askMerge(meshdeliveryclient.MergeMerging, "")}, true, false, "not written"},
{"no merge at the gate", map[string][]byte{}, true, false, "holds no merge"}, {"no merge at the gate", map[string][]byte{}, true, false, "holds no merge"},
} { } {
pub, done := &idPub{}, memRevisions{} pub, done := &idPub{}, memRevisions{}
@@ -5,13 +5,11 @@ import (
"errors" "errors"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
) )
// headsBucket is the module's state bucket heads, `<module>_<name>` as the indexer's buckets are named: the head last // headsBucket is the module's state bucket heads, `<module>_<name>` as the indexer's buckets are named: the head last
// written on FORGE for each pull request (manifest state entry heads, novox/hq ADR 0443 §5). // written on FORGE for each pull request (manifest state entry heads, novox/hq ADR 0443 §5).
const headsBucket = delivery.ForgeBridgeModule + "_heads" const headsBucket = forgeBridgeModule + "_heads"
// kvHeads is the bucket heads as the bridge's headStore. // kvHeads is the bucket heads as the bridge's headStore.
type kvHeads struct{ kv jetstream.KeyValue } type kvHeads struct{ kv jetstream.KeyValue }
@@ -20,8 +20,10 @@ import (
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process" "git.novox.be/novox/mesh-sdk/go/process"
) )
@@ -232,8 +234,8 @@ func startBed(t *testing.T, openReply string, prepare func(ctx context.Context,
t.Fatal(err) t.Fatal(err)
} }
// Its other state bucket, and the gate's buckets the bridge reads, made by the controller from the gate's manifest // Its other state bucket, and the gate's buckets the bridge reads, made by the controller from the gate's manifest
// (mesh-sdk go/delivery MergesBucket, OwedBucket with their histories). // (the gate's merges and owed with their histories).
for name, history := range map[string]uint8{performedBucket: 1, inputsBucket: 1, delivery.MergesBucket: delivery.MergesHistory, delivery.OwedBucket: delivery.OwedHistory} { for name, history := range map[string]uint8{performedBucket: 1, inputsBucket: 1, mergesBucket: 10, owedBucket: 5} {
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil { if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -334,7 +336,7 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := forgemsg.Forge{V: delivery.Version, Kind: "pull", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, want := forgemsg.Forge{V: keys.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": "{}"}} Action: "updated", Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}}
if !forgemsg.SameForge(f, want) { if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want) t.Errorf("message = %+v, want %+v", f, want)
@@ -465,7 +467,7 @@ func TestBridgeWritesMergedMessage(t *testing.T) { // 1236/AC4, 1448/AC5
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := forgemsg.Forge{V: delivery.Version, Kind: "merged", Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, want := forgemsg.Forge{V: keys.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"}, Merge: merge, Root: "r0", Trees: map[string]string{"": "t0", "modules/a": "t1"},
Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)}, Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)},
// No mesh package: only the indexer computes what a fingerprint takes (ADR 0593 §2). // No mesh package: only the indexer computes what a fingerprint takes (ADR 0593 §2).
@@ -690,15 +692,15 @@ func TestBridgeMergesAGroupOneMemberAtATime(t *testing.T) { // 1342/AC3
t.Fatal(err) t.Fatal(err)
} }
kv, err := b.js.KeyValue(b.ctx, delivery.MergesBucket) kv, err := b.js.KeyValue(b.ctx, mergesBucket)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
approved := map[string]string{delivery.ApprovedKey("novox/mesh-delivery", 12): head, delivery.ApprovedKey("novox/mesh-sdk", 4): headB} approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): head, meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB}
pullA := delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head}, State: delivery.MergeMerging} pullA := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 12, Head: head, State: string(meshdeliveryclient.MergeMerging)}
pullB := delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: delivery.MergeMerging} pullB := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerging)}
put := func(pulls ...delivery.MergePull) { put := func(pulls ...seat.MergesByGroupValuePullsItem) {
v, err := delivery.EncodeMerge(delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: pulls}) v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: pulls})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -726,7 +728,7 @@ func TestBridgeMergesAGroupOneMemberAtATime(t *testing.T) { // 1342/AC3
// The gate hands the next member only after the bridge reported the one before merged (ADR 0441 §5). // The gate hands the next member only after the bridge reported the one before merged (ADR 0441 §5).
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && len(forgeMerged(t, b)) < 1; time.Sleep(50 * time.Millisecond) { for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline) && len(forgeMerged(t, b)) < 1; time.Sleep(50 * time.Millisecond) {
} }
pullA.State, pullA.Commit = delivery.MergeMerged, mergeA pullA.State, pullA.Commit = string(meshdeliveryclient.MergeMerged), mergeA
put(pullA, pullB) put(pullA, pullB)
next("mesh-sdk") next("mesh-sdk")
var got []string var got []string
@@ -786,15 +788,15 @@ func TestBridgePerformsNoMeshCheckAct(t *testing.T) { // R2-1, ADR 0521
if err := b.ctl.Flush(); err != nil { if err := b.ctl.Flush(); err != nil {
t.Fatal(err) t.Fatal(err)
} }
kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: delivery.CheckerOwedBucket, History: delivery.CheckerOwedHistory}) kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: "mesh-delivery-checker_owed", History: 5})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
key, err := delivery.CheckerOwedKey("novox/mesh-delivery", 12, head) key, err := meshdeliveryclient.OwedKey("novox/mesh-delivery", 12, head, "status=mesh/check")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
v, err := delivery.EncodeCheckerOwed(delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.0123456789ab"}) v, err := meshdeliveryclient.EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.0123456789ab"})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -10,8 +10,6 @@ import (
"github.com/nats-io/nats.go/jetstream" "github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-forge-bridge/internal/bridge" "mesh-delivery-forge-bridge/internal/bridge"
) )
@@ -19,7 +17,7 @@ import (
// directory, its manifest and lock-file hashes, by the directory's git tree hash, so a merge reads the forge only for the // directory, its manifest and lock-file hashes, by the directory's git tree hash, so a merge reads the forge only for the
// directories it changed (issue 1126; internal/bridge kept.go). A tree hash names its bytes, so a value is never stale; // directories it changed (issue 1126; internal/bridge kept.go). A tree hash names its bytes, so a value is never stale;
// a merge adds a value only for each directory it changed (a few kilobytes each). // a merge adds a value only for each directory it changed (a few kilobytes each).
const inputsBucket = delivery.ForgeBridgeModule + "_inputs" const inputsBucket = forgeBridgeModule + "_inputs"
// keptTimeout bounds one read or write of the bucket: the bucket is a saving, never a reason to fail a merge. // keptTimeout bounds one read or write of the bucket: the bucket is a saving, never a reason to fail a merge.
const keptTimeout = 2 * time.Second const keptTimeout = 2 * time.Second
@@ -1,7 +1,7 @@
// Command mesh-delivery-forge-bridge is delivery's forge bridge (novox/hq ADR 0437 §4, issue 1235): it hears the forge // Command mesh-delivery-forge-bridge is delivery's forge bridge (novox/hq ADR 0437 §4, issue 1235): it hears the forge
// module's pull request events and writes each as a pull message on its stream FORGE. It runs beside today's // module's pull request events and writes each as a pull message on its stream FORGE. It runs beside today's
// mesh-delivery and merges nothing. It serves no request subject and holds no timer: every reaction is an event // mesh-delivery and merges nothing. It serves no request subject and holds no timer: every reaction is an event
// (ADR 0437 §8); a refused act is tried again only on the next event touching its delivery. // (ADR 0437 §8); a refused act is tried again only on the next event touching its meshdeliveryclient.
package main package main
import ( import (
@@ -22,7 +22,6 @@ import (
"github.com/nats-io/nats.go/jetstream" "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-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-forge-bridge/internal/bridge" "mesh-delivery-forge-bridge/internal/bridge"
groupkeys "mesh-delivery-forge-bridge/internal/groups" groupkeys "mesh-delivery-forge-bridge/internal/groups"
@@ -35,7 +34,7 @@ const eventsStream = "EVENTS"
// The bridge's own stream FORGE takes `mesh.stream.<module>.event.<event>` (mesh-control internal/broker // The bridge's own stream FORGE takes `mesh.stream.<module>.event.<event>` (mesh-control internal/broker
// modstreams.go ModuleStreamSubject). // modstreams.go ModuleStreamSubject).
func forgeSubject(event string) string { func forgeSubject(event string) string {
return process.ModuleStreamSubject(delivery.ForgeBridgeModule, event) return process.ModuleStreamSubject(forgeBridgeModule, event)
} }
func main() { func main() {
@@ -158,7 +157,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
for _, w := range []struct { for _, w := range []struct {
bucket string bucket string
react func(ctx context.Context, key string, rev uint64, value []byte) react func(ctx context.Context, key string, rev uint64, value []byte)
}{{delivery.OwedBucket, perf.owed}, {delivery.MergesBucket, func(ctx context.Context, key string, rev uint64, value []byte) { }{{owedBucket, perf.owed}, {mergesBucket, func(ctx context.Context, key string, rev uint64, value []byte) {
perf.merges(ctx, key, rev, value) perf.merges(ctx, key, rev, value)
groups.ended(ctx, key, rev, value) groups.ended(ctx, key, rev, value)
}}} { }}} {
@@ -168,7 +167,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
if kv == nil { if kv == nil {
continue continue
} }
if w.bucket == delivery.MergesBucket { if w.bucket == mergesBucket {
groups.merges = kvMerges(kv) groups.merges = kvMerges(kv)
} }
go func() { go func() {
@@ -0,0 +1,16 @@
package main
import groupkeys "mesh-delivery-forge-bridge/internal/groups"
// The holders and buckets the forge bridge names, taken from its own manifest (module.json: its name, its `reads`, its
// states); a unit row checks them against it (TestTheNamesAreTheManifests). No contract names a holder
// (novox/hq ADR 0625 §4, ADR 0634 §1).
const (
forgeBridgeModule = groupkeys.Module
gateModule = "mesh-delivery-gate"
// mergesBucket and owedBucket are the gate's states merges and owed, read by the bridge (reads: mesh-delivery-gate.merges,
// mesh-delivery-gate.owed).
mergesBucket = gateModule + "_merges"
owedBucket = gateModule + "_owed"
)
@@ -0,0 +1,44 @@
package main
import (
"encoding/json"
"os"
"slices"
"testing"
)
// theNamesAreTheManifests (a subtest of TestAnOwedStatusIsSetOnceForARevision): the holders and buckets the bridge names
// are its manifest's (its module, its `reads`, its states), since no contract names a holder (novox/hq ADR 0634 §1).
func theNamesAreTheManifests(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Module string `json:"module"`
Reads []string `json:"reads"`
State []struct {
Name string `json:"name"`
} `json:"state"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if m.Module != forgeBridgeModule {
t.Errorf("the manifest's module is %q, the bridge names itself %q", m.Module, forgeBridgeModule)
}
for _, read := range []string{gateModule + ".merges", gateModule + ".owed"} {
if !slices.Contains(m.Reads, read) {
t.Errorf("the bridge reads %s, its manifest's reads are %v", read, m.Reads)
}
}
var own []string
for _, s := range m.State {
own = append(own, forgeBridgeModule+"_"+s.Name)
}
for _, b := range []string{headsBucket, performedBucket, inputsBucket} {
if !slices.Contains(own, b) {
t.Errorf("the bridge keeps %s, its manifest's states are %v", b, own)
}
}
}
@@ -11,11 +11,13 @@ import (
"regexp" "regexp"
"strings" "strings"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
"git.novox.be/novox/mesh-sdk/go/keys"
) )
// Version is the version of every value this package writes and the only one it reads: mesh-sdk's delivery values'. // 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 const Version = keys.Version
// ForgeStream is the forge bridge's stream as its manifest declares it; the broker derives the real names. // ForgeStream is the forge bridge's stream as its manifest declares it; the broker derives the real names.
const ForgeStream = "forge" const ForgeStream = "forge"
@@ -156,8 +158,8 @@ type Forge struct {
// that group with its approved head, this one included (novox/hq ADR 0633 §4): the forge bridge sets both from the // 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 // 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. // with no group carries neither, and an older bridge sends neither; an older reader ignores both.
Group string `json:"group,omitempty"` Group string `json:"group,omitempty"`
Members []delivery.Head `json:"members,omitempty"` Members []meshdeliveryclient.Head `json:"members,omitempty"`
} }
// checkGroup refuses a group on anything but a merged message, a group without its members or members without a // checkGroup refuses a group on anything but a merged message, a group without its members or members without a
@@ -187,7 +189,7 @@ func (f Forge) checkGroup() error {
return fmt.Errorf("delivery: the group %q names its member %s twice", f.Group, k) return fmt.Errorf("delivery: the group %q names its member %s twice", f.Group, k)
} }
seen[k] = true seen[k] = true
self = self || (delivery.SameRepository(m.Repository, f.Repository) && m.Number == f.Number) self = self || (trunk.SameRepository(m.Repository, f.Repository) && m.Number == f.Number)
} }
if !self { 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 fmt.Errorf("delivery: the members of the group %q leave out the merged pull request %s#%d", f.Group, f.Repository, f.Number)
@@ -395,7 +397,7 @@ func checkName(kind, name string) error {
return nil return nil
} }
// decode reads a value of Version, refusing another or none with delivery.ErrUnknownVersion, as mesh-sdk delivery's // decode reads a value of Version, refusing another or none with keys.ErrUnknownVersion, as mesh-sdk delivery's
// values are read. // values are read.
func decode(data []byte, into any) error { func decode(data []byte, into any) error {
var probe struct { var probe struct {
@@ -405,10 +407,10 @@ func decode(data []byte, into any) error {
return fmt.Errorf("delivery: value is not JSON: %w", err) return fmt.Errorf("delivery: value is not JSON: %w", err)
} }
if probe.V == nil { if probe.V == nil {
return fmt.Errorf("%w: no version in the value, this reader reads %d", delivery.ErrUnknownVersion, Version) return fmt.Errorf("%w: no version in the value, this reader reads %d", keys.ErrUnknownVersion, Version)
} }
if *probe.V != Version { if *probe.V != Version {
return fmt.Errorf("%w: the value is version %d, this reader reads %d", delivery.ErrUnknownVersion, *probe.V, Version) return fmt.Errorf("%w: the value is version %d, this reader reads %d", keys.ErrUnknownVersion, *probe.V, Version)
} }
if err := json.Unmarshal(data, into); err != nil { if err := json.Unmarshal(data, into); err != nil {
return fmt.Errorf("delivery: value does not fit its shape: %w", err) return fmt.Errorf("delivery: value does not fit its shape: %w", err)
@@ -6,7 +6,8 @@ import (
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-sdk/go/keys"
) )
const oddManifest = "{\n\t\"module\": \"a\",\n \"z\": 1, \"a\": [ \"x\",\"y\" ],\n \"why\": \"é — <b> & \\n\"\n}\n\n" const oddManifest = "{\n\t\"module\": \"a\",\n \"z\": 1, \"a\": [ \"x\",\"y\" ],\n \"why\": \"é — <b> & \\n\"\n}\n\n"
@@ -20,7 +21,7 @@ func sampleForges() []Forge {
{Kind: ForgeOpen, Open: []OpenPull{{Repository: "novox/mesh-sdk", Number: 36}, {Repository: "novox/mesh-control", Number: 7}}}, {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). // 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", {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"}}}, Group: "feat/take-in", Members: []meshdeliveryclient.Head{{Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}, {Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}}},
} }
} }
@@ -107,8 +108,8 @@ func TestForge(t *testing.T) {
"group end": func(b []byte) error { _, err := DecodeForgeGroupEnd(b); return err }, "group end": func(b []byte) error { _, err := DecodeForgeGroupEnd(b); return err },
} { } {
for _, in := range []string{`{"group":"g"}`, `{"v":2,"group":"g"}`} { for _, in := range []string{`{"group":"g"}`, `{"v":2,"group":"g"}`} {
if err := dec([]byte(in)); !errors.Is(err, delivery.ErrUnknownVersion) { if err := dec([]byte(in)); !errors.Is(err, keys.ErrUnknownVersion) {
t.Errorf("%s decode of %s = %v, want delivery.ErrUnknownVersion", name, in, err) t.Errorf("%s decode of %s = %v, want keys.ErrUnknownVersion", name, in, err)
} }
} }
} }
@@ -158,7 +159,7 @@ func TestForge(t *testing.T) {
{"open with locks", "only the open", Forge{Kind: ForgeOpen, Locks: map[string]map[string]string{"a": {"go.sum": "h"}}}}, {"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. // 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) { {"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"}} f.Group, f.Members = "g", []meshdeliveryclient.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 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 and no group", "together", at(4, func(f *Forge) { f.Group = "" })},
@@ -3,3 +3,8 @@ module git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forge
go 1.26.0 go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659 require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104002338-5a38a1221659
require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
)
@@ -3,6 +3,9 @@ module mesh-delivery-forge-bridge
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
@@ -5,8 +5,9 @@ import (
"slices" "slices"
"strings" "strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
) )
// Call is a call of a forge tool: the tool by its manifest key and its arguments. // Call is a call of a forge tool: the tool by its manifest key and its arguments.
@@ -26,7 +27,7 @@ type Act struct {
Call Call Call Call
} }
// The forge's tools, by manifest key; their arguments are the ones of mesh-catalog modules/gitea: delivery.ts // The forge's tools, by manifest key; their arguments are the ones of mesh-catalog modules/gitea: meshdeliveryclient.ts
// commitStatusTool (owner, repo, sha, context, state, description, target_url), tools/index.ts gitea_delivery_view // commitStatusTool (owner, repo, sha, context, state, description, target_url), tools/index.ts gitea_delivery_view
// (owner, repo, number, body), gitea_note_append (owner, repo, sha, ref, line), gitea_add_comment (owner, repo, // (owner, repo, number, body), gitea_note_append (owner, repo, sha, ref, line), gitea_add_comment (owner, repo,
// number, body), merge.ts gitea_merge_pull_request (owner, repo, number, head, approved_heads). // number, body), merge.ts gitea_merge_pull_request (owner, repo, number, head, approved_heads).
@@ -50,15 +51,15 @@ func ownerRepo(repository string) (owner, repo string) {
// it. A key that is no owed key, a value that is no owed act, a value whose act is not the key's, and a view or a comment // it. A key that is no owed key, a value that is no owed act, a value whose act is not the key's, and a view or a comment
// on no pull request are errors, each saying why. // on no pull request are errors, each saying why.
func OwedAct(key string, value []byte) (Act, error) { func OwedAct(key string, value []byte) (Act, error) {
repository, number, head, name, err := delivery.ParseOwedKey(key) repository, number, head, name, err := meshdeliveryclient.ParseOwedKey(key)
if err != nil { if err != nil {
return Act{}, err return Act{}, err
} }
o, err := delivery.DecodeOwed(value) o, err := meshdeliveryclient.DecodeOwed(value)
if err != nil { if err != nil {
return Act{}, fmt.Errorf("the owed act %s: %w", key, err) return Act{}, fmt.Errorf("the owed act %s: %w", key, err)
} }
kind, context, err := delivery.ParseOwedAct(name) kind, context, err := meshdeliveryclient.ParseOwedAct(name)
if err != nil { if err != nil {
return Act{}, err return Act{}, err
} }
@@ -74,26 +75,26 @@ func OwedAct(key string, value []byte) (Act, error) {
return nil return nil
} }
switch kind { switch kind {
case delivery.OwedStatus: case meshdeliveryclient.OwedStatus:
if o.Context != context { if o.Context != context {
return Act{}, fmt.Errorf("the owed status %s says the context %q, not the %q its key names", key, o.Context, context) return Act{}, fmt.Errorf("the owed status %s says the context %q, not the %q its key names", key, o.Context, context)
} }
if o.Context == delivery.StatusContextCheck { if o.Context == meshdeliveryclient.StatusContextCheck {
// No tool writes mesh/check (R4e finding K1), and the bridge performs no act of it: the forge module writes it from // No tool writes mesh/check (R4e finding K1), and the bridge performs no act of it: the forge module writes it from
// the checker's state mesh-delivery-checker.owed, read by a watch (ADR 0521, finding R2-1). // the checker's state mesh-delivery-checker.owed, read by a watch (ADR 0521, finding R2-1).
return Act{}, fmt.Errorf("the owed status %s is %s, which the bridge does not perform: the forge module writes it from mesh-delivery-checker.owed", return Act{}, fmt.Errorf("the owed status %s is %s, which the bridge does not perform: the forge module writes it from mesh-delivery-checker.owed",
key, delivery.StatusContextCheck) key, meshdeliveryclient.StatusContextCheck)
} }
a.Call = Call{Tool: commitStatusTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "context": o.Context, a.Call = Call{Tool: commitStatusTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "context": o.Context,
"state": o.State, "description": o.Text, "target_url": o.Target}} "state": o.State, "description": o.Text, "target_url": o.Target}}
case delivery.OwedView: case meshdeliveryclient.OwedView:
if err := needsPull(); err != nil { if err := needsPull(); err != nil {
return Act{}, err return Act{}, err
} }
a.Call = Call{Tool: deliveryViewTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}} a.Call = Call{Tool: deliveryViewTool, Args: map[string]any{"owner": owner, "repo": repo, "number": number, "body": o.Text}}
case delivery.OwedNote: case meshdeliveryclient.OwedNote:
a.Call = Call{Tool: noteAppendTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "ref": o.Ref, "line": o.Text}} a.Call = Call{Tool: noteAppendTool, Args: map[string]any{"owner": owner, "repo": repo, "sha": head, "ref": o.Ref, "line": o.Text}}
case delivery.OwedComment: case meshdeliveryclient.OwedComment:
if err := needsPull(); err != nil { if err := needsPull(); err != nil {
return Act{}, err return Act{}, err
} }
@@ -102,19 +103,19 @@ func OwedAct(key string, value []byte) (Act, error) {
return a, nil return a, nil
} }
// MergeActs turns the value of a merges key (mesh-sdk go/delivery EncodeMerge) into the merges to perform: one for each // MergeActs turns the value of a merges key (the delivery client's EncodeMerge) into the merges to perform: one for each
// pull request in the state merging, in the order the value names them, at the head it expects and with the approved // pull request in the state merging, in the order the value names them, at the head it expects and with the approved
// heads of every member of the group, `<owner>/<repo>@<full sha>` sorted (the merge tool's approved_heads). A pull // heads of every member of the group, `<owner>/<repo>@<full sha>` sorted (the merge tool's approved_heads). A pull
// request merged or refused already gets none. A value with no approved heads is an error: the forge's merge tool // request merged or refused already gets none. A value with no approved heads is an error: the forge's merge tool
// needs them for a group. // needs them for a group.
func MergeActs(value []byte) ([]Act, error) { func MergeActs(value []byte) ([]Act, error) {
m, err := delivery.DecodeMerge(value) m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var handed []delivery.MergePull var handed []seat.MergesByGroupValuePullsItem
for _, p := range m.Pulls { for _, p := range m.Pulls {
if p.State == delivery.MergeMerging { if meshdeliveryclient.MergeState(p.State) == meshdeliveryclient.MergeMerging {
handed = append(handed, p) handed = append(handed, p)
} }
} }
@@ -136,8 +137,8 @@ func MergeActs(value []byte) ([]Act, error) {
var acts []Act var acts []Act
for _, p := range handed { for _, p := range handed {
owner, repo := ownerRepo(p.Repository) owner, repo := ownerRepo(p.Repository)
acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: p.Number, Head: p.Head.Head, acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: int(p.Number), Head: p.Head,
Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "number": p.Number, "head": p.Head.Head, Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "number": int(p.Number), "head": p.Head,
"approved_heads": approved}}}) "approved_heads": approved}}})
} }
return acts, nil return acts, nil
@@ -5,8 +5,10 @@ import (
"strings" "strings"
"testing" "testing"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/keys"
) )
const ( const (
@@ -15,13 +17,13 @@ const (
) )
// owedValue is an owed value as the SDK writes it (mesh-sdk go/delivery EncodeOwed), and its key as OwedKey spells it. // owedValue is an owed value as the SDK writes it (mesh-sdk go/delivery EncodeOwed), and its key as OwedKey spells it.
func owedValue(t *testing.T, repository string, number int, act string, o delivery.Owed) (string, []byte) { func owedValue(t *testing.T, repository string, number int, act string, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) {
t.Helper() t.Helper()
key, err := delivery.OwedKey(repository, number, headA, act) key, err := meshdeliveryclient.OwedKey(repository, number, headA, act)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
b, err := delivery.EncodeOwed(o) b, err := meshdeliveryclient.EncodeOwed(o)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -30,7 +32,7 @@ func owedValue(t *testing.T, repository string, number int, act string, o delive
func TestOwedStatusIsACommitStatusCall(t *testing.T) { // 1341/AC1, K1 func TestOwedStatusIsACommitStatusCall(t *testing.T) { // 1341/AC1, K1
key, v := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate", key, v := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/merge-gate",
delivery.Owed{Act: delivery.OwedStatus, Context: "mesh/merge-gate", State: "pending", Text: "waiting", Target: "https://forge.example/novox/mesh-delivery/pulls/12"}) seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: "mesh/merge-gate", State: "pending", Text: "waiting", Target: "https://forge.example/novox/mesh-delivery/pulls/12"})
a, err := OwedAct(key, v) a, err := OwedAct(key, v)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -46,26 +48,26 @@ func TestOwedStatusIsACommitStatusCall(t *testing.T) { // 1341/AC1, K1
// R4e findings K1 and R2-1 (issue 1301 S4; ADR 0503 §3, ADR 0521): no tool writes mesh/check, since a tool is told no // R4e findings K1 and R2-1 (issue 1301 S4; ADR 0503 §3, ADR 0521): no tool writes mesh/check, since a tool is told no
// caller and node-tools invokes every tool; the forge module writes it from the checker's owed state, and the bridge // caller and node-tools invokes every tool; the forge module writes it from the checker's owed state, and the bridge
// performs no mesh/check act. // performs no mesh/check act.
ck, cv := checkerKV(t, delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.1"}) ck, cv := checkerKV(t, seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://view.example/checks/g.1"})
if a, err := OwedAct(ck, cv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") { if a, err := OwedAct(ck, cv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") {
t.Fatalf("OwedAct(mesh/check) = %+v, %v; want refused, naming where the forge module reads it", a, err) t.Fatalf("OwedAct(mesh/check) = %+v, %v; want refused, naming where the forge module reads it", a, err)
} }
// Nor through the gate's own owed bucket: the context is refused whichever key carries it. // Nor through the gate's own owed bucket: the context is refused whichever key carries it.
gk, gv := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/check", gk, gv := owedValue(t, "novox/mesh-delivery", 12, "status=mesh/check",
delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "success", Text: "pass", Target: "https://x.example"}) seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "success", Text: "pass", Target: "https://x.example"})
if a, err := OwedAct(gk, gv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") { if a, err := OwedAct(gk, gv); err == nil || !strings.Contains(err.Error(), "mesh-delivery-checker.owed") {
t.Fatalf("OwedAct(gate's mesh/check) = %+v, %v; want refused", a, err) t.Fatalf("OwedAct(gate's mesh/check) = %+v, %v; want refused", a, err)
} }
} }
// checkerKV is the status the checker owes, as it writes it (mesh-sdk go/delivery CheckerOwedKey, EncodeCheckerOwed). // checkerKV is the status the checker owes, as it writes it (the gate's owed key with the act status=mesh/check).
func checkerKV(t *testing.T, o delivery.Owed) (string, []byte) { func checkerKV(t *testing.T, o seat.OwedByRepositoryNumberHeadActValue) (string, []byte) {
t.Helper() t.Helper()
key, err := delivery.CheckerOwedKey("novox/mesh-delivery", 12, headA) key, err := meshdeliveryclient.OwedKey("novox/mesh-delivery", 12, headA, "status=mesh/check")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
v, err := delivery.EncodeCheckerOwed(o) v, err := meshdeliveryclient.EncodeOwed(o)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -75,14 +77,14 @@ func checkerKV(t *testing.T, o delivery.Owed) (string, []byte) {
func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2 func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2
cases := []struct { cases := []struct {
act string act string
o delivery.Owed o seat.OwedByRepositoryNumberHeadActValue
want Call want Call
}{ }{
{"view", delivery.Owed{Act: delivery.OwedView, Text: "the view"}, {"view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "the view"},
Call{Tool: "gitea.gitea_delivery_view", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "the view"}}}, Call{Tool: "gitea.gitea_delivery_view", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "the view"}}},
{"note", delivery.Owed{Act: delivery.OwedNote, Text: "a line", Ref: "mesh-plan"}, {"note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "a line", Ref: "mesh-plan"},
Call{Tool: "gitea.gitea_note_append", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "sha": headA, "ref": "mesh-plan", "line": "a line"}}}, Call{Tool: "gitea.gitea_note_append", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "sha": headA, "ref": "mesh-plan", "line": "a line"}}},
{"comment.2", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}, {"comment.2", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"},
Call{Tool: "gitea.gitea_add_comment", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "hello"}}}, Call{Tool: "gitea.gitea_add_comment", Args: map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "body": "hello"}}},
} }
for _, c := range cases { for _, c := range cases {
@@ -98,11 +100,11 @@ func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2
} }
func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) { func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) {
key, v := owedValue(t, "novox/mesh-delivery", 0, "note", delivery.Owed{Act: delivery.OwedNote, Text: "l", Ref: "mesh-plan"}) key, v := owedValue(t, "novox/mesh-delivery", 0, "note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "l", Ref: "mesh-plan"})
if _, err := OwedAct(key, v); err != nil { if _, err := OwedAct(key, v); err != nil {
t.Errorf("a note on a commit no pull request names: %v", err) t.Errorf("a note on a commit no pull request names: %v", err)
} }
key, v = owedValue(t, "novox/mesh-delivery", 0, "view", delivery.Owed{Act: delivery.OwedView, Text: "v"}) key, v = owedValue(t, "novox/mesh-delivery", 0, "view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "v"})
if _, err := OwedAct(key, v); err == nil { if _, err := OwedAct(key, v); err == nil {
t.Error("a view of no pull request was taken") t.Error("a view of no pull request was taken")
} }
@@ -110,12 +112,12 @@ func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) {
func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) { func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) {
// A dotted repository's key is unescaped, and its act names the repository whole. // 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"}) dk, dv := owedValue(t, "novox/my.repo", 3, "comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "x"})
if a, err := OwedAct(dk, dv); err != nil || a.Call.Args["repo"] != "my.repo" || a.Repository != "novox/my.repo" { 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) 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"}) key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "v"})
v, _ := delivery.EncodeOwed(delivery.Owed{Act: delivery.OwedComment, Text: "v"}) v, _ := meshdeliveryclient.EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "v"})
if _, err := OwedAct(key, v); err == nil { if _, err := OwedAct(key, v); err == nil {
t.Error("a comment under a view's key was taken") t.Error("a comment under a view's key was taken")
} }
@@ -127,9 +129,9 @@ func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) {
} }
} }
func mergeValue(t *testing.T, m delivery.Merge) []byte { func mergeValue(t *testing.T, m seat.MergesByGroupValue) []byte {
t.Helper() t.Helper()
b, err := delivery.EncodeMerge(m) b, err := meshdeliveryclient.EncodeMerge(m)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -137,10 +139,10 @@ func mergeValue(t *testing.T, m delivery.Merge) []byte {
} }
func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { // 1342/AC1 func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { // 1342/AC1
approved := map[string]string{delivery.ApprovedKey("novox/mesh-sdk", 4): headB, delivery.ApprovedKey("novox/mesh-delivery", 12): headA} approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA}
v := mergeValue(t, delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: []delivery.MergePull{ v := mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: []seat.MergesByGroupValuePullsItem{
{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: delivery.MergeMerged, Commit: "c0"}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerged), Commit: "c0"},
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging}, {Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)},
}}) }})
acts, err := MergeActs(v) acts, err := MergeActs(v)
if err != nil { if err != nil {
@@ -158,8 +160,8 @@ func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { /
t.Errorf("act = %+v", acts[0]) t.Errorf("act = %+v", acts[0])
} }
// A merge with no approved heads is refused: the forge's tool would refuse it with nothing to say which. // 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{ v = mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "a", Pulls: []seat.MergesByGroupValuePullsItem{
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging}}}) {Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)}}})
if _, err := MergeActs(v); err == nil { if _, err := MergeActs(v); err == nil {
t.Error("a merge with no approved heads was taken") t.Error("a merge with no approved heads was taken")
} }
@@ -178,7 +180,7 @@ func TestRefusedMessageNamesTheActAndTheForgesWords(t *testing.T) { // 1341/AC3,
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeRefused, Repository: "novox/mesh-delivery", Number: 12, Head: headA, want := forgemsg.Forge{V: keys.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"} Act: "merge", Words: "novox/mesh-delivery#12 is not merged: its head moved"}
if !forgemsg.SameForge(f, want) { if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v", f) t.Errorf("message = %+v", f)
@@ -17,7 +17,7 @@ import (
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "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/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/keys"
) )
// ErrUnreadable is wrapped by every error that says an event cannot be turned into a message; it is never // ErrUnreadable is wrapped by every error that says an event cannot be turned into a message; it is never
@@ -118,7 +118,7 @@ func branchDeleted(subject string, data []byte) (Message, error) {
case branch == Trunk: case branch == Trunk:
return Message{}, fmt.Errorf("%w: %s of the trunk %s", ErrNotHandled, subject, Trunk) return Message{}, fmt.Errorf("%w: %s of the trunk %s", ErrNotHandled, subject, Trunk)
} }
body, err := json.Marshal(BranchDeleted{V: delivery.Version, Kind: ForgeBranchDeleted, Repository: d.Owner + "/" + d.Repo, Branch: branch, Head: d.SHA}) body, err := json.Marshal(BranchDeleted{V: keys.Version, Kind: ForgeBranchDeleted, Repository: d.Owner + "/" + d.Repo, Branch: branch, Head: d.SHA})
if err != nil { if err != nil {
return Message{}, unreadable(subject, "%v", err) return Message{}, unreadable(subject, "%v", err)
} }
@@ -7,7 +7,7 @@ import (
"testing" "testing"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-sdk/go/keys"
) )
const head = "0123456789abcdef0123456789abcdef01234567" const head = "0123456789abcdef0123456789abcdef01234567"
@@ -59,7 +59,7 @@ func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion;
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, want := forgemsg.Forge{V: keys.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": "{}"}} Head: head, Action: forgemsg.PullUpdated, Dirs: []string{"modules/a"}, DirsKnown: true, Manifests: map[string]string{"modules/a": "{}"}}
if !forgemsg.SameForge(f, want) { if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want) t.Errorf("message = %+v, want %+v", f, want)
@@ -105,7 +105,7 @@ func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: forgemsg.PullClosed} Head: head, Action: forgemsg.PullClosed}
if !forgemsg.SameForge(f, want) { if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want) t.Errorf("message = %+v, want %+v", f, want)
@@ -252,7 +252,7 @@ func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, want := forgemsg.Forge{V: keys.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": "{}"}} Head: head, Merge: merge, Root: "r0", Trees: answer, Manifests: map[string]string{"modules/a": "{}", "modules/b": "{}"}}
if !forgemsg.SameForge(f, want) { if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want) t.Errorf("message = %+v, want %+v", f, want)
@@ -272,7 +272,7 @@ func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Action: forgemsg.PullClosed} Head: head, Action: forgemsg.PullClosed}
if msg.Event != "forge.novox.mesh-delivery.pull" || !forgemsg.SameForge(f, want) { 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) t.Errorf("event %q message %+v, want a closed pull message", msg.Event, f)
@@ -304,7 +304,7 @@ func TestNoModuleJSONAnywhereWritesAMergedMessageWithNoTrees(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
f, err := forgemsg.DecodeForge(msg.Data) f, err := forgemsg.DecodeForge(msg.Data)
want := forgemsg.Forge{V: delivery.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, want := forgemsg.Forge{V: keys.Version, Kind: forgemsg.ForgeMerged, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12,
Head: head, Merge: merge, Root: "r0"} Head: head, Merge: merge, Root: "r0"}
if err != nil || msg.Event != "forge.novox.mesh-delivery.merged" || !forgemsg.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) t.Errorf("event %q message %+v said %q, %v; want a merged message with no trees", msg.Event, f, msg.Said, err)
@@ -330,8 +330,8 @@ func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/
t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err) t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err)
} }
for i, want := range []forgemsg.Forge{ 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: keys.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}, {V: keys.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated},
} { } {
f, err := forgemsg.DecodeForge(msgs[i].Data) f, err := forgemsg.DecodeForge(msgs[i].Data)
if err != nil || !forgemsg.SameForge(f, want) { if err != nil || !forgemsg.SameForge(f, want) {
@@ -16,17 +16,20 @@ import (
"strings" "strings"
"time" "time"
"git.novox.be/novox/mesh-sdk/go/delivery" "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
) )
// The bridge's states the tools read and write. // The bridge's states the tools read and write.
const ( const (
// Module is the forge bridge's own name (its manifest's module, checked by the bridge's names test).
Module = "mesh-delivery-forge-bridge"
// PerformedState is the bridge's state of what it performed and published. // PerformedState is the bridge's state of what it performed and published.
PerformedState = "performed" PerformedState = "performed"
// EndAsksState is the state of asks to say a group's end again, keyed by the gate's merges key of the group. // EndAsksState is the state of asks to say a group's end again, keyed by the gate's merges key of the group.
EndAsksState = "end-asks" EndAsksState = "end-asks"
// EndAsksBucket is its bucket on the bus. // EndAsksBucket is its bucket on the bus.
EndAsksBucket = delivery.ForgeBridgeModule + "_" + EndAsksState EndAsksBucket = Module + "_" + EndAsksState
) )
// Key is the key of performed that says a group's end is written: `groups.<merges key>`, its value the revision of the // Key is the key of performed that says a group's end is written: `groups.<merges key>`, its value the revision of the
@@ -75,7 +78,7 @@ func NewEndAsk(group, why, by string, at time.Time) (string, []byte, error) {
if group == "" { if group == "" {
return "", nil, errors.New("name the group whose end is said again (group)") return "", nil, errors.New("name the group whose end is said again (group)")
} }
key, err := delivery.MergesKey(group) key, err := meshdeliveryclient.MergesKey(group)
if err != nil { if err != nil {
return "", nil, err return "", nil, err
} }
@@ -134,7 +137,7 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin
lastAsk[key] = &a lastAsk[key] = &a
} }
for key, raw := range merges { for key, raw := range merges {
m, err := delivery.DecodeMerge(raw) m, err := meshdeliveryclient.DecodeMerge(raw)
if err != nil { if err != nil {
unread = append(unread, key) unread = append(unread, key)
continue continue
@@ -145,9 +148,9 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin
r := Row{Key: key, Group: m.Group} r := Row{Key: key, Group: m.Group}
allMerged := true allMerged := true
for _, p := range m.Pulls { for _, p := range m.Pulls {
pub := p.Commit != "" && performed[PublishedKey(p.Repository, p.Number, p.Commit)] pub := p.Commit != "" && performed[PublishedKey(p.Repository, int(p.Number), p.Commit)]
r.Members = append(r.Members, RowMember{Repository: p.Repository, Number: p.Number, State: string(p.State), Commit: p.Commit, Published: pub}) r.Members = append(r.Members, RowMember{Repository: p.Repository, Number: int(p.Number), State: p.State, Commit: p.Commit, Published: pub})
if p.State != delivery.MergeMerged || !pub { if meshdeliveryclient.MergeState(p.State) != meshdeliveryclient.MergeMerged || !pub {
allMerged = false allMerged = false
} }
} }
@@ -175,8 +178,8 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin
func sortRows(rows []Row) { sort.Slice(rows, func(i, j int) bool { return rows[i].Key < rows[j].Key }) } func sortRows(rows []Row) { sort.Slice(rows, func(i, j int) bool { return rows[i].Key < rows[j].Key }) }
// MembersOf are a merge's approved members with their heads, in the order of their keys. // MembersOf are a merge's approved members with their heads, in the order of their keys.
func MembersOf(m delivery.Merge) []delivery.Head { func MembersOf(m seat.MergesByGroupValue) []meshdeliveryclient.Head {
var out []delivery.Head var out []meshdeliveryclient.Head
for _, k := range slices.Sorted(func(yield func(string) bool) { for _, k := range slices.Sorted(func(yield func(string) bool) {
for k := range m.Approved { for k := range m.Approved {
if !yield(k) { if !yield(k) {
@@ -192,7 +195,7 @@ func MembersOf(m delivery.Merge) []delivery.Head {
if err != nil { if err != nil {
continue continue
} }
out = append(out, delivery.Head{Repository: k[:at], Number: n, Head: m.Approved[k]}) out = append(out, meshdeliveryclient.Head{Repository: k[:at], Number: n, Head: m.Approved[k]})
} }
return out return out
} }