The builder resolves the SDK from the mesh registry, and can publish packages

A new 'package' artifact kind builds a module's own code on a public base image
and publishes it to the mesh's package registry by version (hq ADR 0076) — the
SDK above all, which the toolchain is built from and so cannot be built in the
toolchain. The credential a build needs to resolve or publish packages is
rendered as an .npmrc (basic auth, hq ADR 0048) and given to an image build as a
buildkit secret, never a layer, so a token is not baked into the toolchain image.

Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
2026-09-16 10:27:26 +02:00
parent 2fb700d61b
commit 4b9bc50aad
10 changed files with 588 additions and 26 deletions
+99 -3
View File
@@ -6,6 +6,7 @@ import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"os"
@@ -68,7 +69,7 @@ type Result struct {
// archive failed would otherwise leave half of itself in the store under a digest the mesh never
// records — reachable, unreferenced, and indistinguishable from something in use.
func Build(ctx context.Context, run Runner, publish Publisher,
repository, path, ref, workspace string, held map[string]string, log Log) (Result, error) {
repository, path, ref, workspace string, held map[string]string, npmrc Npmrc, log Log) (Result, error) {
say := logging(log)
say("clone", "%s%s at %s", repository, describePath(path), refOrHead(ref))
@@ -124,6 +125,22 @@ func Build(ctx context.Context, run Runner, publish Publisher,
}
say("manifest", "%s v%s — %d artifact(s)", manifest.Module, manifest.Version, artifactCount(manifest))
// A build-time credential, written where a build can mount it but never where it can be copied
// into an image or committed: under the workspace, beside the clone, not inside it. Absent when
// this mesh has no package registry yet, which is the bootstrap case (novox/hq ADR 0076).
var npmrcPath string
if npmrc.Enabled() {
content, err := npmrc.File()
if err != nil {
return Result{}, err
}
npmrcPath = filepath.Join(workspace, "npmrc")
if err := os.WriteFile(npmrcPath, []byte(content), 0o600); err != nil {
return Result{}, fmt.Errorf("cannot write the package-registry credential for the build: %w", err)
}
say("packages", "resolving %s from the mesh's package registry", npmrc.Scope)
}
var built []catalogue.Built
if manifest.Build != nil {
// What this module said it stands on, answered with what this mesh actually holds. Done
@@ -143,7 +160,7 @@ func Build(ctx context.Context, run Runner, publish Publisher,
sort.Slice(artifacts, func(i, j int) bool { return artifacts[i].Name < artifacts[j].Name })
for _, a := range artifacts {
say("artifact", "%s (%s%s) — starting", a.Name, a.Kind, langSuffix(a))
made, err := one(ctx, run, publish, manifest.Module, within, commit, a, args, held, say)
made, err := one(ctx, run, publish, manifest.Module, within, commit, a, args, held, npmrcPath, say)
if err != nil {
say("artifact", "%s FAILED: %v", a.Name, err)
return Result{}, err
@@ -282,7 +299,7 @@ const ManifestName = "module.json"
func one(ctx context.Context, run Runner, publish Publisher,
module, tree, commit string, a catalogue.Artifact, args []string,
held map[string]string, say func(step, format string, args ...any)) (catalogue.Built, error) {
held map[string]string, npmrc string, say func(step, format string, args ...any)) (catalogue.Built, error) {
switch a.Kind {
case catalogue.ArtifactUpstream:
@@ -311,6 +328,12 @@ func one(ctx context.Context, run Runner, publish Publisher,
if a.Target != "" {
invocation = append(invocation, "--target", a.Target)
}
if npmrc != "" {
// Given to the build as a buildkit secret, so a RUN that needs the package registry mounts
// it at that step and it is in no image layer. A Dockerfile that does not ask for it is
// unaffected; the secret is simply not read (novox/hq ADR 0076).
invocation = append(invocation, "--secret", "id=npmrc,src="+npmrc)
}
invocation = append(invocation, ".")
say("image", "docker build -f %s", a.From)
if _, err := run(ctx, tree, "docker", invocation...); err != nil {
@@ -365,6 +388,19 @@ func one(ctx context.Context, run Runner, publish Publisher,
}
return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: where, Digest: digest}, nil
case catalogue.ArtifactPackage:
// Built and published on a public base, to the mesh's package registry, by version
// (novox/hq ADR 0076). Not an image, not an archive: nothing on a machine references it, so
// 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)
reference, err := publishPackage(ctx, run, module, tree, a, npmrc, say)
if err != nil {
return catalogue.Built{}, fmt.Errorf("%s: publishing %s failed: %w", module, a.Name, err)
}
say("package", "published %s", reference)
return catalogue.Built{Name: a.Name, Kind: a.Kind, Reference: reference}, nil
case catalogue.ArtifactArchive:
body, err := pack(filepath.Join(tree, a.From))
if err != nil {
@@ -541,6 +577,66 @@ func standingOn(manifest catalogue.Manifest, held map[string]string) ([]string,
// where its dependencies resolve upward into the base's own library directory, so what it is
// compiled against is exactly what it will run against — the reason every hand-written Dockerfile
// had to choose a working directory carefully, and the reason none of them has to now.
// publishPackage builds a `package` artifact on a public base image and publishes it to the mesh's
// package registry by version. The credential arrives as an .npmrc file the build was handed
// (novox/hq ADR 0076); it is mounted read-only into the container rather than baked, because a
// package build produces no image to leak it into. The reference returned is name@version, read from
// the module's own package.json — the same two fields npm publishes under.
func publishPackage(ctx context.Context, run Runner, module, dir string, a catalogue.Artifact,
npmrc string, say func(step, format string, args ...any)) (string, error) {
recipe, ok := packageRecipes[a.Language]
if !ok {
return "", fmt.Errorf(
"a package written in %q cannot be built: no public toolchain is known for it", a.Language)
}
if npmrc == "" {
// A package with nowhere to be published is not built. Said here rather than failing inside
// npm publish with a message about a registry that is simply absent.
return "", fmt.Errorf(
"%s is a package and this build was given no package registry to publish it to", a.Name)
}
raw, err := os.ReadFile(filepath.Join(dir, "package.json"))
if err != nil {
return "", fmt.Errorf("a package is published by name and version, and %s has no package.json: %w", module, err)
}
var pkg struct {
Name string `json:"name"`
Version string `json:"version"`
}
if err := json.Unmarshal(raw, &pkg); err != nil {
return "", fmt.Errorf("%s's package.json is not readable: %w", module, err)
}
if pkg.Name == "" || pkg.Version == "" {
return "", fmt.Errorf("%s's package.json names no %s to publish under",
module, either(pkg.Name == "", "name", "version"))
}
const within = "/app/module"
invocation := []string{
"run", "--rm",
"--volume", dir + ":" + within,
// Read-only, so a build cannot alter the credential, and at /root where npm reads it.
"--volume", npmrc + ":/root/.npmrc:ro",
"--workdir", within,
recipe.Base,
"sh", "-c", recipe.Script,
}
if _, err := run(ctx, dir, "docker", invocation...); err != nil {
return "", err
}
return pkg.Name + "@" + pkg.Version, nil
}
// either names whichever of two fields is the missing one, for a message that says which.
func either(first bool, a, b string) string {
if first {
return a
}
return b
}
func compile(ctx context.Context, run Runner, tree string, chain Toolchain,
base string, a catalogue.Artifact) (string, error) {