From 29c525f48de53d1f26480c986fc7f937af8bd081 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 10:11:55 +0100 Subject: [PATCH] A sibling's own siblings come from its go.mod's zero-version requirements, so no beside rule can lack one (issue 1921) The third failure of the kind (tries 1, 3, 4): a sibling a test builds, the node-engine's composer, needs every repository it requires at the zero version beside it. besideFor now reads each sibling's root go.mod through the forge and places those repositories as any sibling is placed; the catalogue's rule names only what its tests read themselves. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../internal/check/beside.go | 96 ++++++++++++++++++- .../internal/check/pins_test.go | 23 ++++- 2 files changed, 113 insertions(+), 6 deletions(-) diff --git a/modules/mesh-delivery-checker/internal/check/beside.go b/modules/mesh-delivery-checker/internal/check/beside.go index dd4359d1..559113f4 100644 --- a/modules/mesh-delivery-checker/internal/check/beside.go +++ b/modules/mesh-delivery-checker/internal/check/beside.go @@ -2,7 +2,13 @@ package check import ( "context" + "errors" + "fmt" + "slices" "strconv" + "strings" + + "git.novox.be/novox/mesh-sdk/go/delivery" ) // BesideRef is a sibling repository a repository's tests read, placed beside the fetched tree as a plain directory (ADR @@ -42,13 +48,13 @@ const ( // unassignment (novox/hq ADR 0590, issue 1126), and its repository is emptied and archived (ADR 0593 §11), so it has no // siblings placed either. mesh-sdk reads mesh-contracts and mesh-delivery (its bootstrapped seats against their contracts files). The catalogue's tests read the node-engine since its composer command moved there (ADR 0593 // §7): mesh-build-go builds it for a compose ask, and with it every repository its composer requires at the zero version -// (mesh-contracts, mesh-delivery, mesh-networking; issue 1921: with mesh-sdk alone beside, that build looked them up on the -// network and failed). The node-engine's read mesh-networking since route-proxy's proofs of a -// composition moved into them (ADR 0593). +// (issue 1921: with mesh-sdk alone beside, that build looked them up on the network and failed): those are not named +// here but derived from each sibling's go.mod (zeroVersionSiblings), so no rule can lack one. The node-engine's read +// mesh-networking since route-proxy's proofs of a composition moved into them (ADR 0593). var besideRules = map[string][]string{ nodeEngineRepo: {catalogueRepository, SDKRepository, networkingRepository}, SDKRepository: {catalogueRepository, nodeEngineRepo, contractsRepository, deliveryRepository}, - catalogueRepository: {SDKRepository, nodeEngineRepo, contractsRepository, deliveryRepository, networkingRepository}, + catalogueRepository: {SDKRepository, nodeEngineRepo}, "novox/mesh-delivery": {SDKRepository, catalogueRepository}, } @@ -69,7 +75,7 @@ func (j *Examiner) besideFor(ctx context.Context, c *Case, p Pull) ([]BesideRef, if p.Trunk != "" { out = append(out, BesideRef{Repository: p.Repository, Commit: p.Trunk}) } - for _, repo := range besideRules[p.Repository] { + place := func(repo string) { switch { case repo == nodeEngineRepo && c.member(repo) != nil: // A group's check judges each member against the group's heads (ADR 0573 §4, ADR 0482 §2), and the group's @@ -99,9 +105,89 @@ func (j *Examiner) besideFor(ctx context.Context, c *Case, p Pull) ([]BesideRef, } } } + for _, repo := range besideRules[p.Repository] { + place(repo) + } + // A sibling a test builds needs every repository its go.mod requires at the zero version beside it too: the build + // resolves them from what lies beside, with nothing fetched (issue 1921). Its go.mod names them all, indirect ones + // included, so one level is enough. + derived, err := j.zeroVersionSiblings(ctx, p.Repository, out) + if err != nil { + return nil, err + } + for _, repo := range derived { + place(repo) + } return out, nil } +// zeroVersionSiblings are the repositories the root go.mod of each sibling in refs requires at the zero version that are +// not beside already, sorted; none where the checker reads no forge. A sibling with no root go.mod requires none; one the +// forge does not answer for is an error, never a sibling left out in silence. +func (j *Examiner) zeroVersionSiblings(ctx context.Context, own string, refs []BesideRef) ([]string, error) { + if j.Forge == nil { + return nil, nil + } + have := map[string]bool{own: true} + for _, r := range refs { + have[r.Repository] = true + } + var out []string + for _, r := range refs { + if r.Repository == own { + continue + } + text, err := j.Forge.File(ctx, r.Repository, "go.mod", r.Commit) + if errors.Is(err, ErrNotFound) { + continue + } + if err != nil { + return nil, fmt.Errorf("the go.mod of %s at %.12s, beside the change, cannot be read%s: %w", r.Repository, r.Commit, unheardWords(err), err) + } + for _, path := range zeroVersionRequires(text) { + repo, _, ok := delivery.PackageRepository(path) + if !ok || have[repo] { + continue + } + have[repo] = true + out = append(out, repo) + } + } + slices.Sort(out) + return out, nil +} + +// zeroVersionRequires are the module paths a go.mod's require lines (a block or single ones) name at the zero version. +func zeroVersionRequires(gomod string) []string { + var out []string + block := false + for _, line := range strings.Split(gomod, "\n") { + line, _, _ = strings.Cut(line, "//") + f := strings.Fields(line) + switch { + case len(f) == 0: + continue + case f[0] == "require" && len(f) > 1 && f[1] == "(": + block = true + continue + case block && f[0] == ")": + block = false + continue + case f[0] == "require": + f = f[1:] + case !block: + continue + } + if len(f) >= 2 && f[1] == zeroVersion { + out = append(out, f[0]) + } + } + return out +} + +// zeroVersion is the version a go.mod requires a mesh package at (ADR 0573 §4): the builder writes the version it builds. +const zeroVersion = "v0.0.0-00010101000000-000000000000" + func (c *Case) member(repository string) *Pull { for i := range c.Pulls { if c.Pulls[i].Repository == repository { diff --git a/modules/mesh-delivery-checker/internal/check/pins_test.go b/modules/mesh-delivery-checker/internal/check/pins_test.go index cfef6a4b..36705d16 100644 --- a/modules/mesh-delivery-checker/internal/check/pins_test.go +++ b/modules/mesh-delivery-checker/internal/check/pins_test.go @@ -1,6 +1,7 @@ package check import ( + "context" "maps" "slices" "strings" @@ -200,8 +201,15 @@ func TestACatalogueChangeHasTheCatalogueBesideItAtItsTrunk(t *testing.T) { hostRuns := strings.Repeat("7", 40) b := newBench(t) b.ex.Trunks = trunksOf(t, map[string]string{catalogueRepository: trunkA, SDKRepository: trunkS, "novox/mesh-controller": trunkA2, nodeEngineRepo: trunkA2, - contractsRepository: trunkA2, deliveryRepository: trunkA2, networkingRepository: trunkA2}) + contractsRepository: trunkA2, deliveryRepository: trunkA2, networkingRepository: trunkA2, "novox/mesh-tools": trunkA2}) b.ex.Runs = runsAt{nodeEngineModule: hostRuns} + // The node-engine's go.mod at the commit the mesh runs: what it requires at the zero version is placed beside too, + // derived, not named in a rule (issue 1921); a version a build wrote, or a package outside the forge, is not. + b.ex.Forge = headFiles{nodeEngineRepo + " go.mod " + hostRuns: "module github.com/novox/mesh-host\n\ngo 1.26\n\nrequire (\n" + + "\tgit.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000\n" + + "\tgit.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000 // indirect\n" + + "\tgit.novox.be/novox/mesh-tools/go v0.1.2\n\tgithub.com/nats-io/nats.go v1.40.0\n)\n\n" + + "require git.novox.be/novox/mesh-networking/modules/mesh-wireguard/go v0.0.0-00010101000000-000000000000\n"} out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 12, head1))) // ADR 0548 §6: the outcome names the siblings read at their trunk's head or the commit the mesh runs, never the // repository it merges. @@ -219,6 +227,9 @@ func TestACatalogueChangeHasTheCatalogueBesideItAtItsTrunk(t *testing.T) { t.Errorf("%s is not beside a catalogue change: %v", repo, got) } } + if _, ok := got["novox/mesh-tools"]; ok { + t.Errorf("mesh-tools, required at a version a build wrote, is beside a catalogue change: %v", got) + } if h := got[nodeEngineRepo]; h.Commit != hostRuns { t.Errorf("the node-engine beside a catalogue change = %+v, want the commit the mesh runs %s", h, hostRuns) } @@ -257,3 +268,13 @@ func TestAMeshLabHeadGetsACheck(t *testing.T) { t.Fatalf("mesh-lab was not fetched: %+v", fetched) } } + +// headFiles are the forge's files at a ref, by " ". +type headFiles map[string]string + +func (h headFiles) File(_ context.Context, repository, path, ref string) (string, error) { + if text, ok := h[repository+" "+path+" "+ref]; ok { + return text, nil + } + return "", ErrNotFound +}