Let the build agent run on the genesis document until its registry binds (hq issue 121, ADR 0336)
mesh/merge-gate head a641ed7d15c7 merged into trunk b12c4b82e820 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head a641ed7d15c7 merged into trunk b12c4b82e820 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/121-genesis-package-registry delivered: every member is delivered
mesh/merge-gate head a641ed7d15c7 merged into trunk b12c4b82e820 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head a641ed7d15c7 merged into trunk b12c4b82e820 — pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/121-genesis-package-registry delivered: every member is delivered
With npm-package-registry optional, the binding file the agent's environment names may be absent, and mesh-builder failed every build on reading it. It now reads the genesis document the installer seals at genesis's packages step when no binding is present, prefers the binding when both are, and each build's log says which it resolved from (§4). A build that needs the registry - a package, an image naming .npmrc, TypeScript with an @novox/ dependency - on an agent with neither is refused before anything is built or published, naming the machine, the unbound requirement and gitea (§6); it never falls back to the public registry. The captured catalogue moves to the mesh-catalog change it is judged with.
This commit is contained in:
@@ -2,9 +2,16 @@ package builder
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// Npmrc is what a build needs to resolve the mesh's own shared library — and any other package the
|
||||
@@ -37,8 +44,36 @@ type Npmrc struct {
|
||||
// username is the consumer's mesh identity. Ignored when Token is set.
|
||||
Username string
|
||||
Password string
|
||||
|
||||
// From says where the build agent resolved this registry from (novox/hq ADR 0336 §4): its
|
||||
// npm-package-registry binding, the genesis document the installer sealed to it, or the
|
||||
// environment of a builder run by a person. Empty when it resolved none.
|
||||
From string
|
||||
// Machine is the machine the build agent runs on, named when a build that needs the registry is
|
||||
// refused for having none (ADR 0336 §6).
|
||||
Machine string
|
||||
}
|
||||
|
||||
// Where a build agent resolves the mesh's package registry from (novox/hq ADR 0336 §3, §4).
|
||||
const (
|
||||
// FromBinding is the npm-package-registry binding the mesh wrote, with the credential it sealed.
|
||||
FromBinding = "the npm-package-registry binding"
|
||||
// FromGenesis is the genesis document the installer sealed at genesis's packages step, read only
|
||||
// while no binding is present.
|
||||
FromGenesis = "the genesis document"
|
||||
// FromEnvironment is the environment of a builder run by a person.
|
||||
FromEnvironment = "the environment"
|
||||
)
|
||||
|
||||
// RegistryRequirement is the build agent's optional requirement the registry is bound through, and
|
||||
// RegistryProvider the catalogue module that provides it (novox/hq ADR 0336 §1, §6): named when a
|
||||
// build that needs the registry is refused, so the outcome says what to assign. The controller's
|
||||
// catalogue test holds the provider to the catalogue's.
|
||||
const (
|
||||
RegistryRequirement = "npm-package-registry"
|
||||
RegistryProvider = "gitea"
|
||||
)
|
||||
|
||||
// Enabled reports whether there is a registry to resolve against at all. A bootstrap build that
|
||||
// runs before any package registry exists has none, and must still build whatever needs no
|
||||
// mesh-published dependency.
|
||||
@@ -90,6 +125,77 @@ func (n Npmrc) File() (string, error) {
|
||||
return fmt.Sprintf("%s:registry=%s\n%s", scope, reg, auth), nil
|
||||
}
|
||||
|
||||
// needsRegistry says why this module's build needs the mesh's package registry, or "" when it needs
|
||||
// none (novox/hq ADR 0336 §5): a `package` artifact, an image whose Dockerfile names .npmrc, or a
|
||||
// TypeScript artifact with an `@novox/` dependency. Every Go artifact, every image that does not name
|
||||
// .npmrc, and every image pulled upstream builds without it.
|
||||
func needsRegistry(manifest catalogue.Manifest, within string) (string, error) {
|
||||
if manifest.Build == nil {
|
||||
return "", nil
|
||||
}
|
||||
for _, a := range manifest.Build.Artifacts {
|
||||
switch {
|
||||
case a.Kind == catalogue.ArtifactPackage:
|
||||
return fmt.Sprintf("%s is a package, published to the registry", a.Name), nil
|
||||
case a.Kind == catalogue.ArtifactImage:
|
||||
raw, err := os.ReadFile(filepath.Join(within, a.From))
|
||||
if err == nil && strings.Contains(string(raw), ".npmrc") {
|
||||
return fmt.Sprintf("%s's Dockerfile names .npmrc", a.Name), nil
|
||||
}
|
||||
case a.Language == "typescript":
|
||||
scoped, err := scopedDependencies(within)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(scoped) > 0 {
|
||||
return fmt.Sprintf("%s is TypeScript and depends on %s", a.Name, strings.Join(scoped, ", ")), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// scopedDependencies is every `@novox/` package the module's package.json depends on, the SDK
|
||||
// included.
|
||||
func scopedDependencies(tree string) ([]string, error) {
|
||||
raw, err := os.ReadFile(filepath.Join(tree, "package.json"))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var p struct {
|
||||
Dependencies map[string]string `json:"dependencies"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &p); err != nil {
|
||||
return nil, fmt.Errorf("the module's package.json is not JSON: %w", err)
|
||||
}
|
||||
var names []string
|
||||
for name := range p.Dependencies {
|
||||
if strings.HasPrefix(name, "@novox/") {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// unbound is the refusal of a build that needs the registry on a build agent that has none (novox/hq
|
||||
// ADR 0336 §6): it names the machine, the requirement left unbound there and the module that provides
|
||||
// it. Nothing is built, nothing is published, and the `@novox` scope is never resolved from the
|
||||
// public registry instead.
|
||||
func (n Npmrc) unbound(module, why string) error {
|
||||
machine := n.Machine
|
||||
if machine == "" {
|
||||
machine = "this machine"
|
||||
}
|
||||
return fmt.Errorf("%s needs the mesh's package registry (%s), and %s is unbound on %s: the build "+
|
||||
"agent there has neither its binding nor the genesis document. It is provided by the %s module; "+
|
||||
"once %s is assigned the requirement binds (novox/hq ADR 0336 §6)",
|
||||
module, why, RegistryRequirement, machine, RegistryProvider, RegistryProvider)
|
||||
}
|
||||
|
||||
// packageRecipe is how a `package` artifact is built and published: on a PUBLIC base image, never
|
||||
// the mesh toolchain, because the toolchain is built from the package this produces (the SDK). The
|
||||
// script builds the module, then publishes it to the mesh's package registry unless that exact
|
||||
|
||||
Reference in New Issue
Block a user