diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bridge_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bridge_test.go index b3e2c98b4..fb246c590 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bridge_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bridge_test.go @@ -137,6 +137,17 @@ func (s *fakeSource) Move(update func(prev *Source) (Source, bool)) error { return nil } +// Read is source.main as held, and its revision: the number of writes. +func (s *fakeSource) Read() (*Source, uint64, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.main == nil { + return nil, 0, nil + } + held := *s.main + return &held, uint64(len(s.moves)), nil +} + func sortStrings(s []string) { sort.Strings(s) } func itoa(n int) string { return strconv.Itoa(n) } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go index 7eaeaa47d..a33421423 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/bus_test.go @@ -631,6 +631,11 @@ func TestAPassingVerdictAsksTheApproveAndItsApproveMergesWithinSecondsOverARealB src, err := mdc.DecodeSource(e.Value()) return err == nil && src.Group == "fix/x" && src.Heads["novox/app"] == strings.Repeat("b", 40) }) + // The seat's verb source answers it from the bucket, with its revision on the bus. + said := strings.ReplaceAll(rt.call(t, Seat+".source", map[string]any{}), `\"`, `"`) + if !strings.Contains(said, `{"head":"`+strings.Repeat("b", 40)+`","repository":"novox/app","trunk":"main"}`) || !strings.Contains(said, `"revision":`) { + t.Fatalf("source answers %s", said) + } // The delivery ends merged (ADR 0539 §17), and the gate asked the controller nothing (R6-C3). rt.until("the delivery to be merged", eventWithin, func() bool { e, err := b.kvs["deliveries"].Get(ctx, kvKey(IDOf("novox/app", head))) diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue888_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue888_test.go index 68cd6b853..ab6b42d94 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue888_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/issue888_test.go @@ -5,6 +5,7 @@ import ( "time" mdc "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" + seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery" ) // Issue 888: the checker judges on the trunk source.main names (mesh-delivery modules/mesh-delivery-checker main.go), and the @@ -38,6 +39,12 @@ func TestAMergeHeardOnlyFromTheForgeMovesSourceMain(t *testing.T) { if w.source.main == nil || w.source.main.Heads[i888App] != i888Merge1 || w.source.main.Heads[i888Other] != i888OtherH { t.Fatalf("source.main is %+v; wanted %s at %s and %s kept", w.source.main, i888App, i888Merge1, i888Other) } + // The seat's verb source answers each repository's trunk branch and head, and the revision, from the same key. + if a := w.h.SourceRevision(); a.Error != "" || a.Revision != 1 || len(a.Repositories) != 2 || + a.Repositories[0] != (seat.SourceHead{Repository: i888App, Trunk: MergeBranch, Head: i888Merge1}) || + a.Repositories[1].Repository != i888Other || a.Repositories[1].Head != i888OtherH { + t.Fatalf("source answers %+v", a) + } n := len(w.source.moves) w.settleAll() w.h.PullMerged(i888MergedAt(i888App, 3, i888Head1, i888Merge1, w.now.Add(-time.Minute))) // heard again diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go index 50a28b098..f6841d265 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go @@ -354,6 +354,12 @@ func tools(h *Holder, l *listening) []stdio.Tool { Input: map[string]any{"type": "object", "properties": map[string]any{ "all": map[string]any{"type": "string", "enum": []string{"true", "false"}}}}, Run: func(a map[string]any) (any, error) { return h.Groups(boolArg(a, "all")), nil }}, + {Name: seat + "source", + Description: "The source revision the gate holds: every repository it names, its trunk branch and that trunk's head, the " + + "group that moved it last and its revision. Read, never written: a node-engine reads the repositories here to call a " + + "reverted build's catch-up tool once per repository, never the gate's state.", + Input: map[string]any{"type": "object", "properties": map[string]any{}}, + Run: func(map[string]any) (any, error) { return h.SourceRevision(), nil }}, {Name: seat + "checks", Description: "What the mesh's checks said of a pull request's head or of one commit: each of the commit's " + "statuses (mesh/check, mesh/delivery, …) with its state, description and when it " + diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go index 28891258d..e61ec7458 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/manifest_test.go @@ -75,7 +75,7 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) { } // The eight verbs the seat keeps (ADR 0539 §17): times, what-if, release, stop and retire-history went with the plans; and // settle, a person's ask to settle merges held without a passing check (ADR 0556 clause 5). - if want := []string{"deliveries", "show", "groups", "checks", "table", "stalled", "recheck", "close", "settle"}; !reflect.DeepEqual(m.Claims[0].Serves, want) { + if want := []string{"deliveries", "show", "groups", "source", "checks", "table", "stalled", "recheck", "close", "settle"}; !reflect.DeepEqual(m.Claims[0].Serves, want) { t.Fatalf("serves %v, wanted %v", m.Claims[0].Serves, want) } // The forge messages' layout is the bridge's published package forgemsg, taken by its series (ADR 0634 §3, design 32). diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go index 2cbf862af..1fd3797f7 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go @@ -11,6 +11,7 @@ import ( "time" mdc "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" stdio "git.novox.be/novox/mesh-sdk/go" ) @@ -32,6 +33,8 @@ type SourceStore interface { // Move reads source.main and writes what update makes of it, unless update says nothing is to be written; a write // another writer won is read again and tried again. Move(update func(prev *Source) (Source, bool)) error + // Read is source.main as held and its revision on the bus; nil and 0 before it is first written. + Read() (*Source, uint64, error) } // kvMerges is merges over the runtime. @@ -107,6 +110,40 @@ func (kvSource) Move(update func(prev *Source) (Source, bool)) error { return fmt.Errorf("source.main was written by another writer first %d times: %w", changeAttempts, stdio.ErrChanged) } +func (kvSource) Read() (*Source, uint64, error) { + e, err := stdio.State("source").Get(mdc.SourceMainKey) + if err != nil || e == nil { + return nil, 0, err + } + held, err := mdc.DecodeSource(e.Value) + if err != nil { + return nil, 0, fmt.Errorf("source.main is not readable: %w", err) + } + return &held, e.Revision, nil +} + +// SourceRevision is the seat's verb source: every repository source.main names, its trunk branch and head, the group that +// moved it last and its revision, or why it cannot be read. source.main names each repository's main only (MergeBranch). +// Outside the lock. +func (h *Holder) SourceRevision() seat.SourceAnswer { + if h.Source == nil { + return seat.SourceAnswer{Error: "no source bucket"} + } + held, revision, err := h.Source.Read() + if err != nil { + return seat.SourceAnswer{Error: err.Error()} + } + if held == nil { + return seat.SourceAnswer{} + } + out := seat.SourceAnswer{Group: held.Group, Revision: int64(revision)} + for repository, head := range held.Heads { + out.Repositories = append(out.Repositories, seat.SourceHead{Repository: repository, Trunk: MergeBranch, Head: head}) + } + sort.Slice(out.Repositories, func(i, j int) bool { return out.Repositories[i].Repository < out.Repositories[j].Repository }) + return out +} + // SourceMain is source.main as the bucket source holds it, read and never written: its group and each repository's trunk // head, or why it cannot be read. Outside the lock. func (h *Holder) SourceMain() map[string]any { diff --git a/modules/mesh-delivery-gate/module.json b/modules/mesh-delivery-gate/module.json index 190578972..cf20565ca 100644 --- a/modules/mesh-delivery-gate/module.json +++ b/modules/mesh-delivery-gate/module.json @@ -10,6 +10,7 @@ "deliveries", "show", "groups", + "source", "checks", "table", "stalled",