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:
+152
-53
@@ -18,11 +18,13 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -54,6 +56,8 @@ is dialled except the broker.
|
||||
MESH_BINDING a file the mesh wrote saying where the artifact store is
|
||||
MESH_PACKAGE_BINDING a file the mesh wrote saying where the package registry is
|
||||
MESH_NPM_TOKEN_FILE a file the mesh sealed holding the token for it
|
||||
MESH_GENESIS_PACKAGE_REGISTRY the document the installer sealed at genesis, read while no
|
||||
binding is present (novox/hq ADR 0336)
|
||||
MESH_NPM_REGISTRY a package registry URL, when the mesh has not said (a person, the bootstrap)
|
||||
MESH_NPM_TOKEN the token for it, likewise
|
||||
MESH_NPM_SCOPE the scope it answers for (default: @novox)
|
||||
@@ -254,7 +258,10 @@ func answer(ctx context.Context, publisher builder.Publisher, on, workspace stri
|
||||
}
|
||||
say("build", what)
|
||||
|
||||
npmrc, err := packagesFrom()
|
||||
npmrc, err := packagesFrom(on)
|
||||
if err == nil {
|
||||
say("packages", saysRegistry(npmrc))
|
||||
}
|
||||
var built builder.Result
|
||||
if request.Check != nil {
|
||||
// **A pull request's merge check, not a build** (novox/hq to-be 45 §9): nothing is built,
|
||||
@@ -386,79 +393,171 @@ func layerOf(l *builder.Layer) *link.CheckLayer {
|
||||
}
|
||||
|
||||
// packagesFrom is where a build resolves the mesh's own published packages — the SDK above all
|
||||
// (novox/hq ADR 0076, issue 053).
|
||||
// (novox/hq ADR 0076, issue 053) — on this machine.
|
||||
//
|
||||
// Preferably from the mesh: a package-registry binding names the endpoint the way the artifact
|
||||
// store's binding does, and a sealed token file the credential the way the broker's does. The
|
||||
// environment variables remain for a builder run by a person, and for the bootstrap, where there is
|
||||
// no registry yet — there the result is disabled and a build that needs no mesh-published dependency
|
||||
// builds anyway.
|
||||
func packagesFrom() (builder.Npmrc, error) {
|
||||
// **The binding wins, then the genesis document** (novox/hq ADR 0336 §3, §4). The build agent's
|
||||
// npm-package-registry requirement is optional: on genesis's node, before the gitea module is assigned,
|
||||
// nothing provides it, and the installer sealed the forge it raised to this agent as one document
|
||||
// instead. Once the requirement binds, the binding names the endpoint the way the artifact store's
|
||||
// does and a sealed token file the credential, and the genesis document is left unread. A binding
|
||||
// file named and absent is a requirement not bound here, never a fault: the environment names the
|
||||
// path whether or not the mesh wrote it.
|
||||
//
|
||||
// The environment variables remain for a builder run by a person. With none of the three the result
|
||||
// is disabled and says this machine, so a build that needs the registry is refused naming it (§6),
|
||||
// and a build that needs none builds anyway.
|
||||
func packagesFrom(machine string) (builder.Npmrc, error) {
|
||||
scope := strings.TrimSpace(os.Getenv("MESH_NPM_SCOPE"))
|
||||
if scope == "" {
|
||||
scope = "@novox"
|
||||
}
|
||||
none := builder.Npmrc{Machine: machine}
|
||||
|
||||
registry := strings.TrimSpace(os.Getenv("MESH_NPM_REGISTRY"))
|
||||
var username string
|
||||
if path := strings.TrimSpace(os.Getenv("MESH_PACKAGE_BINDING")); path != "" {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return builder.Npmrc{}, fmt.Errorf("cannot read what the mesh said about the package registry: %w", err)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
// Not bound on this machine: the genesis document, or nothing.
|
||||
case err != nil:
|
||||
return none, fmt.Errorf("cannot read what the mesh said about the package registry: %w", err)
|
||||
default:
|
||||
return fromBinding(path, raw, scope, machine)
|
||||
}
|
||||
var told struct {
|
||||
From string `json:"from"`
|
||||
At string `json:"at"`
|
||||
As string `json:"as"`
|
||||
Serves map[string]any `json:"serves"`
|
||||
}
|
||||
if path := strings.TrimSpace(os.Getenv("MESH_GENESIS_PACKAGE_REGISTRY")); path != "" {
|
||||
raw, err := os.ReadFile(path)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
// Not this mesh's genesis node, or the installer has not sealed it yet.
|
||||
case err != nil:
|
||||
return none, fmt.Errorf("cannot read the genesis document for the package registry: %w", err)
|
||||
default:
|
||||
return fromGenesis(path, raw, scope, machine)
|
||||
}
|
||||
if err := json.Unmarshal(raw, &told); err != nil {
|
||||
return builder.Npmrc{}, fmt.Errorf("%s is not a binding: %w", path, err)
|
||||
}
|
||||
if told.At == "" {
|
||||
return builder.Npmrc{}, fmt.Errorf(
|
||||
"%s says the package registry is on %q and gives no address for it", path, told.From)
|
||||
}
|
||||
// Composed from what the provider serves, so nothing here knows gitea's URL shape from
|
||||
// another registry's: it states its port, the path its registry answers on, and the scheme.
|
||||
scheme := "https"
|
||||
if s, ok := told.Serves["scheme"]; ok {
|
||||
scheme = fmt.Sprintf("%v", s)
|
||||
}
|
||||
port, ok := told.Serves["port"]
|
||||
if !ok {
|
||||
return builder.Npmrc{}, fmt.Errorf("%s says nothing about which port the package registry answers on", path)
|
||||
}
|
||||
npmPath, ok := told.Serves["npm-path"]
|
||||
if !ok {
|
||||
return builder.Npmrc{}, fmt.Errorf("%s says nothing about the path the package registry answers on", path)
|
||||
}
|
||||
registry = fmt.Sprintf("%s://%s:%v%v", scheme, told.At, port, npmPath)
|
||||
username = told.As
|
||||
}
|
||||
|
||||
// The credential the mesh sealed to this machine. The mesh authenticates the ordinary way — a
|
||||
// generated password the provider only applies (novox/hq ADR 0048) — so with a username this is
|
||||
// a password (basic auth); without one it is a bearer token a provider minted.
|
||||
// A builder run by a person.
|
||||
registry := strings.TrimSpace(os.Getenv("MESH_NPM_REGISTRY"))
|
||||
secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN"))
|
||||
if path := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); path != "" {
|
||||
raw, err := os.ReadFile(path)
|
||||
username := strings.TrimSpace(os.Getenv("MESH_NPM_USER"))
|
||||
if registry == "" && secret == "" {
|
||||
return none, nil
|
||||
}
|
||||
n := builder.Npmrc{Scope: scope, Registry: registry, From: builder.FromEnvironment, Machine: machine}
|
||||
if username != "" {
|
||||
n.Username, n.Password = username, secret
|
||||
} else {
|
||||
n.Token = secret
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// fromBinding composes the registry from the npm-package-registry binding the mesh wrote and the
|
||||
// credential it sealed beside it. The mesh authenticates the ordinary way — a generated password the
|
||||
// provider only applies (novox/hq ADR 0048) — so with a username the secret is a password (basic
|
||||
// auth); without one it is a bearer token a provider minted.
|
||||
func fromBinding(path string, raw []byte, scope, machine string) (builder.Npmrc, error) {
|
||||
none := builder.Npmrc{Machine: machine}
|
||||
var told struct {
|
||||
From string `json:"from"`
|
||||
At string `json:"at"`
|
||||
As string `json:"as"`
|
||||
Serves map[string]any `json:"serves"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &told); err != nil {
|
||||
return none, fmt.Errorf("%s is not a binding: %w", path, err)
|
||||
}
|
||||
if told.At == "" {
|
||||
return none, fmt.Errorf(
|
||||
"%s says the package registry is on %q and gives no address for it", path, told.From)
|
||||
}
|
||||
// Composed from what the provider serves, so nothing here knows gitea's URL shape from
|
||||
// another registry's: it states its port, the path its registry answers on, and the scheme.
|
||||
scheme := "https"
|
||||
if s, ok := told.Serves["scheme"]; ok {
|
||||
scheme = fmt.Sprintf("%v", s)
|
||||
}
|
||||
port, ok := told.Serves["port"]
|
||||
if !ok {
|
||||
return none, fmt.Errorf("%s says nothing about which port the package registry answers on", path)
|
||||
}
|
||||
npmPath, ok := told.Serves["npm-path"]
|
||||
if !ok {
|
||||
return none, fmt.Errorf("%s says nothing about the path the package registry answers on", path)
|
||||
}
|
||||
secret := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN"))
|
||||
if file := strings.TrimSpace(os.Getenv("MESH_NPM_TOKEN_FILE")); file != "" {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return builder.Npmrc{}, fmt.Errorf("cannot read this builder's package-registry credential: %w", err)
|
||||
return none, fmt.Errorf("cannot read this builder's package-registry credential: %w", err)
|
||||
}
|
||||
secret = strings.TrimSpace(string(raw))
|
||||
}
|
||||
if u := strings.TrimSpace(os.Getenv("MESH_NPM_USER")); u != "" {
|
||||
username = u
|
||||
n := builder.Npmrc{Scope: scope, Registry: fmt.Sprintf("%s://%s:%v%v", scheme, told.At, port, npmPath),
|
||||
From: builder.FromBinding, Machine: machine}
|
||||
if told.As != "" {
|
||||
n.Username, n.Password = told.As, secret
|
||||
} else {
|
||||
n.Token = secret
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
if registry == "" && secret == "" {
|
||||
return builder.Npmrc{}, nil
|
||||
// GenesisRegistry is the genesis document (novox/hq ADR 0336 §3): what the installer sealed to the
|
||||
// build agent on its node at genesis's packages step, as the `genesis-package-registry` own-secret —
|
||||
// the forge it raised, the port this node gave the package registry, its npm path, and the account it
|
||||
// made with that account's password. mesh-host writes it; the field names are the two repositories'
|
||||
// agreement.
|
||||
type GenesisRegistry struct {
|
||||
Scheme string `json:"scheme"`
|
||||
At string `json:"at"`
|
||||
Port int `json:"port"`
|
||||
NpmPath string `json:"npm-path"`
|
||||
As string `json:"as"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// fromGenesis composes the registry from the genesis document.
|
||||
func fromGenesis(path string, raw []byte, scope, machine string) (builder.Npmrc, error) {
|
||||
none := builder.Npmrc{Machine: machine}
|
||||
var g GenesisRegistry
|
||||
if err := json.Unmarshal(raw, &g); err != nil {
|
||||
return none, fmt.Errorf("%s is not the genesis document for the package registry: %w", path, err)
|
||||
}
|
||||
if username != "" {
|
||||
return builder.Npmrc{Scope: scope, Registry: registry, Username: username, Password: secret}, nil
|
||||
var missing []string
|
||||
for name, value := range map[string]string{"at": g.At, "npm-path": g.NpmPath, "as": g.As, "password": g.Password} {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
missing = append(missing, name)
|
||||
}
|
||||
}
|
||||
return builder.Npmrc{Scope: scope, Registry: registry, Token: secret}, nil
|
||||
if g.Port <= 0 {
|
||||
missing = append(missing, "port")
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
sort.Strings(missing)
|
||||
return none, fmt.Errorf("%s, the genesis document for the package registry, says nothing of %s",
|
||||
path, strings.Join(missing, ", "))
|
||||
}
|
||||
scheme := g.Scheme
|
||||
if scheme == "" {
|
||||
scheme = "http"
|
||||
}
|
||||
return builder.Npmrc{Scope: scope, Registry: fmt.Sprintf("%s://%s:%d%s", scheme, g.At, g.Port, g.NpmPath),
|
||||
Username: g.As, Password: g.Password, From: builder.FromGenesis, Machine: machine}, nil
|
||||
}
|
||||
|
||||
// saysRegistry is the line each build's log carries about the package registry (novox/hq ADR 0336
|
||||
// §4): which of the binding and the genesis document it resolved from, or that it has neither here.
|
||||
func saysRegistry(n builder.Npmrc) string {
|
||||
if n.Enabled() {
|
||||
return "the package registry is resolved from " + n.From
|
||||
}
|
||||
machine := n.Machine
|
||||
if machine == "" {
|
||||
machine = "this machine"
|
||||
}
|
||||
return fmt.Sprintf("no package registry: %s is unbound on %s and no genesis document is here; "+
|
||||
"a build needing it is refused", builder.RegistryRequirement, machine)
|
||||
}
|
||||
|
||||
// forgeFrom is the git credential this builder may offer a clone, composed from the same binding
|
||||
|
||||
Reference in New Issue
Block a user