Narrow both build modules' fetch with mesh-sdk's buildstep.Narrow, the one copy (issue 1758, ADR 0499 §4)

The local narrowing of mesh-build-go and mesh-build-node moves into mesh-sdk (feat/r4e-sdk
d1af87a), with its tests; each module keeps its fetch-level test on real git. The three
build modules are pinned to d1af87a.
This commit is contained in:
2026-10-22 18:42:17 +02:00
parent 035c950ff2
commit 59c28971cb
12 changed files with 15 additions and 333 deletions
@@ -84,7 +84,7 @@ func (m *Module) fetchTool() StepTool {
func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
// What the builder and the checker ask, in mesh-sdk's one form (ADR 0499 §4): with the module's directory and its
// named paths, the workspace is narrowed to them (narrow.go).
// named paths, the workspace is narrowed to them (buildstep.Narrow).
req, err := buildstep.ParseFetchRequest(args)
if err != nil {
return nil, err
@@ -115,7 +115,7 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
return nil, err
}
if (base == "" || res.Merge != "") && len(req.Paths) > 0 {
if err := narrow(tree, req); err != nil {
if err := buildstep.Narrow(tree, req); err != nil {
return nil, err
}
// Said beside the tree, so the steps that list the tree's go.mod files leave out those narrowed away.
@@ -187,7 +187,7 @@ func (m *Module) goModDirs(ctx context.Context, tree string) ([]string, error) {
if l = strings.TrimSpace(l); l != "" && !inTestdata(l) {
if narrowed(tree) {
if _, err := os.Stat(filepath.Join(tree, filepath.FromSlash(l))); err != nil {
continue // outside a narrowed workspace's paths (narrow.go): no module of this build
continue // outside a narrowed workspace's paths (buildstep.Narrow): no module of this build
}
}
dirs = append(dirs, filepath.ToSlash(filepath.Dir(l)))
@@ -1,83 +0,0 @@
package main
// The narrowed workspace (novox/hq ADR 0499 §4; issue 1758, the lead's decision L8): a build of a module whose every
// artifact names its paths sees only those paths, the module's manifest and its lock files. The paths come from the
// builder's request (buildstep.FetchRequest's Dir and Paths, from the indexer's kept inputs), never from a manifest.
// Everything else of the fetched tree is removed before any run step, so a read outside fails, naming the path.
import (
"fmt"
"os"
"path"
"path/filepath"
"sort"
"git.novox.be/novox/mesh-sdk/go/buildstep"
)
// narrowKept are the files of the module's directory a narrowed workspace keeps beside its paths: the manifest and the
// lock files of both languages.
var narrowKept = []string{"module.json", "package.json", "package-lock.json", "go.mod", "go.sum"}
// narrow removes from tree everything but r's paths in its module directory, the manifest and the lock files (and the
// repository's .git). A request naming no paths leaves the tree whole. A named path the tree lacks fails, naming it.
func narrow(tree string, r buildstep.FetchRequest) error {
if len(r.Paths) == 0 {
return nil
}
dir := path.Clean(r.Dir)
keep := map[string]bool{}
for _, p := range r.Paths {
rel := path.Join(dir, path.Clean(p))
if _, err := os.Lstat(filepath.Join(tree, filepath.FromSlash(rel))); err != nil {
return fmt.Errorf("the path %s the request names is not in the commit: %w", rel, err)
}
keep[rel] = true
}
for _, f := range narrowKept {
keep[path.Join(dir, f)] = true
}
return prune(tree, "", keep)
}
// prune removes under tree/rel everything that is neither kept nor above or below a kept path.
func prune(tree, rel string, keep map[string]bool) error {
entries, err := os.ReadDir(filepath.Join(tree, filepath.FromSlash(rel)))
if err != nil {
return err
}
names := make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Name())
}
sort.Strings(names)
for _, name := range names {
p := path.Join(rel, name)
if rel == "" && name == ".git" {
continue
}
switch {
case keep[p]:
continue // a kept path, whole
case holdsKept(p, keep):
if err := prune(tree, p, keep); err != nil {
return err
}
default:
if err := os.RemoveAll(filepath.Join(tree, filepath.FromSlash(p))); err != nil {
return err
}
}
}
return nil
}
// holdsKept says whether a kept path lies below p.
func holdsKept(p string, keep map[string]bool) bool {
for k := range keep {
if len(k) > len(p) && k[:len(p)+1] == p+"/" {
return true
}
}
return false
}
@@ -1,76 +0,0 @@
package main
import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/buildstep"
)
func files(t *testing.T, root string) []string {
t.Helper()
var out []string
filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
if err == nil && !d.IsDir() {
rel, _ := filepath.Rel(root, p)
if !strings.HasPrefix(rel, ".git/") {
out = append(out, filepath.ToSlash(rel))
}
}
return nil
})
slices.Sort(out)
return out
}
func narrowFixture(t *testing.T) string {
t.Helper()
tree := t.TempDir()
for _, f := range []string{".git/HEAD", "README.md", "module.json", "package.json", "package-lock.json", "tsconfig.json",
"src/index.ts", "src/deep/a.ts", "test/a.test.ts", "go/go.mod", "go/x.go", "other/module.json", "srcx/b.ts"} {
p := filepath.Join(tree, filepath.FromSlash(f))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(f), 0o644); err != nil {
t.Fatal(err)
}
}
return tree
}
// ADR 0499 §4: a request naming paths leaves only those, the manifest and the lock files (and .git); a path the commit
// lacks fails naming it; a request naming none leaves the tree whole.
func TestTheWorkspaceIsNarrowedToTheNamedPathsTheManifestAndTheLocks(t *testing.T) {
tree := narrowFixture(t)
r := buildstep.FetchRequest{Dir: ".", Paths: []string{"src", "test", "tsconfig.json"}}
if err := narrow(tree, r); err != nil {
t.Fatal(err)
}
want := []string{"module.json", "package-lock.json", "package.json", "src/deep/a.ts", "src/index.ts", "test/a.test.ts", "tsconfig.json"}
if got := files(t, tree); !slices.Equal(got, want) {
t.Errorf("kept %v, want %v", got, want)
}
if _, err := os.Stat(filepath.Join(tree, ".git", "HEAD")); err != nil {
t.Errorf("the repository's .git went: %v", err)
}
tree = narrowFixture(t)
if err := narrow(tree, buildstep.FetchRequest{Dir: "go", Paths: []string{"x.go"}}); err != nil {
t.Fatal(err)
}
if got := files(t, tree); !slices.Equal(got, []string{"go/go.mod", "go/x.go"}) {
t.Errorf("a module in a directory kept %v", got)
}
tree = narrowFixture(t)
if err := narrow(tree, buildstep.FetchRequest{Dir: ".", Paths: []string{"lib"}}); err == nil || !strings.Contains(err.Error(), "lib") {
t.Errorf("a path the commit lacks answered %v", err)
}
tree = narrowFixture(t)
before := files(t, tree)
if err := narrow(tree, buildstep.FetchRequest{}); err != nil || !slices.Equal(files(t, tree), before) {
t.Errorf("a request naming no paths changed the tree: %v", err)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildgo
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7 h1:wK9O1CgNk7mo3IdJqW/CCx/hebg+1F0rkvW8CvJfnRM=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52 h1:5k04XjpfYCq6j7rCM6Dy6FfrsBk5iUJlbwGf6WhUOUc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildimage
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7 h1:wK9O1CgNk7mo3IdJqW/CCx/hebg+1F0rkvW8CvJfnRM=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52 h1:5k04XjpfYCq6j7rCM6Dy6FfrsBk5iUJlbwGf6WhUOUc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52/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=
@@ -107,7 +107,7 @@ func (m *Module) fetchTool() StepTool {
func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
// What the builder and the checker ask, in mesh-sdk's one form (ADR 0499 §4): with the module's directory and its
// named paths, the workspace is narrowed to them (narrow.go).
// named paths, the workspace is narrowed to them (buildstep.Narrow).
req, err := buildstep.ParseFetchRequest(args)
if err != nil {
return nil, err
@@ -142,7 +142,7 @@ func (m *Module) fetch(ctx context.Context, args map[string]any) (any, error) {
}
}
if len(req.Paths) > 0 {
if err := narrow(tree, req); err != nil {
if err := buildstep.Narrow(tree, req); err != nil {
return nil, err
}
// Said beside the tree, so the listing of the tree's modules leaves out those narrowed away.
@@ -208,7 +208,7 @@ func (m *Module) typeScriptModules(ctx context.Context, tree string) (dirs []str
}
raw, err := os.ReadFile(filepath.Join(tree, filepath.FromSlash(l)))
if os.IsNotExist(err) && narrowed(tree) {
continue // outside a narrowed workspace's paths (narrow.go): no module of this build
continue // outside a narrowed workspace's paths (buildstep.Narrow): no module of this build
}
if err != nil {
return nil, nil, err
@@ -1,83 +0,0 @@
package main
// The narrowed workspace (novox/hq ADR 0499 §4; issue 1758, the lead's decision L8): a build of a module whose every
// artifact names its paths sees only those paths, the module's manifest and its lock files. The paths come from the
// builder's request (buildstep.FetchRequest's Dir and Paths, from the indexer's kept inputs), never from a manifest.
// Everything else of the fetched tree is removed before any run step, so a read outside fails, naming the path.
import (
"fmt"
"os"
"path"
"path/filepath"
"sort"
"git.novox.be/novox/mesh-sdk/go/buildstep"
)
// narrowKept are the files of the module's directory a narrowed workspace keeps beside its paths: the manifest and the
// lock files of both languages.
var narrowKept = []string{"module.json", "package.json", "package-lock.json", "go.mod", "go.sum"}
// narrow removes from tree everything but r's paths in its module directory, the manifest and the lock files (and the
// repository's .git). A request naming no paths leaves the tree whole. A named path the tree lacks fails, naming it.
func narrow(tree string, r buildstep.FetchRequest) error {
if len(r.Paths) == 0 {
return nil
}
dir := path.Clean(r.Dir)
keep := map[string]bool{}
for _, p := range r.Paths {
rel := path.Join(dir, path.Clean(p))
if _, err := os.Lstat(filepath.Join(tree, filepath.FromSlash(rel))); err != nil {
return fmt.Errorf("the path %s the request names is not in the commit: %w", rel, err)
}
keep[rel] = true
}
for _, f := range narrowKept {
keep[path.Join(dir, f)] = true
}
return prune(tree, "", keep)
}
// prune removes under tree/rel everything that is neither kept nor above or below a kept path.
func prune(tree, rel string, keep map[string]bool) error {
entries, err := os.ReadDir(filepath.Join(tree, filepath.FromSlash(rel)))
if err != nil {
return err
}
names := make([]string, 0, len(entries))
for _, e := range entries {
names = append(names, e.Name())
}
sort.Strings(names)
for _, name := range names {
p := path.Join(rel, name)
if rel == "" && name == ".git" {
continue
}
switch {
case keep[p]:
continue // a kept path, whole
case holdsKept(p, keep):
if err := prune(tree, p, keep); err != nil {
return err
}
default:
if err := os.RemoveAll(filepath.Join(tree, filepath.FromSlash(p))); err != nil {
return err
}
}
}
return nil
}
// holdsKept says whether a kept path lies below p.
func holdsKept(p string, keep map[string]bool) bool {
for k := range keep {
if len(k) > len(p) && k[:len(p)+1] == p+"/" {
return true
}
}
return false
}
@@ -1,76 +0,0 @@
package main
import (
"os"
"path/filepath"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/buildstep"
)
func files(t *testing.T, root string) []string {
t.Helper()
var out []string
filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
if err == nil && !d.IsDir() {
rel, _ := filepath.Rel(root, p)
if !strings.HasPrefix(rel, ".git/") {
out = append(out, filepath.ToSlash(rel))
}
}
return nil
})
slices.Sort(out)
return out
}
func narrowFixture(t *testing.T) string {
t.Helper()
tree := t.TempDir()
for _, f := range []string{".git/HEAD", "README.md", "module.json", "package.json", "package-lock.json", "tsconfig.json",
"src/index.ts", "src/deep/a.ts", "test/a.test.ts", "go/go.mod", "go/x.go", "other/module.json", "srcx/b.ts"} {
p := filepath.Join(tree, filepath.FromSlash(f))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(f), 0o644); err != nil {
t.Fatal(err)
}
}
return tree
}
// ADR 0499 §4: a request naming paths leaves only those, the manifest and the lock files (and .git); a path the commit
// lacks fails naming it; a request naming none leaves the tree whole.
func TestTheWorkspaceIsNarrowedToTheNamedPathsTheManifestAndTheLocks(t *testing.T) {
tree := narrowFixture(t)
r := buildstep.FetchRequest{Dir: ".", Paths: []string{"src", "test", "tsconfig.json"}}
if err := narrow(tree, r); err != nil {
t.Fatal(err)
}
want := []string{"module.json", "package-lock.json", "package.json", "src/deep/a.ts", "src/index.ts", "test/a.test.ts", "tsconfig.json"}
if got := files(t, tree); !slices.Equal(got, want) {
t.Errorf("kept %v, want %v", got, want)
}
if _, err := os.Stat(filepath.Join(tree, ".git", "HEAD")); err != nil {
t.Errorf("the repository's .git went: %v", err)
}
tree = narrowFixture(t)
if err := narrow(tree, buildstep.FetchRequest{Dir: "go", Paths: []string{"x.go"}}); err != nil {
t.Fatal(err)
}
if got := files(t, tree); !slices.Equal(got, []string{"go/go.mod", "go/x.go"}) {
t.Errorf("a module in a directory kept %v", got)
}
tree = narrowFixture(t)
if err := narrow(tree, buildstep.FetchRequest{Dir: ".", Paths: []string{"lib"}}); err == nil || !strings.Contains(err.Error(), "lib") {
t.Errorf("a path the commit lacks answered %v", err)
}
tree = narrowFixture(t)
before := files(t, tree)
if err := narrow(tree, buildstep.FetchRequest{}); err != nil || !slices.Equal(files(t, tree), before) {
t.Errorf("a request naming no paths changed the tree: %v", err)
}
}
+1 -1
View File
@@ -3,7 +3,7 @@ module meshbuildnode
go 1.26.0
require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52
github.com/distribution/distribution/v3 v3.1.2
github.com/nats-io/nats.go v1.54.0
)
+2 -2
View File
@@ -1,5 +1,5 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7 h1:wK9O1CgNk7mo3IdJqW/CCx/hebg+1F0rkvW8CvJfnRM=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022160024-9cb4d59093a7/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52 h1:5k04XjpfYCq6j7rCM6Dy6FfrsBk5iUJlbwGf6WhUOUc=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022163107-d1af87a83a52/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI=
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=