BG8, BN8, BI2: each artifact carries the version the ask names

A Go program is linked with -X main.meshVersion beside main.builtFor, a Node program's package.json version is set, an
image is labelled org.opencontainers.image.version; a build with no version stamps none.

Builds: design 47 G §458, §573

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 05:15:43 +01:00
parent f339ea5936
commit 48ad5ce947
6 changed files with 45 additions and 3 deletions
@@ -26,6 +26,8 @@ const (
recipeFlags = "-trimpath -buildvcs=false"
recipeLinkerFlags = "-s -w"
recipeSystemStamp = "main.builtFor"
// versionStamp is the Go variable a program carries its version in (design 47 G §458).
versionStamp = "main.meshVersion"
// outDir is where a build leaves an artifact in the module's directory (builder.Out).
outDir = ".mesh-build"
)
@@ -153,6 +155,10 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
if system := strings.TrimSpace(art.System); system != "" {
linker += " -X " + recipeSystemStamp + "=" + system
}
// A build asked with a version carries it in main.meshVersion beside main.builtFor; one with none stamps none (§458).
if version != "" {
linker += " -X " + versionStamp + "=" + version
}
cmd := []string{"-C", dir, "build", "-ldflags", linker, "-o", filepath.Join(out, binary), "./" + strings.Trim(art.From, "./")}
// Shut off (ADR 0448): the shared caches are written by a trunk build alone (publish), a candidate's go build writes
// beneath a throwaway layer.
@@ -273,6 +273,13 @@ func TestTheBuildGetsTheVersionTheAskCarriesAsMeshVersion(t *testing.T) {
}
got := ""
for _, r := range *calls {
// §458: the program is linked with main.meshVersion beside main.builtFor, and with no version none is stamped.
if strings.Contains(r.line, " build ") && strings.Contains(r.line, "-X main.meshVersion=") != (version != "") {
t.Errorf("ask version %q: the go build line is %q", version, r.line)
}
if version != "" && strings.Contains(r.line, " build ") && !strings.Contains(r.line, "-X main.meshVersion="+version) {
t.Errorf("ask version %q: the go build line lacks main.meshVersion: %q", version, r.line)
}
if strings.HasPrefix(r.line, "go ") {
for _, e := range r.env {
if strings.HasPrefix(e, "MESH_VERSION=") {
@@ -172,7 +172,7 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
argv = append(argv, imageArgs(manifest)...)
// The version the build ask carries is the build argument MESH_VERSION (design 47 G §370).
if version != "" {
argv = append(argv, "--build-arg", "MESH_VERSION="+version)
argv = append(argv, "--build-arg", "MESH_VERSION="+version, "--label", "org.opencontainers.image.version="+version) // the label of §458
}
argv = append(argv, contextDir)
if r := m.Run(ctx, m.env(), "buildah", argv...); r.Status != 0 || r.Err != "" {
@@ -338,12 +338,12 @@ func TestTheImageBuildGetsTheVersionTheAskCarriesAsMeshVersion(t *testing.T) {
t.Fatalf("step = %+v", st)
}
bud := f.called("bud")
if len(bud) != 1 || !strings.Contains(bud[0].line, "--build-arg MESH_VERSION=1.4.3") {
if len(bud) != 1 || !strings.Contains(bud[0].line, "--build-arg MESH_VERSION=1.4.3") || !strings.Contains(bud[0].line, "--label org.opencontainers.image.version=1.4.3") {
t.Fatalf("bud = %+v", bud)
}
m, f, states = setup(t, same)
runBuild(t, m, states, nil)
if bud := f.called("bud"); len(bud) != 1 || strings.Contains(bud[0].line, "MESH_VERSION") {
if bud := f.called("bud"); len(bud) != 1 || (strings.Contains(bud[0].line, "MESH_VERSION") || strings.Contains(bud[0].line, "image.version")) {
t.Fatalf("a build with no version: %+v", bud)
}
}
@@ -137,6 +137,14 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) {
if err := copyPackages(filepath.Join(m.toolchainDir(), "node_modules"), filepath.Join(stage, "node_modules")); err != nil {
return nil, fmt.Errorf("providing the toolchain's packages to the build: %w", err)
}
// A program reads its own version from its package.json `version`, which is the version the ask carries (§458); the
// staged copy is the build's own, so nothing is restored. A build with no version, or a module with no package.json,
// keeps what it carries.
if _, err := os.Stat(filepath.Join(dir, "package.json")); err == nil && version != "" {
if _, err := setPackageVersion(dir, version); err != nil {
return nil, err
}
}
out, final := Out(a.Name), Out(a.Name)+".bundled"
if err := os.MkdirAll(filepath.Join(dir, out), 0o755); err != nil {
return nil, err
@@ -328,3 +328,24 @@ func TestTheBundleBuildGetsTheVersionTheAskCarriesAsMeshVersion(t *testing.T) {
}
}
}
// Design 47 G §458 (BN8): a Node program reads its version from its package.json `version`, which is the version the
// ask carries; a build with no version keeps what the package.json carries.
func TestTheBundleCarriesTheAsksVersionInItsPackageJSON(t *testing.T) {
for _, c := range []struct{ version, want string }{{"1.4.3", "1.4.3"}, {"", "0.0.0"}} {
m, f, work := builtMachine(t, "")
workspaceWith(t, work, map[string]string{"modules/m/package.json": `{"name":"m","version":"0.0.0"}`})
args := buildArgs()
if c.version != "" {
args["version"] = c.version
}
if _, err := m.build(context.Background(), args); err != nil {
t.Fatal(err)
}
_ = f
raw, err := os.ReadFile(filepath.Join(work, "w1", "stage-tools", "modules", "module", "package.json"))
if err != nil || !strings.Contains(string(raw), `"version": "`+c.want+`"`) && !strings.Contains(string(raw), `"version":"`+c.want+`"`) {
t.Errorf("ask version %q: the staged package.json is %q (%v), want version %s", c.version, raw, err, c.want)
}
}
}