BL3: the build seat's ask carries the version the build reserved, empty for a build reserving none

Builds: design 47 G §573, §370

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 20:28:23 +01:00
parent 7ecae5a6dd
commit 848c2b035e
2 changed files with 23 additions and 4 deletions
@@ -9,6 +9,7 @@ import (
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-delivery-builder/internal/versions"
@@ -65,7 +66,16 @@ func TestABuildReservesItsVersionAtItsStartOnceTheCatalogFilled(t *testing.T) {
}
res.filled = true
// Design 47 G §573: the build's ask carries the reserved version.
var askedVersions []string
r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
askedVersions = append(askedVersions, a.Version)
return r.answer(a)
}
v := r.consider(t, "modules/m")
if len(askedVersions) == 0 || slices.ContainsFunc(askedVersions, func(v string) bool { return v != "0.1.5" }) {
t.Fatalf("the asks carried versions %q, want the reserved 0.1.5 on each", askedVersions)
}
if v.State != builds.BuildDone || len(res.reserved) != 1 || !slices.Equal(res.reserved[0], []string{"m:tool"}) || res.series[0] != "0.1" {
t.Fatalf("build %+v reserved %v in %v, want done and one reservation of m:tool in 0.1", v.State, res.reserved, res.series)
}
@@ -81,11 +91,19 @@ func TestABuildReservesItsVersionAtItsStartOnceTheCatalogFilled(t *testing.T) {
r2 := newRig(t, goBundle)
res2 := &fakeReservations{filled: true}
r2.b.Reserved = res2
var unversioned []string
r2.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
unversioned = append(unversioned, a.Version)
return r2.answer(a)
}
var journal []string
r2.b.Logf = func(f string, a ...any) { journal = append(journal, fmt.Sprintf(f, a...)) }
if v := r2.consider(t, "modules/m"); v.State != builds.BuildDone || len(res2.reserved) != 0 {
t.Errorf("a manifest with no MAJOR.MINOR: state %s, reserved %v", v.State, res2.reserved)
}
if len(unversioned) == 0 || slices.ContainsFunc(unversioned, func(v string) bool { return v != "" }) {
t.Errorf("a build reserving none asked with versions %q, want none", unversioned)
}
// G §505: the builder writes in its journal, naming the module, that it reserved nothing.
if !slices.ContainsFunc(journal, func(l string) bool { return strings.HasPrefix(l, "m: ") && strings.Contains(l, "not MAJOR.MINOR") }) {
t.Errorf("journal %q, want a line naming m and saying its version is not MAJOR.MINOR", journal)
@@ -552,7 +552,7 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB
b.mu.Lock()
b.builds[key] = withdraw
b.mu.Unlock()
result = b.build(run, module, m, plan, planErr, prior, only, b.notes(cl))
result = b.build(run, module, m, plan, planErr, prior, only, b.notes(cl), reserved.Version)
b.mu.Lock()
delete(b.builds, key)
b.mu.Unlock()
@@ -646,6 +646,7 @@ func failed(format string, args ...any) builds.BuilderBuild {
// lane is the build of a plan's tasks of one build module on one node.
type lane struct {
module, node string // node is the one that pulled the lane's ask
version string // the version the build reserved, MAJOR.MINOR.PATCH, empty for a build that reserves none (design 47 G §573)
tasks []Task
toolchain *builds.ToolchainUsed
digests map[string]string // artifact -> digest
@@ -656,7 +657,7 @@ type lane struct {
err error
}
func (b *Builder) build(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, planErr error, prior *builds.BuilderBuild, only map[string]bool, asked *askNotes) builds.BuilderBuild {
func (b *Builder) build(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, planErr error, prior *builds.BuilderBuild, only map[string]bool, asked *askNotes, version string) builds.BuilderBuild {
switch {
case planErr != nil:
return failed("%s", planErr)
@@ -697,7 +698,7 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module
}
}
// One ask builds each fingerprint's part of a build module, on whichever node pulls it (ADR 0525 §3).
lanes = append(lanes, &lane{module: bm, tasks: tasks, toolchain: toolchain, digests: map[string]string{}, references: map[string]string{}})
lanes = append(lanes, &lane{module: bm, version: version, tasks: tasks, toolchain: toolchain, digests: map[string]string{}, references: map[string]string{}})
}
if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil {
b.handLanes(key, len(lanes))
@@ -952,7 +953,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes
if needsFetch(l.tasks) {
job.Fetch = &req
}
ask := sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: kind, Language: language, Asker: builds.BuilderModule, At: time.Now(), Build: job}
ask := sdkstep.StepAsk{ID: sdkstep.NewID(), Kind: kind, Language: language, Asker: builds.BuilderModule, At: time.Now(), Version: l.version, Build: job}
if language == sdkstep.AskGo && job.Fetch != nil && len(m.Packages) > 0 {
// The versions of the mesh packages the fingerprint was computed with (novox/hq ADR 0573 §2): the build module writes
// them into the workspace, whatever the repository's go.mod names.