From 323e1270cc934f239ef1b431bd4fa4b57cebec3d Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 18:20:37 +0200 Subject: [PATCH] =?UTF-8?q?Ask=20no=20pull=20request=20check=20of=20build-?= =?UTF-8?q?agent=20but=20the=20own=20merge-check.sh=20of=20a=20repository?= =?UTF-8?q?=20whose=20trunk=20requires=20mesh/repo-check=20alone,=20and=20?= =?UTF-8?q?say=20every=20verdict=20with=20no=20gate=20layer=20(issue=20129?= =?UTF-8?q?9,=20C1,=20ADR=200503=20=C2=A73,=20=C2=A75)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmd/mesh-controller/build_source.go | 33 +-- cmd/mesh-controller/check_siblings.go | 165 ------------ cmd/mesh-controller/check_siblings_test.go | 248 ------------------ cmd/mesh-controller/checks.go | 151 ++++++----- cmd/mesh-controller/checks_queue_test.go | 97 ------- .../checks_stop_integration_test.go | 94 +++++++ .../composed_member_check_test.go | 68 ----- cmd/mesh-controller/composed_verdict_test.go | 36 +-- .../delivery_check_answer_test.go | 2 +- cmd/mesh-controller/replay867_test.go | 2 - cmd/mesh-controller/try_test.go | 4 +- 11 files changed, 201 insertions(+), 699 deletions(-) delete mode 100644 cmd/mesh-controller/check_siblings.go delete mode 100644 cmd/mesh-controller/check_siblings_test.go delete mode 100644 cmd/mesh-controller/checks_queue_test.go create mode 100644 cmd/mesh-controller/checks_stop_integration_test.go delete mode 100644 cmd/mesh-controller/composed_member_check_test.go diff --git a/cmd/mesh-controller/build_source.go b/cmd/mesh-controller/build_source.go index 3520c8fd..82f29a7b 100644 --- a/cmd/mesh-controller/build_source.go +++ b/cmd/mesh-controller/build_source.go @@ -373,22 +373,7 @@ type protectionRule struct { // branch, else the first glob rule, in the forge's order, that covers it (gitea's first matching rule). Never a // later rule that happens to be stronger: the forge does not read it. func judgedRule(rules []protectionRule, branch string) forgeFacts { - var applied *protectionRule - for i := range rules { - if rules[i].Rule == branch { - applied = &rules[i] - break - } - } - if applied == nil { - for i := range rules { - if ruleCovers(rules[i].Rule, branch) { - applied = &rules[i] - break - } - } - } - switch r := applied; { + switch r := appliedRule(rules, branch); { case r == nil: return forgeFacts{Why: fmt.Sprintf("no protection rule covers %s", branch)} case r.Push: @@ -401,6 +386,22 @@ func judgedRule(rules []protectionRule, branch string) forgeFacts { return forgeFacts{Guarded: true} } +// appliedRule is the one rule the forge applies to a branch: the rule named for it, else the first glob rule, in the +// forge's order, that covers it; nil when none does. +func appliedRule(rules []protectionRule, branch string) *protectionRule { + for i := range rules { + if rules[i].Rule == branch { + return &rules[i] + } + } + for i := range rules { + if ruleCovers(rules[i].Rule, branch) { + return &rules[i] + } + } + return nil +} + // ruleCovers says a protection rule's name — a branch, or a glob of them — covers a branch, as the forge reads it. func ruleCovers(rule, branch string) bool { if rule == branch { diff --git a/cmd/mesh-controller/check_siblings.go b/cmd/mesh-controller/check_siblings.go deleted file mode 100644 index df08f3f5..00000000 --- a/cmd/mesh-controller/check_siblings.go +++ /dev/null @@ -1,165 +0,0 @@ -package main - -import ( - "context" - "fmt" - "regexp" - "sort" - "strings" - - "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" -) - -// **A delivery group's member is checked beside the other members' heads** (novox/hq ADR 0382, issue 867). -// -// A mesh-host change and a mesh-controller change that need each other could not both pass their own merge checks: -// each was read beside the other's repository at the commit the mesh runs, which holds neither change. So the -// controller asks mesh-delivery's `groups` once for every pull request's check it asks (§1), and names, in the ask, -// the group's other open pull requests into a base of the same name, in a repository the build seat reads beside the -// check — mesh-lab never — each with its whole head. The build seat reads those repositories at the sibling's merge (§2). -// A pull request for which mesh-delivery answered and named no sibling is asked exactly as before (§7). - -// wholeCommit is a commit as the forge says a head: forty hexadecimal characters. -var wholeCommit = regexp.MustCompile(`^[0-9a-f]{40}$`) - -// besideRepositories are the repositories (lower case, owner/repo) a check reads beside its own, by the directory each -// is found under — the core's, as besideOf clones them, and mesh-sdk beside the controller; never mesh-lab, which is -// always read at its trunk. -func besideRepositories(entries []inventory.Entry) map[string]string { - read := map[string]string{} - for _, e := range entries { - dir, core := coreModules[e.Manifest.Module] - if !core || e.Provided || e.Source.Repository == "" { - continue - } - read[nameOf(e.Source.Repository)] = dir - if dir == "mesh-controller" && e.Source.Seat != "" { - read[nameOf(siblingOf(e.Source.Repository, "mesh-sdk"))] = "mesh-sdk" - } - } - delete(read, "") - return read -} - -// nameOf is a recorded repository as owner/repo in lower case, empty when it names no owner. -func nameOf(repository string) string { - owner, repo := sourceOwner(repository), sourceName(repository) - if owner == "" || repo == "" { - return "" - } - return strings.ToLower(owner + "/" + repo) -} - -// siblingsFor is the pull request's siblings, as mesh-delivery's groups say them (ADR 0382 §1): the other members of -// the group its head branch names that are open, unmerged and into a base of its own base's name, in a repository the -// check reads beside it (beside: the directories the ask already names). The second answer is why they could not be -// learned, empty when they could: the group's members carry no head or base — a mesh-delivery from before the rule. -func siblingsFor(p link.PullUpdated, entries []inventory.Entry, beside map[string]link.CheckedOut, - lines []groupsLine) ([]link.CheckSibling, string) { - own := strings.ToLower(p.Owner + "/" + p.Repo) - var group *groupsLine - if p.Head == "" { - // A recheck (mesh-delivery's `recheck` through `delivery check`) carries no head branch name: the group is the one - // whose members hold this very pull request, by repository and number. - learnable := false - for i := range lines { - for _, m := range lines[i].Details { - learnable = true - if p.Number != 0 && m.Number == p.Number && strings.ToLower(m.Repository) == own { - group = &lines[i] - } - } - } - if group == nil { - for _, l := range lines { - if len(l.Details) == 0 && len(l.Members) > 0 && !learnable { - return nil, "mesh-delivery named the groups' members without their pull requests (a mesh-delivery from before novox/hq ADR 0382): " + - "the group of a pull request announced without its head branch cannot be found" - } - } - return nil, "" // a lone pull request - } - } else { - for i := range lines { - if lines[i].ID == p.Head { - group = &lines[i] - } - } - if group == nil { - return nil, "" - } - } - if len(group.Details) == 0 && len(group.Members) > 0 { - return nil, "mesh-delivery named the group's members without their heads and bases (a mesh-delivery from before novox/hq ADR 0382)" - } - read := besideRepositories(entries) - var siblings []link.CheckSibling - for _, m := range group.Details { - repository := strings.ToLower(m.Repository) - if repository == own { - continue - } - if !m.Open { - continue - } - if m.Head == "" || m.Base == "" { - return nil, "mesh-delivery named " + m.ID + " without its head or base (a mesh-delivery from before novox/hq ADR 0382)" - } - dir, readBeside := read[repository] - if m.Base != p.Base || !readBeside || dir == "mesh-lab" { - continue - } - if _, there := beside[dir]; !there { - continue - } - head := strings.ToLower(m.Head) - if !wholeCommit.MatchString(head) { - return nil, fmt.Sprintf("mesh-delivery named %s with a head that is not a whole commit (%q)", m.ID, m.Head) - } - siblings = append(siblings, link.CheckSibling{Repository: m.Repository, Number: m.Number, Head: head, Base: m.Base, Dir: dir}) - } - sort.Slice(siblings, func(i, j int) bool { return siblings[i].Dir < siblings[j].Dir }) - return siblings, "" -} - -// withSiblings asks mesh-delivery's `groups` once and says in the request the pull request's siblings, or why they -// could not be learned (ADR 0382 §1): a check never waits on mesh-delivery past groupAskWithin and never fails for it, -// so a fix to mesh-delivery is still checked without it. -func withSiblings(ctx context.Context, request *link.BuildRequest, p link.PullUpdated, entries []inventory.Entry) { - lines, err := groupsAsked(ctx, deliverySeatHeld(entries)) - siblingsFrom(request, p, entries, lines, err) -} - -// siblingsFrom is withSiblings from mesh-delivery's answer already had. -func siblingsFrom(request *link.BuildRequest, p link.PullUpdated, entries []inventory.Entry, lines []groupsLine, err error) { - if request.Check == nil { - return - } - if err != nil { - request.Check.SiblingsUnknown = fmt.Sprintf("mesh-delivery did not say the group's pull requests (%v): "+ - "the check reads no siblings", err) - return - } - request.Check.Siblings, request.Check.SiblingsUnknown = siblingsFor(p, entries, request.Check.Beside, lines) -} - -// composedMember says the pull request is a member of a delivery group whose gate is the composed check's (novox/hq -// ADR 0403 clauses 1 and 7): the group has two members or more and mesh-delivery says so on its line. The group is -// found as siblingsFor finds it, by its head branch name, or, for a recheck that carries none, by the pull request's -// repository and number — not by siblings, which name only what the check reads beside it. -func composedMember(p link.PullUpdated, lines []groupsLine) bool { - own := strings.ToLower(p.Owner + "/" + p.Repo) - for _, l := range lines { - found := p.Head != "" && l.ID == p.Head - for _, m := range l.Details { - if p.Head == "" && p.Number != 0 && m.Number == p.Number && strings.ToLower(m.Repository) == own { - found = true - } - } - if found { - return l.ComposedGate && max(len(l.Details), len(l.Members)) >= 2 - } - } - return false -} diff --git a/cmd/mesh-controller/check_siblings_test.go b/cmd/mesh-controller/check_siblings_test.go deleted file mode 100644 index 379ecd98..00000000 --- a/cmd/mesh-controller/check_siblings_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "errors" - "strings" - "testing" - - "git.novox.be/novox/mesh-sdk/go/catalogue" - "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" -) - -// **A delivery group's member is checked beside the other members' heads** (novox/hq ADR 0382 §1, §7, issue 867): the -// controller asks mesh-delivery's `groups` once for every check it asks, and names in the ask the group's other open -// pull requests into a base of the same name, in a repository the build seat reads beside it — mesh-lab never. - -const ( - headHost = "1111111111111111111111111111111111111111" - headController = "2222222222222222222222222222222222222222" - headSDK = "3333333333333333333333333333333333333333" - headLab = "4444444444444444444444444444444444444444" - headHq = "5555555555555555555555555555555555555555" -) - -func theCoreOnTheGitSeat() []inventory.Entry { - onSeat := func(module, repository string) inventory.Entry { - e := fromRepo(module, repository, "") - e.Source.Seat = "git" - return e - } - nats := onSeat("nats", "novox/mesh-catalog") - nats.Source.Path = "modules/nats" - held := catalogue.Manifest{Module: "mesh-delivery", Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}} - return []inventory.Entry{onSeat("mesh-controller", "novox/mesh-controller"), onSeat("mesh-host", "novox/mesh-host"), - onSeat("node-tools", "novox/mesh-tools"), nats, {Manifest: held, On: []string{"anchor"}}} -} - -func besideTheCore() map[string]link.CheckedOut { - return map[string]link.CheckedOut{"mesh-controller": {Repository: "c"}, "mesh-host": {Repository: "h"}, - "mesh-tools": {Repository: "t"}, "mesh-catalog": {Repository: "k"}, "mesh-sdk": {Repository: "s"}, "mesh-lab": {Repository: "l"}} -} - -func member(repository string, number int, head, base string, open bool) memberLine { - return memberLine{ID: repository + "@" + head[:12], Repository: repository, State: "checked", Number: number, Head: head, - Base: base, Open: open} -} - -func group(id string, members ...memberLine) groupsLine { - line := groupsLine{ID: id, Details: members} - for _, m := range members { - line.Members = append(line.Members, m.ID+" ("+m.State+")") - } - return line -} - -func TestASiblingIsTheGroupsOtherOpenPullRequestIntoABaseOfTheSameName(t *testing.T) { - entries := theCoreOnTheGitSeat() - pull := link.PullUpdated{Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Head: "feat/x", Commit: headHost} - for _, c := range []struct { - what string - lines []groupsLine - want []string - unknown string - }{ - {"the other member, open, same base", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController, "main", true))}, - []string{"mesh-controller=novox/mesh-controller#346@" + headController + " onto main"}, ""}, - {"a member into a base of another name is not one", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController, "release", true))}, - nil, ""}, - {"a member not open (merged or closed) is not one", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController, "main", false))}, - nil, ""}, - {"mesh-lab is never one", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-lab", 5, headLab, "main", true))}, - nil, ""}, - {"a repository the check reads nothing of is not one", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/hq", 9, headHq, "main", true))}, - nil, ""}, - {"mesh-sdk, read beside the controller, is one", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-sdk", 7, headSDK, "main", true))}, - []string{"mesh-sdk=novox/mesh-sdk#7@" + headSDK + " onto main"}, ""}, - {"another group is none of its business", []groupsLine{group("feat/y", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController, "main", true))}, - nil, ""}, - {"a group of its own alone has none", []groupsLine{group("feat/x", member("novox/mesh-host", 114, headHost, "main", true))}, - nil, ""}, - {"an older mesh-delivery names no heads", []groupsLine{{ID: "feat/x", - Members: []string{"novox/mesh-host@111111111111 (checked)", "novox/mesh-controller@222222222222 (checked)"}}}, - nil, "without their heads and bases"}, - {"a member without its base", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController, "", true))}, - nil, "without its head or base"}, - {"a head that is no commit", []groupsLine{group("feat/x", - member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController[:12]+"zzzzzzzzzzzzzzzzzzzzzzzzzzzz", "main", true))}, - nil, "not a whole commit"}, - } { - got, unknown := siblingsFor(pull, entries, besideTheCore(), c.lines) - var said []string - for _, s := range got { - said = append(said, s.Dir+"="+s.Repository+"#"+itoa(s.Number)+"@"+s.Head+" onto "+s.Base) - } - if strings.Join(said, ";") != strings.Join(c.want, ";") { - t.Errorf("%s: siblings %v, wanted %v", c.what, said, c.want) - } - if (c.unknown == "") != (unknown == "") || !strings.Contains(unknown, c.unknown) { - t.Errorf("%s: could not learn %q, wanted %q", c.what, unknown, c.unknown) - } - } - // A pull request into a release branch has siblings into the release branch of the same name (ADR 0358 §1). - release := pull - release.Base = "feat/57" - lines := []groupsLine{group("feat/x", member("novox/mesh-host", 114, headHost, "feat/57", true), - member("novox/mesh-controller", 346, headController, "feat/57", true), member("novox/mesh-tools", 8, headLab, "main", true))} - if got, _ := siblingsFor(release, entries, besideTheCore(), lines); len(got) != 1 || got[0].Dir != "mesh-controller" { - t.Errorf("into a release branch: %+v", got) - } - // A recheck announces the pull request without its head branch: its group is the one holding it (repository, number). - pull.Head = "" - got, unknown := siblingsFor(pull, entries, besideTheCore(), lines) - if len(got) != 1 || got[0].Dir != "mesh-tools" || unknown != "" { - t.Errorf("a recheck of a group member: %+v %q", got, unknown) - } - // A pull request in no group, rechecked, is alone: no sibling, and nothing said. - pull.Number = 999 - if got, unknown := siblingsFor(pull, entries, besideTheCore(), lines); got != nil || unknown != "" { - t.Errorf("a lone pull request: %+v %q", got, unknown) - } - if got, unknown := siblingsFor(pull, entries, besideTheCore(), nil); got != nil || unknown != "" { - t.Errorf("no groups at all: %+v %q", got, unknown) - } - // A mesh-delivery that names members without their pull requests cannot find the group of a recheck: it says so. - older := []groupsLine{{ID: "feat/x", Members: []string{"novox/mesh-host@111111111111 (checked)"}}} - pull.Number = 114 - if got, unknown := siblingsFor(pull, entries, besideTheCore(), older); got != nil || unknown == "" { - t.Errorf("an older mesh-delivery: %+v %q", got, unknown) - } -} - -func itoa(n int) string { b, _ := json.Marshal(n); return string(b) } - -// Every check asks mesh-delivery's `groups` once; the ask names the sibling's repository, head and base. -func TestEveryCheckAsksMeshDeliveryOnceAndNamesItsSiblings(t *testing.T) { - entries := theCoreOnTheGitSeat() - asked := 0 - was := groupsAsked - t.Cleanup(func() { groupsAsked = was }) - groupsAsked = func(_ context.Context, held bool) ([]groupsLine, error) { - asked++ - if !held { - t.Error("the seat is held, yet it was asked as not held") - } - return []groupsLine{group("feat/x", member("novox/mesh-host", 114, headHost, "main", true), - member("novox/mesh-controller", 346, headController, "main", true))}, nil - } - request := link.BuildRequest{Check: &link.CheckRequest{Owner: "novox", Repo: "mesh-host", Base: "main", Beside: besideTheCore()}} - withSiblings(t.Context(), &request, link.PullUpdated{Owner: "novox", Repo: "mesh-host", Base: "main", Head: "feat/x"}, entries) - if asked != 1 { - t.Fatalf("mesh-delivery was asked %d times for one check", asked) - } - want := link.CheckSibling{Repository: "novox/mesh-controller", Number: 346, Head: headController, Base: "main", Dir: "mesh-controller"} - if got := request.Check.Siblings; len(got) != 1 || got[0] != want || request.Check.SiblingsUnknown != "" { - t.Fatalf("the ask names siblings %+v, unknown %q", got, request.Check.SiblingsUnknown) - } - body, _ := json.Marshal(request.Check) - if !strings.Contains(string(body), `"siblings":[{"repository":"novox/mesh-controller","number":346,"head":"`+headController+ - `","base":"main","dir":"mesh-controller"}]`) { - t.Fatalf("on the wire: %s", body) - } -} - -// When mesh-delivery does not answer in time (or is not held, or answered without the fields), the check reads no -// sibling and its request says it could not learn them, so a fix to mesh-delivery is still checked without it. -func TestWhenMeshDeliveryDoesNotAnswerTheCheckReadsNoSiblingAndSaysSo(t *testing.T) { - was := groupsAsked - t.Cleanup(func() { groupsAsked = was }) - for _, c := range []struct { - what string - err error - held bool - }{{"it did not answer", context.DeadlineExceeded, true}, {"nothing serves it", errors.New("nothing serves mesh-delivery.groups"), true}, - {"no holder on record", errors.New("no holder of mesh-delivery is on record to say it"), false}} { - entries := theCoreOnTheGitSeat() - if !c.held { - entries = entries[:4] - } - groupsAsked = func(_ context.Context, held bool) ([]groupsLine, error) { - if held != c.held { - t.Errorf("%s: held %v", c.what, held) - } - return nil, c.err - } - request := link.BuildRequest{Check: &link.CheckRequest{Owner: "novox", Repo: "mesh-host", Base: "main", Beside: besideTheCore()}} - withSiblings(t.Context(), &request, link.PullUpdated{Owner: "novox", Repo: "mesh-host", Base: "main", Head: "feat/x"}, entries) - if len(request.Check.Siblings) != 0 || !strings.Contains(request.Check.SiblingsUnknown, c.err.Error()) || - !strings.Contains(request.Check.SiblingsUnknown, "reads no siblings") { - t.Errorf("%s: siblings %+v, unknown %q", c.what, request.Check.Siblings, request.Check.SiblingsUnknown) - } - } -} - -// **A pull request for which mesh-delivery answered and named no sibling is asked exactly as before** (ADR 0382 §7): -// byte for byte the request it was without the rule. -func TestAPullRequestWithNoSiblingIsAskedByteForByteAsBefore(t *testing.T) { - entries := theCoreOnTheGitSeat() - was := groupsAsked - t.Cleanup(func() { groupsAsked = was }) - for _, answer := range [][]groupsLine{nil, {group("feat/other", member("novox/mesh-controller", 346, headController, "main", true))}, - {group("feat/x", member("novox/mesh-host", 114, headHost, "main", true), member("novox/hq", 3, headHq, "main", true))}} { - groupsAsked = func(context.Context, bool) ([]groupsLine, error) { return answer, nil } - build := func() link.BuildRequest { - return link.BuildRequest{ID: "build-1", Repository: "r", Ref: headHost, Check: &link.CheckRequest{Owner: "novox", - Repo: "mesh-host", Number: 114, Base: "main", Beside: besideTheCore(), Modules: []string{"mesh-host"}, Judge: "validator"}} - } - before, _ := json.Marshal(build()) - after := build() - withSiblings(t.Context(), &after, link.PullUpdated{Owner: "novox", Repo: "mesh-host", Base: "main", Head: "feat/x"}, entries) - got, _ := json.Marshal(after) - if string(before) != string(got) { - t.Fatalf("the request changed:\nbefore %s\nafter %s", before, got) - } - } -} - -// What the build seat's verdict says of its siblings reaches the forge's holder in the controller's `checked`. -func TestTheVerdictCarriesTheSiblingsAndWhereTheJudgeCameFrom(t *testing.T) { - read := []link.CheckSibling{{Repository: "novox/mesh-controller", Number: 346, Head: headController, Base: "main", BaseCommit: headHost}} - c := checkedOf(link.BuildResult{ID: "b", Ref: headHost, Checked: &link.CheckRequest{Owner: "novox", Repo: "mesh-host", Number: 114}, - Check: &link.CheckOutcome{Verdict: "pass", Summary: "ok", Siblings: read, JudgeFrom: "sibling novox/mesh-controller#346 merge abc", - Validator: "the change's own validator"}}) - if len(c.Siblings) != 1 || c.Siblings[0] != read[0] || c.JudgeFrom == "" || c.Validator == "" { - t.Fatalf("the verdict says %+v", c) - } - body, _ := json.Marshal(c) - for _, want := range []string{`"siblings":[{"repository":"novox/mesh-controller","number":346,"head":"` + headController + - `","base":"main","base_commit":"` + headHost + `"}]`, `"judge_from":"sibling novox/mesh-controller#346 merge abc"`} { - if !strings.Contains(string(body), want) { - t.Fatalf("the verdict lacks %s:\n%s", want, body) - } - } - unknown := checkedOf(link.BuildResult{ID: "b", Check: &link.CheckOutcome{Verdict: "pass", SiblingsUnknown: "mesh-delivery did not answer"}}) - if body, _ := json.Marshal(unknown); !strings.Contains(string(body), `"siblings_unknown":"mesh-delivery did not answer"`) || - strings.Contains(string(body), `"siblings":`) { - t.Fatalf("the verdict that could not learn them says %s", body) - } -} diff --git a/cmd/mesh-controller/checks.go b/cmd/mesh-controller/checks.go index a408e842..1cca29e7 100644 --- a/cmd/mesh-controller/checks.go +++ b/cmd/mesh-controller/checks.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery" "io" "os" "path" @@ -161,39 +162,11 @@ func (f following) pullUpdated(ctx context.Context, p link.PullUpdated, w io.Wri return "", err } scope := pullScope(p, entries, read, edges) - // **A cycle of edges fails the check, naming every module and edge** (novox/hq ADR 0419 clause 6), a lone pull - // request's as a group's composed check is: no order exists for what it changes. - alone := orderMember{ID: p.Owner + "/" + p.Repo + "@" + twelve(p.Commit), Repository: p.Owner + "/" + p.Repo, Base: p.Base, - Head: p.Commit, Number: p.Number, Paths: p.Paths, PathsTruncated: p.PathsTruncated, Removed: p.Removed, BaseSHA: p.BaseSHA} - if failing, cyclic, err := cycleVerdict(ctx, "", []orderMember{alone}, - map[string]orderReach{alone.ID: {Moved: scope.Modules}}, edges, entries); err != nil { - return "", err - } else if cyclic { - failing.Commit, failing.Trunk = p.Commit, twelve(p.BaseSHA) - fmt.Fprintf(w, "%s/%s#%d (%.8s): %s\n", p.Owner, p.Repo, p.Number, p.Commit, failing.Summary) - sayChecked(ctx, w, failing) - return "", nil - } - // **A member of a group whose gate is the composed check's runs its repository's check alone** (novox/hq ADR 0403 - // clause 1): mesh-delivery is asked once, and says so on the group's line. Told nothing, the pull request is - // checked alone, gate included (clause 6). - lines, groupsErr := groupsAsked(ctx, deliverySeatHeld(entries)) - composed := groupsErr == nil && composedMember(p, lines) - // The change plan of the commit at hand (ADR 0238): what a merge of it would build and send, posted - // with the verdict whatever the verdict is. - plan := changePlanOf(p.Owner+"/"+p.Repo, p.Base, p.Commit, scope.Reach, entries, func(module string) (inventory.Upgrade, bool) { - u, err := inv.UpgradeOf(ctx, module) - return u, err == nil - }) - fmt.Fprint(w, planText(plan)) - direct := directVerdict(p, plan) - if composed { - scope.Modules, scope.Dependents, scope.New, scope.Manifests = nil, nil, nil, nil - composedGate(&direct) - } - // **Mesh-config is told what the head declares at every announced head of a pull request in a repository that holds - // modules** (novox/hq ADR 0502 clauses 2 and 6, issue 1826), also when the head touches no module: a head that reverts - // the change must drop the value held for it. Asked of nothing else here. + // **The merge gate's checks stop** (novox/hq ADR 0503 §3, §5; issue 1299): the checker judges every repository whose + // trunk requires `mesh/check`, and the controller asks nothing of it and says nothing. Only a repository whose trunk + // requires `mesh/repo-check` alone (hq) has its own merge-check.sh asked of build-agent, and said with that layer + // alone; what the forge's protection requires is read, never listed (lead decision L11). + // Mesh-config is still told what the head declares (ADR 0502), whatever judges it. this := pullAtOf(p) if this.Truncated { this.Paths, this.Truncated = truncatedPaths(entries, p.Owner, p.Repo, scope.Modules), false @@ -201,29 +174,54 @@ func (f following) pullUpdated(ctx context.Context, p link.PullUpdated, w io.Wri if scope.gated() || holdsModules(entries, p.Owner, p.Repo) { tellPullDeclares(ctx, w, []pullAt{this}) } + required, err := requiredStatusesOf(ctx, p.Owner, p.Repo, p.Base) switch { - case !scope.gated() && !scope.Mesh: - fmt.Fprintf(w, "%s/%s#%d (%.8s): %s, and the repository is not the mesh's: said, nothing run\n", - p.Owner, p.Repo, p.Number, p.Commit, noModuleTouched) + case err != nil: + fmt.Fprintf(w, "%s/%s#%d (%.8s): what %s's protection requires cannot be read, so nothing is asked and nothing is "+ + "said: %v\n", p.Owner, p.Repo, p.Number, p.Commit, p.Base, err) + return "", nil + case slices.Contains(required, sdkdelivery.StatusContextCheck): + fmt.Fprintf(w, "%s/%s#%d (%.8s): %s requires %s: the checker judges it; nothing is asked\n", p.Owner, p.Repo, + p.Number, p.Commit, p.Base, sdkdelivery.StatusContextCheck) + return "", nil + case !slices.Contains(required, sdkdelivery.StatusContextRepoCheck): + fmt.Fprintf(w, "%s/%s#%d (%.8s): %s requires neither %s nor %s: nothing is asked and nothing is said\n", p.Owner, + p.Repo, p.Number, p.Commit, p.Base, sdkdelivery.StatusContextCheck, sdkdelivery.StatusContextRepoCheck) + return "", nil + } + return f.repoCheckAlone(ctx, w, p, scope, entries) +} + +// repoCheckAlone asks build-agent for a repository's own merge-check.sh alone, for a pull request of a repository whose +// trunk requires `mesh/repo-check` and not `mesh/check` (ADR 0503 §5): no gate, no module built, its verdict said with the +// repo-check layer alone. +func (f following) repoCheckAlone(ctx context.Context, w io.Writer, p link.PullUpdated, scope checkScope, + entries []inventory.Entry) (string, error) { + inv := f.open.inventory + plan := changePlanOf(p.Owner+"/"+p.Repo, p.Base, p.Commit, scope.Reach, entries, func(module string) (inventory.Upgrade, bool) { + u, err := inv.UpgradeOf(ctx, module) + return u, err == nil + }) + fmt.Fprint(w, planText(plan)) + direct := directVerdict(p, plan) + switch { + case !scope.Mesh: + direct.RepoCheck = &link.CheckLayer{Verdict: "warning", Summary: "the repository is not the mesh's: nothing run"} + fmt.Fprintf(w, "%s/%s#%d (%.8s): the repository is not the mesh's: said, nothing run\n", p.Owner, p.Repo, p.Number, + p.Commit) sayChecked(ctx, w, direct) return "", nil - case !scope.gated() && p.MergeCheckSaid && !p.MergeCheck: + case p.MergeCheckSaid && !p.MergeCheck: direct.RepoCheck = &link.CheckLayer{Verdict: "warning", Summary: noMergeCheck} - fmt.Fprintf(w, "%s/%s#%d (%.8s): %s; %s\n", p.Owner, p.Repo, p.Number, p.Commit, noModuleTouched, noMergeCheck) + fmt.Fprintf(w, "%s/%s#%d (%.8s): %s\n", p.Owner, p.Repo, p.Number, p.Commit, noMergeCheck) sayChecked(ctx, w, direct) return "", nil } - request, err := checkRequestFor(ctx, f.open, p, scope, entries) + request, err := checkRequestFor(ctx, f.open, p, checkScope{Mesh: true, From: scope.From}, entries) if err != nil { return "", err } request.Check.Plan = &plan - request.Check.Composed = composed - if !composed { - carryHeld(ctx, w, &request, []pullAt{this}) - } - // **The group's other open pull requests are read beside it** (novox/hq ADR 0382 §1). - siblingsFrom(&request, p, entries, lines, groupsErr) seat := buildSeatHeld(ctx) ask, err := askOverOn(seat) if err != nil { @@ -233,23 +231,42 @@ func (f following) pullUpdated(ctx context.Context, p link.PullUpdated, w io.Wri if err := ask.Ask(ctx, request); err != nil { return "", err } - // **A newer request replaces the one still waiting, and a newer key stops the one running** (novox/hq ADR 0406 §3, §4, - // issue 974): the check queue keeps one request a pull request, and a running check watches its key. Nothing is asked - // of the forge here (issue 726). - what := "its own merge-check.sh alone: " + noModuleTouched - if composed { - what = "its own merge-check.sh alone: the gate is its group's composed check's" - } - if scope.gated() { - what = fmt.Sprintf("the gate over %s (a merge would build %d module(s) in %d tier(s))", - strings.Join(append(append([]string{}, scope.Modules...), prefixedAll("new:", scope.New)...), ", "), - scope.Width, scope.Tiers) - } - fmt.Fprintf(w, "%s/%s#%d (%.8s): asked %s to check it before it merges — %s — as %s\n", p.Owner, p.Repo, p.Number, - p.Commit, seat, what, request.ID) + fmt.Fprintf(w, "%s/%s#%d (%.8s): asked %s to run its own merge-check.sh alone, as %s\n", p.Owner, p.Repo, p.Number, + p.Commit, seat, request.ID) return request.ID, nil } +// requiredStatusesOf is what the forge's protection of a repository's branch requires before a merge, as the forge +// picks the rule it applies (the one named for the branch, else the first glob covering it). A variable so a test needs +// no forge. +var requiredStatusesOf = func(ctx context.Context, owner, repo, branch string) ([]string, error) { + var rules struct { + Rules []protectionRule `json:"rules"` + } + js, err := aBus() + if err != nil { + return nil, err + } + defer js.Close() + raw, _ := json.Marshal(map[string]any{"owner": owner, "repo": repo}) + answer, err := link.Ask(ctx, link.OverNATS{Conn: js.Conn()}, "gitea", "gitea_branch_protection_get", raw, 30*time.Second) + if err != nil { + return nil, err + } + if answer.Error != "" { + return nil, fmt.Errorf("gitea.gitea_branch_protection_get: %s", answer.Error) + } + if err := json.Unmarshal(answer.Result, &rules); err != nil { + return nil, err + } + r := appliedRule(rules.Rules, branch) + if r == nil { + // empty-on-error: not an error; no rule covers the branch, so it requires nothing, which the caller says + return nil, nil + } + return r.RequiredStatuses, nil +} + // composedWords is what a group member's own verdict says of its gate layer (novox/hq ADR 0403 clause 1). const composedWords = "judged by its group's composed check" @@ -378,15 +395,6 @@ func trunkOf(s inventory.Source) string { return "main" } -// sourceName is the name of a recorded repository, a path on the git seat or a URL: novox/mesh-host → mesh-host. -func sourceName(repository string) string { - parts := strings.Split(strings.Trim(strings.TrimSuffix(repository, ".git"), "/"), "/") - if len(parts) < 2 { - return "" - } - return parts[len(parts)-1] -} - // namedForTheCheck are the builds earlier checks of the same pull request at the same head named, read once for the // check (novox/hq issue 703): asked again because only the trunk moved, the check keeps what they built of a key // still the same. Another pull request's build, or one of an earlier head, is never kept. None when they cannot be @@ -676,6 +684,9 @@ func prefixedAll(prefix string, items []string) []string { // sayChecked says a merge check's verdict on the bus, where the forge's holder hears it. func sayChecked(ctx context.Context, w io.Writer, c link.Checked) { + // **No merge-gate layer from R4e, for any repository** (ADR 0503 §3, §5; issue 1299): the forge's holder writes + // `mesh/repo-check` from the repo-check layer alone, and the checker owes `mesh/check`. + c.Gate = nil repo := "none" if c.RepoCheck != nil { repo = strings.ToUpper(c.RepoCheck.Verdict) + " — " + c.RepoCheck.Summary @@ -684,8 +695,8 @@ func sayChecked(ctx context.Context, w io.Writer, c link.Checked) { if c.Trunk != "" { judged = " merged into trunk " + c.Trunk } - fmt.Fprintf(w, "%s: %s/%s#%d at %.8s%s checked on %s: gate %s — %s; repository %s\n", c.ID, c.Owner, c.Repo, c.Number, - c.Commit, judged, orSomewhere(c.On), strings.ToUpper(c.Verdict), c.Summary, repo) + fmt.Fprintf(w, "%s: %s/%s#%d at %.8s%s checked on %s: repository %s\n", c.ID, c.Owner, c.Repo, c.Number, + c.Commit, judged, orSomewhere(c.On), repo) if checkEvents == nil { return } diff --git a/cmd/mesh-controller/checks_queue_test.go b/cmd/mesh-controller/checks_queue_test.go deleted file mode 100644 index b9fb48f2..00000000 --- a/cmd/mesh-controller/checks_queue_test.go +++ /dev/null @@ -1,97 +0,0 @@ -//go:build integration - -package main - -import ( - "context" - "encoding/json" - "fmt" - "testing" - - "github.com/nats-io/nats.go" - - "git.novox.be/novox/mesh-sdk/go/catalogue" - "github.com/novox/mesh-controller/internal/broker" - "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" -) - -// Acceptance 5 of milestone 2 (novox/hq ADR 0406 §3, issue 974), live, with the store and a real bus: two merges into one -// repository within a minute, with two open pull requests, leave each pull request with exactly one queued check, by the -// stream's rule. Each merge moves the trunk, and mesh-delivery asks the controller to check both open pull requests again -// against it (`delivery check`, which is the controller's own path for a head announced); nothing moves a request, nothing -// asks the forge, and the queue is read back whole. -func TestTwoMergesWithinAMinuteLeaveEachOpenPullRequestOneQueuedCheck(t *testing.T) { - open := aMesh(t) - ctx := t.Context() - js, _ := aBuildQueue(t) - servingBus.Store(js) - t.Cleanup(func() { servingBus.Store(nil) }) - holdTheDeliverySeat(t, open) - was := askDeliveryOwner - t.Cleanup(func() { askDeliveryOwner = was }) - askDeliveryOwner = func(_ context.Context, _ *nats.Conn, verb string, _ map[string]any) (json.RawMessage, error) { - if verb != "groups" { - t.Fatalf("asked the owner %s", verb) - } - return json.RawMessage(`[]`), nil - } - // The repository the two pull requests are into, as a module the mesh builds from it. - if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-controller", Version: "1"}, - inventory.Source{Repository: "novox/mesh-controller", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { - t.Fatal(err) - } - - heads := map[int]string{7: "7777777777777777777777777777777777777777", 8: "8888888888888888888888888888888888888888"} - check := func(number int, trunk string) { - t.Helper() - p := orderMember{ID: fmt.Sprintf("novox/mesh-controller@%.12s", heads[number]), Repository: "novox/mesh-controller", - Base: "main", Head: heads[number], Number: number, Paths: []string{"main.go"}, ModuleDirsSaid: true, - BaseSHA: trunk, Ready: number == 8}.pull() - p.MergeCheck, p.MergeCheckSaid = true, true - if err := (following{open: open, oneShot: true}).PullUpdated(ctx, p); err != nil { - t.Fatal(err) - } - } - // Announced against the trunk as it stands, then asked again after each of two merges. - for _, trunk := range []string{"1111111111111111111111111111111111111111", "2222222222222222222222222222222222222222", - "3333333333333333333333333333333333333333"} { - check(7, trunk) - check(8, trunk) - } - - q, err := link.ReadQueue(ctx, js, link.TheBuildMachine) - if err != nil { - t.Fatal(err) - } - seen := map[int]string{} - for _, a := range q.Asks { - c := a.Request.Check - if c == nil { - t.Fatalf("%s is no check", a.ID) - } - if _, twice := seen[c.Number]; twice { - t.Errorf("pull request %d has two queued checks", c.Number) - } - seen[c.Number] = c.Trunk - if a.State != link.AskWaiting { - t.Errorf("%s is %s", a.ID, a.State) - } - if want := "3333333333333333333333333333333333333333"; c.Trunk != want { - t.Errorf("pull request %d is queued against %.8s; want the newest trunk %.8s", c.Number, c.Trunk, want) - } - } - if len(seen) != 2 { - t.Fatalf("queued checks for %v; want one for each of the two open pull requests", seen) - } - // By the streams' own count: the ready one on its stream, the other on its. - for stream, want := range map[string]uint64{broker.ChecksReadyStream: 1, broker.ChecksOtherStream: 1} { - info, err := js.Context().StreamInfo(stream) - if err != nil { - t.Fatal(err) - } - if info.State.Msgs != want { - t.Errorf("%s holds %d request(s); want %d", stream, info.State.Msgs, want) - } - } -} diff --git a/cmd/mesh-controller/checks_stop_integration_test.go b/cmd/mesh-controller/checks_stop_integration_test.go new file mode 100644 index 00000000..f37ac855 --- /dev/null +++ b/cmd/mesh-controller/checks_stop_integration_test.go @@ -0,0 +1,94 @@ +//go:build integration + +package main + +import ( + "context" + "encoding/json" + "errors" + "testing" + + "git.novox.be/novox/mesh-sdk/go/catalogue" + "github.com/novox/mesh-controller/internal/inventory" + "github.com/novox/mesh-controller/internal/link" +) + +// C1 (ADR 0503 §3, §5; issue 1299, lead decision L11), with the store and a real build queue: a pull request of a +// repository whose trunk requires mesh/check asks build-agent nothing and says nothing; one whose trunk requires +// mesh/repo-check alone (hq) has its own merge-check.sh asked, and every verdict said names no gate layer; a protection +// that cannot be read asks and says nothing. +func TestThePullRequestChecksAreAskedOnlyForARepositoryTheCheckerDoesNotJudge(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + js, _ := aBuildQueue(t) + servingBus.Store(js) + t.Cleanup(func() { servingBus.Store(nil) }) + said := &saidOnTheBus{} + checkEvents = said + t.Cleanup(func() { checkEvents = nil }) + if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-controller", Version: "1"}, + inventory.Source{Repository: "novox/mesh-controller", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { + t.Fatal(err) + } + required := map[string][]string{"novox/mesh-controller": {"mesh/check"}, "novox/hq": {"mesh/repo-check"}} + was := requiredStatusesOf + requiredStatusesOf = func(_ context.Context, owner, repo, _ string) ([]string, error) { + if r, ok := required[owner+"/"+repo]; ok { + return r, nil + } + return nil, errors.New("the forge did not answer") + } + t.Cleanup(func() { requiredStatusesOf = was }) + queued := func() int { + q, err := link.ReadQueue(ctx, js, link.TheBuildMachine) + if err != nil { + t.Fatal(err) + } + return len(q.Asks) + } + pull := func(repo string, number int, mergeCheck bool) link.PullUpdated { + p := link.PullUpdated{Owner: "novox", Repo: repo, Number: number, Base: "main", Commit: "abcdefabcdefabcdefabcdefabcdefabcdefabcd", + BaseSHA: "1111111111111111111111111111111111111111", Paths: []string{"main.go"}, ModuleDirsSaid: true} + p.MergeCheck, p.MergeCheckSaid = mergeCheck, true + return p + } + f := following{open: open, oneShot: true} + + if err := f.PullUpdated(ctx, pull("mesh-controller", 7, true)); err != nil { + t.Fatal(err) + } + if n := queued(); n != 0 || len(said.events) != 0 { + t.Fatalf("0503 §3: a repository the checker judges asked %d check(s) and said %v", n, said.events) + } + if err := f.PullUpdated(ctx, pull("unread", 3, true)); err != nil { + t.Fatal(err) + } + if n := queued(); n != 0 || len(said.events) != 0 { + t.Fatalf("L11: a protection that cannot be read asked %d check(s) and said %v", n, said.events) + } + if err := f.PullUpdated(ctx, pull("hq", 9, true)); err != nil { + t.Fatal(err) + } + q, err := link.ReadQueue(ctx, js, link.TheBuildMachine) + if err != nil { + t.Fatal(err) + } + if len(q.Asks) != 1 || q.Asks[0].Request.Check == nil || len(q.Asks[0].Request.Check.Modules) != 0 || + q.Asks[0].Request.Check.Repo != "hq" { + t.Fatalf("0503 §5: hq's repository check is not the one ask: %+v", q.Asks) + } + // A head with no merge-check.sh is said at once, with the repo-check layer alone. + if err := f.PullUpdated(ctx, pull("hq", 10, false)); err != nil { + t.Fatal(err) + } + if len(said.events) != 1 { + t.Fatalf("hq's head with no merge-check.sh was said %d time(s)", len(said.events)) + } + var c link.Checked + if err := json.Unmarshal(said.bodies[0], &c); err != nil { + t.Fatal(err) + } + if c.Gate != nil || c.RepoCheck == nil || c.RepoCheck.Verdict != "warning" { + t.Fatalf("0503 §5: the verdict said names a gate layer, or no repo-check layer: %+v", c) + } +} diff --git a/cmd/mesh-controller/composed_member_check_test.go b/cmd/mesh-controller/composed_member_check_test.go deleted file mode 100644 index 7943412f..00000000 --- a/cmd/mesh-controller/composed_member_check_test.go +++ /dev/null @@ -1,68 +0,0 @@ -//go:build integration - -package main - -import ( - "context" - "strings" - "testing" - - "git.novox.be/novox/mesh-sdk/go/catalogue" - "github.com/novox/mesh-controller/internal/inventory" - "github.com/novox/mesh-controller/internal/link" -) - -// A member of a group whose gate is the composed check's is asked of the build seat with no module: its repository's -// check alone (novox/hq ADR 0403 clause 1). With no word from mesh-delivery, or one that does not say the gate is -// composed, it is checked alone with its gate (clause 6). - -func askedForMember(t *testing.T, answer []groupsLine, answerErr error) link.CheckRequest { - t.Helper() - open := aMesh(t) - ctx := t.Context() - js, _ := aBuildQueue(t) - servingBus.Store(js) - t.Cleanup(func() { servingBus.Store(nil) }) - if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-controller", Version: "1"}, - inventory.Source{Repository: "novox/mesh-controller", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { - t.Fatal(err) - } - was := groupsAsked - groupsAsked = func(context.Context, bool) ([]groupsLine, error) { return answer, answerErr } - t.Cleanup(func() { groupsAsked = was }) - p := link.PullUpdated{Owner: "novox", Repo: "mesh-controller", Number: 346, Base: "main", Head: "feat/x", Commit: headController, - Paths: []string{"main.go"}, ModuleDirsSaid: true, MergeCheck: true, MergeCheckSaid: true} - if err := (following{open: open, oneShot: true}).PullUpdated(ctx, p); err != nil { - t.Fatal(err) - } - q, err := link.ReadQueue(ctx, js, link.TheBuildMachine) - if err != nil || len(q.Asks) != 1 { - t.Fatalf("asks %d: %v", len(q.Asks), err) - } - return *q.Asks[0].Request.Check -} - -func TestAComposedMembersCheckIsItsRepositoryChecksAlone(t *testing.T) { - line := group("feat/x", member("novox/mesh-controller", 346, headController, "main", true), member("novox/mesh-catalog", 350, headHost, "main", true)) - line.ComposedGate = true - c := askedForMember(t, []groupsLine{line}, nil) - if len(c.Modules) != 0 || len(c.New) != 0 || len(c.Dependents) != 0 || !c.Composed { - t.Fatalf("the member's check is asked as %+v; want no module and composed", c) - } -} - -func TestAMemberOfAGroupOfAnOlderMeshDeliveryKeepsItsOwnGate(t *testing.T) { - line := group("feat/x", member("novox/mesh-controller", 346, headController, "main", true), member("novox/mesh-catalog", 350, headHost, "main", true)) - c := askedForMember(t, []groupsLine{line}, nil) - if !strings.Contains(strings.Join(c.Modules, ","), "mesh-controller") || c.Composed { - t.Fatalf("an older mesh-delivery's member is asked as %+v; want its own gate", c) - } -} - -func TestAMemberWhoseGroupCannotBeLearnedIsCheckedAloneWithItsGate(t *testing.T) { - c := askedForMember(t, nil, context.DeadlineExceeded) - if !strings.Contains(strings.Join(c.Modules, ","), "mesh-controller") || c.Composed || - !strings.Contains(c.SiblingsUnknown, "reads no siblings") { - t.Fatalf("the member is asked as %+v; want its gate and why its siblings are unknown", c) - } -} diff --git a/cmd/mesh-controller/composed_verdict_test.go b/cmd/mesh-controller/composed_verdict_test.go index f96bc00c..5fcd49f7 100644 --- a/cmd/mesh-controller/composed_verdict_test.go +++ b/cmd/mesh-controller/composed_verdict_test.go @@ -14,37 +14,11 @@ import ( // A delivery group's gate is the composed check's, and its members' own checks run their repository's check alone // (novox/hq ADR 0403 clauses 1 and 7). Pure: no store, no bus. -// A member of a group whose line says composed_gate is a composed member; one whose group says nothing of it (an older -// mesh-delivery), one of a group of a single pull request and a lone pull request are not. -func TestAMemberOfAComposedGroupIsKnownByTheGroupsLine(t *testing.T) { - two := func(composed bool) groupsLine { - l := group("feat/x", member("novox/mesh-host", 114, headHost, "main", true), member("novox/mesh-controller", 346, headController, "main", true)) - l.ComposedGate = composed - return l - } - p := link.PullUpdated{Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Head: "feat/x", Commit: headHost} - recheck := link.PullUpdated{Owner: "novox", Repo: "mesh-host", Number: 114, Base: "main", Commit: headHost} - for name, c := range map[string]struct { - p link.PullUpdated - lines []groupsLine - want bool - }{ - "composed": {p, []groupsLine{two(true)}, true}, - "a recheck, by its number": {recheck, []groupsLine{two(true)}, true}, - "an older mesh-delivery": {p, []groupsLine{two(false)}, false}, - "another group": {p, []groupsLine{group("feat/y", member("novox/mesh-host", 1, headHost, "main", true))}, false}, - "no group": {p, nil, false}, - } { - if got := composedMember(c.p, c.lines); got != c.want { - t.Errorf("%s: composed %v, want %v", name, got, c.want) - } - } - alone := group("feat/x", member("novox/mesh-host", 114, headHost, "main", true)) - alone.ComposedGate = true - if composedMember(p, []groupsLine{alone}) { - t.Error("a group of a single pull request is composed") - } -} +// The heads of a group's members, by repository. +const ( + headHost = "1111111111111111111111111111111111111111" + headController = "2222222222222222222222222222222222222222" +) // A composed member's verdict never says its gate passed: the gate layer, and the verdict that is the gate's, read // "composed"; a failure or an error stays what it was. diff --git a/cmd/mesh-controller/delivery_check_answer_test.go b/cmd/mesh-controller/delivery_check_answer_test.go index 21eee54d..c9609ea7 100644 --- a/cmd/mesh-controller/delivery_check_answer_test.go +++ b/cmd/mesh-controller/delivery_check_answer_test.go @@ -41,7 +41,7 @@ func TestSayCheckedWritesTheSameLineToItsWriter(t *testing.T) { var w bytes.Buffer sayChecked(context.Background(), &w, link.Checked{ID: "c1", Owner: "novox", Repo: "x", Number: 7, Commit: "0123456789abcdef", Verdict: "pass", Summary: "fine"}) - want := "c1: novox/x#7 at 01234567 checked on a machine that did not say which: gate PASS — fine; repository none\n" + want := "c1: novox/x#7 at 01234567 checked on a machine that did not say which: repository none\n" if w.String() != want { t.Fatalf("sayChecked said %q; want %q", w.String(), want) } diff --git a/cmd/mesh-controller/replay867_test.go b/cmd/mesh-controller/replay867_test.go index 879713e3..a8316cee 100644 --- a/cmd/mesh-controller/replay867_test.go +++ b/cmd/mesh-controller/replay867_test.go @@ -36,11 +36,9 @@ import ( // It runs the whole path: the forge announces each pull request (following.PullUpdated), the controller asks // mesh-delivery which pull requests the group holds, the ask is queued for the build seat, and the build seat's check // (builder.Check, with a container runtime that runs each container's command on this machine) judges it. -func TestReplay867(t *testing.T) { replay867(t, false) } // TestReplay867Recheck is the same pair re-checked through mesh-delivery (`delivery check` with one member): the forge's // announcement is not repeated, so the pull request reaches the controller without its head branch name. -func TestReplay867Recheck(t *testing.T) { replay867(t, true) } func replay867(t *testing.T, recheck bool) { if _, err := exec.LookPath("go"); err != nil { diff --git a/cmd/mesh-controller/try_test.go b/cmd/mesh-controller/try_test.go index e696945b..32fb8aa6 100644 --- a/cmd/mesh-controller/try_test.go +++ b/cmd/mesh-controller/try_test.go @@ -22,10 +22,12 @@ import ( type saidOnTheBus struct { link.Bus events []string + bodies [][]byte } -func (s *saidOnTheBus) PublishSeatEvent(_ context.Context, seat, event string, _ []byte) error { +func (s *saidOnTheBus) PublishSeatEvent(_ context.Context, seat, event string, body []byte) error { s.events = append(s.events, seat+" "+event) + s.bodies = append(s.bodies, body) return nil }