U2c: a package left unpublished by a build that reserved no version counts as done

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
This commit is contained in:
2026-11-12 16:15:41 +01:00
parent e313248b5f
commit 9fdd8a76c2
3 changed files with 30 additions and 12 deletions
@@ -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;
@@ -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)
}
}
}
@@ -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