Ask build-agent for itself and its toolchain images, never a builder key (ADR 0520, issue 1851)

The builder writes no key of build-agent or of the toolchain bases its
builds stand on, so a walk moving one would hold its tier for ever.
Until R4g they are build-agent's own, as the builder's module is; the
bases are read from the toolchains, not copied.
This commit is contained in:
2026-10-22 21:06:33 +02:00
parent 93fc2f48a1
commit 9713ceb84d
3 changed files with 81 additions and 5 deletions
+45
View File
@@ -59,3 +59,48 @@ func TestAWalkAsksBuildAgentForTheBuildersOwnModuleAlone(t *testing.T) {
t.Fatalf("the other module is not taken from its key: %+v", s)
}
}
// ADR 0520 (issue 1851): until R4g build-agent and the toolchain images its builds stand on are build-agent's own, as the
// builder's module is: the builder writes no key of them, so a walk asks build-agent for them and reads no key; any other
// module of the same tier is still taken from its key.
func TestAWalkAsksBuildAgentForItselfAndItsToolchainsAndReadsNoKeyOfThem(t *testing.T) {
own := []string{"build-agent", "mesh-tools", "mesh-tools-go", "mesh-tools-python"}
wasModule := askedModuleOf
askedModuleOf = func(_ context.Context, _ buildSource, path string) (string, error) {
return strings.TrimPrefix(path, "modules/"), nil
}
t.Cleanup(func() { askedModuleOf = wasModule })
for _, m := range own {
if err := onlyTheBuildersOwn(t.Context(), buildSource{Repository: "novox/x", Seat: "git"}, "modules/"+m); err != nil {
t.Fatalf("an ask of %s is refused: %v", m, err)
}
}
var asked []string
was := askABuild
askABuild = func(_ context.Context, _ buildSource, path, _ string) (string, error) {
asked = append(asked, path)
return "build-" + strings.TrimPrefix(path, "modules/"), nil
}
t.Cleanup(func() { askABuild = was })
p, byName := aWalkOfOne("sudo")
p.Tiers = [][]string{append(append([]string{}, own...), "sudo")}
for _, m := range own {
e := byName["sudo"]
e.Source.Path = "modules/" + m
byName[m] = e
}
for _, m := range p.Tiers[0] {
askModule(t.Context(), p, m, byName)
}
if strings.Join(asked, " ") != "modules/build-agent modules/mesh-tools modules/mesh-tools-go modules/mesh-tools-python" {
t.Fatalf("build-agent was asked for %v", asked)
}
for _, m := range own {
if s := p.Modules[m]; s.Key || s.Build != "build-"+m || s.State != "asked" {
t.Fatalf("%s is not asked of build-agent, or is read from a key: %+v", m, s)
}
}
if s := p.Modules["sudo"]; !s.Key || s.Build != "" {
t.Fatalf("the other module is not taken from its key: %+v", s)
}
}
+18 -5
View File
@@ -8,6 +8,8 @@ import (
"time"
sdkdelivery "git.novox.be/novox/mesh-sdk/go/delivery"
"github.com/novox/mesh-controller/internal/builder"
"github.com/novox/mesh-controller/internal/builderkeys"
"github.com/novox/mesh-controller/internal/inventory"
)
@@ -22,13 +24,24 @@ import (
// yet, or waiting for a reference, holds the tier and says so; a failed key fails the walk in the builder's own words.
// **No timer**: the builder's and the indexer's writes wake the look (builderkeys.Watch, watchNewModules).
// isBuildersOwn says a module is the builder's own: the one module build-agent still builds, asked on a merge, until R4g
// (ADR 0453 §5, ADR 0498 §7).
// isBuildersOwn says a module is build-agent's own: asked of build-agent on a merge and taken from its build record, never
// from the builder's key, until R4g (ADR 0453 §5, ADR 0498 §7, ADR 0520). They are the builder's module, build-agent itself
// (its image's context is another repository's) and the toolchain images build-agent's builds stand on
// (builder.Bases): the builder writes no key of any of them, and a walk waiting for one would hold its tier for ever.
func isBuildersOwn(module string) bool { return buildersOwn(module) }
// buildersOwn is isBuildersOwn's rule: a variable only so a test of the one ask left (its bus waits, its plan key) may
// name modules of its own as the builder's.
var buildersOwn = func(module string) bool { return module == sdkdelivery.BuilderModule }
// buildAgentsOwn are those modules: the builder's, build-agent's and every toolchain's base, read from the toolchains.
var buildAgentsOwn = func() map[string]bool {
own := map[string]bool{sdkdelivery.BuilderModule: true, builder.AgentModule: true}
for _, b := range builder.Bases() {
own[b] = true
}
return own
}()
// buildersOwn is isBuildersOwn's rule: a variable only so a test of the asks left (their bus waits, their plan key) may
// name modules of its own as build-agent's.
var buildersOwn = func(module string) bool { return buildAgentsOwn[module] }
// planKeyReads is what a walk reads of the indexer and the builder (builderkeys.Reader, or a test's).
type planKeyReads interface {
+18
View File
@@ -234,6 +234,24 @@ func spoken() string {
}
// Languages is every language the mesh can build, for anything that wants to say so.
// AgentModule is build-agent's own module: the machine's builder, whose image is built from another repository's context.
const AgentModule = "build-agent"
// Bases are the modules whose artifacts provide the toolchains' compilers (Toolchain.Base), each once, sorted: images
// build-agent's builds stand on.
func Bases() []string {
seen := map[string]bool{}
var out []string
for _, t := range toolchains {
if t.Base != "" && !seen[t.Base] {
seen[t.Base] = true
out = append(out, t.Base)
}
}
sort.Strings(out)
return out
}
func Languages() []string {
names := make([]string, 0, len(toolchains))
for _, t := range toolchains {