From 088f55f94cb742cb9ad7db74439e048af25c0eca Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:28:11 +0100 Subject: [PATCH] BL5 (watched-branch half): a build carries the branch and source revision of its catalog revision, and only the first watched branch reserves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An entry the builder takes from the catalog half carries its origin: the watched branch, the source revision of the catalog revision it came from and whether the branch is the first of the module's registration. A build of another branch reserves no version and is built and delivered by its fingerprint; a reservation of the first branch keeps the branch and the source revision for the publication. Builds: design 47 G §371, A-57 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-builder/catalog.go | 8 +++- .../internal/build/numbers.go | 4 +- .../internal/build/reserve_test.go | 38 +++++++++++++++- .../internal/build/run.go | 45 ++++++++++++++----- .../internal/versions/reserve.go | 12 ++++- 5 files changed, 89 insertions(+), 18 deletions(-) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go index 19515895..cf2693f4 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/catalog.go @@ -91,6 +91,12 @@ func (c *catalogHalf) Changed(ctx context.Context) func(branch, module string, e c.b.EntryGone(branch, first, module) return } + origin := build.Origin{Branch: branch, First: first} + if cat, ok := c.branches.Catalog(branch); ok { + if half, ok := cat.View(); ok { + origin.Source = half.Revision.Source + } + } var e meshcatalog.CatalogEntry if err := json.Unmarshal(entry, &e); err != nil || e.Module != module { c.logf("the catalog half of the branch %q holds what is not the entry of %s, left: %v", branch, module, err) @@ -99,7 +105,7 @@ func (c *catalogHalf) Changed(ctx context.Context) func(branch, module string, e if err := c.manifests.keep(e); err != nil { c.logf("%v", err) } - c.b.Entry(ctx, branch, first, e) + c.b.Entry(ctx, origin, e) c.mu.Lock() was := c.named[module] now := namesOf(e) diff --git a/modules/mesh-delivery-builder/internal/build/numbers.go b/modules/mesh-delivery-builder/internal/build/numbers.go index 26dd0fe5..1729a15a 100644 --- a/modules/mesh-delivery-builder/internal/build/numbers.go +++ b/modules/mesh-delivery-builder/internal/build/numbers.go @@ -172,12 +172,12 @@ func (b *Builder) waitFill(module string, m meshcatalog.ModulesByModuleValue) bo // reserve reserves the version of the build that starts; a reservation that cannot be made is said and the build goes on // unreserved, as an unnumbered build does. -func (b *Builder) reserve(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan) (versions.ReservedVersion, bool) { +func (b *Builder) reserve(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan, o Origin) (versions.ReservedVersion, bool) { refs := make([]string, 0, len(plan.Tasks)) for _, t := range plan.Tasks { refs = append(refs, module+":"+t.Artifact) } - src := versions.Source{Repository: m.Repository, Commit: m.Commit} + src := versions.Source{Repository: m.Repository, Commit: m.Commit, Branch: o.Branch, Revision: o.Source} for _, t := range plan.Tasks { src.GoPackage = src.GoPackage || (t.Kind == catalogue.ArtifactPackage && t.Language == "go") } diff --git a/modules/mesh-delivery-builder/internal/build/reserve_test.go b/modules/mesh-delivery-builder/internal/build/reserve_test.go index ae1627f1..c1b85a65 100644 --- a/modules/mesh-delivery-builder/internal/build/reserve_test.go +++ b/modules/mesh-delivery-builder/internal/build/reserve_test.go @@ -3,11 +3,13 @@ package build import ( "context" "fmt" + "reflect" "slices" "strings" "testing" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "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" @@ -21,12 +23,13 @@ type fakeReservations struct { reserved [][]string series []string built map[string]string + sources []versions.Source } func (f *fakeReservations) Filled() bool { return f.filled } -func (f *fakeReservations) Reserve(_ context.Context, module, series string, artifacts []string, _ versions.Source) (versions.ReservedVersion, error) { - f.reserved, f.series = append(f.reserved, artifacts), append(f.series, series) +func (f *fakeReservations) Reserve(_ context.Context, module, series string, artifacts []string, src versions.Source) (versions.ReservedVersion, error) { + f.reserved, f.series, f.sources = append(f.reserved, artifacts), append(f.series, series), append(f.sources, src) return versions.ReservedVersion{Module: module, Series: series, Version: series + ".5"}, nil } @@ -127,3 +130,34 @@ func TestADropOfAReservationIsAReasksValueOfItsOwnKey(t *testing.T) { } } } + +// TestOnlyABuildOfTheFirstWatchedBranchReservesAndItCarriesItsCatalogRevision: design 47 G §371, A-57 — a build of the registration's +// first watched branch reserves a version and the reservation carries the branch and the source revision of the catalog revision its +// entry came from; a build of another watched branch reserves none and is built and done. +func TestOnlyABuildOfTheFirstWatchedBranchReservesAndItCarriesItsCatalogRevision(t *testing.T) { + for _, c := range []struct { + name string + origin Origin + want []versions.Source + }{ + {"the first branch", Origin{Branch: "main", Source: 41, First: true}, + []versions.Source{{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Branch: "main", Revision: 41}}}, + {"another branch", Origin{Branch: "beta", Source: 42}, nil}, + } { + t.Run(c.name, func(t *testing.T) { + r := newRig(t, goBundle) + r.mf.byDigest[mdigest('a')] = catalogue.Manifest{Module: "m", Version: "0.1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle}}} + res := &fakeReservations{filled: true} + r.b.Reserved = res + r.b.Entry(context.Background(), c.origin, meshcatalog.CatalogEntry{Module: "m", Repository: "novox/mesh-catalog", Path: "modules/m", + Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: fp1, ManifestDigest: mdigest('a'), Inputs: &meshcatalog.EntryInputs{Toolchains: CatalogToolchains(agreedToolchains)}}) + r.b.Wait() + if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone { + t.Fatalf("build = %+v, want done", v) + } + if !reflect.DeepEqual(res.sources, c.want) { + t.Errorf("reserved from %+v, want %+v", res.sources, c.want) + } + }) + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index f6f5574b..75bc44e7 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -119,6 +119,7 @@ type Builder struct { fillWaits sync.Map known map[string]meshcatalog.ModulesByModuleValue // by modules key: the first watched branch's heard map[string]meshcatalog.ModulesByModuleValue // by branch and module (take): what supersedes what + origin map[string]Origin // by build key: the catalog revision its entry came from (take) inflight map[string]bool // by build key again map[string]meshcatalog.ModulesByModuleValue // by build key: the newest value that arrived while the key was being built plans map[string]planned // by manifest digest @@ -171,6 +172,7 @@ func (b *Builder) init() { b.sem = make(chan struct{}, limit) b.cands, b.via, b.trunkFP = map[string]meshcatalog.CandidatesByGroupValue{}, map[string]via{}, map[string]bool{} b.heard, b.known = map[string]meshcatalog.ModulesByModuleValue{}, map[string]meshcatalog.ModulesByModuleValue{} + b.origin = map[string]Origin{} b.inflight, b.again, b.plans, b.tried = map[string]bool{}, map[string]meshcatalog.ModulesByModuleValue{}, map[string]planned{}, map[string]bool{} b.published, b.held, b.waiting = map[string]bool{}, map[string]bool{}, map[string]map[string]chan string{} b.hands = map[string]*handWatch{} @@ -198,20 +200,28 @@ func (b *Builder) Module(ctx context.Context, key string, value []byte) { b.logf("modules.%s holds what is not a module, left: %v", key, err) return } - b.take(ctx, "", true, key, m) + b.take(ctx, Origin{First: true}, key, m) +} + +// Origin is the catalog revision an entry came from (design 47 G §371, A-57): the watched branch it is of, as named, the source +// revision the revision was taken in from, and whether the branch is the first of the module's registration, the only one whose +// builds reserve a version and whose entry is the module's value for what the builder reads by name (a version, a build again). +type Origin struct { + Branch string + Source int64 + First bool } // Entry takes the catalog graph entry of one module on one watched branch in (design 47 G §348, §614) and builds its -// fingerprint if it is to be built. first says the branch is the first of the module's registration, whose entry is the module's -// value for what the builder reads by name (a version, a build again); an entry of another branch builds its fingerprint and +// fingerprint if it is to be built. An entry of a branch other than the first builds its fingerprint, reserves none, and // supersedes only that branch's older one. An entry naming no module is said and left. -func (b *Builder) Entry(ctx context.Context, branch string, first bool, e meshcatalog.CatalogEntry) { +func (b *Builder) Entry(ctx context.Context, o Origin, e meshcatalog.CatalogEntry) { b.init() if e.Module == "" { - b.logf("the catalog half of the branch %q holds an entry that names no module, left", branch) + b.logf("the catalog half of the branch %q holds an entry that names no module, left", o.Branch) return } - b.take(ctx, branch, first, e.Module, ModuleOf(e)) + b.take(ctx, o, e.Module, ModuleOf(e)) } // EntryGone takes a module's entry out of a branch's half (the module was dropped from it, or the branch is followed no more). @@ -225,12 +235,12 @@ func (b *Builder) EntryGone(branch string, first bool, module string) { } } -func (b *Builder) take(ctx context.Context, branch string, first bool, key string, m meshcatalog.ModulesByModuleValue) { +func (b *Builder) take(ctx context.Context, o Origin, key string, m meshcatalog.ModulesByModuleValue) { b.mu.Lock() - hk := branch + "\x00" + key + hk := o.Branch + "\x00" + key old, had := b.heard[hk] b.heard[hk] = m - if first { + if o.First { b.known[key] = m } var superseded context.CancelCauseFunc @@ -242,6 +252,7 @@ func (b *Builder) take(ctx context.Context, branch string, first bool, key strin if bk, err := builds.BuilderBuildKey(key, m.Fp2); err == nil && m.Fp2 != "" { // A fingerprint a watched branch carries is built from that branch's commit, whoever asked for it first. b.trunkFP[bk] = true + b.origin[bk] = o delete(b.via, bk) } b.mu.Unlock() @@ -279,6 +290,17 @@ func ModuleOf(e meshcatalog.CatalogEntry) meshcatalog.ModulesByModuleValue { return m } +// originOf is the catalog revision the entry of a build key came from; a key no entry named is of the first branch, as a build +// of the trunk's value was before branches were watched. +func (b *Builder) originOf(key string) Origin { + b.mu.Lock() + defer b.mu.Unlock() + if o, ok := b.origin[key]; ok { + return o + } + return Origin{First: true} +} + // NodesChanged is Reconsider after a toolchain moved, so a build that waited for the nodes to agree on one may go on. func (b *Builder) NodesChanged(ctx context.Context) { b.Reconsider(ctx) } @@ -583,7 +605,8 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB return // a package member of the group is not built (design 47 G §373): its end looks at this key again } } - reserving := trunk && planErr == nil && !nothing && b.reserves(ctx, module, m, plan) + // Only a build of the registration's first watched branch reserves and releases a version (design 47 G §371). + reserving := trunk && b.originOf(key).First && planErr == nil && !nothing && b.reserves(ctx, module, m, plan) if reserving && b.waitFill(module, m) { return } @@ -624,7 +647,7 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB var reserved versions.ReservedVersion hasReserved := false if reserving && cl.Build.State != builds.BuildDone { - reserved, hasReserved = b.reserve(ctx, module, m, plan) + reserved, hasReserved = b.reserve(ctx, module, m, plan, b.originOf(key)) } var result builds.BuilderBuild if numbering.Refused != "" && cl.Build.State != builds.BuildDone { diff --git a/modules/mesh-delivery-builder/internal/versions/reserve.go b/modules/mesh-delivery-builder/internal/versions/reserve.go index 9e190a6f..3db6a5e6 100644 --- a/modules/mesh-delivery-builder/internal/versions/reserve.go +++ b/modules/mesh-delivery-builder/internal/versions/reserve.go @@ -54,7 +54,11 @@ type ReservedVersion struct { // package's, GoTagOf): empty for a module with no Go package (design 47 G §370). Repository string `json:"repository,omitempty"` Commit string `json:"commit,omitempty"` - Tag string `json:"tag,omitempty"` + // Branch and Source are the watched branch and the source revision of the catalog revision whose entry the build came from + // (design 47 G §371, A-57): what `package-published` says of the publication. + Branch string `json:"branch,omitempty"` + Source int64 `json:"source,omitempty"` + Tag string `json:"tag,omitempty"` } // Source is what a build reserving a version is made from (design 47 G §370): the repository and commit the git tag is @@ -62,6 +66,10 @@ type ReservedVersion struct { type Source struct { Repository, Commit string GoPackage bool + // Branch is the watched branch of the catalog revision whose entry the build came from, and Revision the source revision of + // that catalog revision (design 47 G §371, A-57), kept on the reservation for the publication. + Branch string + Revision int64 } // ReservedKey is the key of module's reservation of series (MAJOR.MINOR). @@ -136,7 +144,7 @@ func (r *Reserver) Reserve(ctx context.Context, module, series string, artifacts } } out := ReservedVersion{V: 1, Module: module, Series: series, Version: fmt.Sprintf("%s.%d", series, next+1), Artifacts: map[string]string{}, At: r.now(), - Repository: src.Repository, Commit: src.Commit} + Repository: src.Repository, Commit: src.Commit, Branch: src.Branch, Source: src.Revision} if src.GoPackage { out.Tag = GoTagOf(out.Version) }