189 lines
8.0 KiB
Go
189 lines
8.0 KiB
Go
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
|