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 13770632f..c31a424ac 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 @@ -178,17 +178,27 @@ 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) { +// Read is a branch's source key as held, and its revision: the number of writes. The fake holds the key of MergeBranch only. +func (s *fakeSource) Read(branch string) (*Source, uint64, error) { s.mu.Lock() defer s.mu.Unlock() - if s.main == nil { + if s.main == nil || branch != MergeBranch { return nil, 0, nil } held := *s.main return &held, uint64(len(s.moves)), nil } +// Branches are the branches whose source key the fake holds. +func (s *fakeSource) Branches() ([]string, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.main == nil { + return nil, nil + } + return []string{MergeBranch}, 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 13b2afa6b..29f225922 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 @@ -639,7 +639,7 @@ func TestAPassingVerdictAsksTheApproveAndItsApproveMergesWithinSecondsOverARealB // The forge's own word that the pull request merged follows the bridge's (its webhook): the delivery ends merged. b.publish(t, "mesh.seat.git.event.pull.merged", seatMerged(merged("novox/app", 7, head, strings.Repeat("b", 40)))) rt.until("source.main to hold the merge", eventWithin, func() bool { - e, err := b.kvs["source"].Get(ctx, mdc.SourceMainKey) + e, err := b.kvs["source"].Get(ctx, mdc.SourceKey(MergeBranch)) if err != nil { return false } @@ -864,7 +864,7 @@ func TestAGroupOfTwoWithGreenChecksGetsOneAskAndOnItsApproveBothMergeOneAfterThe b.forgeSays(t, forgemsg.Forge{Kind: forgemsg.ForgeMerged, Repository: two.Repository, Number: two.Number, Head: two.Head.Head, Merge: strings.Repeat("b", 40), Root: "r", Trees: map[string]string{"x": "t"}}) rt.until("source.main to hold both merges", eventWithin, func() bool { - e, err := b.kvs["source"].Get(ctx, mdc.SourceMainKey) + e, err := b.kvs["source"].Get(ctx, mdc.SourceKey(MergeBranch)) if err != nil { return false } 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 d28d6d1a1..86a09ffcc 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 @@ -1,6 +1,7 @@ package main import ( + "reflect" "testing" "time" @@ -41,11 +42,18 @@ func TestAMergeHeardOnlyFromTheForgeMovesSourceMain(t *testing.T) { 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 || + if a := w.h.SourceRevision(MergeBranch); 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) } + // §690: given no branch it answers the branches of the keys it keeps, and no repository; a branch it keeps no key of has none. + if a := w.h.SourceRevision(""); a.Error != "" || !reflect.DeepEqual(a.Branches, []string{MergeBranch}) || len(a.Repositories) != 0 || a.Revision != 0 { + t.Fatalf("source with no branch answers %+v, want the branches %v alone", a, []string{MergeBranch}) + } + if a := w.h.SourceRevision("beta"); a.Error != "" || a.Branch != "beta" || a.Revision != 0 || len(a.Repositories) != 0 { + t.Fatalf("source of a branch with no key 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 fdb7beceb..6430fa230 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/main.go @@ -345,8 +345,9 @@ func tools(h *Holder, l *listening) []stdio.Tool { 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 }}, + Input: map[string]any{"type": "object", "properties": map[string]any{ + "branch": str("the watched branch, escaped as a key name is; absent: the list of the branches the gate keeps a source revision of")}}, + Run: func(a map[string]any) (any, error) { return h.SourceRevision(strArg(a, "branch")), 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/merges.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go index e9701a0af..150b7f9e2 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/merges.go @@ -14,6 +14,7 @@ import ( 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" + "git.novox.be/novox/mesh-sdk/go/keys" ) // The gate merges through the forge bridge, never itself (novox/hq ADR 0437 §8, ADR 0441 §5, ADR 0450 §3). It writes into @@ -48,13 +49,15 @@ func (kvOld) Get(state, key string) ([]byte, uint64, error) { return e.Value, e.Revision, nil } -// SourceStore is the gate's bucket source, one key, moved by compare-and-set. +// SourceStore is the gate's bucket source, one key per watched branch (design 47 G §15), moved by compare-and-set. 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 reads the source key of the gate's merge branch (MergeBranch) 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) + // Read is a branch's source key as held and its revision on the bus; nil and 0 before it is first written. + Read(branch string) (*Source, uint64, error) + // Branches are the branch names of the source keys the gate keeps (§614, §690). + Branches() ([]string, error) } // kvMerges is merges over the runtime. @@ -95,7 +98,7 @@ type kvSource struct{} func (kvSource) Move(update func(prev *Source) (Source, bool)) error { state := stdio.State("source") for range changeAttempts { - e, err := state.Get(mdc.SourceMainKey) + e, err := state.Get(mdc.SourceKey(MergeBranch)) if err != nil { return err } @@ -116,9 +119,9 @@ func (kvSource) Move(update func(prev *Source) (Source, bool)) error { return err } if e == nil { - _, err = state.Create(mdc.SourceMainKey, json.RawMessage(raw)) + _, err = state.Create(mdc.SourceKey(MergeBranch), json.RawMessage(raw)) } else { - _, err = state.Update(mdc.SourceMainKey, json.RawMessage(raw), e.Revision) + _, err = state.Update(mdc.SourceKey(MergeBranch), json.RawMessage(raw), e.Revision) } if err == nil { return nil @@ -130,33 +133,64 @@ 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) +func (kvSource) Read(branch string) (*Source, uint64, error) { + e, err := stdio.State("source").Get(mdc.SourceKey(branch)) 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 nil, 0, fmt.Errorf("source.%s is not readable: %w", branch, 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 { +func (kvSource) Branches() ([]string, error) { + names, err := stdio.State("source").Keys() + if err != nil { + return nil, err + } + var out []string + for _, key := range names { + escaped, ok := seat.ParseSourceByBranchKey(key) + if !ok { + continue + } + branch, err := keys.UnescapeName(escaped) + if err != nil { + return nil, fmt.Errorf("the source key %q is not a branch's: %w", key, err) + } + out = append(out, branch) + } + sort.Strings(out) + return out, nil +} + +// SourceRevision is the seat's verb source (§690): given a branch, every repository that branch's source key names, its +// trunk branch and head, the group that moved it last and its revision, or why it cannot be read; given none, `branches`, +// the branch of each source key the gate keeps. A key names each repository's merge branch only (MergeBranch). Outside the lock. +func (h *Holder) SourceRevision(branch string) seat.SourceAnswer { if h.Source == nil { return seat.SourceAnswer{Error: "no source bucket"} } - held, revision, err := h.Source.Read() + if branch == "" { + branches, err := h.Source.Branches() + if err != nil { + return seat.SourceAnswer{Error: err.Error()} + } + if branches == nil { + branches = []string{} + } + return seat.SourceAnswer{Branches: branches} + } + held, revision, err := h.Source.Read(branch) if err != nil { return seat.SourceAnswer{Error: err.Error()} } if held == nil { - return seat.SourceAnswer{} + return seat.SourceAnswer{Branch: branch} } - out := seat.SourceAnswer{Group: held.Group, Revision: int64(revision)} + out := seat.SourceAnswer{Branch: branch, 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}) } diff --git a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go index 20b00411e..7b3b2247d 100644 --- a/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go +++ b/modules/mesh-delivery-gate/cmd/mesh-delivery-gate/values.go @@ -22,7 +22,7 @@ const ( // are converted at the edge, so a stored value decodes unchanged (novox/hq ADR 0634 §2). type ( // Source is the state source's value. - Source = seat.SourceByMainValue + Source = seat.SourceByBranchValue // Owed is the state owed's value: what the forge bridge performs. Owed = seat.OwedByRepositoryNumberHeadActValue // Answer is the operator's answer as the gate acts on it.