From 4b8e71370cc2fb4fb795511ba8dcce9b50b538df Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 19 Oct 2026 21:21:37 +0200 Subject: [PATCH] Recognise a trunk build by repository identity and an empty ref in the indexer and the view (issue 1281) Build records name their repository by clone URL and a plan-asked build has no ref, so no module fingerprint was ever written or compared; both now use the SDK's tests. --- modules/mesh-delivery-indexer/go.mod | 2 +- modules/mesh-delivery-indexer/go.sum | 4 +- .../internal/index/builds.go | 14 ++- .../internal/index/live_builds_test.go | 99 +++++++++++++++++++ modules/mesh-delivery-view/go.mod | 2 +- modules/mesh-delivery-view/go.sum | 4 +- .../internal/view/fingerprints_test.go | 23 ++++- .../mesh-delivery-view/internal/view/view.go | 4 +- 8 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 modules/mesh-delivery-indexer/internal/index/live_builds_test.go diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index d3b0aad..0947c10 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-indexer go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019203925-84a764372337 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-indexer/go.sum b/modules/mesh-delivery-indexer/go.sum index f74bae2..afcb604 100644 --- a/modules/mesh-delivery-indexer/go.sum +++ b/modules/mesh-delivery-indexer/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9 h1:XL1dbtSleGAixg6FX4gEnSG/AJxRG/tWw6/7U1RuzHc= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019195003-4580d80a32d9/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019203925-84a764372337 h1:cZyFnvDiBpnNx8ifSUG9WA61OHsn7PC2ml2s4eD7AkA= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019203925-84a764372337/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/internal/index/builds.go b/modules/mesh-delivery-indexer/internal/index/builds.go index e959430..5386390 100644 --- a/modules/mesh-delivery-indexer/internal/index/builds.go +++ b/modules/mesh-delivery-indexer/internal/index/builds.go @@ -57,7 +57,11 @@ func (b *Builds) Delete(key string) { } } -func dirKey(repository, dir string) string { return repository + "\x00" + strings.Trim(dir, "/") } +// dirKey names a directory of a repository whichever way the repository is spelled: a build record names it by its clone +// URL, a forge message by "owner/name" (issue 1281). +func dirKey(repository, dir string) string { + return strings.ToLower(delivery.RepositoryName(repository)) + "\x00" + strings.Trim(dir, "/") +} // Module is the module a build record of the repository names the directory as its path, if one does. func (b *Builds) Module(repository, dir string) (string, bool) { @@ -85,8 +89,8 @@ func (b *Builds) ModuleOf(repository, dir string) string { return path.Base(strings.Trim(dir, "/")) } -// Known is a module the builds know of in a repository: its directory there and its newest successful trunk build (ref -// main, state built, with source inputs) if any. The newest key of a module is any state and ref, so it is not what +// Known is a module the builds know of in a repository: its directory there and its newest successful trunk build +// (delivery.BuildRecord.IsTrunkBuild) if any. The newest key of a module is any state and ref, so it is not what // finds this (mesh-sdk go/delivery/buildrecord.go NewestKey); the records are. type Known struct { Module string @@ -115,7 +119,7 @@ func (b *Builds) Modules(repository string) []Known { } else if newer(r, m.any) { m.any = r } - if r.Ref == "main" && r.State == "built" && r.SourceInputs != nil && (m.trunk == nil || newer(r, *m.trunk)) { + if r.IsTrunkBuild() && (m.trunk == nil || newer(r, *m.trunk)) { c := r m.trunk = &c } @@ -127,7 +131,7 @@ func (b *Builds) Modules(repository string) []Known { if m.trunk != nil { k.Repository, k.Path = m.trunk.Repository, strings.Trim(m.trunk.Path, "/") } - if k.Repository == repository { + if delivery.SameRepository(k.Repository, repository) { out = append(out, k) } } diff --git a/modules/mesh-delivery-indexer/internal/index/live_builds_test.go b/modules/mesh-delivery-indexer/internal/index/live_builds_test.go new file mode 100644 index 0000000..9308d7c --- /dev/null +++ b/modules/mesh-delivery-indexer/internal/index/live_builds_test.go @@ -0,0 +1,99 @@ +package index + +import ( + "context" + "testing" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// The build records below are the live ones, as `mesh-controller.command builds` showed them on 2026-10-19 at about +// 21:20 local (the last 100 builds): id, state, module, repository (the clone URL, as the record names it), commit (the +// eight characters the listing shows), the time (the listing's local minute, as UTC), the source fingerprint, and the +// ref only where the listing shows "at main"; a build a delivery plan asked after a merge shows none, so the record +// carries none. No reading command shows a whole record, so two fields come from elsewhere: the path is the module's +// directory in the catalogue (a clone of mesh-catalog and mesh-media-catalog, origin/main at the same time), and the +// source-inputs are mailu's live inputs (build-1792391134620131370, captured in mesh-sdk go/delivery/fingerprint_test.go +// TestFingerprintLiveBuilds) with the module's name put in; the indexer reads only their bases, contexts and toolchains. +const ( + liveTree = "cfc6fa8ff5fc1e220132e51ade653b9d3f387b3e" + liveInputsTail = `"Tree":"` + liveTree + `","Bases":["sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228"],"Contexts":null,` + + `"Toolchains":{"code":"typescript sha256:4b81682df5665c1f461d3f720a3a0fd7593f4aa71e4fc272dbfdc4bbd235b489 20125bd590041690"},"Unpinned":""}` + catalogURL = "http://novox.internal:20000/novox/mesh-catalog.git" + mediaCatalogURL = "http://novox.internal:20000/novox/mesh-media-catalog.git" +) + +func liveRecord(id, module, repository, ref, commit, path, at, source string) [2]any { + raw := `{"id":"` + id + `","state":"built","repository":"` + repository + `",` + if ref != "" { + raw += `"ref":"` + ref + `",` + } + raw += `"module":"` + module + `","commit":"` + commit + `","path":"` + path + `","at":"` + at + `","source-fingerprint":"` + source + + `","source-inputs":{"Module":"` + module + `",` + liveInputsTail + `}` + return [2]any{delivery.BuildKey(module, id), []byte(raw)} +} + +func liveBuilds() [][2]any { + return [][2]any{ + // a plan-asked build after a merge: no ref + liveRecord("build-1792417485811374131", "zsh", catalogURL, "", "e8c34487", "modules/zsh", "2026-10-19T13:57:00Z", "src1:7e600dde59f3d7f019af00741583304288883e7d3ed955f4c61da26474f3be63"), + liveRecord("build-1792435539606203626", "mesh-delivery", catalogURL, "", "9efdd87a", "modules/mesh-delivery", "2026-10-19T18:45:00Z", "src1:f79e2eedc2689cf6b3591ebfdb81ec65899131122fb2fe3dd9e384091d8a2a6d"), + // a build at main + liveRecord("build-1792417463192745880", "mailu", catalogURL, "main", "e8c34487", "modules/mailu", "2026-10-19T13:50:00Z", "src1:98d35b17d3a3295e3a0efed2b8c4316c9a9b32ec155f8343824654661b7383a2"), + // a module of another repository + liveRecord("build-1792417481951691634", "sonarr", mediaCatalogURL, "main", "197abb06", "modules/sonarr", "2026-10-19T13:57:00Z", "src1:0fff753b8c66aa8a8ac1f38d210cea9512a78a8592dd2475bfe6ade77ff21b61"), + } +} + +func liveIndexer(t *testing.T) (*Indexer, *memStore) { + t.Helper() + ix, mods := modulesIndexer(t, liveBuilds()...) + return ix, mods +} + +func TestMergeWritesEveryModuleTheLiveRecordsNameInTheRepository(t *testing.T) { // 1281/AC3 + ix, mods := liveIndexer(t) + msg := mergedMsg("novox/mesh-catalog", map[string]string{ + "modules/zsh": liveTree, "modules/mesh-delivery": "newtree", "modules/mailu": "newtree", "modules/sonarr": "newtree", + }) + if err := ix.Apply(context.Background(), msg); err != nil { + t.Fatal(err) + } + for _, name := range []string{"zsh", "mesh-delivery", "mailu"} { + m, ok := moduleOf(t, mods, name) + if !ok { + t.Errorf("%s: no module key written", name) + continue + } + if (m.FP == "") == (m.None == "") || m.Repository != "novox/mesh-catalog" || m.Commit != merge { + t.Errorf("%s: module = %+v, want an fp or a none, repository novox/mesh-catalog, commit %s", name, m, merge) + } + } + // the tree of zsh at the merge is the tree its inputs record, so its fingerprint is the one those inputs give + var in delivery.SourceInputs + in.Module, in.Tree = "zsh", liveTree + in.Bases = []string{"sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228"} + in.Toolchains = map[string]string{"code": "typescript sha256:4b81682df5665c1f461d3f720a3a0fd7593f4aa71e4fc272dbfdc4bbd235b489 20125bd590041690"} + want, _ := delivery.SourceFingerprint(in) + if m, _ := moduleOf(t, mods, "zsh"); m.FP != want { + t.Errorf("zsh fp = %q, want %q", m.FP, want) + } + if _, ok := mods.vals["sonarr"]; ok { + t.Error("the key of a module of mesh-media-catalog was written at a merge of mesh-catalog") + } +} + +func TestChangedDirectoryMapsToTheModuleItsLiveRecordNamesByPath(t *testing.T) { // 1281/AC4 + ix, _ := liveIndexer(t) + for dir, want := range map[string]string{"modules/zsh": "zsh", "modules/mesh-delivery": "mesh-delivery", "modules/mailu/": "mailu"} { + if got, ok := ix.Builds.Module("novox/mesh-catalog", dir); !ok || got != want { + t.Errorf("Module(novox/mesh-catalog, %q) = %q, %v; want %q", dir, got, ok, want) + } + } + if got, ok := ix.Builds.Module("novox/mesh-catalog", "modules/sonarr"); ok { + t.Errorf("Module found %q for a directory of mesh-media-catalog under mesh-catalog", got) + } + if got := ix.Builds.ModuleOf("novox/mesh-media-catalog", "modules/sonarr"); got != "sonarr" { + t.Errorf("ModuleOf(mesh-media-catalog, modules/sonarr) = %q, want sonarr", got) + } +} diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 12c8880..168c76f 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019124204-9ade199740e3 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019203925-84a764372337 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index cb9936c..afcb604 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019124204-9ade199740e3 h1:Mu2QiGbL/sZFU5ya4lGUyQOB5OKXPy/KB40XfXTf2EU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019124204-9ade199740e3/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019203925-84a764372337 h1:cZyFnvDiBpnNx8ifSUG9WA61OHsn7PC2ml2s4eD7AkA= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261019203925-84a764372337/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/internal/view/fingerprints_test.go b/modules/mesh-delivery-view/internal/view/fingerprints_test.go index 24189a9..0661b7e 100644 --- a/modules/mesh-delivery-view/internal/view/fingerprints_test.go +++ b/modules/mesh-delivery-view/internal/view/fingerprints_test.go @@ -55,7 +55,8 @@ func modEntry(t *testing.T, name string, m delivery.Module) Entry { func trunkBuild(module, id, ref, state, fp, at string) []Entry { rec := `{"id":"` + id + `","state":"` + state + `","repository":"novox/mesh-delivery","ref":"` + ref + `","module":"` + module + `","commit":"7a74cee83f88e6585a5511c3cc53bdf0f5303d20","path":"modules/` + module + `","at":"` + at + `"` if fp != "" { - rec += `,"source-fingerprint":"` + fp + `"` + // the controller writes the inputs whenever it writes the fingerprint (mesh-sdk testdata/build-record-build-agent.json) + rec += `,"source-fingerprint":"` + fp + `","source-inputs":{"Module":"` + module + `","Tree":"t","Bases":null,"Contexts":null,"Toolchains":null,"Unpinned":""}` } rec += `}` return []Entry{ @@ -188,3 +189,23 @@ func TestUnreadableIndexerValuesAreNamed(t *testing.T) { // never dropped silent t.Errorf("snapshot = %+v", s) } } + +func TestAPlanAskedBuildWithNoRefIsATrunkBuild(t *testing.T) { // 1281/AC5 + // The build record of module mesh-delivery from mesh-catalog, build-1792435539606203626, as `mesh-controller.command + // builds` showed it on 2026-10-19 at about 21:20 local: built on ace at 20:45 local, the repository the clone URL, the + // source fingerprint below, and no ref (a delivery plan asked it after a merge; the listing shows no "at main"). + // No reading command shows the whole record: its path is the module's directory in the catalogue, and its + // source-inputs are mailu's live ones (mesh-sdk go/delivery/fingerprint_test.go) with the module's name put in. + const live = "src1:f79e2eedc2689cf6b3591ebfdb81ec65899131122fb2fe3dd9e384091d8a2a6d" + const rec = `{"id":"build-1792435539606203626","state":"built","repository":"http://novox.internal:20000/novox/mesh-catalog.git",` + + `"module":"mesh-delivery","commit":"9efdd87a","path":"modules/mesh-delivery","at":"2026-10-19T18:45:00Z","source-fingerprint":"` + live + `",` + + `"source-inputs":{"Module":"mesh-delivery","Tree":"cfc6fa8ff5fc1e220132e51ade653b9d3f387b3e","Bases":["sha256:25f3cfeaceca14921366af4d1240b56457ef46273bdb508c7b0e8f469f6fd228"],"Contexts":null,"Toolchains":null,"Unpinned":""}}` + m := New() + fill(m, + Entry{Bucket: BuildsBucket, Key: "mesh-delivery.build.build-1792435539606203626", Value: []byte(rec), At: t0}, + Entry{Bucket: BuildsBucket, Key: "mesh-delivery.newest", Value: []byte("build-1792435539606203626"), At: t0}, + modEntry(t, "mesh-delivery", delivery.Module{FP: live, Repository: "novox/mesh-catalog", Commit: hA})) + if r := fpRow(t, m.Snapshot(), "mesh-delivery"); r.State != FPSame || r.Build != live { + t.Errorf("mesh-delivery = %+v, want same as the no-ref build's fingerprint", r) + } +} diff --git a/modules/mesh-delivery-view/internal/view/view.go b/modules/mesh-delivery-view/internal/view/view.go index e3b8e08..4addb11 100644 --- a/modules/mesh-delivery-view/internal/view/view.go +++ b/modules/mesh-delivery-view/internal/view/view.go @@ -454,12 +454,12 @@ func (m *Model) groupRows() []GroupRow { } // fingerprintRows compare each module the indexer fingerprinted with the source fingerprint of the module's newest -// successful trunk build (ref main, state built) that recorded one. The newest key is any state and ref, so it is not +// successful trunk build (delivery.BuildRecord.IsTrunkBuild: built, on main or with no ref) that recorded one. The newest key is any state and ref, so it is not // used: the records are. func (m *Model) fingerprintRows() []FingerprintRow { newest := map[string]buildEntry{} for _, b := range m.builds { - if b.rec.Module == "" || b.rec.Ref != "main" || b.rec.State != "built" || b.rec.SourceFingerprint == "" { + if b.rec.Module == "" || !b.rec.IsTrunkBuild() || b.rec.SourceFingerprint == "" { continue } if old, ok := newest[b.rec.Module]; !ok || b.rec.At.After(old.rec.At) || (b.rec.At.Equal(old.rec.At) && b.rec.ID > old.rec.ID) {