From 8ea78f709a9bccea378a0cc65da4dcf095d6ae51 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:30:45 +0100 Subject: [PATCH] BL1: the whole package-published per artifact on the registry seat's published, from the reservation and the step result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a registry seat says published, the builder says the seat mesh-builds' package-published once per artifact the reservation newly marks: the artifact reference, version and digest published, the watched branch and the source revision kept on the reservation from the entry the build came from (A-57), and the dependencies of kind 1 (resolved) and 3 (toolchain) the step result named (§466, §673), their parts the artifact references the entry's edges name. The reservation keeps the dependencies from the build's end until the publication. Builds: design 47 G §52, §334, §349, §371, §466, §673, A-57 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- .../cmd/mesh-delivery-builder/main.go | 4 + .../cmd/mesh-delivery-builder/reserve.go | 19 +++- .../cmd/mesh-delivery-builder/reserve_test.go | 13 ++- .../internal/build/numbers.go | 6 +- .../internal/build/reserve_test.go | 45 +++++++- .../internal/build/run.go | 105 ++++++++++++++++-- .../internal/versions/reserve.go | 41 ++++++- .../internal/versions/reserve_test.go | 20 +++- 8 files changed, 223 insertions(+), 30 deletions(-) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index 6f2702b0..9a5c7818 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -209,6 +209,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro // and version-released heard; nothing is reserved before the fill. reservedKV := process.AwaitBucket(ctx, js, conn, versions.ReservedBucket, "no version is reserved: builds are made unreserved", logger.Printf) reserved := newReservations(awaitedReserved{awaitedVersions{b: reservedKV, bucket: versions.ReservedBucket}}, calls.Call, pubBoundsKV.KV, found, b.Reconsider, logger.Printf) + // Each artifact the holder says published at a reserved version is said as the seat mesh-builds' package-published (design 47 G §52). + reserved.say = func(ctx context.Context, p meshbuilds.PackagePublished) error { + return emitSeatEvent(context.WithoutCancel(ctx), js, cred, meshbuilds.SubjectPackagePublished, "package-published-"+p.Module+"-"+p.Version+"-"+p.Artifact, p) + } b.Reserved = reserved // A state bound later numbers the builds looked at without it. versionsKV.Ready(func(jetstream.KeyValue) { b.Reconsider(ctx) }) diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve.go index 7d286318..ee2812f6 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "strings" "sync/atomic" "time" @@ -48,6 +49,8 @@ type reservations struct { logf func(string, ...any) // call asks a seat's verb: the git seat's tag (design 47 G §370). call toolCaller + // say says package-published on the seat mesh-builds; nil says none. + say func(context.Context, meshbuilds.PackagePublished) error } // newReservations makes the reserving over store; the released versions of a module are asked of the seat mesh-catalog's @@ -82,9 +85,9 @@ func (rv *reservations) Reserve(ctx context.Context, module, series string, arti // Built records the digests; once every artifact has one, the publish's bound starts (a key of the state bounds with its // own time to live): its end with the reservation standing is version-unreleased. -func (rv *reservations) Built(ctx context.Context, module, version string, digests map[string]string) error { +func (rv *reservations) Built(ctx context.Context, module, version string, digests map[string]string, deps map[string][]meshbuilds.Dependency) error { for ref, d := range digests { - if _, err := rv.r.Built(ctx, module, version, ref, d); err != nil { + if _, err := rv.r.Built(ctx, module, version, ref, d, deps[ref]); err != nil { return err } } @@ -198,9 +201,19 @@ func (rv *reservations) heard(ctx context.Context, m jetstream.Msg) bool { // published marks the artifacts of the reservation a registry seat's published names (its version and digest). func (rv *reservations) published(ctx context.Context, o versions.Outcome) { - if _, err := rv.r.Published(ctx, o.Version, o.Digest); err != nil { + marked, err := rv.r.Published(ctx, o.Version, o.Digest) + if err != nil { rv.logf("a published of %s@%s is not recorded in the reservations: %v", o.Name, o.Version, err) } + // Each artifact the holder's published newly marked is said as the seat mesh-builds' package-published (design 47 G §52, §334). + for _, m := range marked { + if rv.say == nil { + continue + } + if err := rv.say(ctx, m.PackagePublished(o.Digest, time.Now())); err != nil { + rv.logf("the package-published of %s at %s is not said: %v", m.Artifact, m.Reserved.Version, err) + } + } } // dropped is builder_reserved_drop: the reservation is deleted and its bound ended. diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve_test.go index dca519a1..24a349b5 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve_test.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve_test.go @@ -11,6 +11,7 @@ import ( "git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshconditionsclient" "git.novox.be/novox/mesh-delivery/contracts/go/seats/git" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "mesh-delivery-builder/internal/versions" @@ -93,11 +94,13 @@ func TestTheCatalogsWordsMoveTheReservations(t *testing.T) { if !rv.heard(ctx, filled) || !filled.acked || !rv.Filled() || woke != 1 { t.Fatalf("versions-filled: acked %v, filled %v, woke %d", filled.acked, rv.Filled(), woke) } - res, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, versions.Source{}) + var said []meshbuilds.PackagePublished + rv.say = func(_ context.Context, p meshbuilds.PackagePublished) error { said = append(said, p); return nil } + res, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, versions.Source{Branch: "main", Revision: 41}) if err != nil || res.Version != "1.4.3" { t.Fatalf("reserved %q (%v), want 1.4.3 after the catalog's 1.4.2", res.Version, err) } - if err := rv.Built(ctx, "m", "1.4.3", map[string]string{"m:a": "sha256:a", "m:b": "sha256:b"}); err != nil { + if err := rv.Built(ctx, "m", "1.4.3", map[string]string{"m:a": "sha256:a", "m:b": "sha256:b"}, nil); err != nil { t.Fatal(err) } boundKey := versions.ReservedBoundKey("m", "1.4") @@ -105,6 +108,12 @@ func TestTheCatalogsWordsMoveTheReservations(t *testing.T) { t.Fatalf("the publish's bound is not armed: %v", bounds.keys) } rv.published(ctx, versions.Outcome{Name: "m-a", Version: "1.4.3", Digest: "sha256:a"}) + // Design 47 G §52, A-57: the holder's published is said once, per artifact, as the seat mesh-builds' package-published with the + // branch and source revision of the reservation; a second published of the same artifact says nothing. + rv.published(ctx, versions.Outcome{Name: "m-a", Version: "1.4.3", Digest: "sha256:a"}) + if len(said) != 1 || said[0].Artifact != "m:a" || said[0].Version != "1.4.3" || said[0].Digest != "sha256:a" || said[0].Branch != "main" || said[0].Source != 41 { + t.Fatalf("package-published said %+v, want one of m:a at 1.4.3 for main at source 41", said) + } if !rv.ended(ctx, purged{key: boundKey}) { t.Fatal("a reservation's bound was not taken in") } diff --git a/modules/mesh-delivery-builder/internal/build/numbers.go b/modules/mesh-delivery-builder/internal/build/numbers.go index 1729a15a..6fd808cb 100644 --- a/modules/mesh-delivery-builder/internal/build/numbers.go +++ b/modules/mesh-delivery-builder/internal/build/numbers.go @@ -3,6 +3,7 @@ package build import ( "context" "fmt" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-sdk/go/catalogue" "strconv" "strings" @@ -135,8 +136,9 @@ type Reservations interface { Filled() bool // Reserve reserves module's next version in series for the artifacts (their references, :). Reserve(ctx context.Context, module, series string, artifacts []string, src versions.Source) (versions.ReservedVersion, error) - // Built records the digests of the artifacts built under the reserved version, by reference. - Built(ctx context.Context, module, version string, digests map[string]string) error + // Built records the digests of the artifacts built under the reserved version, by reference, and what each resolved and was + // built with (design 47 G §466, §673). + Built(ctx context.Context, module, version string, digests map[string]string, deps map[string][]meshbuilds.Dependency) error } // reserves says whether a look at a trunk build is one that reserves: the build has a series of the form MAJOR.MINOR diff --git a/modules/mesh-delivery-builder/internal/build/reserve_test.go b/modules/mesh-delivery-builder/internal/build/reserve_test.go index c1b85a65..63b40d4d 100644 --- a/modules/mesh-delivery-builder/internal/build/reserve_test.go +++ b/modules/mesh-delivery-builder/internal/build/reserve_test.go @@ -2,6 +2,7 @@ package build import ( "context" + "encoding/json" "fmt" "reflect" "slices" @@ -9,6 +10,7 @@ import ( "testing" "time" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "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" @@ -24,6 +26,7 @@ type fakeReservations struct { series []string built map[string]string sources []versions.Source + deps map[string][]meshbuilds.Dependency } func (f *fakeReservations) Filled() bool { return f.filled } @@ -33,7 +36,8 @@ func (f *fakeReservations) Reserve(_ context.Context, module, series string, art return versions.ReservedVersion{Module: module, Series: series, Version: series + ".5"}, nil } -func (f *fakeReservations) Built(_ context.Context, _, version string, digests map[string]string) error { +func (f *fakeReservations) Built(_ context.Context, _, version string, digests map[string]string, deps map[string][]meshbuilds.Dependency) error { + f.deps = deps f.built = map[string]string{version: ""} for ref, d := range digests { f.built[ref] = d @@ -161,3 +165,42 @@ func TestOnlyABuildOfTheFirstWatchedBranchReservesAndItCarriesItsCatalogRevision }) } } + +// TestAReservedBuildKeepsWhatItsStepResolvedAndWasBuiltWith: design 47 G §466, §673, A-57 — the `resolved` of each artifact and the +// toolchain digest of the step's result are kept on the reservation as dependencies of kind 1 and 3, their parts the artifact +// references the entry's edges name; a package no edge names is left out and said. +func TestAReservedBuildKeepsWhatItsStepResolvedAndWasBuiltWith(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 + var journal []string + r.b.Logf = func(f string, a ...any) { journal = append(journal, fmt.Sprintf(f, a...)) } + r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step { + st := r.answer(a) + var out buildstep.BuildAnswer + if err := json.Unmarshal(st.Result, &out); err != nil || len(out.Artifacts) == 0 { + return st + } + out.Artifacts[0].Resolved = []buildstep.AskResolved{{Package: "git.novox.be/novox/mesh-sdk/go", Version: "0.1.14", Digest: "sha256:11"}, + {Package: "unnamed/pkg", Version: "1.0.0", Digest: "sha256:22"}} + out.Toolchain = &buildstep.ToolchainUsed{Language: "go", Version: "go1.27.1", Recipe: "1", Digest: "sha256:tc"} + st.Result, _ = json.Marshal(out) + return st + } + r.b.Entry(context.Background(), Origin{Branch: "main", Source: 41, First: true}, meshcatalog.CatalogEntry{Module: "m", Repository: "novox/mesh-catalog", + Path: "modules/m", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: fp1, ManifestDigest: mdigest('a'), + Inputs: &meshcatalog.EntryInputs{Toolchains: CatalogToolchains(agreedToolchains)}, + Edges: []meshcatalog.Edge{{Kind: 1, From: "mesh-sdk:go", To: "m:tool", Why: "git.novox.be/novox/mesh-sdk/go"}, {Kind: 3, From: "go-toolchain:program", To: "m:tool"}}}) + r.b.Wait() + want := map[string][]meshbuilds.Dependency{"m:tool": {{Kind: 1, Part: "mesh-sdk:go", Version: "0.1.14", Digest: "sha256:11"}, + {Kind: 3, Part: "go-toolchain:program", Version: "go1.27.1", Digest: "sha256:tc"}}} + if !reflect.DeepEqual(res.deps, want) { + t.Errorf("dependencies = %+v, want %+v", res.deps, want) + } + if !slices.ContainsFunc(journal, func(l string) bool { + return strings.Contains(l, "unnamed/pkg") && strings.Contains(l, "names no artifact") + }) { + t.Errorf("journal %q, want the package no edge names said", journal) + } +} diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index 75bc44e7..b0e4e4c7 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -1,6 +1,7 @@ package build import ( + "cmp" "context" "encoding/json" "errors" @@ -15,6 +16,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep" + "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" @@ -120,6 +122,8 @@ type Builder struct { 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) + edges map[string][]meshcatalog.Edge // by build key: the edges of its entry (take), whose parts a package-published names + deps map[string]map[string][]meshbuilds.Dependency // by build key: what each artifact resolved and was built with (keepDependencies) 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 @@ -172,7 +176,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.origin, b.edges = map[string]Origin{}, map[string][]meshcatalog.Edge{} 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{} @@ -200,7 +204,7 @@ 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, Origin{First: true}, key, m) + b.take(ctx, Origin{First: true}, key, m, nil) } // 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 @@ -221,7 +225,7 @@ func (b *Builder) Entry(ctx context.Context, o Origin, e meshcatalog.CatalogEntr b.logf("the catalog half of the branch %q holds an entry that names no module, left", o.Branch) return } - b.take(ctx, o, e.Module, ModuleOf(e)) + b.take(ctx, o, e.Module, ModuleOf(e), e.Edges) } // 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). @@ -235,7 +239,7 @@ func (b *Builder) EntryGone(branch string, first bool, module string) { } } -func (b *Builder) take(ctx context.Context, o Origin, key string, m meshcatalog.ModulesByModuleValue) { +func (b *Builder) take(ctx context.Context, o Origin, key string, m meshcatalog.ModulesByModuleValue, e []meshcatalog.Edge) { b.mu.Lock() hk := o.Branch + "\x00" + key old, had := b.heard[hk] @@ -253,6 +257,7 @@ func (b *Builder) take(ctx context.Context, o Origin, key string, m meshcatalog. // 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 + b.edges[bk] = e delete(b.via, bk) } b.mu.Unlock() @@ -716,7 +721,7 @@ func (b *Builder) try(ctx context.Context, module string, m meshcatalog.ModulesB for _, a := range result.Artifacts { digests[module+":"+a.Name] = a.Digest } - if err := b.Reserved.Built(ctx, module, reserved.Version, digests); err != nil { + if err := b.Reserved.Built(ctx, module, reserved.Version, digests, b.takeDependencies(key)); err != nil { b.logf("%s: the digests of %s are not recorded: %v", cl.Key, reserved.Version, err) } } @@ -763,11 +768,14 @@ type lane struct { 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 - references map[string]string // artifact -> the reference an image was pushed to (a trunk build only) - imports []string // the mesh packages the lane's programs import, as its build module answered them - kept string // the digest of the artifact the build node kept instead of storing it (BuildAnswer.Kept), else empty - outOfStep []string // the generated files its build module's compare found out of step, passed or failed + digests map[string]string // artifact -> digest + references map[string]string // artifact -> the reference an image was pushed to (a trunk build only) + imports []string // the mesh packages the lane's programs import, as its build module answered them + resolved map[string][]sdkstep.AskResolved // artifact -> the mesh packages its build resolved (design 47 G §466) + tcDigest string // the digest of the toolchain program the building node ran, kind 3's (design 47 G §466, §472) + tcVersion string // and the version it ran + kept string // the digest of the artifact the build node kept instead of storing it (BuildAnswer.Kept), else empty + outOfStep []string // the generated files its build module's compare found out of step, passed or failed err error } @@ -816,7 +824,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, version: version, 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{}, resolved: map[string][]sdkstep.AskResolved{}}) } if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { b.handLanes(key, len(lanes)) @@ -829,6 +837,7 @@ func (b *Builder) build(ctx context.Context, module string, m meshcatalog.Module wg.Wait() if key, err := builds.BuilderBuildKey(module, m.Fp2); err == nil { b.keepOutOfStep(key, lanes) + b.keepDependencies(key, module, lanes) } var errs []string for _, l := range lanes { @@ -1138,6 +1147,9 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes } l.imports = answer.Imports l.kept = answer.Kept + if answer.Toolchain != nil { + l.tcDigest, l.tcVersion = answer.Toolchain.Digest, answer.Toolchain.Version + } built := map[string]sdkstep.AskBuilt{} for _, a := range answer.Artifacts { built[a.Artifact] = a @@ -1149,6 +1161,7 @@ func (b *Builder) runLane(ctx context.Context, l *lane, repository string, m mes return } l.digests[t.Artifact] = a.Digest + l.resolved[t.Artifact] = a.Resolved if a.Reference != "" && (stored(t.Kind) || !candidate) { l.references[t.Artifact] = a.Reference } @@ -1283,3 +1296,73 @@ func SittingOutWords(language string, a sdkstep.Agreement) []string { } return out } + +// keepDependencies keeps, for a build key, what its step results say each artifact resolved (kind 1) and was built with (kind 3) as +// the dependencies `package-published` carries (design 47 G §466, §673, A-57): the part of a kind-1 item is the artifact reference of +// the package, and of the kind-3 item the toolchain's part, each as the edges of the entry name it. A part no edge names is said and +// its item left out. +func (b *Builder) keepDependencies(key, module string, lanes []*lane) { + b.mu.Lock() + edges := b.edges[key] + b.mu.Unlock() + out := map[string][]meshbuilds.Dependency{} + for _, l := range lanes { + for _, t := range l.tasks { + ref := module + ":" + t.Artifact + var deps []meshbuilds.Dependency + for _, r := range l.resolved[t.Artifact] { + part := "" + for _, e := range edges { + if e.Kind == 1 && e.To == ref && e.Why == r.Package { + part = cmp.Or(e.Part, e.From) + } + } + if part == "" && b.Members != nil { + if mod, art, ok, err := b.Members(context.Background(), r.Package); ok && err == nil { + part = mod + ":" + art + } + } + if part == "" { + b.logf("%s: the package %s the build of %s resolved names no artifact: its dependency is not said", key, r.Package, ref) + continue + } + deps = append(deps, meshbuilds.Dependency{Kind: 1, Part: part, Version: r.Version, Digest: r.Digest}) + } + if l.tcDigest != "" { + part := "" + for _, e := range edges { + if e.Kind == 3 && e.To == ref { + part = cmp.Or(e.Part, e.From) + } + } + if part == "" { + b.logf("%s: no edge of kind 3 names the toolchain part the build of %s used: its dependency is not said", key, ref) + } else { + deps = append(deps, meshbuilds.Dependency{Kind: 3, Part: part, Version: l.tcVersion, Digest: l.tcDigest}) + } + } + if len(deps) > 0 { + out[ref] = deps + } + } + } + b.mu.Lock() + defer b.mu.Unlock() + if b.deps == nil { + b.deps = map[string]map[string][]meshbuilds.Dependency{} + } + if len(out) == 0 { + delete(b.deps, key) + return + } + b.deps[key] = out +} + +// takeDependencies answers the dependencies held for a build key and forgets them. +func (b *Builder) takeDependencies(key string) map[string][]meshbuilds.Dependency { + b.mu.Lock() + defer b.mu.Unlock() + d := b.deps[key] + delete(b.deps, key) + return d +} diff --git a/modules/mesh-delivery-builder/internal/versions/reserve.go b/modules/mesh-delivery-builder/internal/versions/reserve.go index 3db6a5e6..c135f0d4 100644 --- a/modules/mesh-delivery-builder/internal/versions/reserve.go +++ b/modules/mesh-delivery-builder/internal/versions/reserve.go @@ -58,7 +58,10 @@ type ReservedVersion struct { // (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"` + // Dependencies are, by artifact reference, what the step's result says each artifact resolved (kind 1) and was built with + // (kind 3), kept for `package-published` (design 47 G §466, §673, A-57). + Dependencies map[string][]meshbuilds.Dependency `json:"dependencies,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 @@ -181,7 +184,7 @@ func (r *Reserver) get(ctx context.Context, key string) (ReservedVersion, uint64 // Built records artifact's digest under the reserved version of module's series; once every artifact has one the build is // ended (BuiltAt). A reservation moved on to another version (a newer build took the next) is left alone and answers false. -func (r *Reserver) Built(ctx context.Context, module, version, artifact, digest string) (bool, error) { +func (r *Reserver) Built(ctx context.Context, module, version, artifact, digest string, deps []meshbuilds.Dependency) (bool, error) { series, _, err := SplitVersion(version) if err != nil { return false, err @@ -193,6 +196,12 @@ func (r *Reserver) Built(ctx context.Context, module, version, artifact, digest return false, err } cur.Artifacts[artifact] = digest + if len(deps) > 0 { + if cur.Dependencies == nil { + cur.Dependencies = map[string][]meshbuilds.Dependency{} + } + cur.Dependencies[artifact] = deps + } if cur.BuiltAt.IsZero() && allSet(cur.Artifacts) { cur.BuiltAt = r.now() } @@ -313,9 +322,24 @@ func (r *Reserver) Drop(ctx context.Context, module, series string) (bool, error return true, r.Store.Delete(ctx, key) } +// Marked is an artifact of a reservation a registry seat's published newly marked: the reservation as it stands then. +type Marked struct { + Reserved ReservedVersion + Artifact string +} + +// PackagePublished is the seat mesh-builds' package-published of one artifact the holder said published at the reservation's +// version (design 47 G §52, §334, §673, A-57): the artifact reference, the version and the digest published, the watched branch and +// the source revision the build was made from, and the dependencies its step result named. +func (m Marked) PackagePublished(digest string, at time.Time) meshbuilds.PackagePublished { + at = at.UTC() + return meshbuilds.PackagePublished{V: 1, Artifact: m.Artifact, At: &at, Branch: m.Reserved.Branch, Source: m.Reserved.Source, Module: m.Reserved.Module, + Version: m.Reserved.Version, Digest: digest, Dependencies: m.Reserved.Dependencies[m.Artifact]} +} + // Published takes a registry seat's published in: the artifacts of a reservation at version (a leading v is the Go form) -// whose built digest it names are marked published. It answers the keys it marked. -func (r *Reserver) Published(ctx context.Context, version, digest string) ([]string, error) { +// whose built digest it names are marked published. It answers each artifact it newly marked, with the reservation as marked. +func (r *Reserver) Published(ctx context.Context, version, digest string) ([]Marked, error) { version = strings.TrimPrefix(version, "v") if version == "" || digest == "" { return nil, nil @@ -325,7 +349,7 @@ func (r *Reserver) Published(ctx context.Context, version, digest string) ([]str return nil, fmt.Errorf("the reservations cannot be listed: %w", err) } sort.Strings(keys) - var marked []string + var marked []Marked for _, k := range keys { again: cur, rev, found, err := r.get(ctx, k) @@ -336,14 +360,17 @@ func (r *Reserver) Published(ctx context.Context, version, digest string) ([]str continue } changed := false + var arts []string for a, d := range cur.Artifacts { if d == digest && !cur.Published[a] { if cur.Published == nil { cur.Published = map[string]bool{} } cur.Published[a], changed = true, true + arts = append(arts, a) } } + sort.Strings(arts) if !changed { continue } @@ -353,7 +380,9 @@ func (r *Reserver) Published(ctx context.Context, version, digest string) ([]str } else if err != nil { return marked, fmt.Errorf("%s cannot be written: %w", k, err) } - marked = append(marked, k) + for _, a := range arts { + marked = append(marked, Marked{Reserved: cur, Artifact: a}) + } } return marked, nil } diff --git a/modules/mesh-delivery-builder/internal/versions/reserve_test.go b/modules/mesh-delivery-builder/internal/versions/reserve_test.go index cfecc500..49ab3a06 100644 --- a/modules/mesh-delivery-builder/internal/versions/reserve_test.go +++ b/modules/mesh-delivery-builder/internal/versions/reserve_test.go @@ -78,11 +78,11 @@ func TestReservation(t *testing.T) { // The artifacts' digests, then the bound. for _, a := range arts { - if ok, err := r.Built(ctx, "m", "0.1.23", a, "sha256:"+a); err != nil || !ok { + if ok, err := r.Built(ctx, "m", "0.1.23", a, "sha256:"+a, nil); err != nil || !ok { t.Fatalf("built %s: %v %v", a, ok, err) } } - if ok, _ := r.Built(ctx, "m", "0.1.22", "m:a", "x"); ok { + if ok, _ := r.Built(ctx, "m", "0.1.22", "m:a", "x", nil); ok { t.Error("a digest was written under a version a newer build superseded") } published := func(_, _, a string) bool { return a == "m:a" } @@ -118,11 +118,12 @@ func TestReservationIsReadAndMarkedPublished(t *testing.T) { ctx := context.Background() store := &reserveMem{mem: newMem()} r := &Reserver{Store: store, ReleasedOf: func(context.Context, string) ([]string, error) { return []string{"1.4.2"}, nil }} - v, err := r.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, Source{}) + v, err := r.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, Source{Branch: "main", Revision: 41}) if err != nil || v.Version != "1.4.3" { t.Fatalf("reserved %q (%v), want 1.4.3", v.Version, err) } - if _, err := r.Built(ctx, "m", "1.4.3", "m:a", "sha256:aa"); err != nil { + deps := []meshbuilds.Dependency{{Kind: 1, Part: "mesh-sdk:go", Version: "0.1.14", Digest: "sha256:dd"}, {Kind: 3, Part: "go-toolchain:program", Version: "go1.27", Digest: "sha256:tt"}} + if _, err := r.Built(ctx, "m", "1.4.3", "m:a", "sha256:aa", deps); err != nil { t.Fatal(err) } rd := Reader{ @@ -142,9 +143,18 @@ func TestReservationIsReadAndMarkedPublished(t *testing.T) { if got, err := AnswerReserved(rd, "other"); err != nil || len(got["reserved"].([]ReservedVersion)) != 0 { t.Errorf("builder_reserved of another module = %v (%v), want none", got, err) } - if marked, err := r.Published(ctx, "v1.4.3", "sha256:aa"); err != nil || len(marked) != 1 { + marked, err := r.Published(ctx, "v1.4.3", "sha256:aa") + if err != nil || len(marked) != 1 { t.Fatalf("a published of 1.4.3 with a built digest marked %v (%v)", marked, err) } + // Design 47 G §52, §673, A-57: the package-published of that artifact carries the digest published, the branch and the source + // revision the reservation was made for, and the dependencies its step result named. + at := time.Unix(100, 0) + said := marked[0].PackagePublished("sha256:aa", at) + atUTC := at.UTC() + if wantSaid := (meshbuilds.PackagePublished{V: 1, Artifact: "m:a", At: &atUTC, Branch: "main", Source: 41, Module: "m", Version: "1.4.3", Digest: "sha256:aa", Dependencies: deps}); !reflect.DeepEqual(said, wantSaid) { + t.Errorf("package-published = %+v, want %+v", said, wantSaid) + } if marked, _ := r.Published(ctx, "1.4.3", "sha256:other"); len(marked) != 0 { t.Errorf("a published with another digest marked %v", marked) }