Read the manifest word paths, so a package names the files it is built from (issue 1758, S1, ADR 0499 §1)

A package or a non-Go bundle may name its build source; a Go program, an archive, an
image and an upstream artifact refuse the word by name, as do a path leaving the
module's directory, the whole directory and a pattern.
This commit is contained in:
2026-10-22 17:25:02 +02:00
parent 5652312f09
commit 06fe1ee8b3
3 changed files with 96 additions and 0 deletions
+39
View File
@@ -394,6 +394,45 @@ func (b *Build) problems(module string) []string {
"%s: %q mirrors %q, which names no tag or digest", module, a.Name, a.From))
}
problems = append(problems, upstreamFileProblems(module, a)...)
problems = append(problems, pathsProblems(module, a)...)
}
return problems
}
// pathsProblems is what is wrong with an artifact's `paths` (novox/hq ADR 0499 clause 1): named on a kind whose build
// source is derived or named otherwise, or a path that leaves the module's directory, is its whole directory, or is a
// pattern.
func pathsProblems(module string, a Artifact) []string {
if a.Paths == nil {
return nil
}
switch {
case a.Kind == ArtifactBundle && a.Language == "go":
return []string{fmt.Sprintf("%s: %q is a Go program and names paths: its build source is derived from the "+
"package it compiles (novox/hq ADR 0267 clause 1, ADR 0499 clause 1)", module, a.Name)}
case a.Kind != ArtifactBundle && a.Kind != ArtifactPackage:
return []string{fmt.Sprintf("%s: %q is a %q and names paths; only a package or a bundle in a language other "+
"than Go names the paths it is built from (novox/hq ADR 0499 clause 1)", module, a.Name, a.Kind)}
case len(a.Paths) == 0:
return []string{fmt.Sprintf("%s: %q names no path in paths; leave the word out to build from the module's "+
"directory", module, a.Name)}
}
var problems []string
for _, p := range a.Paths {
clean := path.Clean(p)
switch {
case strings.TrimSpace(p) == "":
problems = append(problems, fmt.Sprintf("%s: %q names an empty path", module, a.Name))
case strings.HasPrefix(p, "/") || clean == ".." || strings.HasPrefix(clean, "../"):
problems = append(problems, fmt.Sprintf("%s: %q names the path %q, which leaves the module's directory",
module, a.Name, p))
case clean == ".":
problems = append(problems, fmt.Sprintf("%s: %q names the module's whole directory (%q) as a path; leave "+
"the word out for that", module, a.Name, p))
case strings.ContainsAny(p, "*?[]{}\\"):
problems = append(problems, fmt.Sprintf("%s: %q names %q, which is a pattern; paths names files and "+
"directories", module, a.Name, p))
}
}
return problems
}
+11
View File
@@ -1191,6 +1191,17 @@ type Artifact struct {
// (novox/hq ADR 0193). Absent for nearly every bundle, which is then one file per entrypoint.
External []string `json:"external,omitempty"`
// Paths are the files and directories, relative to the module's directory, a package or a bundle in a language
// other than Go is built from (novox/hq ADR 0499 clause 1): with the module's manifest they are its build source,
// and a module whose every artifact names them takes its fingerprint's tree from them alone (clause 2). Absent means
// the module's directory, as before.
//
// **Named, because the build source of a package sharing a repository cannot be derived.** A Go program's is
// (ADR 0267 clause 1), an archive's `from` names it and an image's is the context its manifest names, so each of those
// refuses the word. A path stays inside the module's directory and is no pattern: a build source that could reach
// out of it, or grow with a directory listing, would be one no fingerprint pins.
Paths []string `json:"paths,omitempty"`
// Env is what a tools bundle is given on a machine (novox/hq ADR 0192): words and their values,
// paths and constants composed with ${dir:…} and ${port:…} exactly as a container's environment
// is, never a secret's content. The node's runtime hands it to this bundle and to no other.
+46
View File
@@ -0,0 +1,46 @@
package catalogue
import (
"strings"
"testing"
)
// An artifact names the paths it is built from with `paths` (novox/hq ADR 0499 clause 1): a package, or a bundle in a
// language other than Go, may; a Go program's artifact, an archive, an image and an upstream artifact may not, and the
// refusal names the artifact and its kind. Each path stays inside the module's directory: no `..`, no absolute path,
// no pattern.
func TestAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","slug":"sdk","build":{"artifacts":[
{"name":"lib","kind":"package","language":"typescript",
"paths":["src","test","package.json","package-lock.json","tsconfig.json"]}]},"resources":[]}`))
if err != nil {
t.Fatalf("a package naming its paths was refused: %v", err)
}
if got := strings.Join(m.Build.Artifacts[0].Paths, ","); got != "src,test,package.json,package-lock.json,tsconfig.json" {
t.Fatalf("the paths were not kept as written: %q", got)
}
if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"],"loads":["index.js"],"paths":["src","package.json"]}]}}`)); err != nil {
t.Fatalf("a TypeScript bundle naming its paths was refused: %v", err)
}
for _, c := range []struct{ artifact, says string }{
{`{"name":"x","kind":"bundle","language":"go","system":"linux-amd64","paths":["cmd"]}`, `"x" is a Go program`},
{`{"name":"x","kind":"archive","from":"files","paths":["files"]}`, `"x" is a "archive"`},
{`{"name":"x","kind":"image","from":"Dockerfile","paths":["src"]}`, `"x" is a "image"`},
{`{"name":"x","kind":"upstream","from":"nats:2.10","paths":["src"]}`, `"x" is a "upstream"`},
{`{"name":"x","kind":"package","language":"typescript","paths":["../other"]}`, "leaves the module's directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":["src/../../x"]}`, "leaves the module's directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":["/etc"]}`, "leaves the module's directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":["src/*.ts"]}`, "is a pattern"},
{`{"name":"x","kind":"package","language":"typescript","paths":["src/[ab]"]}`, "is a pattern"},
{`{"name":"x","kind":"package","language":"typescript","paths":[""]}`, "names an empty path"},
{`{"name":"x","kind":"package","language":"typescript","paths":["."]}`, "names the module's whole directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":[]}`, "names no path"},
} {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}`))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v, wanted a refusal saying %q", c.artifact, err, c.says)
}
}
}