From 9fdd8a76c295caee758a2ec7e7a6e61e0ee771ec Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 16:15:41 +0100 Subject: [PATCH] U2c: a package left unpublished by a build that reserved no version counts as done MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The builder's unpublished count held every package without a reference and named the Node build module for all of them, so a Go package built by a build reserving none (another watched branch, a manifest with no MAJOR.MINOR, a candidate) left its key never done and was sent to the wrong seat. A reserving build's package is published under its version or its build fails, so the count now holds images alone. Builds: design 47 G A-289, §371 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../internal/build/claim.go | 18 ++++++--------- .../internal/build/claim_test.go | 22 +++++++++++++++++++ .../internal/build/run.go | 2 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/modules/mesh-delivery-builder/internal/build/claim.go b/modules/mesh-delivery-builder/internal/build/claim.go index 2a7aa006..bfda68d4 100644 --- a/modules/mesh-delivery-builder/internal/build/claim.go +++ b/modules/mesh-delivery-builder/internal/build/claim.go @@ -96,22 +96,22 @@ func GoneStep(gone []string) string { return "claimed again: the artifact store no longer holds " + strings.Join(gone, ", ") } -// Unpublished is whether a done build holds an image built without a push or a package built without a publish: it has -// no reference (a candidate's, ADR 0487 clause 12, ADR 0498 clauses 5 and 6). +// Unpublished is whether a done build holds an image built without a push: it has no reference (a candidate's, ADR 0487 +// clause 12). A package left unpublished is a build's that reserved no version, which counts as done (design 47 G A-289, +// §371): a reserving build's package is published under its version or its build fails. func Unpublished(b builds.BuilderBuild, seats *buildrun.BuildSeats) bool { return len(UnpublishedModules(b, seats)) > 0 } // UnpublishedModules are the build modules that publish what a done build holds unpublished: the image build module for -// an image, the Node build module for a package; sorted, none when it holds nothing unpublished. +// an image; none when it holds nothing unpublished, and none for a package (A-289: never one seat for every package). func UnpublishedModules(b builds.BuilderBuild, seats *buildrun.BuildSeats) []string { if b.State != builds.BuildDone { return nil } - var image, pkg bool + var image bool for _, a := range b.Artifacts { image = image || (a.Kind == catalogue.ArtifactImage && a.Reference == "") - pkg = pkg || (a.Kind == catalogue.ArtifactPackage && a.Reference == "") } var out []string if image { @@ -119,11 +119,6 @@ func UnpublishedModules(b builds.BuilderBuild, seats *buildrun.BuildSeats) []str out = append(out, bm.Module) } } - if pkg { - if bm, err := seats.For("typescript", catalogue.ArtifactPackage); err == nil { - out = append(out, bm.Module) - } - } sort.Strings(out) return out } @@ -159,7 +154,8 @@ func Unreferenced(b builds.BuilderBuild) bool { // - no key: it is created building, by this builder; a create that finds the key made meanwhile gets nothing; // - a done key is not taken again (the same source is never built twice), except one whose bundle, archive or upstream // copy names no reference (Unreferenced, a key done before R4e: ADR 0498 clause 8), which is claimed building as a new -// revision and built whole, and one whose image or package was built unpublished as a candidate's once the trunk +// revision and built whole, and one whose image was built unpublished as a candidate's once the trunk (a package left +// unpublished counts as done, A-289) // names it (ClaimOptions.Trunk): that is taken as it stands, with the done value, and its builder writes the new // revision when the publishing build ends (ADR 0498 clause 6). From R4e one node builds each fingerprint (ADR 0478 // clause 4), so a key built on one node is never built again for a second; diff --git a/modules/mesh-delivery-builder/internal/build/claim_test.go b/modules/mesh-delivery-builder/internal/build/claim_test.go index c553addd..ebbe4ec6 100644 --- a/modules/mesh-delivery-builder/internal/build/claim_test.go +++ b/modules/mesh-delivery-builder/internal/build/claim_test.go @@ -3,6 +3,7 @@ package build import ( "context" "errors" + "slices" "sync" "testing" @@ -234,3 +235,24 @@ func hex64(c byte) string { } return string(b) } + +// Design 47 G A-289 (§371): a done key's package left unpublished (a Go package or a TypeScript one, of a build that +// reserved no version) counts as done and is published by no build seat; an image built without a push still is not done +// and is published by the image build module. +func TestAnUnpublishedPackageCountsAsDoneAndAnUnpushedImageDoesNot(t *testing.T) { + for _, c := range []struct { + name string + art builds.BuiltArtifact + want []string + }{ + {"a Go package", builds.BuiltArtifact{Name: "go-overlay", Kind: "package", Digest: digest}, nil}, + {"a TypeScript package", builds.BuiltArtifact{Name: "lib", Kind: "package", Digest: digest}, nil}, + {"a published package", builds.BuiltArtifact{Name: "lib", Kind: "package", Digest: digest, Reference: "git.example/m/go@0.1.5"}, nil}, + {"an image built without a push", builds.BuiltArtifact{Name: "img", Kind: "image", Digest: digest}, []string{testImage}}, + } { + b := builds.BuilderBuild{State: builds.BuildDone, Artifacts: []builds.BuiltArtifact{c.art}} + if got := UnpublishedModules(b, testSeats()); !slices.Equal(got, c.want) || Unpublished(b, testSeats()) != (len(c.want) > 0) { + t.Errorf("%s: unpublished by %v, want %v", c.name, got, c.want) + } + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 23a56aa7..7f743f31 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -628,7 +628,7 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB // Said aloud (issue 1937): the key was done, and the store's collector deleted what it names. b.logf("%s: built again: the artifact store no longer holds %s", cl.Key, strings.Join(cl.Gone, ", ")) } - // A done key taken again builds only what is missing: the image or the package built unpublished as a candidate's; the + // A done key taken again builds only what is missing: the image built unpublished as a candidate's (A-289); the // other artifacts stay as the done key has them (ADR 0487 clauses 5 and 8, ADR 0498 clause 6). var prior *builds.BuilderBuild var only map[string]bool