package main import ( "context" "io" "os" "path" "regexp" "runtime/debug" "slices" "sort" "strings" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // What a pull request reaches of the mesh's graph (novox/hq to-be 45 §9, issue 280). The controller no longer checks a // pull request or says a verdict on it (R4g, ADR 0503 §3, ADR 0501 §5): the checker judges the heads, and mesh-delivery // asks the operator's Approve on its verdict. What stays here is the mapping of a pull request's changed paths onto the // mesh's module graph by the planner's own answer (reachOfMerge, touchedBy), which the delivery verbs (`delivery order`, // the change plan) and the head's declared config (tellPullDeclares) read: **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 — and a directory the change adds a module.json in is a new module. // 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 // Unread the changed files no module's build reads. 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 // 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, 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)) } 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 hears a pull request's new head. It asks no check and says no verdict (pullUpdated). func (f following) PullUpdated(ctx context.Context, p link.PullUpdated) error { return f.pullUpdated(ctx, p, os.Stdout) } // pullUpdated is PullUpdated saying what it did to w. It asks nothing of anybody and says no verdict (R4g; ADR 0503 §3, §5, // ADR 0527): the checker judges every repository whose trunk requires `mesh/check`, hq included. All it still does is tell // mesh-config what the head declares (ADR 0502), whatever judges it. func (f following) pullUpdated(ctx context.Context, p link.PullUpdated, w io.Writer) 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) this := pullAtOf(p) if this.Truncated { this.Paths, this.Truncated = truncatedPaths(entries, p.Owner, p.Repo, scope.Modules), false } if scope.gated() || holdsModules(entries, p.Owner, p.Repo) { tellPullDeclares(ctx, w, []pullAt{this}) } return nil } // 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" } // checkEvents is where the serving controller says a check's verdict; nil in a command. var checkEvents link.Bus