Ask no pull request check of build-agent but the own merge-check.sh of a repository whose trunk requires mesh/repo-check alone, and say every verdict with no gate layer (issue 1299, C1, ADR 0503 §3, §5)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user