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 }