Files
mesh-controller/internal/inventory/dependencies.go
T
jochen 6c616838a5 Move a module on a merge only when its build source holds a changed file (hq ADR 0267, issue 363)
Every merge to the controller's repository planned the controller, the
build seat's holder and the route proxy in three gated tiers, whatever it
changed (issue 338). The planner now maps a merge's files onto the build
source each module's newest trunk build said: a README moves nothing, the
controller's command the controller alone, the proxy's program the proxy
alone. A module with none said, or one an open plan has yet to build, is
read whole as before. Sharing a repository draws no packages edge any more,
and one recorded before neither widens nor orders a plan.
2026-10-10 03:20:36 +02:00

150 lines
5.3 KiB
Go

package inventory
import (
"context"
"sort"
"strings"
"github.com/novox/mesh-controller/internal/builder"
"github.com/novox/mesh-controller/internal/catalogue"
)
// The kinds of edge in the catalogue's one dependency relation (novox/hq ADR 0162).
const (
// EdgeStandsOn: the module's artifact is built on the other's.
EdgeStandsOn = "stands-on"
// EdgePackages: the module's build read the other's repository. **No longer drawn** (novox/hq ADR 0267
// rule 4): sharing a repository is no dependency, and a changed file moves every module whose build
// source holds it, directly. Kept so an edge recorded or snapshotted before is read, and then neither
// widens nor orders a plan.
EdgePackages = "packages"
// EdgeBuiltBy: the module is built by the holder of the build-machine seat.
EdgeBuiltBy = "built-by"
// EdgeDeclared: the manifest's own `build.on`.
EdgeDeclared = "declared"
// EdgeWorkerOf: the module holds the build seat, whose worker the control plane defines
// (novox/hq issue 206). The one place a *running* order enters the graph: a build machine rolled
// before the controller that redefines its worker cannot bind it, and nothing can then build the
// controller that would end that — so the holder of the build seat follows the controller, and
// the controller is built by whichever build machine is running, as it always was.
EdgeWorkerOf = "worker-of"
)
// TheControlPlane is the module that defines every seat's worker on the bus.
const TheControlPlane = "mesh-controller"
// Edge is one dependency: From depends on To, in the way Kind says.
type Edge struct {
From string `json:"from"`
To string `json:"to"`
Kind string `json:"kind"`
}
// Dependencies is the catalogue's dependency relation, whole: every module the mesh holds, with
// an edge to each module it depends on and the kind of dependency on the edge. One answer, so
// nothing else computes an edge (novox/hq ADR 0162) — the merge handler, `build --on` and the
// overview all read this.
//
// Three sources, one relation: a manifest's `build.on`; the artifacts the latest build was made
// against (an `artifact-store://<module>/…` reference is an edge to that module); and the build machine,
// which every source-built module is built by. The repositories a build read are no edge (novox/hq ADR
// 0267 rule 4).
func (i *Inventory) Dependencies(ctx context.Context) ([]Edge, error) {
entries, err := i.Catalogued(ctx)
if err != nil {
return nil, err
}
against, err := i.BuiltAgainst(ctx)
if err != nil {
return nil, err
}
return dependenciesOf(entries, against, nil), nil
}
// dependenciesOf is Dependencies over what was read, so a test can hand it a catalogue.
//
// `read` draws no edge (novox/hq ADR 0267 rule 4): what a build read moves it through its build source, in
// the planner, never through the relation.
func dependenciesOf(entries []Entry, against map[string][]string, _ map[string][]ReadRepository) []Edge {
known := map[string]bool{}
var builders []string
for _, e := range entries {
name := e.Manifest.Module
known[name] = true
if e.Manifest.ClaimsSeat("node-build-agent") || e.Manifest.ClaimsSeat("mesh-build-machine") {
builders = append(builders, name)
}
}
seen := map[Edge]bool{}
var out []Edge
add := func(from, to, kind string) {
if from == to || !known[to] {
return
}
e := Edge{From: from, To: to, Kind: kind}
if !seen[e] {
seen[e] = true
out = append(out, e)
}
}
for _, e := range entries {
name := e.Manifest.Module
if e.Manifest.Build != nil {
for _, on := range e.Manifest.Build.On {
if on.Module != "" {
add(name, on.Module, EdgeDeclared)
}
}
// **A bundle stands on the toolchain it is compiled in** (novox/hq 04-ISSUES/211). A
// manifest names its toolchain by language, not in `build.on`, so the edge was implicit
// and a merge that moved the toolchain and a bundle together built both in one tier —
// the bundle against the toolchain as it was, recorded as built from the new commit. Read
// from the manifest, so it holds before any build has recorded what it stood on; and a
// toolchain that moves rebuilds every bundle compiled in it, which is what a toolchain
// carrying a bundle's dependencies requires.
for _, a := range e.Manifest.Build.Artifacts {
if a.Kind != catalogue.ArtifactBundle {
continue
}
if chain, err := builder.ToolchainFor(a.Language); err == nil {
add(name, chain.Base, EdgeStandsOn)
}
}
}
for _, ref := range against[name] {
if rest, ok := strings.CutPrefix(ref, catalogue.ArtifactStoreScheme); ok {
// A copy of an upstream image is no module's artifact (novox/hq ADR 0257): its
// repository is named for the image, and its first segment names no module.
if strings.HasPrefix(rest, builder.MirrorPrefix) {
continue
}
if base, _, found := strings.Cut(rest, "/"); found {
add(name, base, EdgeStandsOn)
}
}
}
if e.Source.Repository != "" {
for _, b := range builders {
add(name, b, EdgeBuiltBy)
}
}
}
if known[TheControlPlane] {
for _, b := range builders {
if b != TheControlPlane {
add(b, TheControlPlane, EdgeWorkerOf)
}
}
}
sort.Slice(out, func(a, b int) bool {
if out[a].From != out[b].From {
return out[a].From < out[b].From
}
if out[a].To != out[b].To {
return out[a].To < out[b].To
}
return out[a].Kind < out[b].Kind
})
return out
}