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"
"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"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
@@ -26,6 +28,9 @@ import (
"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.
const (
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)
}
for _, key := range slices.Sorted(maps.Keys(raws)) {
m, err := delivery.DecodeMerge(raws[key])
m, err := meshdeliveryclient.DecodeMerge(raws[key])
if err != nil {
continue
}
at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool {
return delivery.SameRepository(p.Repository, repository) && p.Commit == merge
at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool {
return trunk.SameRepository(p.Repository, repository) && p.Commit == merge
})
if at < 0 {
continue
}
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 {
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 {
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)
}
out, err := forgemsg.EncodeForge(f)
@@ -253,7 +258,7 @@ func askCaller(_ context.Context, key string, body any) (json.RawMessage, error)
}
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,
func() ([]groups.Row, []string, error) { return read(merges, performed, asks) },
func(repository, merge string) (json.RawMessage, error) {
@@ -8,8 +8,9 @@ import (
"testing"
"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-sdk/go/delivery"
"mesh-delivery-forge-bridge/internal/bridge"
@@ -22,19 +23,19 @@ import (
func TestTheBridgeToolsReadTheGroupsAndAskTheEndAgain(t *testing.T) {
h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40)
c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40)
enc := func(group string, state2 delivery.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},
Pulls: []delivery.MergePull{{Head: delivery.Head{Repository: "novox/a", Number: 1, Head: h1}, State: delivery.MergeMerged, Commit: c1},
{Head: delivery.Head{Repository: "novox/b", Number: 2, Head: h2}, State: state2, Commit: c}}})
enc := func(group string, state2 meshdeliveryclient.MergeState, c string) []byte {
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: group, Ask: "ask", Approved: map[string]string{"novox/a@1": h1, "novox/b@2": h2},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 1, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1},
{Repository: "novox/b", Number: 2, Head: h2, State: string(state2), Commit: c}}})
if err != nil {
t.Fatal(err)
}
return v
}
lone, _ := delivery.EncodeMerge(delivery.Merge{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}}})
merges := map[string][]byte{"ended": enc("ended", delivery.MergeMerged, c2), "ready": enc("ready", delivery.MergeMerged, c2),
"gathered": enc("gathered", delivery.MergeMerging, ""), "solo": lone, "bad": []byte("{")}
lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "ask", Approved: map[string]string{"novox/a@5": h1},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}})
merges := map[string][]byte{"ended": enc("ended", meshdeliveryclient.MergeMerged, c2), "ready": enc("ready", meshdeliveryclient.MergeMerged, c2),
"gathered": enc("gathered", meshdeliveryclient.MergeMerging, ""), "solo": lone, "bad": []byte("{")}
performed := map[string]bool{}
for _, c := range []string{c1, c2} {
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 {
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)
}
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
}
lone, _ := delivery.EncodeMerge(delivery.Merge{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}}})
group, _ := delivery.EncodeMerge(delivery.Merge{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},
{Head: delivery.Head{Repository: "novox/b", Number: 7, Head: h2}, State: delivery.MergeMerging}}})
lone, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "solo", Ask: "a", Approved: map[string]string{"novox/a@5": h1},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 5, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1}}})
group, _ := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "feat/g", Ask: "a", Approved: map[string]string{"novox/a@6": h1, "novox/b@7": h2},
Pulls: []seat.MergesByGroupValuePullsItem{{Repository: "novox/a", Number: 6, Head: h1, State: string(meshdeliveryclient.MergeMerged), Commit: c1},
{Repository: "novox/b", Number: 7, Head: h2, State: string(meshdeliveryclient.MergeMerging)}}})
for _, c := range []struct {
name string
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 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.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
}()},
{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"
"git.novox.be/novox/mesh-sdk/go/delivery"
"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
// 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.
const performedBucket = delivery.ForgeBridgeModule + "_performed"
const performedBucket = forgeBridgeModule + "_performed"
// kvRevisions is the bucket performed as the performer's revisions.
type kvRevisions struct{ kv jetstream.KeyValue }
@@ -9,8 +9,9 @@ import (
"strings"
"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-sdk/go/delivery"
)
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.
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()
key, err := delivery.OwedKey(repository, number, headA, act)
key, err := meshdeliveryclient.OwedKey(repository, number, headA, act)
if err != nil {
t.Fatal(err)
}
v, err := delivery.EncodeOwed(o)
v, err := meshdeliveryclient.EncodeOwed(o)
if err != nil {
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
t.Run("the names are the manifest's", theNamesAreTheManifests)
f := &forgeFake{}
p, _, _, _ := newPerformer(f)
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)
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"}` {
@@ -90,11 +92,11 @@ func TestViewNoteAndCommentEachCallTheirToolOnce(t *testing.T) { // 1341/AC2
p, _, _, _ := newPerformer(f)
for _, c := range []struct {
act string
o delivery.Owed
o seat.OwedByRepositoryNumberHeadActValue
}{
{"view", delivery.Owed{Act: delivery.OwedView, Text: "the view"}},
{"note", delivery.Owed{Act: delivery.OwedNote, Text: "a line", Ref: "mesh-plan"}},
{"comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "hello"}},
{"view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "the view"}},
{"note", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedNote, Text: "a line", Ref: "mesh-plan"}},
{"comment.1", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "hello"}},
} {
key, v := owedKV(t, "novox/mesh-delivery", 12, c.act, c.o)
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
f := &forgeFake{refuse: map[string]string{"gitea.gitea_add_comment": "the pull request is locked"}}
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)
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()
v, err := delivery.EncodeMerge(m)
v, err := meshdeliveryclient.EncodeMerge(m)
if err != nil {
t.Fatal(err)
}
return v
}
func twoMembers(a, b delivery.MergeState) delivery.Merge {
return delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab",
Approved: map[string]string{delivery.ApprovedKey("novox/mesh-sdk", 4): headB, delivery.ApprovedKey("novox/mesh-delivery", 12): headA},
Pulls: []delivery.MergePull{
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: a, Commit: map[bool]string{true: "c1"}[a == delivery.MergeMerged]},
{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: b, Commit: map[bool]string{true: "c2"}[b == delivery.MergeMerged]},
func twoMembers(a, b meshdeliveryclient.MergeState) seat.MergesByGroupValue {
return seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab",
Approved: map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA},
Pulls: []seat.MergesByGroupValuePullsItem{
{Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(a), Commit: map[bool]string{true: "c1"}[a == meshdeliveryclient.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
f := &forgeFake{}
p, _, _, _ := newPerformer(f)
m := twoMembers(delivery.MergeMerging, delivery.MergeMerged)
m := twoMembers(meshdeliveryclient.MergeMerging, meshdeliveryclient.MergeMerged)
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"}`
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
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)
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)
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
f := &forgeFake{refuse: map[string]string{"gitea.gitea_merge_pull_request": "no"}}
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 {
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"
"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-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
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
// 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 == "" {
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 {
return delivery.Merge{}, 0, false
return seat.MergesByGroupValue{}, 0, false
}
value, rev, found, err := g.merges(ctx, key)
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)
return delivery.Merge{}, 0, false
return seat.MergesByGroupValue{}, 0, false
}
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 {
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 {
return delivery.Merge{}, 0, false
if _, ok := m.Approved[meshdeliveryclient.ApprovedKey(f.Repository, f.Number)]; !ok || len(m.Approved) < 2 {
return seat.MergesByGroupValue{}, 0, false
}
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)
return
}
key, err := delivery.MergesKey(f.Branch)
key, err := meshdeliveryclient.MergesKey(f.Branch)
if err != nil {
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 {
m, err := delivery.DecodeMerge(value)
m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil || len(m.Approved) < 2 {
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}
for _, member := range groupkeys.MembersOf(m) {
at := slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool {
return delivery.SameRepository(p.Repository, member.Repository) && p.Number == member.Number
at := slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool {
return trunk.SameRepository(p.Repository, member.Repository) && int(p.Number) == member.Number
})
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
@@ -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.
slices.SortStableFunc(end.Members, func(a, b forgemsg.GroupMerge) int {
return slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == a.Merge }) -
slices.IndexFunc(m.Pulls, func(p delivery.MergePull) bool { return p.Commit == b.Merge })
return slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == a.Merge }) -
slices.IndexFunc(m.Pulls, func(p seat.MergesByGroupValuePullsItem) bool { return p.Commit == b.Merge })
})
data, err := forgemsg.EncodeForgeGroupEnd(end)
if err != nil {
@@ -8,8 +8,9 @@ import (
"testing"
"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-sdk/go/delivery"
groupkeys "mesh-delivery-forge-bridge/internal/groups"
)
@@ -29,19 +30,19 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
ctx := context.Background()
h1, h2 := strings.Repeat("1", 40), strings.Repeat("2", 40)
c1, c2 := strings.Repeat("c", 40), strings.Repeat("d", 40)
merge := func(states ...delivery.MergePull) []byte {
v, err := delivery.EncodeMerge(delivery.Merge{Group: "feat/take-in", Ask: "ask", Pulls: states,
merge := func(states ...seat.MergesByGroupValuePullsItem) []byte {
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}})
if err != nil {
t.Fatal(err)
}
return v
}
sdk := func(state delivery.MergeState, commit, words string) delivery.MergePull {
return delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 37, Head: h1}, State: state, Commit: commit, Words: words}
sdk := func(state meshdeliveryclient.MergeState, commit, words string) seat.MergesByGroupValuePullsItem {
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 {
return delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 9, Head: h2}, State: state, Commit: commit}
dlv := func(state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
return seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 9, Head: h2, State: string(state), Commit: commit}
}
gate := map[string][]byte{}
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)
}
// 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))
if back, err := forgemsg.DecodeForge(data); !ok || err != nil || back.Group != "feat/take-in" || len(back.Members) != 2 {
t.Fatalf("a member's message: %+v %v", back, err)
}
g.published(ctx, f)
// 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"])
// 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"])
if len(pub.subjects) != 0 {
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
// 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 {
return delivery.MergePull{Head: delivery.Head{Repository: repo, Number: n, Head: head}, State: state, Commit: commit}
pull := func(repo string, n int, head string, state meshdeliveryclient.MergeState, commit string) seat.MergesByGroupValuePullsItem {
return seat.MergesByGroupValuePullsItem{Repository: repo, Number: int64(n), Head: head, State: string(state), Commit: commit}
}
askMerge := func(second delivery.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},
Pulls: []delivery.MergePull{pull("novox/mesh-sdk", 37, h1, delivery.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}})
askMerge := func(second meshdeliveryclient.MergeState, commit2 string) []byte {
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: []seat.MergesByGroupValuePullsItem{pull("novox/mesh-sdk", 37, h1, meshdeliveryclient.MergeMerged, c1), pull("novox/mesh-delivery", 9, h2, second, commit2)}})
if err != nil {
t.Fatal(err)
}
@@ -116,9 +117,9 @@ func TestAGroupsMembersNameItAndItsEndIsSaidOnce(t *testing.T) { // 1585/T16, T1
written bool
answer string
}{
{"merged and published: written again", map[string][]byte{"feat/take-in": askMerge(delivery.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 not merged", map[string][]byte{"feat/take-in": askMerge(delivery.MergeMerging, "")}, true, false, "not written"},
{"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(meshdeliveryclient.MergeMerged, c2)}, false, 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"},
} {
pub, done := &idPub{}, memRevisions{}
@@ -5,13 +5,11 @@ import (
"errors"
"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
// 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.
type kvHeads struct{ kv jetstream.KeyValue }
@@ -20,8 +20,10 @@ import (
"github.com/nats-io/nats.go"
"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-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"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)
}
// 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).
for name, history := range map[string]uint8{performedBucket: 1, inputsBucket: 1, delivery.MergesBucket: delivery.MergesHistory, delivery.OwedBucket: delivery.OwedHistory} {
// (the gate's merges and owed with their histories).
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 {
t.Fatal(err)
}
@@ -334,7 +336,7 @@ func TestBridgeWritesPullMessages(t *testing.T) { // 1235/AC2
if err != nil {
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": "{}"}}
if !forgemsg.SameForge(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 {
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"},
Manifests: map[string]string{"": string(manifest), "modules/a": string(manifest)},
// 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)
}
kv, err := b.js.KeyValue(b.ctx, delivery.MergesBucket)
kv, err := b.js.KeyValue(b.ctx, mergesBucket)
if err != nil {
t.Fatal(err)
}
approved := map[string]string{delivery.ApprovedKey("novox/mesh-delivery", 12): head, delivery.ApprovedKey("novox/mesh-sdk", 4): headB}
pullA := delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: head}, State: delivery.MergeMerging}
pullB := delivery.MergePull{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: delivery.MergeMerging}
put := func(pulls ...delivery.MergePull) {
v, err := delivery.EncodeMerge(delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: pulls})
approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): head, meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB}
pullA := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-delivery", Number: 12, Head: head, State: string(meshdeliveryclient.MergeMerging)}
pullB := seat.MergesByGroupValuePullsItem{Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerging)}
put := func(pulls ...seat.MergesByGroupValuePullsItem) {
v, err := meshdeliveryclient.EncodeMerge(seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: pulls})
if err != nil {
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).
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)
next("mesh-sdk")
var got []string
@@ -786,15 +788,15 @@ func TestBridgePerformsNoMeshCheckAct(t *testing.T) { // R2-1, ADR 0521
if err := b.ctl.Flush(); err != nil {
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 {
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 {
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 {
t.Fatal(err)
}
@@ -10,8 +10,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"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
// 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).
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.
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
// 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
// (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
import (
@@ -22,7 +22,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-forge-bridge/forgemsg"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-forge-bridge/internal/bridge"
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
// modstreams.go ModuleStreamSubject).
func forgeSubject(event string) string {
return process.ModuleStreamSubject(delivery.ForgeBridgeModule, event)
return process.ModuleStreamSubject(forgeBridgeModule, event)
}
func main() {
@@ -158,7 +157,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
for _, w := range []struct {
bucket string
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)
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 {
continue
}
if w.bucket == delivery.MergesBucket {
if w.bucket == mergesBucket {
groups.merges = kvMerges(kv)
}
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"
"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'.
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.
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
// gate's merges state, and the indexer gathers the group's merges until the group's end (ForgeGroupEnd). A merge
// with no group carries neither, and an older bridge sends neither; an older reader ignores both.
Group string `json:"group,omitempty"`
Members []delivery.Head `json:"members,omitempty"`
Group string `json:"group,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
@@ -187,7 +189,7 @@ func (f Forge) checkGroup() error {
return fmt.Errorf("delivery: the group %q names its member %s twice", f.Group, k)
}
seen[k] = true
self = self || (delivery.SameRepository(m.Repository, f.Repository) && m.Number == f.Number)
self = self || (trunk.SameRepository(m.Repository, f.Repository) && m.Number == f.Number)
}
if !self {
return fmt.Errorf("delivery: the members of the group %q leave out the merged pull request %s#%d", f.Group, f.Repository, f.Number)
@@ -395,7 +397,7 @@ func checkName(kind, name string) error {
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.
func decode(data []byte, into any) error {
var probe struct {
@@ -405,10 +407,10 @@ func decode(data []byte, into any) error {
return fmt.Errorf("delivery: value is not JSON: %w", err)
}
if probe.V == nil {
return fmt.Errorf("%w: no version in the value, this reader reads %d", delivery.ErrUnknownVersion, Version)
return fmt.Errorf("%w: no version in the value, this reader reads %d", keys.ErrUnknownVersion, 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 {
return fmt.Errorf("delivery: value does not fit its shape: %w", err)
@@ -6,7 +6,8 @@ import (
"strings"
"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"
@@ -20,7 +21,7 @@ func sampleForges() []Forge {
{Kind: ForgeOpen, Open: []OpenPull{{Repository: "novox/mesh-sdk", Number: 36}, {Repository: "novox/mesh-control", Number: 7}}},
// 1585/T1: a group member's merged message names its group and every approved member (ADR 0633 §4).
{Kind: ForgeMerged, Repository: "novox/mesh-sdk", Number: 37, Head: "dddd", Merge: "eeee", Root: "t0",
Group: "feat/take-in", Members: []delivery.Head{{Repository: "novox/mesh-sdk", Number: 37, Head: "dddd"}, {Repository: "novox/mesh-delivery", Number: 9, Head: "ffff"}}},
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 },
} {
for _, in := range []string{`{"group":"g"}`, `{"v":2,"group":"g"}`} {
if err := dec([]byte(in)); !errors.Is(err, delivery.ErrUnknownVersion) {
t.Errorf("%s decode of %s = %v, want delivery.ErrUnknownVersion", name, in, err)
if err := dec([]byte(in)); !errors.Is(err, keys.ErrUnknownVersion) {
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"}}}},
// 1585/T1: a group only on a merged message, with its members, the message's own pull request among them.
{"pull with a group", "only a merged message names its delivery group", at(0, func(f *Forge) {
f.Group, f.Members = "g", []delivery.Head{{Repository: "novox/mesh-sdk", Number: 36, Head: "aaaa"}}
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 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
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
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
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
@@ -5,8 +5,9 @@ import (
"slices"
"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-sdk/go/delivery"
)
// 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
}
// 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
// (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).
@@ -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
// on no pull request are errors, each saying why.
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 {
return Act{}, err
}
o, err := delivery.DecodeOwed(value)
o, err := meshdeliveryclient.DecodeOwed(value)
if err != nil {
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 {
return Act{}, err
}
@@ -74,26 +75,26 @@ func OwedAct(key string, value []byte) (Act, error) {
return nil
}
switch kind {
case delivery.OwedStatus:
case meshdeliveryclient.OwedStatus:
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)
}
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
// 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",
key, delivery.StatusContextCheck)
key, meshdeliveryclient.StatusContextCheck)
}
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}}
case delivery.OwedView:
case meshdeliveryclient.OwedView:
if err := needsPull(); err != nil {
return Act{}, err
}
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}}
case delivery.OwedComment:
case meshdeliveryclient.OwedComment:
if err := needsPull(); err != nil {
return Act{}, err
}
@@ -102,19 +103,19 @@ func OwedAct(key string, value []byte) (Act, error) {
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
// 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
// needs them for a group.
func MergeActs(value []byte) ([]Act, error) {
m, err := delivery.DecodeMerge(value)
m, err := meshdeliveryclient.DecodeMerge(value)
if err != nil {
return nil, err
}
var handed []delivery.MergePull
var handed []seat.MergesByGroupValuePullsItem
for _, p := range m.Pulls {
if p.State == delivery.MergeMerging {
if meshdeliveryclient.MergeState(p.State) == meshdeliveryclient.MergeMerging {
handed = append(handed, p)
}
}
@@ -136,8 +137,8 @@ func MergeActs(value []byte) ([]Act, error) {
var acts []Act
for _, p := range handed {
owner, repo := ownerRepo(p.Repository)
acts = append(acts, Act{Name: MergeAct, Repository: p.Repository, Number: p.Number, Head: p.Head.Head,
Call: Call{Tool: mergePullTool, Args: map[string]any{"owner": owner, "repo": repo, "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": int(p.Number), "head": p.Head,
"approved_heads": approved}}})
}
return acts, nil
@@ -5,8 +5,10 @@ import (
"strings"
"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-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
)
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.
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()
key, err := delivery.OwedKey(repository, number, headA, act)
key, err := meshdeliveryclient.OwedKey(repository, number, headA, act)
if err != nil {
t.Fatal(err)
}
b, err := delivery.EncodeOwed(o)
b, err := meshdeliveryclient.EncodeOwed(o)
if err != nil {
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
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)
if err != nil {
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
// 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.
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") {
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.
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") {
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).
func checkerKV(t *testing.T, o delivery.Owed) (string, []byte) {
// 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 seat.OwedByRepositoryNumberHeadActValue) (string, []byte) {
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 {
t.Fatal(err)
}
v, err := delivery.EncodeCheckerOwed(o)
v, err := meshdeliveryclient.EncodeOwed(o)
if err != nil {
t.Fatal(err)
}
@@ -75,14 +77,14 @@ func checkerKV(t *testing.T, o delivery.Owed) (string, []byte) {
func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2
cases := []struct {
act string
o delivery.Owed
o seat.OwedByRepositoryNumberHeadActValue
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"}}},
{"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"}}},
{"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"}}},
}
for _, c := range cases {
@@ -98,11 +100,11 @@ func TestOwedViewNoteAndCommentCalls(t *testing.T) { // 1341/AC2
}
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 {
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 {
t.Error("a view of no pull request was taken")
}
@@ -110,12 +112,12 @@ func TestOwedActWithoutAPullRequestNeedsOneOnlyWhereTheToolDoes(t *testing.T) {
func TestOwedActThatDisagreesWithItsKeyIsRefused(t *testing.T) {
// A dotted repository's key is unescaped, and its act names the repository whole.
dk, dv := owedValue(t, "novox/my.repo", 3, "comment.1", delivery.Owed{Act: delivery.OwedComment, Text: "x"})
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" {
t.Errorf("a dotted repository's act = %+v, %v", a, err)
}
key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", delivery.Owed{Act: delivery.OwedView, Text: "v"})
v, _ := delivery.EncodeOwed(delivery.Owed{Act: delivery.OwedComment, Text: "v"})
key, _ := owedValue(t, "novox/mesh-delivery", 12, "view", seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedView, Text: "v"})
v, _ := meshdeliveryclient.EncodeOwed(seat.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedComment, Text: "v"})
if _, err := OwedAct(key, v); err == nil {
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()
b, err := delivery.EncodeMerge(m)
b, err := meshdeliveryclient.EncodeMerge(m)
if err != nil {
t.Fatal(err)
}
@@ -137,10 +139,10 @@ func mergeValue(t *testing.T, m delivery.Merge) []byte {
}
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}
v := mergeValue(t, delivery.Merge{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: []delivery.MergePull{
{Head: delivery.Head{Repository: "novox/mesh-sdk", Number: 4, Head: headB}, State: delivery.MergeMerged, Commit: "c0"},
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging},
approved := map[string]string{meshdeliveryclient.ApprovedKey("novox/mesh-sdk", 4): headB, meshdeliveryclient.ApprovedKey("novox/mesh-delivery", 12): headA}
v := mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "merge.g.0123456789ab", Approved: approved, Pulls: []seat.MergesByGroupValuePullsItem{
{Repository: "novox/mesh-sdk", Number: 4, Head: headB, State: string(meshdeliveryclient.MergeMerged), Commit: "c0"},
{Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)},
}})
acts, err := MergeActs(v)
if err != nil {
@@ -158,8 +160,8 @@ func TestMergeActsHandOnlyTheMemberHandedWithEveryApprovedHead(t *testing.T) { /
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.
v = mergeValue(t, delivery.Merge{Group: "g", Ask: "a", Pulls: []delivery.MergePull{
{Head: delivery.Head{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, State: delivery.MergeMerging}}})
v = mergeValue(t, seat.MergesByGroupValue{Group: "g", Ask: "a", Pulls: []seat.MergesByGroupValuePullsItem{
{Repository: "novox/mesh-delivery", Number: 12, Head: headA, State: string(meshdeliveryclient.MergeMerging)}}})
if _, err := MergeActs(v); err == nil {
t.Error("a merge with no approved heads was taken")
}
@@ -178,7 +180,7 @@ func TestRefusedMessageNamesTheActAndTheForgesWords(t *testing.T) { // 1341/AC3,
if err != nil {
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"}
if !forgemsg.SameForge(f, want) {
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-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
@@ -118,7 +118,7 @@ func branchDeleted(subject string, data []byte) (Message, error) {
case branch == 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 {
return Message{}, unreadable(subject, "%v", err)
}
@@ -7,7 +7,7 @@ import (
"testing"
"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"
@@ -59,7 +59,7 @@ func TestUpdatedBecomesAPullMessage(t *testing.T) { // 1235/AC2 (the conversion;
if err != nil {
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": "{}"}}
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
@@ -105,7 +105,7 @@ func TestClosedBecomesAClosedPullMessage(t *testing.T) { // 1235/AC4
if err != nil {
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}
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
@@ -252,7 +252,7 @@ func TestMergeIntoMainBecomesAMergedMessage(t *testing.T) { // 1236/AC1
if err != nil {
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": "{}"}}
if !forgemsg.SameForge(f, want) {
t.Errorf("message = %+v, want %+v", f, want)
@@ -272,7 +272,7 @@ func TestMergeIntoAnotherBranchIsAClosedPull(t *testing.T) { // 1236/AC2
if err != nil {
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}
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)
@@ -304,7 +304,7 @@ func TestNoModuleJSONAnywhereWritesAMergedMessageWithNoTrees(t *testing.T) {
t.Fatal(err)
}
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"}
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)
@@ -330,8 +330,8 @@ func TestOpenPullsAreUpdatedPullMessagesWithUnknownDirs(t *testing.T) { // 1237/
t.Fatalf("msgs %d skipped %v err %v", len(msgs), skipped, err)
}
for i, want := range []forgemsg.Forge{
{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-delivery", Branch: "feat/x", Number: 12, Head: head, Action: forgemsg.PullUpdated},
{V: delivery.Version, Kind: forgemsg.ForgePull, Repository: "novox/mesh-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated},
{V: keys.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-catalog", Branch: "feat/x", Number: 300, Head: h2, Action: forgemsg.PullUpdated},
} {
f, err := forgemsg.DecodeForge(msgs[i].Data)
if err != nil || !forgemsg.SameForge(f, want) {
@@ -16,17 +16,20 @@ import (
"strings"
"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.
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 = "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 = "end-asks"
// 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
@@ -75,7 +78,7 @@ func NewEndAsk(group, why, by string, at time.Time) (string, []byte, error) {
if 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 {
return "", nil, err
}
@@ -134,7 +137,7 @@ func Rows(merges map[string][]byte, performed map[string]bool, endedAt map[strin
lastAsk[key] = &a
}
for key, raw := range merges {
m, err := delivery.DecodeMerge(raw)
m, err := meshdeliveryclient.DecodeMerge(raw)
if err != nil {
unread = append(unread, key)
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}
allMerged := true
for _, p := range m.Pulls {
pub := p.Commit != "" && performed[PublishedKey(p.Repository, 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})
if p.State != delivery.MergeMerged || !pub {
pub := p.Commit != "" && performed[PublishedKey(p.Repository, int(p.Number), p.Commit)]
r.Members = append(r.Members, RowMember{Repository: p.Repository, Number: int(p.Number), State: p.State, Commit: p.Commit, Published: pub})
if meshdeliveryclient.MergeState(p.State) != meshdeliveryclient.MergeMerged || !pub {
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 }) }
// MembersOf are a merge's approved members with their heads, in the order of their keys.
func MembersOf(m delivery.Merge) []delivery.Head {
var out []delivery.Head
func MembersOf(m seat.MergesByGroupValue) []meshdeliveryclient.Head {
var out []meshdeliveryclient.Head
for _, k := range slices.Sorted(func(yield func(string) bool) {
for k := range m.Approved {
if !yield(k) {
@@ -192,7 +195,7 @@ func MembersOf(m delivery.Merge) []delivery.Head {
if err != nil {
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
}