From e072abb35e88bbfd76452c3997dfa12f6e297b7e Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 18:16:34 +0100 Subject: [PATCH] Go build module: a package whose tests read MESH_NODE_NAMES has the node list in its digest, so a new node misses a kept verdict --- .../mesh-build-go/cmd/mesh-build-go/digest.go | 46 ++++++++++++++----- .../cmd/mesh-build-go/digest_test.go | 17 ++++++- .../mesh-build-go/cmd/mesh-build-go/level.go | 2 +- 3 files changed, 51 insertions(+), 14 deletions(-) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/digest.go b/modules/mesh-build-go/cmd/mesh-build-go/digest.go index 0e0133e2..dc6b4858 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/digest.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/digest.go @@ -37,6 +37,10 @@ const DigestVersion = "6" // depends on the siblings' commits. const besideVariable = "MESH_CHECK_BESIDE" +// namesVariable is the same for the mesh's node names: a package whose tests reach a file naming it depends on the node +// list the ask carries, so its digest holds the sorted names and a new node misses a kept verdict (ADR 0567 §12). +const namesVariable = "MESH_NODE_NAMES" + // ReadsOutsideMarker is the name of a file in a package's directory that says its tests read files outside it (a // repository's documents, a sibling directory), which the Go toolchain does not list (issue 1935). It lists, one per // line, what those tests read, as paths from the repository's root; a path may be a glob (filepath.Match), and a @@ -164,6 +168,8 @@ func (m *Module) digestTool() StepTool { "workspace": map[string]any{"type": "string", "description": "the workspace id build_go_fetch answered"}, "level": map[string]any{"type": "string", "enum": []string{LevelUnit, LevelIntegration}, "description": "the level"}, "runner": runnerProperty, + delivery.NodeNamesArg: map[string]any{"type": "array", "items": map[string]any{"type": "string"}, + "description": "optional, the names of the mesh's nodes: a package whose tests read " + delivery.NodeNamesEnv + " has them in its digest"}, }, "required": []string{"workspace", "level"}, }, @@ -178,6 +184,10 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) { if err != nil { return nil, err } + names, err := nodeNamesArg(args[delivery.NodeNamesArg]) + if err != nil { + return nil, err + } if runner == RunnerWorkspace { if err := m.workspaceThere(workspace); err != nil { return nil, err @@ -191,7 +201,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) { default: return nil, fmt.Errorf("the level %q is neither unit nor integration", level) } - a, judged, err := m.asCandidate(ctx, levelSpec{Step: stepDigest, Level: level, Workspace: workspace}) + a, judged, err := m.asCandidate(ctx, levelSpec{Step: stepDigest, Level: level, Workspace: workspace, NodeNames: names}) if err != nil { return nil, err } @@ -201,7 +211,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) { a.Digest.JudgedBy = judged return *a.Digest, nil } - res, err := m.digestHere(ctx, workspace, level) + res, err := m.digestHere(ctx, workspace, level, names) if err != nil { return nil, err } @@ -210,7 +220,7 @@ func (m *Module) digest(ctx context.Context, args map[string]any) (any, error) { } // digestHere is the digest step as this program runs it. -func (m *Module) digestHere(ctx context.Context, workspace, level string) (DigestResult, error) { +func (m *Module) digestHere(ctx context.Context, workspace, level string, names []string) (DigestResult, error) { if !workspaceOK.MatchString(workspace) { return DigestResult{}, fmt.Errorf("%q is not a workspace id", workspace) } @@ -241,6 +251,14 @@ func (m *Module) digestHere(ctx context.Context, workspace, level string) (Diges besideHeader = append(besideHeader, "beside "+b) } + // The node names, sorted, as a header line for a package that reads them. + sortedNames := append([]string{}, names...) + sort.Strings(sortedNames) + var namesHeader []string + if len(sortedNames) > 0 { + namesHeader = []string{"node-names " + strings.Join(sortedNames, ",")} + } + dirs, err := m.goModDirs(ctx, tree) if err != nil { return DigestResult{}, err @@ -251,7 +269,7 @@ func (m *Module) digestHere(ctx context.Context, workspace, level string) (Diges if err != nil { return DigestResult{}, err } - if err := digestModule(&res, pkgs, tree, header, besideHeader, level == LevelIntegration); err != nil { + if err := digestModule(&res, pkgs, tree, header, besideHeader, namesHeader, level == LevelIntegration); err != nil { return DigestResult{}, err } } @@ -302,7 +320,7 @@ func parsePackages(out string) (map[string]*goPackage, error) { // digestModule adds to res the digest of each root package of one go.mod directory. A root is a package of the main // module that is listed, not only a dependency, and not a variant of another. -func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header, beside []string, needs bool) error { +func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, header, beside, names []string, needs bool) error { roots := []string{} for path, p := range pkgs { if p.ForTest == "" && !p.DepOnly && !p.Standard && !strings.HasSuffix(path, ".test") && p.Module != nil && p.Module.Main { @@ -349,7 +367,7 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he res.neverKept[path] = why continue } - d, err := packageDigest(root, testMain, pkgs, tree, header, beside, declared) + d, err := packageDigest(root, testMain, pkgs, tree, header, beside, names, declared) if err != nil { return fmt.Errorf("%s: %w", path, err) } @@ -361,9 +379,9 @@ func digestModule(res *DigestResult, pkgs map[string]*goPackage, tree string, he // packageDigest is the digest of one package: a hash over a sorted text of the header, the files of every package its // tests reach in the module's own tree (by their content, under their path in the tree), the version and checksum of // every module outside it, and the package's own test data. The beside lines (the siblings' commits) join the header -// only when a file the tests reach names MESH_CHECK_BESIDE. A package of the standard library is the Go version's. +// only when a file the tests reach names MESH_CHECK_BESIDE, and the node names likewise for MESH_NODE_NAMES. A package of the standard library is the Go version's. // The lines a package's ReadsOutsideMarker declares join them (declaredReads). -func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside, declared []string) (string, error) { +func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree string, header, beside, names, declared []string) (string, error) { reached := map[string]*goPackage{root.ImportPath: root} for _, dep := range testMain.Deps { if p := pkgs[dep]; p != nil { @@ -371,7 +389,7 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s } } lines := append([]string{}, declared...) - readsBeside := false + readsBeside, readsNames := false, false for _, p := range reached { if p.Standard || p.Module == nil { continue @@ -390,9 +408,10 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s return "", err } lines = append(lines, l) - if !readsBeside { - if raw, err := os.ReadFile(filepath.Join(p.Dir, f)); err == nil && bytes.Contains(raw, []byte(besideVariable)) { - readsBeside = true + if !readsBeside || !readsNames { + if raw, err := os.ReadFile(filepath.Join(p.Dir, f)); err == nil { + readsBeside = readsBeside || bytes.Contains(raw, []byte(besideVariable)) + readsNames = readsNames || bytes.Contains(raw, []byte(namesVariable)) } } } @@ -424,6 +443,9 @@ func packageDigest(root, testMain *goPackage, pkgs map[string]*goPackage, tree s if readsBeside { header = append(append([]string{}, header...), beside...) } + if readsNames { + header = append(append([]string{}, header...), names...) + } h := sha256.New() for _, l := range append(append([]string{}, header...), lines...) { h.Write([]byte(l + "\n")) diff --git a/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go b/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go index a6dac3a4..49d3f9a1 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go @@ -172,8 +172,9 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) { root := &goPackage{ImportPath: "x/r", Dir: dir, Module: &goModule{Path: "x", Main: true}, GoFiles: []string{"r.go"}, TestGoFiles: []string{"r.go"}} testMain := &goPackage{ImportPath: "x/r.test", Deps: []string{ext.ImportPath}} pkgs[root.ImportPath] = root + var names []string digest := func() string { - d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil, nil) + d, err := packageDigest(root, testMain, pkgs, dir, []string{"digest 1"}, nil, names, nil) if err != nil { t.Fatal(err) } @@ -188,6 +189,20 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) { if d3 := digest(); d3 == d1 { t.Errorf("a new version left the digest as it was") } + // A different node list misses the cache only for a package whose tests read MESH_NODE_NAMES (ADR 0567 §12). + names = []string{"node-names a"} + d4 := digest() + names = []string{"node-names a", "node-names b"} + if d5 := digest(); d5 != d4 { + t.Errorf("a package that does not read the node names changed with them") + } + os.WriteFile(filepath.Join(dir, "r.go"), []byte("package r\n// "+namesVariable+"\n"), 0o644) + names = []string{"node-names a"} + d6 := digest() + names = []string{"node-names a", "node-names b"} + if d7 := digest(); d7 == d6 { + t.Errorf("a new node left the digest of a package that reads the node names as it was") + } } // Issue 1935, 1936: a package whose reads-outside marker lists nothing it can trust is never kept: its digest is one no diff --git a/modules/mesh-build-go/cmd/mesh-build-go/level.go b/modules/mesh-build-go/cmd/mesh-build-go/level.go index d755c694..99a40937 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/level.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/level.go @@ -222,7 +222,7 @@ func (m *Module) answer(ctx context.Context, spec levelSpec) levelAnswer { tree := filepath.Join(m.Work, spec.Workspace, "tree") switch spec.Step { case stepDigest: - d, err := m.digestHere(ctx, spec.Workspace, spec.Level) + d, err := m.digestHere(ctx, spec.Workspace, spec.Level, spec.NodeNames) if err != nil { return levelAnswer{Error: err.Error()} }