From 73d15c08f267c8540f602f922dc0a92dab0e1391 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 13:57:00 +0100 Subject: [PATCH 1/2] CT3 (answers): the catalog graph entry carries manifest-digest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §506 (the entry gains manifest-digest, the sha256: digest of the manifest's bytes as the forge answered them, set by the indexer at take-in). Regenerated by seatgen (-check clean). Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- contracts/go/seats/meshcatalog/contract.go | 4 +++- contracts/go/seats/meshcatalog/contract_test.go | 2 +- contracts/seats/mesh-catalog/fixtures/states.json | 1 + contracts/seats/mesh-catalog/seat.json | 4 ++++ contracts/ts/seats/meshcatalog/index.d.ts | 2 ++ 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/contracts/go/seats/meshcatalog/contract.go b/contracts/go/seats/meshcatalog/contract.go index 1a6b43f6..db822342 100644 --- a/contracts/go/seats/meshcatalog/contract.go +++ b/contracts/go/seats/meshcatalog/contract.go @@ -736,7 +736,9 @@ type CatalogEntry struct { Main string `json:"main,omitempty"` // the validated manifest whole, kept as an object by the entry's digest, so no size limit applies; read by anyone but the indexer through the entry, never as a manifest (design 47 G section 443, section 442) Manifest map[string]any `json:"manifest"` - Module string `json:"module"` + // the sha256: digest of the manifest's bytes as the forge answered them, set by the indexer at take-in and never computed again from the manifest whole; the module's value and the manifests key are this digest (design 47 G section 506) + ManifestDigest string `json:"manifest-digest,omitempty"` + Module string `json:"module"` // the manifest reads a node-engine must state to compose it, its templates' forms included, as the forge bridge read them at the merge (ADR 0614 §4, ADR 0632 §2) Needs []string `json:"needs,omitempty"` // why it has none diff --git a/contracts/go/seats/meshcatalog/contract_test.go b/contracts/go/seats/meshcatalog/contract_test.go index 5ef39f04..e6bca015 100644 --- a/contracts/go/seats/meshcatalog/contract_test.go +++ b/contracts/go/seats/meshcatalog/contract_test.go @@ -85,7 +85,7 @@ func TestFixtures(t *testing.T) { err := json.Unmarshal(b, &v) return v, err }, subjectIs: nil}, - {name: "a catalog graph entry (xclip)", subject: "", given: "{\"bases\":[\"arch\"],\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"inputs\":{\"fp-src\":41,\"from\":{\"bases\":[\"arch\"],\"locks\":{\"go.sum\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"},\"packages\":[\"git.novox.be/novox/mesh-sdk/go@v0.0.0-20261101000000-f469eb387a30\"],\"tree\":\"4b825dc642cb6eb9a060e54bf8d69288fbee4904\"},\"src\":41,\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}}},\"invokes\":[\"node-files.read\"],\"manifest\":{\"module\":\"xclip\",\"version\":\"1\"},\"module\":\"xclip\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"v0.0.0-20261101000000-f469eb387a30\"},\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"module\":\"mesh-build-go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1,\"verdict\":\"accepted\"}", wire: "{\"bases\":[\"arch\"],\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"inputs\":{\"fp-src\":41,\"from\":{\"bases\":[\"arch\"],\"locks\":{\"go.sum\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"},\"packages\":[\"git.novox.be/novox/mesh-sdk/go@v0.0.0-20261101000000-f469eb387a30\"],\"tree\":\"4b825dc642cb6eb9a060e54bf8d69288fbee4904\"},\"src\":41,\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}}},\"invokes\":[\"node-files.read\"],\"manifest\":{\"module\":\"xclip\",\"version\":\"1\"},\"module\":\"xclip\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"v0.0.0-20261101000000-f469eb387a30\"},\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"module\":\"mesh-build-go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1,\"verdict\":\"accepted\"}", + {name: "a catalog graph entry (xclip)", subject: "", given: "{\"bases\":[\"arch\"],\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"inputs\":{\"fp-src\":41,\"from\":{\"bases\":[\"arch\"],\"locks\":{\"go.sum\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"},\"packages\":[\"git.novox.be/novox/mesh-sdk/go@v0.0.0-20261101000000-f469eb387a30\"],\"tree\":\"4b825dc642cb6eb9a060e54bf8d69288fbee4904\"},\"src\":41,\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}}},\"invokes\":[\"node-files.read\"],\"manifest\":{\"module\":\"xclip\",\"version\":\"1\"},\"manifest-digest\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\"module\":\"xclip\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"v0.0.0-20261101000000-f469eb387a30\"},\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"module\":\"mesh-build-go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1,\"verdict\":\"accepted\"}", wire: "{\"bases\":[\"arch\"],\"commit\":\"f469eb387a3015569d9b16c66ed418e9dd098f64\",\"fp\":\"src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56\",\"inputs\":{\"fp-src\":41,\"from\":{\"bases\":[\"arch\"],\"locks\":{\"go.sum\":\"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\"},\"packages\":[\"git.novox.be/novox/mesh-sdk/go@v0.0.0-20261101000000-f469eb387a30\"],\"tree\":\"4b825dc642cb6eb9a060e54bf8d69288fbee4904\"},\"src\":41,\"toolchains\":{\"go\":{\"language\":\"go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}}},\"invokes\":[\"node-files.read\"],\"manifest\":{\"module\":\"xclip\",\"version\":\"1\"},\"manifest-digest\":\"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\"module\":\"xclip\",\"packages\":{\"git.novox.be/novox/mesh-sdk/go\":\"v0.0.0-20261101000000-f469eb387a30\"},\"path\":\"modules/xclip\",\"repository\":\"novox/mesh-catalog\",\"toolchains\":{\"go\":{\"module\":\"mesh-build-go\",\"recipe\":\"1\",\"version\":\"1.26.0\"}},\"v\":1,\"verdict\":\"accepted\"}", decode: func(b []byte) (any, error) { var v CatalogRevisionsByEntryDigestValue err := json.Unmarshal(b, &v) diff --git a/contracts/seats/mesh-catalog/fixtures/states.json b/contracts/seats/mesh-catalog/fixtures/states.json index c3c1aacc..b1174d11 100644 --- a/contracts/seats/mesh-catalog/fixtures/states.json +++ b/contracts/seats/mesh-catalog/fixtures/states.json @@ -302,6 +302,7 @@ "module": "xclip", "version": "1" }, + "manifest-digest": "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "verdict": "accepted", "fp": "src2:b427bdde1f100e2f52490cfa9df0c5112dd3700226b4892349f0c14a15a4af56", "inputs": { diff --git a/contracts/seats/mesh-catalog/seat.json b/contracts/seats/mesh-catalog/seat.json index fd80e3bf..978e31dd 100644 --- a/contracts/seats/mesh-catalog/seat.json +++ b/contracts/seats/mesh-catalog/seat.json @@ -1872,6 +1872,10 @@ "type": "object", "description": "the validated manifest whole, kept as an object by the entry's digest, so no size limit applies; read by anyone but the indexer through the entry, never as a manifest (design 47 G section 443, section 442)" }, + "manifest-digest": { + "type": "string", + "description": "the sha256: digest of the manifest's bytes as the forge answered them, set by the indexer at take-in and never computed again from the manifest whole; the module's value and the manifests key are this digest (design 47 G section 506)" + }, "verdict": { "type": "string", "description": "its manifest's verdict: accepted, refused or held" diff --git a/contracts/ts/seats/meshcatalog/index.d.ts b/contracts/ts/seats/meshcatalog/index.d.ts index f935a038..53832de7 100644 --- a/contracts/ts/seats/meshcatalog/index.d.ts +++ b/contracts/ts/seats/meshcatalog/index.d.ts @@ -247,6 +247,8 @@ export interface CatalogEntry { "main"?: string; /** the validated manifest whole, kept as an object by the entry's digest, so no size limit applies; read by anyone but the indexer through the entry, never as a manifest (design 47 G section 443, section 442) */ "manifest": Record; + /** the sha256: digest of the manifest's bytes as the forge answered them, set by the indexer at take-in and never computed again from the manifest whole; the module's value and the manifests key are this digest (design 47 G section 506) */ + "manifest-digest"?: string; "module": string; /** the manifest reads a node-engine must state to compose it, its templates' forms included, as the forge bridge read them at the merge (ADR 0614 §4, ADR 0632 §2) */ "needs"?: string[]; From cb6dc7c3208c659e55c6ce130c48fa3b8d8ea7f6 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 13:57:54 +0100 Subject: [PATCH 2/2] BL4: the git tag go/vMAJOR.MINOR.PATCH is written on the catalog's version-released, at the commit the version was reserved from MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reservation keeps the repository and commit of the build and whether the module has a Go package; version-released writes the tag through the seat git's verb tag before the key goes, and a tag the forge did not write leaves the release to be heard again. Builds: design 47 G §370 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/mesh-delivery-builder/reserve.go | 43 +++++++++++- .../cmd/mesh-delivery-builder/reserve_test.go | 68 ++++++++++++++++++- modules/mesh-delivery-builder/go.mod | 2 + .../internal/build/numbers.go | 11 ++- .../internal/build/reserve_test.go | 2 +- .../internal/build/run.go | 2 +- .../internal/versions/reserve.go | 20 +++++- .../internal/versions/reserve_test.go | 16 ++--- .../internal/versions/tag.go | 4 ++ .../mesh-delivery-builder/manifest_test.go | 1 + modules/mesh-delivery-builder/module.json | 1 + 11 files changed, 149 insertions(+), 21 deletions(-) create mode 100644 modules/mesh-delivery-builder/internal/versions/tag.go 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 bb41bbd8..7d286318 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/reserve.go @@ -12,6 +12,7 @@ import ( "github.com/nats-io/nats.go/jetstream" "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/meshcatalog" "mesh-delivery-builder/internal/build" @@ -45,12 +46,14 @@ type reservations struct { bound time.Duration wake func(context.Context) logf func(string, ...any) + // call asks a seat's verb: the git seat's tag (design 47 G §370). + call toolCaller } // newReservations makes the reserving over store; the released versions of a module are asked of the seat mesh-catalog's // verb versions through call (design 47 G §348), the only source numbering reads them from. func newReservations(store versions.ReserveStore, call toolCaller, bounds func() jetstream.KeyValue, found *findings, wake func(context.Context), logf func(string, ...any)) *reservations { - rv := &reservations{bounds: bounds, found: found, bound: versions.UnreleasedBound, wake: wake, logf: logf} + rv := &reservations{call: call, bounds: bounds, found: found, bound: versions.UnreleasedBound, wake: wake, logf: logf} rv.r = &versions.Reserver{Store: store, Filled: rv.filled.Load, ReleasedOf: func(_ context.Context, module string) ([]string, error) { raw, err := call("seat:"+meshcatalog.Seat+".versions", meshcatalog.VersionsInput{Module: module}) if err != nil { @@ -73,8 +76,8 @@ var _ build.Reservations = (*reservations)(nil) func (rv *reservations) Filled() bool { return rv.filled.Load() } -func (rv *reservations) Reserve(ctx context.Context, module, series string, artifacts []string) (versions.ReservedVersion, error) { - return rv.r.Reserve(ctx, module, series, artifacts) +func (rv *reservations) Reserve(ctx context.Context, module, series string, artifacts []string, src versions.Source) (versions.ReservedVersion, error) { + return rv.r.Reserve(ctx, module, series, artifacts, src) } // Built records the digests; once every artifact has one, the publish's bound starts (a key of the state bounds with its @@ -165,6 +168,13 @@ func (rv *reservations) heard(ctx context.Context, m jetstream.Msg) bool { rv.logf("%s holds what is no version-released, left: %v", m.Subject(), err) break } + if err := rv.tagged(ctx, ev); err != nil { + rv.logf("%v; heard again", err) + if nakErr := m.Nak(); nakErr != nil { + rv.logf("leaving %s for the bus to give again: %v", m.Subject(), nakErr) + } + return true + } gone, err := rv.r.Released(ctx, ev.Module, ev.Version) if err != nil { rv.logf("%v; heard again", err) @@ -206,3 +216,30 @@ func (rv *reservations) dropped(ctx context.Context, module, series, by, why str rv.found.clear(ctx, versions.VersionUnreleased, meshconditionsclient.ScopeModule, module) return fmt.Sprintf("the reservation of %s %s is dropped (%s, %s)", module, series, by, why), nil } + +// tagged writes the git tag of a released version (design 47 G §370): on the catalog's version-released, through the seat +// git's verb tag, at the commit the reservation of that version was made from. A version with no reservation (a superseded +// build's version is never released, and the key of a version released by another builder is gone) or one that names no +// tag (a module with no Go package) is tagged nowhere. A tag the forge could not write is an error, the message heard again. +func (rv *reservations) tagged(ctx context.Context, ev meshcatalog.VersionReleasedEvent) error { + res, found, err := rv.r.Of(ctx, ev.Module, ev.Version) + if err != nil { + return fmt.Errorf("the reservation of %s %s cannot be read to tag it: %w", ev.Module, ev.Version, err) + } + if !found || res.Tag == "" || res.Commit == "" { + return nil + } + raw, err := rv.call("seat:"+git.Seat+".tag", git.TagInput{Repository: res.Repository, Commit: res.Commit, Tag: res.Tag}) + if err != nil { + return fmt.Errorf("the tag %s of %s is not written: %w", res.Tag, ev.Module, err) + } + var a git.TagAnswer + if err := json.Unmarshal(raw, &a); err != nil { + return fmt.Errorf("the git seat answered what is no tag answer for %s: %w", res.Tag, err) + } + if a.Error != "" || !a.Performed { + return fmt.Errorf("the tag %s of %s is not written: %s (%s)", res.Tag, ev.Module, a.Error, a.ErrorKind) + } + rv.logf("%s is tagged %s at %s", ev.Module, res.Tag, res.Commit) + return nil +} 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 d1a04e2f..679e1b95 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 @@ -10,6 +10,7 @@ import ( "github.com/nats-io/nats.go/jetstream" "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/meshcatalog" "mesh-delivery-builder/internal/versions" @@ -85,14 +86,14 @@ func TestTheCatalogsWordsMoveTheReservations(t *testing.T) { } rv := newReservations(&memReserve{vals: map[string][]byte{}, revs: map[string]uint64{}}, call, func() jetstream.KeyValue { return bounds }, found, func(context.Context) { woke++ }, t.Logf) - if _, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a"}); err == nil || rv.Filled() { + if _, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a"}, versions.Source{}); err == nil || rv.Filled() { t.Fatalf("a version was reserved before versions-filled (%v)", err) } filled := &wakeMsg{subject: meshcatalog.SubjectVersionsFilled, data: []byte(`{"v":1}`)} 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"}) + res, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, versions.Source{}) 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) } @@ -119,7 +120,7 @@ func TestTheCatalogsWordsMoveTheReservations(t *testing.T) { if all, _ := rv.r.All(ctx); len(all) != 0 || bounds.keys[boundKey] || len(st.vals) != 0 { t.Errorf("after version-released: reservations %v, bounds %v, findings %v; want none", all, bounds.keys, st.vals) } - if _, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a"}); err != nil { + if _, err := rv.Reserve(ctx, "m", "1.4", []string{"m:a"}, versions.Source{}); err != nil { t.Fatal(err) } if answer, err := rv.dropped(ctx, "m", "1.4", "op", "never released", time.Unix(0, 0)); err != nil || !strings.Contains(answer, "dropped") { @@ -129,3 +130,64 @@ func TestTheCatalogsWordsMoveTheReservations(t *testing.T) { t.Error("a reservation that is gone was dropped again") } } + +// TestAReleasedVersionIsTaggedAtTheCommitItWasBuiltFrom: design 47 G §370 — the git tag go/vMAJOR.MINOR.PATCH is written through +// the seat git's tag on the catalog's version-released, at the commit the version's reservation was made from; a module with no +// Go package, and a version never released (a superseded build's), are tagged nowhere; a tag the forge did not write leaves +// version-released to be heard again with the reservation standing. +func TestAReleasedVersionIsTaggedAtTheCommitItWasBuiltFrom(t *testing.T) { + ctx := context.Background() + var tags []git.TagInput + refuse := "" + call := func(addr string, body any) (json.RawMessage, error) { + if addr != "seat:git.tag" { + return json.Marshal(meshcatalog.VersionsAnswer{}) + } + tags = append(tags, body.(git.TagInput)) + return json.Marshal(git.TagAnswer{Performed: refuse == "", Error: refuse}) + } + found := &findings{store: &memFindings{vals: map[string][]byte{}}, logf: t.Logf, now: func() time.Time { return time.Unix(0, 0) }} + rv := newReservations(&memReserve{vals: map[string][]byte{}, revs: map[string]uint64{}}, call, func() jetstream.KeyValue { return &boundsKV{keys: map[string]bool{}} }, found, nil, t.Logf) + rv.filled.Store(true) + goPkg := versions.Source{Repository: "o/r", Commit: "c0ffee", GoPackage: true} + if _, err := rv.Reserve(ctx, "pkg", "0.1", []string{"pkg:lib"}, goPkg); err != nil { + t.Fatal(err) + } + if _, err := rv.Reserve(ctx, "prog", "2.0", []string{"prog:bin"}, versions.Source{Repository: "o/p", Commit: "beef"}); err != nil { + t.Fatal(err) + } + release := func(module, version string) *wakeMsg { + m := &wakeMsg{subject: meshcatalog.SubjectVersionReleased, data: []byte(`{"v":1,"module":"` + module + `","version":"` + version + `","source":1,"artifacts":[]}`)} + rv.heard(ctx, m) + return m + } + if m := release("prog", "2.0.0"); !m.acked || len(tags) != 0 { + t.Fatalf("a module with no Go package: acked %v, tags %v; want none written", m.acked, tags) + } + refuse = "forge down" + if m := release("pkg", "0.1.0"); !m.naked || m.acked { + t.Fatalf("a tag the forge did not write: naked %v acked %v; want the release heard again", m.naked, m.acked) + } + if all, _ := rv.r.All(ctx); len(all) != 1 { + t.Fatalf("the reservation went with an unwritten tag: %v", all) + } + refuse = "" + tags = nil + if m := release("pkg", "0.1.0"); !m.acked || len(tags) != 1 || tags[0] != (git.TagInput{Repository: "o/r", Commit: "c0ffee", Tag: "go/v0.1.0"}) { + t.Fatalf("the release: acked %v, tags %+v; want go/v0.1.0 at c0ffee of o/r", m.acked, tags) + } + tags = nil + if _, err := rv.Reserve(ctx, "pkg", "0.1", []string{"pkg:lib"}, goPkg); err != nil { // build A: 0.1.0 (the stub catalog holds none released) + t.Fatal(err) + } + if v, err := rv.Reserve(ctx, "pkg", "0.1", []string{"pkg:lib"}, versions.Source{Repository: "o/r", Commit: "b0b", GoPackage: true}); err != nil || v.Version != "0.1.1" { // build B supersedes A + t.Fatalf("second build reserved %q (%v)", v.Version, err) + } + release("pkg", "0.1.0") // A's version is never released in fact; were it said, it is no reservation of the key + if len(tags) != 0 { + t.Fatalf("a superseded build's version was tagged: %+v", tags) + } + if release("pkg", "0.1.1"); len(tags) != 1 || tags[0].Commit != "b0b" { + t.Fatalf("the newer build's version: tags %+v; want go/v0.1.1 at b0b", tags) + } +} diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 916455c4..3d333a4d 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -11,6 +11,8 @@ require ( git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/git v0.0.0-00010101000000-000000000000 + git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 diff --git a/modules/mesh-delivery-builder/internal/build/numbers.go b/modules/mesh-delivery-builder/internal/build/numbers.go index f1b9cef5..2fc70614 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-sdk/go/catalogue" "strconv" "strings" "time" @@ -148,7 +149,7 @@ type Reservations interface { // Filled says the catalog said versions-filled: nothing is reserved before it. Filled() bool // Reserve reserves module's next version in series for the artifacts (their references, :). - Reserve(ctx context.Context, module, series string, artifacts []string) (versions.ReservedVersion, error) + 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 } @@ -184,12 +185,16 @@ 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, plan Plan) (versions.ReservedVersion, bool) { +func (b *Builder) reserve(ctx context.Context, module string, m meshcatalog.ModulesByModuleValue, plan Plan) (versions.ReservedVersion, bool) { refs := make([]string, 0, len(plan.Tasks)) for _, t := range plan.Tasks { refs = append(refs, module+":"+t.Artifact) } - res, err := b.Reserved.Reserve(ctx, module, plan.Series, refs) + src := versions.Source{Repository: m.Repository, Commit: m.Commit} + for _, t := range plan.Tasks { + src.GoPackage = src.GoPackage || (t.Kind == catalogue.ArtifactPackage && t.Language == "go") + } + res, err := b.Reserved.Reserve(ctx, module, plan.Series, refs, src) if err != nil { b.logf("%s: no version is reserved for this build: %v", module, err) return versions.ReservedVersion{}, false diff --git a/modules/mesh-delivery-builder/internal/build/reserve_test.go b/modules/mesh-delivery-builder/internal/build/reserve_test.go index cc1fc2dd..b9a33735 100644 --- a/modules/mesh-delivery-builder/internal/build/reserve_test.go +++ b/modules/mesh-delivery-builder/internal/build/reserve_test.go @@ -23,7 +23,7 @@ type fakeReservations struct { func (f *fakeReservations) Filled() bool { return f.filled } -func (f *fakeReservations) Reserve(_ context.Context, module, series string, artifacts []string) (versions.ReservedVersion, error) { +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) return versions.ReservedVersion{Module: module, Series: series, Version: series + ".5"}, nil } diff --git a/modules/mesh-delivery-builder/internal/build/run.go b/modules/mesh-delivery-builder/internal/build/run.go index f5f8bf3a..4a242237 100644 --- a/modules/mesh-delivery-builder/internal/build/run.go +++ b/modules/mesh-delivery-builder/internal/build/run.go @@ -536,7 +536,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, plan) + reserved, hasReserved = b.reserve(ctx, module, m, plan) } var result builds.BuilderBuild if nothing { diff --git a/modules/mesh-delivery-builder/internal/versions/reserve.go b/modules/mesh-delivery-builder/internal/versions/reserve.go index 36b02425..89b0dc32 100644 --- a/modules/mesh-delivery-builder/internal/versions/reserve.go +++ b/modules/mesh-delivery-builder/internal/versions/reserve.go @@ -49,6 +49,18 @@ type ReservedVersion struct { // Published are the artifacts a registry seat said published at this version with the built digest. Published map[string]bool `json:"published,omitempty"` At time.Time `json:"at"` + // Repository and Commit are what the build was made from, and Tag the git tag its release is written as (a Go + // 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"` +} + +// Source is what a build reserving a version is made from (design 47 G §370): the repository and commit the git tag is +// written at, and whether the module has a Go package, whose version is tagged. +type Source struct { + Repository, Commit string + GoPackage bool } // ReservedKey is the key of module's reservation of series (MAJOR.MINOR). @@ -96,7 +108,7 @@ func SplitVersion(version string) (series string, patch int, err error) { // Reserve reserves the next version of module's series for a build that starts, for artifacts (their names), and answers // it. The next patch is the one after the higher of the highest released and the one reserved, 0 when there is neither. A // conflicting write (another build reserved first) is read again and the next taken. -func (r *Reserver) Reserve(ctx context.Context, module, series string, artifacts []string) (ReservedVersion, error) { +func (r *Reserver) Reserve(ctx context.Context, module, series string, artifacts []string, src Source) (ReservedVersion, error) { if r.Filled != nil && !r.Filled() { return ReservedVersion{}, ErrNotFilled } @@ -122,7 +134,11 @@ func (r *Reserver) Reserve(ctx context.Context, module, series string, artifacts next = p } } - out := ReservedVersion{V: 1, Module: module, Series: series, Version: fmt.Sprintf("%s.%d", series, next+1), Artifacts: map[string]string{}, At: r.now()} + 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} + if src.GoPackage { + out.Tag = GoTagOf(out.Version) + } for _, a := range artifacts { out.Artifacts[a] = "" } diff --git a/modules/mesh-delivery-builder/internal/versions/reserve_test.go b/modules/mesh-delivery-builder/internal/versions/reserve_test.go index c02c0c96..cfecc500 100644 --- a/modules/mesh-delivery-builder/internal/versions/reserve_test.go +++ b/modules/mesh-delivery-builder/internal/versions/reserve_test.go @@ -46,7 +46,7 @@ func TestReservation(t *testing.T) { arts := []string{"m:a", "m:b"} filled = false - if _, err := r.Reserve(ctx, "m", "0.1", arts); !errors.Is(err, ErrNotFilled) { + if _, err := r.Reserve(ctx, "m", "0.1", arts, Source{}); !errors.Is(err, ErrNotFilled) { t.Fatalf("before versions-filled: %v, want ErrNotFilled", err) } if got, _ := r.All(ctx); len(got) != 0 { @@ -54,25 +54,25 @@ func TestReservation(t *testing.T) { } filled = true - first, err := r.Reserve(ctx, "m", "0.1", arts) + first, err := r.Reserve(ctx, "m", "0.1", arts, Source{}) if err != nil || first.Version != "0.1.20" { t.Fatalf("first build reserves %q (%v), want 0.1.20 after the highest released 0.1.19", first.Version, err) } - second, err := r.Reserve(ctx, "m", "0.1", arts) + second, err := r.Reserve(ctx, "m", "0.1", arts, Source{}) if err != nil || second.Version != "0.1.21" { t.Fatalf("second build reserves %q (%v), want 0.1.21: never the first's", second.Version, err) } // Two builds at once: the rival reserves between this one's read and write, and this one takes the next. store.onUpdate = func() { - if _, err := r.Reserve(ctx, "m", "0.1", arts); err != nil { + if _, err := r.Reserve(ctx, "m", "0.1", arts, Source{}); err != nil { t.Error(err) } } - third, err := r.Reserve(ctx, "m", "0.1", arts) + third, err := r.Reserve(ctx, "m", "0.1", arts, Source{}) if err != nil || third.Version != "0.1.23" { t.Fatalf("a build that lost the compare-and-set reserves %q (%v), want 0.1.23", third.Version, err) } - if v, _ := r.Reserve(ctx, "m", "0.3", arts); v.Version != "0.3.0" { + if v, _ := r.Reserve(ctx, "m", "0.3", arts, Source{}); v.Version != "0.3.0" { t.Errorf("a series with nothing released reserves %q, want 0.3.0", v.Version) } @@ -103,7 +103,7 @@ func TestReservation(t *testing.T) { if all, _ := r.All(ctx); len(all) != 0 { t.Errorf("reservations left: %v", all) } - if again, _ := r.Reserve(ctx, "m", "0.1", arts); again.Version != "0.1.20" { + if again, _ := r.Reserve(ctx, "m", "0.1", arts, Source{}); again.Version != "0.1.20" { t.Logf("with the catalog list unchanged the next is %s", again.Version) } if ok, _ := r.Drop(ctx, "m", "0.1"); !ok { @@ -118,7 +118,7 @@ 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"}) + v, err := r.Reserve(ctx, "m", "1.4", []string{"m:a", "m:b"}, Source{}) if err != nil || v.Version != "1.4.3" { t.Fatalf("reserved %q (%v), want 1.4.3", v.Version, err) } diff --git a/modules/mesh-delivery-builder/internal/versions/tag.go b/modules/mesh-delivery-builder/internal/versions/tag.go new file mode 100644 index 00000000..e83bff9e --- /dev/null +++ b/modules/mesh-delivery-builder/internal/versions/tag.go @@ -0,0 +1,4 @@ +package versions + +// GoTagOf is the git tag a Go package's released version is written as (design 47 G §370, design 51 *The versions*). +func GoTagOf(version string) string { return "go/v" + version } diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index d03eb00c..6fe5c6b9 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -132,6 +132,7 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1 // The watchdogs seat's presence says a node's machine is heard (issue 1966, ADR 0596 §2, ADR 0668 §3). if want := []string{"mesh-delivery-assigner.assigner_read_assignments", "mesh-delivery-forge-bridge.bridge_tags", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests", "mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions", + "seat:git.tag", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held", "seat:mesh-catalog.versions", "seat:mesh-hand-acts.hand-act", "seat:mesh-watchdogs.presence", "seat:node-build-go.drop", "seat:node-build-image.drop", "seat:node-build-node.drop", "seat:node-engine.manifest-reads", "seat:npm-package-registry.remove", "seat:npm-package-registry.versions", "seat:oci-artifact-registry.remove", "seat:oci-artifact-registry.versions"}; !reflect.DeepEqual(sorted(m.Invokes), want) || len(m.Config) != 0 { t.Errorf("invokes = %v, config = %v, want %v and no config", m.Invokes, m.Config, want) diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index 39a0cc6b..8ed6a82d 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -77,6 +77,7 @@ "mesh-delivery-indexer.indexer_requirements", "seat:container-image-registry.remove", "seat:container-image-registry.versions", + "seat:git.tag", "seat:go-package-registry.remove", "seat:go-package-registry.versions", "seat:mesh-artifact-sweep.held",