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:
@@ -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) {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user