package main import ( "context" "encoding/json" "errors" "fmt" "path" "regexp" "runtime/debug" "slices" "sort" "strings" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // A pull request's merge check (novox/hq to-be 45 §9, ADR 0237 as amended 2026-10-06): the forge // announces every pull request's new head, the controller decides what is checked, asks the build seat to // check it, and says the verdict as `checked`, which the forge's holder sets as the pull request's // statuses. **Before merge, never after**: every check the mesh had ran after a merge, on a machine. // // **The mesh's module graph decides, not the repository.** The controller holds the graph — every module, // the repository and directory it is built from — and maps the pull request's changed paths onto it by // the planner's own answer (reachOfMerge, touchedBy — the one place a changed file is mapped onto modules, // for the merge handler, the release planner, the merge gate and this check; issue 280): **a changed file // touches exactly the modules whose build reads it** — a module's own directory (the whole repository for // one built from its root), or a repository its recipe packages. A file no build reads — a script at the // root, a README — touches no module. A directory the change adds a module.json in, which the graph does // not hold yet (said by the head, issue 278), is a new module and is checked too. // // - touches a module: the build seat runs **the gate** — `mesh/merge-gate`, the touched manifests, every // machine composed with the change, the replays — and the repository's own merge-check.sh beside it; // - touches none, in a repository that is the mesh's (it sources a module on some branch, or shares the // core's owner): the gate is a pass that says so — a fact, not a missing check — and the repository's // own merge-check.sh runs as `mesh/repo-check`, a warning when it has none; // - touches none, anywhere else: the gate is a pass that says so, and nothing more is said. // // What is checked is decided here and run there (internal/builder/check.go). // checkTimeout is how long one check may run on the build seat. Said here so the ask's watchdog (S6) // and the builder agree on what late means. const checkTimeout = 45 * time.Minute // noModuleTouched is the gate's word for a change that touches nothing of the graph. const noModuleTouched = "the change touches no module of the mesh's graph" // noMergeCheck is the repository layer's word for a repository of the mesh with no merge-check.sh. const noMergeCheck = "the repository declares no merge-check.sh: none of its own tests run before it merges" // coreModules are the modules whose repositories are the mesh's core, by the directory a check finds // each beside it — and whose owner is the mesh's own. var coreModules = map[string]string{"mesh-controller": "mesh-controller", "mesh-host": "mesh-host", "node-tools": "mesh-tools", "nats": "mesh-catalog"} // checkScope is what a pull request reaches of the mesh's graph, as the planner reckons it. type checkScope struct { // Modules are the modules a merge of the change would move itself — built from the repository into // the pull request's base and reading a changed file, or packaging the repository's source — and // Dependents those the plan would build after them; New the directories it adds a module in. Modules []string Dependents []string New []string // Manifests are the moved modules' and the new ones' manifests in the change's tree. Manifests []string // Width is how many modules a merge would build, in how many tiers; Unread the changed files no // module's build reads. Width, Tiers int Unread []string // Mesh says the repository is the mesh's: modules are built from it on some branch, its owner is the // core's, or the change adds a module to it. Mesh bool // Judge is who judges the gate (link.JudgeSelf, link.JudgeValidator, or the running controller). Judge string // From is a module built from the repository, for how the mesh clones it; nil when none is. From *inventory.Entry // Reach is the planner's whole answer, which the change plan is made from. Reach mergeReach } func (s checkScope) gated() bool { return len(s.Modules)+len(s.New) > 0 } // pullScope is what a pull request reaches: **the planner's own answer** (reachOfMerge), asked as if the // head were merged into the base — never a mapping of its own, so a change to what a merge touches // changes what is checked with it (novox/hq ADR 0238). func pullScope(p link.PullUpdated, entries []inventory.Entry, read map[string][]inventory.ReadRepository, edges []inventory.Edge) checkScope { m := link.SourceMoved{Owner: p.Owner, Repo: p.Repo, Base: p.Base, CloneURL: p.CloneURL, Commit: p.Commit, Paths: p.Paths, PathsTruncated: p.PathsTruncated, Removed: p.Removed, ModuleDirs: p.ModuleDirs, ModuleDirsSaid: p.ModuleDirsSaid} r := reachOfMerge(m, entries, read, edges) s := checkScope{Modules: r.Moved(), Dependents: r.Dependents(), New: r.Added, Unread: r.Unread, Width: len(r.Plan.Modules), Tiers: len(r.Plan.Tiers), Reach: r} for _, e := range append(append([]inventory.Entry{}, r.Touched...), r.Deleted...) { s.Manifests = append(s.Manifests, path.Join(strings.Trim(e.Source.Path, "/"), moduleManifestFile)) switch e.Manifest.Module { case "mesh-controller": s.Judge = link.JudgeSelf case "mesh-host": if s.Judge == "" { s.Judge = link.JudgeValidator } } } for _, d := range r.Added { s.Manifests = append(s.Manifests, path.Join(d, moduleManifestFile)) } sort.Strings(s.Manifests) s.Manifests = slices.Compact(s.Manifests) // Whose repository it is, for how it is cloned and whether its own check is the mesh's to run. owners := map[string]bool{} for i, e := range entries { if e.Provided { continue } if _, core := coreModules[e.Manifest.Module]; core { if owner := sourceOwner(e.Source.Repository); owner != "" { owners[owner] = true } } if sameRepository(e.Source.Repository, m) && s.From == nil { s.From = &entries[i] } } s.Mesh = s.From != nil || owners[strings.ToLower(p.Owner)] || s.gated() return s } // sourceOwner is the owner of a recorded repository, a path on the git seat or a URL: novox/mesh-host → novox. func sourceOwner(repository string) string { parts := strings.Split(strings.Trim(strings.TrimSuffix(repository, ".git"), "/"), "/") if len(parts) < 2 { return "" } return strings.ToLower(parts[len(parts)-2]) } // PullUpdated decides a pull request's merge check, and asks for it when there is something to run. func (f following) PullUpdated(ctx context.Context, p link.PullUpdated) error { inv := f.open.inventory entries, err := inv.Catalogued(ctx) if err != nil { return err } read, err := readForPlanning(ctx, inv) if err != nil { return err } edges, err := inv.Dependencies(ctx) if err != nil { return err } scope := pullScope(p, entries, read, edges) // 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.Print(planText(plan)) direct := directVerdict(p, plan) switch { case !scope.gated() && !scope.Mesh: fmt.Printf("%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) sayChecked(ctx, direct) return nil case !scope.gated() && p.MergeCheckSaid && !p.MergeCheck: direct.RepoCheck = &link.CheckLayer{Verdict: "warning", Summary: noMergeCheck} fmt.Printf("%s/%s#%d (%.8s): %s; %s\n", p.Owner, p.Repo, p.Number, p.Commit, noModuleTouched, noMergeCheck) sayChecked(ctx, direct) return nil } request, err := checkRequestFor(ctx, f.open, p, scope, entries) if err != nil { return err } request.Check.Plan = &plan seat := buildSeatHeld(ctx) ask, err := askOverOn(seat) if err != nil { return err } defer ask.Close() if err := ask.Ask(ctx, request); err != nil { return err } what := "its own merge-check.sh alone: " + noModuleTouched 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.Printf("%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) return nil } // directVerdict is the verdict of a change no build judges — it touches no module, and either the repository is not // the mesh's or the head holds no merge-check.sh. **It names the trunk commit it holds for** (novox/hq ADR 0323 §3): // the base branch's head the forge announced with the head, so the merge tool can weigh it against the trunk's head // and does not refuse it for ever (a verdict naming no trunk is never the trunk's head's). Nothing was built to // judge: the trunk commit is the one the forge said the change was announced against. func directVerdict(p link.PullUpdated, plan link.ChangePlan) link.Checked { trunk := p.BaseSHA if len(trunk) > 12 { trunk = trunk[:12] } if trunk != "" { plan.Trunk = trunk } return link.Checked{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Commit: p.Commit, ID: link.NewBuildID(time.Now()), Verdict: "pass", Summary: noModuleTouched, Gate: &link.CheckLayer{Verdict: "pass", Summary: noModuleTouched}, Plan: &plan, Trunk: trunk} } // checkRequestFor is the ask for one pull request's head: the repository as the mesh clones it, the head, // and what is read beside it. func checkRequestFor(ctx context.Context, open *stores, p link.PullUpdated, scope checkScope, entries []inventory.Entry) (link.BuildRequest, error) { shelf := map[string]catalogue.Manifest{} for _, e := range entries { shelf[e.Manifest.Module] = e.Manifest } world, err := theRestOfTheMesh(ctx, open.inventory, shelf, "") if err != nil { return link.BuildRequest{}, err } clone := func(s inventory.Source) (string, error) { if s.Seat == "" { return s.Repository, nil } return clonedFromSeat(world, s.Seat, s.Repository) } // As the mesh clones a module built from it; a repository no module is built from, from the forge. source := inventory.Source{Seat: gitSeat, Repository: p.Owner + "/" + p.Repo} if scope.From != nil { source = scope.From.Source } repository, err := clone(source) if err != nil { return link.BuildRequest{}, err } current, err := open.inventory.CurrentBuilds(ctx) if err != nil { return link.BuildRequest{}, err } // **A pull request into a release branch is judged by that branch** (novox/hq ADR 0358): asked of the forge once. release, err := releaseBranchOf(ctx, p, scope, entries) if err != nil { return link.BuildRequest{}, err } beside, err := besideOf(entries, func(module string) string { return current[module].Commit }, clone, release) if err != nil { return link.BuildRequest{}, err } builds := checkBuildsOf(p, scope, entries, clone) return link.BuildRequest{ ID: link.NewBuildID(time.Now()), Repository: repository, Ref: p.Commit, Held: heldBy(ctx), Seats: seatBases(ctx), Source: sourceOnSeat(source), Check: &link.CheckRequest{Owner: p.Owner, Repo: p.Repo, Number: p.Number, Base: p.Base, Paths: p.Paths, Beside: beside, Modules: scope.Modules, Dependents: scope.Dependents, New: scope.New, Manifests: scope.Manifests, Judge: scope.Judge, Builds: builds, Named: namedForTheCheck(ctx, p, builds), Release: release}, }, nil } // besideOf is what a check reads beside its own repository, by the directory it is found under: each core repository // by the module the mesh builds from it, at besideRefs — or, for a pull request into a release branch (novox/hq ADR // 0358 §3), at its branch of that name where it has one and at its trunk otherwise, the controller's main kept for // its judge, and mesh-lab at its trunk whatever the base: its replays run with the container runtime's socket. running // is the commit the mesh runs of a module. func besideOf(entries []inventory.Entry, running func(module string) string, clone func(inventory.Source) (string, error), release string) (map[string]link.CheckedOut, error) { beside := map[string]link.CheckedOut{} for _, e := range entries { dir, core := coreModules[e.Manifest.Module] if !core || e.Provided || e.Source.Repository == "" { continue } url, err := clone(e.Source) if err != nil { return nil, err } refs := besideRefs(dir, running(e.Manifest.Module)) at := func(dir, trunk string) link.CheckedOut { if release == "" { return link.CheckedOut{Repository: url, Ref: refs[dir]} } if trunk == "" { trunk = "main" } return link.CheckedOut{Repository: url, Ref: release, Else: trunk} } beside[dir] = at(dir, followedBranch(e.Source.Ref)) if dir == "mesh-controller" { // The controller's main stays its main: it judges only a check the running controller predates. beside["mesh-controller-main"] = link.CheckedOut{Repository: url, Ref: refs["mesh-controller-main"]} if release != "" { // mesh-controller has the release branch: that is what makes it one (ADR 0358 §1). beside[dir] = link.CheckedOut{Repository: url, Ref: release} } if e.Source.Seat != "" { for _, sibling := range []string{"mesh-lab", "mesh-sdk"} { if url, err := clone(inventory.Source{Seat: e.Source.Seat, Repository: siblingOf(e.Source.Repository, sibling)}); err == nil { b := link.CheckedOut{Repository: url, Ref: refs[sibling]} // **mesh-lab is read at its trunk, always** (ADR 0358): its replays run with the container // runtime's socket, so they are only ever reviewed code — never a branch anyone may create. if release != "" && sibling != "mesh-lab" { b = link.CheckedOut{Repository: url, Ref: release, Else: "main"} } beside[sibling] = b } } } } } return beside, nil } // releaseBranchOf is the release branch a pull request merges into (novox/hq ADR 0358 §1): its base, when that is not // its repository's trunk and mesh-controller has a branch of that name; empty otherwise, and the pull request is // checked as before it. **A base that is a trunk asks nothing**: the branch a module built from the repository follows // (main when it records none), or mesh-controller's own trunk. Any other base is asked of the forge once, and an // answer it cannot give is an error, never read as "no release branch". func releaseBranchOf(ctx context.Context, p link.PullUpdated, scope checkScope, entries []inventory.Entry) (string, error) { base := p.Base if base == "" { return "", nil } if scope.From != nil && base == trunkOf(scope.From.Source) { return "", nil } var controller *inventory.Entry for i, e := range entries { if e.Manifest.Module == "mesh-controller" && !e.Provided && e.Source.Repository != "" { controller = &entries[i] break } } if controller == nil || base == trunkOf(controller.Source) { return "", nil } if !tryRef.MatchString(base) || strings.Contains(base, "..") { return "", nil // no branch the forge could hold: checked as before } owner, repo := sourceOwner(controller.Source.Repository), sourceName(controller.Source.Repository) if owner == "" || repo == "" { return "", nil } has, err := forgeHasBranch(ctx, owner, repo, base) if err != nil { return "", fmt.Errorf("whether %s/%s#%d merges into a release branch cannot be told — the forge did not say "+ "whether %s/%s has a branch %s (novox/hq ADR 0358): %w", p.Owner, p.Repo, p.Number, owner, repo, base, err) } if !has { return "", nil } return base, nil } // trunkOf is the branch a module's source follows: main when it records none, or records a commit. func trunkOf(s inventory.Source) string { if t := strings.TrimSpace(followedBranch(s.Ref)); t != "" { return t } 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] } // forgeHasBranch asks the forge, through its module's tools on the bus, whether a repository has a branch. A // variable so a test needs no forge. var forgeHasBranch = func(ctx context.Context, owner, repo, branch string) (bool, error) { js, err := aBus() if err != nil { return false, err } defer js.Close() raw, _ := json.Marshal(map[string]any{"path": "/repos/" + owner + "/" + repo + "/branches/" + branch}) answer, err := link.Ask(ctx, link.OverNATS{Conn: js.Conn()}, "gitea", "gitea_api", raw, 30*time.Second) if err != nil { return false, err } return branchAnswered(answer, branch) } // branchAnswered reads the forge's answer about a branch: a 404 is "no such branch"; an answer naming the branch is // "it has it"; anything else — another error, an unreadable answer, one naming no branch or another — is an error, // never read as "no release branch". func branchAnswered(answer link.Answer, branch string) (bool, error) { if answer.Error != "" { if strings.Contains(answer.Error, " 404 ") { return false, nil } return false, errors.New(answer.Error) } var found struct { Result struct { Name string `json:"name"` } `json:"result"` } if err := json.Unmarshal(answer.Result, &found); err != nil { return false, fmt.Errorf("the forge's answer about branch %s cannot be read: %w", branch, err) } if found.Result.Name != branch { return false, fmt.Errorf("the forge answered about branch %s naming %q", branch, found.Result.Name) } return true, nil } // 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 // read, and the check builds, as before. func namedForTheCheck(ctx context.Context, p link.PullUpdated, builds []link.CheckBuild) []link.CheckedBuild { if len(builds) == 0 || p.Commit == "" { return nil } pull := link.PullRef{Owner: p.Owner, Repo: p.Repo, Number: p.Number} read, err := checkedBuildsOf(ctx, []link.PullRef{pull}) if err != nil { fmt.Printf(" the earlier checks' builds of %s/%s#%d could not be read, so the check builds: %v\n", p.Owner, p.Repo, p.Number, err) // empty-on-error: said above; a check handed no named build builds every module, as before issue 703 return nil } var named []link.CheckedBuild for _, b := range read { if b.Pull == pull && link.SameHead(b.Head, p.Commit) { named = append(named, b) } } return named } // checkBuildsOf are the modules a merge of the change would build, in the walk's order, as the check builds them // for the walk to take (novox/hq ADR 0350 §1): a module built from the repository checked, at the branch it merges // into, from the merge the check makes; any other — a module standing on one — from its own repository at the // branch it follows, as the walk builds it. A module whose build reads the repository checked through a context, // or that the merge deletes, cannot be built as the walk would: said, and nothing after it is built. A change that // moves no module builds nothing. func checkBuildsOf(p link.PullUpdated, scope checkScope, entries []inventory.Entry, clone func(inventory.Source) (string, error)) []link.CheckBuild { if len(scope.Reach.Plan.Tiers) == 0 { return nil } m := link.SourceMoved{Owner: p.Owner, Repo: p.Repo, Base: p.Base, CloneURL: p.CloneURL} byName := map[string]inventory.Entry{} for _, e := range entries { byName[e.Manifest.Module] = e } skip := map[string]string{} for _, e := range scope.Reach.Packaging { skip[e.Manifest.Module] = "its build reads " + p.Owner + "/" + p.Repo + " through a context, which a check " + "cannot hand the merge" } for _, e := range scope.Reach.Deleted { skip[e.Manifest.Module] = "the merge deletes it" } var out []link.CheckBuild for tier, names := range scope.Reach.Plan.Tiers { for _, name := range names { e, known := byName[name] b := link.CheckBuild{Module: name, Tier: tier, Skip: skip[name]} switch { case !known: b.Skip = "the catalogue does not hold it" case b.Skip != "": case sourceIs(e.Source, m): b.Merged, b.Path = true, e.Source.Path default: url, err := clone(e.Source) if err != nil { b.Skip = "its repository cannot be cloned: " + err.Error() break } b.Repository, b.Path, b.Ref = url, e.Source.Path, followedBranch(e.Source.Ref) } out = append(out, b) } } return out } // besideRefs is the ref each repository is cloned at beside a check, by the directory it is found under: // a core repository at the commit the mesh runs of the module built from it (`running`) — but the // catalogue, whose checkout beside is what tests read its files from, at its main, what the next merge // builds from; and beside the controller its main, for a judge the running controller predates, and the // lab's main, whose replays every check runs. **One rule, read by the check the controller asks for and by // the facts snapshot** (Facts.Beside), so a check run by hand clones what the build seat clones. Beside the // controller also the SDK, checked out at the commit the running controller's go.mod pins (sdkPinned), not // one a desktop holds (novox/hq issue 449). The checkout only places the clone: the conformance test reads the // fixtures at the pin of the tree under check, from the clone's history, so a pull request moving the SDK is // judged against the SDK it moves to (internal/link/conformance_test.go). func besideRefs(dir, running string) map[string]string { switch dir { case "mesh-catalog": return map[string]string{dir: "main"} case "mesh-controller": return map[string]string{dir: running, "mesh-controller-main": "main", "mesh-lab": "main", "mesh-sdk": sdkPinned()} } return map[string]string{dir: running} } // sdkModule is the Go module of the SDK the controller is built against. const sdkModule = "git.novox.be/novox/mesh-sdk/go" // pseudoCommit is the commit a Go pseudo-version names: v0.1.11-0.20261009143344-f047d0a4a970 → f047d0a4a970. var pseudoCommit = regexp.MustCompile(`-([0-9a-f]{12})$`) // sdkPinned is the ref of the SDK repository this controller was built from, as its go.mod pins it and its // build records it: a pseudo-version's commit, or a release's tag (the SDK tags its Go module under go/). // "main" only for a binary that records no SDK version — a local replace — which a running controller is // not (novox/hq issue 449). func sdkPinned() string { info, ok := debug.ReadBuildInfo() if !ok { return "main" } for _, dep := range info.Deps { if dep.Path != sdkModule { continue } if dep.Replace != nil { dep = dep.Replace } if m := pseudoCommit.FindStringSubmatch(dep.Version); m != nil { return m[1] } if strings.HasPrefix(dep.Version, "v") { return "go/" + dep.Version } } return "main" } // siblingOf is another repository of the same owner: novox/mesh-controller → novox/mesh-lab. func siblingOf(repository, name string) string { if cut := strings.LastIndex(repository, "/"); cut >= 0 { return repository[:cut+1] + name } return name } // sourceOnSeat is a source's seat form, nil for one on no seat. func sourceOnSeat(s inventory.Source) *link.SourceOnSeat { if s.Seat == "" { return nil } return &link.SourceOnSeat{Seat: s.Seat, Repository: s.Repository} } // checkEvents is where the serving controller says a check's verdict; nil in a command. var checkEvents link.Bus // maxCheckReport is how much of a check's report travels in its verdict: enough for the failures and // the machines, never a log. const maxCheckReport = 60 << 10 // checked says a merge check's verdict as the controller's `checked`. Nothing is recorded or // registered: a check builds nothing (issue 240's rule for a dry run, kept for a check). func checked(ctx context.Context, result link.BuildResult) { sayChecked(ctx, checkedOf(result)) } // checkedOf is what a check's outcome says: each layer, and an error — never a pass — for a check that // could not run. func checkedOf(result link.BuildResult) link.Checked { c := link.Checked{ID: result.ID, On: result.On, Commit: result.Ref} if result.Checked != nil { c.Owner, c.Repo, c.Number = result.Checked.Owner, result.Checked.Repo, result.Checked.Number } switch { case result.Check != nil: c.Verdict, c.Summary, c.Report = result.Check.Verdict, result.Check.Summary, result.Check.Report c.Gate, c.RepoCheck = result.Check.Gate, result.Check.RepoCheck // What was judged (novox/hq ADR 0323 §3): the head merged into the trunk commit, and the commits of the // repositories read beside it. c.Trunk, c.Beside, c.Took = result.Check.Trunk, result.Check.Beside, result.Check.Took c.Built = result.Check.Built c.Release, c.JudgedBy, c.BesideAt = result.Check.Release, result.Check.JudgedBy, result.Check.BesideAt c.Validator = result.Check.Validator if c.Gate == nil { // A build seat from before the layers: its verdict is the gate's. c.Gate = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary} } case result.Failed != "": // The check could not run: an error, never read as a pass — on both layers it was asked for. c.Verdict, c.Summary = "error", "the check could not run: "+firstLine(result.Failed) default: c.Verdict, c.Summary = "error", "the build seat answered the check with no verdict" } if c.Verdict == "" { c.Verdict = "error" } if result.Check == nil { c.Gate = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary} c.RepoCheck = &link.CheckLayer{Verdict: c.Verdict, Summary: c.Summary} } if result.Checked != nil && c.Gate != nil && len(c.Gate.Modules) == 0 { c.Gate.Modules = append(append([]string{}, result.Checked.Modules...), prefixedAll("new:", result.Checked.New)...) } if result.Checked != nil && c.Gate != nil && len(c.Gate.Dependents) == 0 { c.Gate.Dependents = result.Checked.Dependents } if result.Checked != nil { c.Plan = result.Checked.Plan if c.Plan != nil && c.Trunk != "" { // The kept delivery plan records the trunk commit with its other inputs (ADR 0323 §3). plan := *c.Plan plan.Trunk = c.Trunk c.Plan = &plan } // A delivery group's composed check (novox/hq ADR 0239): every head it judged, this one first. if g := result.Checked; g.Group != "" { c.Group = g.Group c.Members = append(c.Members, link.CheckedMember{Owner: g.Owner, Repo: g.Repo, Number: g.Number, Commit: result.Ref, Trunk: c.Trunk}) for _, m := range g.Members { c.Members = append(c.Members, link.CheckedMember{Owner: m.Owner, Repo: m.Repo, Number: m.Number, Commit: m.Ref, Trunk: memberTrunk(result.Check, m)}) } } } for _, l := range []*link.CheckLayer{c.Gate, c.RepoCheck} { if l != nil && l.Verdict == "" { l.Verdict = "error" } } // **A check run again says so** (novox/hq issue 594). The summaries are the lines the pull request // shows, clipped by the forge to a few words, so they get only a short marker after the verdict's own // words; the report gets the whole sentence. Layers copied, so the build's own outcome is left as it came. if mark := link.RunMarker(result.Delivery); mark != "" { c.Summary += " " + mark for _, l := range []**link.CheckLayer{&c.Gate, &c.RepoCheck} { if *l != nil { layer := **l layer.Summary += " " + mark *l = &layer } } c.Report = link.Restarted(result.Delivery) + "\n\n" + c.Report } if len(c.Report) > maxCheckReport { c.Report = "…" + c.Report[len(c.Report)-maxCheckReport:] } return c } // memberTrunk is the trunk commit the build seat merged one of a group's heads into (novox/hq ADR 0323 §6), empty // when it said none. func memberTrunk(outcome *link.CheckOutcome, m link.GroupMember) string { if outcome == nil { return "" } for _, j := range outcome.Members { if strings.EqualFold(j.Owner, m.Owner) && strings.EqualFold(j.Repo, m.Repo) && strings.HasPrefix(m.Ref, j.Commit) { return j.Trunk } } return "" } func prefixedAll(prefix string, items []string) []string { out := make([]string, 0, len(items)) for _, i := range items { out = append(out, prefix+i) } return out } // sayChecked says a merge check's verdict on the bus, where the forge's holder hears it. func sayChecked(ctx context.Context, c link.Checked) { repo := "none" if c.RepoCheck != nil { repo = strings.ToUpper(c.RepoCheck.Verdict) + " — " + c.RepoCheck.Summary } judged := "" if c.Trunk != "" { judged = " merged into trunk " + c.Trunk } fmt.Printf("%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) if checkEvents == nil { return } body, err := json.Marshal(c) if err != nil { return } stating, stop := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second) defer stop() if err := checkEvents.PublishSeatEvent(stating, link.MeshControllerSeat, link.KeyChecked, body); err != nil { fmt.Printf("%s: the verdict could not be said, so the pull request is not told it: %v\n", c.ID, err) } }