Say what a build was made from, and hand an image compiling Go only that (hq ADR 0267, issue 363)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

A merge to the controller's repository moved the route proxy and the build
seat's holder whatever it changed, because nothing said which files their
builds read. A build of a trunk commit now says its build source per
repository: a Go program's import closure, an archive's directory, an
image's recipe and the package it names in the new 'compiles' field. That
image is built from its build source alone, so a recipe reading past it
fails by name, and its fingerprint is over what it was handed.
This commit is contained in:
jochen
2026-10-10 02:47:35 +02:00
parent 9517f590ac
commit 4d1b81b6cb
10 changed files with 1222 additions and 6 deletions
+87 -5
View File
@@ -11,6 +11,7 @@ import (
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"sort"
@@ -87,6 +88,23 @@ type Result struct {
// pin the build. Two builds with one fingerprint are one build, whatever digests they made
// (novox/hq issue 280).
Source string
// Sources are what this build was made from, as files (novox/hq ADR 0267 rule 1): per repository, the
// entries a changed file is tested against (SourceHolds). The module's own repository has an empty
// Repository. Said only for a build of a commit on the trunk, and only for a repository whose every
// artifact's build source is known — a Go program's import closure, an archive's directory, an image's
// recipe and the package it compiles; for any other, nothing is said and the whole of what the build
// sees stays its source, as before.
Sources []BuildSource
}
// BuildSource is the build source a build read in one repository (novox/hq ADR 0267).
type BuildSource struct {
// Repository and Ref are a context's, as the manifest names it; empty for the module's own.
Repository string
Ref string
// Paths are the entries, relative to the repository's root (SourceHolds).
Paths []string
}
// GitCredential is the forge credential a clone may present when the server asks for one.
@@ -206,6 +224,10 @@ func build(ctx context.Context, run Runner, publish Publisher,
// What it is made from, for its source fingerprint: the module's own tree first.
src := newSourceInputs(manifest.Module)
src.prefix = strings.Trim(filepath.ToSlash(filepath.Clean(path)), "/")
if src.prefix == "." {
src.prefix = ""
}
if src.tree, err = gitTree(ctx, run, tree, path); err != nil {
src.notPinned("its tree could not be named: " + err.Error())
}
@@ -298,9 +320,22 @@ func build(ctx context.Context, run Runner, publish Publisher,
if fingerprint == "" {
say("source", "no source fingerprint: %s", orNoTree(src.unpinned))
}
// **Only a trunk build says its build source** (novox/hq ADR 0267): the planner maps the next merge onto
// the build source of the trunk's last build, and a branch's closure is not the trunk's.
var sources []BuildSource
if trunk != "" && onTrunk {
sources = src.buildSources()
for _, s := range sources {
where := "its own repository"
if s.Repository != "" {
where = s.Repository
}
say("source", "%d path(s) of %s", len(s.Paths), where)
}
}
return Result{Manifest: resolved, Commit: commit, Built: built,
Against: against(within, manifest, stoodOn), Read: readBy(manifest), Source: fingerprint,
Trunk: trunk, OnTrunk: onTrunk, Branches: branches}, nil
Trunk: trunk, OnTrunk: onTrunk, Branches: branches, Sources: sources}, nil
}
// branchesHolding is every branch of a fresh clone's origin the commit is on, without `origin/`.
@@ -648,15 +683,48 @@ func one(ctx context.Context, run Runner, publish Publisher,
} else if src != nil {
src.contexts[a.Name] = t
}
// docker build accepts -f outside the context it is given; the recipe stays exactly
// where it was read from and validated against, absolute so the working directory
// switching to the cloned context does not change which file that is.
buildDir = cloned
}
// docker build accepts -f outside the context it is given; the recipe stays exactly where it was
// read from and validated against, absolute so a context elsewhere does not change which file
// that is.
if buildDir != tree || a.Compiles != "" {
absRecipe, err := filepath.Abs(filepath.Join(tree, a.From))
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: %s's recipe: %w", module, a.Name, err)
}
recipePath = absRecipe
buildDir = cloned
}
// **An image that compiles a Go program is handed its build source and nothing else** (novox/hq
// ADR 0267 rules 1 and 3): the program's import closure, read from the context it is built in, so a
// merge elsewhere in that repository is no change to it — and a recipe that copies a file outside
// it fails here, naming the file, rather than building from something no merge is mapped onto.
if a.Compiles != "" {
paths, err := GoBuildSource(buildDir, a.Compiles)
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: %s compiles %s, whose build source cannot be read: %w",
module, a.Name, a.Compiles, err)
}
narrowed := filepath.Join(workspace, "narrow-"+a.Name)
sum, err := narrowTree(buildDir, narrowed, paths)
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: handing %s its build source: %w", module, a.Name, err)
}
say("image", "%s is handed its build source: %d path(s) of %s", a.Name, len(paths), a.Compiles)
if a.Context != nil {
src.contexts[a.Name] = sum
src.readIn(*a.Context, paths)
} else {
src.ownHas(paths...)
}
buildDir = narrowed
} else if a.Context != nil {
src.readWhole(*a.Context)
}
if a.Compiles != "" || a.Context != nil {
src.ownHas(a.From)
} else {
src.ownWhole()
}
invocation := append([]string{"build", "-f", recipePath, "-t", local}, args...)
if a.Target != "" {
@@ -708,6 +776,18 @@ func one(ctx context.Context, run Runner, publish Publisher,
if src != nil {
src.toolchains[a.Name] = toolchainOf(chain, base)
}
// A Go program's build source is its import closure (novox/hq ADR 0267 rule 1). Not read, it is the
// module's whole directory, as before — said, so the wider plan has a reason a person can find.
if chain.Language == "go" {
if paths, err := GoBuildSource(tree, a.From); err != nil {
say("bundle", "%s's build source is its whole directory: %v", a.Name, err)
src.ownWhole()
} else {
src.ownHas(paths...)
}
} else {
src.ownWhole()
}
if chain.Language == "typescript" {
if own, _ := ownDependencies(tree); len(own) > 0 {
src.notPinned(a.Name + " resolves packages of its own at build time")
@@ -754,6 +834,7 @@ func one(ctx context.Context, run Runner, publish Publisher,
// there is no Publisher call — the container itself publishes, with the credential the
// build was handed.
say("package", "building and publishing %s (%s)", a.Name, a.Language)
src.ownWhole()
src.notPinned(a.Name + " is a package, built from what the registry holds when it is built")
reference, err := publishPackage(ctx, run, module, tree, a, npmrc, say)
if err != nil {
@@ -763,6 +844,7 @@ func one(ctx context.Context, run Runner, publish Publisher,
return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: reference}, nil
case catalogue.ArtifactArchive:
src.ownHas(strings.Trim(path.Clean("/"+filepath.ToSlash(a.From)), "/") + "/**")
body, err := pack(filepath.Join(tree, a.From))
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: packing %s failed: %w", module, a.Name, err)