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.
189 lines
7.0 KiB
Go
189 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/builder"
|
|
)
|
|
|
|
// buildOnce is the builder doing one build and stopping, with no broker and no mesh.
|
|
//
|
|
// **This is how a mesh is raised** (novox/hq ADR 0073). The installer carries this program and runs
|
|
// it once, before anything else exists, to produce the control plane from the same repository and
|
|
// path that every later rebuild of it will use. What raises the mesh is therefore the same thing
|
|
// that maintains it — not a second mechanism that has to be kept in step with the first and is
|
|
// exercised once per new mesh, which is how often enough to rot.
|
|
//
|
|
// It takes no work from a queue and answers nobody: there is no broker yet, and the only thing
|
|
// waiting for the answer is the installer that started it. So the result goes to standard output as
|
|
// JSON, which is what the installer reads.
|
|
//
|
|
// With no --registry it publishes nowhere and the image stays in this machine's container runtime,
|
|
// named by the digest of its own configuration (see builder.Local). That is the genesis case. With
|
|
// one, it publishes as it always does — the same command is how an operator builds a module by hand
|
|
// on a mesh that already exists.
|
|
func buildOnce(ctx context.Context, args []string) error {
|
|
set := flag.NewFlagSet("build", flag.ContinueOnError)
|
|
path := set.String("path", "", "the module's directory inside the repository (default: its root)")
|
|
ref := set.String("ref", "", "the commit to build; a branch is a moving target somebody else controls")
|
|
registry := set.String("registry", "",
|
|
"host:port to publish to. Without it the artifacts stay in this machine's container runtime, which is the genesis case")
|
|
workspace := set.String("workspace", "", "where to clone and build (default: a temporary directory)")
|
|
on := multiple(set, "on",
|
|
"a base this build stands on, as <module>/<artifact>=<reference>. Repeatable")
|
|
positionals, err := parseAround(set, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(positionals) != 1 {
|
|
return errors.New("mesh-builder build <repository> [--path P] [--ref COMMIT] [--registry HOST:PORT]")
|
|
}
|
|
repository := positionals[0]
|
|
|
|
where := *workspace
|
|
if where == "" {
|
|
where = os.TempDir() + "/mesh-builder-once"
|
|
}
|
|
|
|
// What this build may stand on, said on the command line because there is no mesh to ask.
|
|
//
|
|
// **This is the genesis case and it is meant to be awkward.** On a running mesh the control
|
|
// plane answers this, because only it knows what this mesh holds. Here nothing has been built
|
|
// yet, so whoever runs this says what to use — and for the control plane, which is the one
|
|
// module raised before anything else exists, the answer is ordinarily nothing at all.
|
|
bases := map[string]string{}
|
|
for _, pair := range *on {
|
|
key, reference, found := strings.Cut(pair, "=")
|
|
if !found || key == "" || reference == "" {
|
|
return fmt.Errorf(
|
|
"--on takes <module>/<artifact>=<reference>, and %q is not that", pair)
|
|
}
|
|
bases[key] = reference
|
|
}
|
|
|
|
// Local unless told otherwise, because the moment this exists for has nowhere to publish. A
|
|
// default pointing at a registry would mean genesis failing at a push to something that is not
|
|
// there yet, one step away from the thing that could explain it.
|
|
var publisher builder.Publisher = builder.Local{Run: builder.Command}
|
|
if *registry != "" {
|
|
publisher = builder.Registry{Address: *registry, Run: builder.Command}
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "building %s", repository)
|
|
if *path != "" {
|
|
fmt.Fprintf(os.Stderr, " at %s", *path)
|
|
}
|
|
if *ref != "" {
|
|
fmt.Fprintf(os.Stderr, " at %s", *ref)
|
|
}
|
|
fmt.Fprintln(os.Stderr)
|
|
|
|
machine := os.Getenv("MESH_NODE")
|
|
if machine == "" {
|
|
machine, _ = os.Hostname()
|
|
}
|
|
npmrc, err := packagesFrom(machine)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Fprintf(os.Stderr, " [packages] %s\n", saysRegistry(npmrc))
|
|
built, buildErr := builder.Build(ctx, builder.Command, publisher, repository, *path, *ref, where, bases, npmrc,
|
|
forgeFrom(),
|
|
func(step, message string) { fmt.Fprintf(os.Stderr, " [%s] %s\n", step, message) })
|
|
if buildErr != nil {
|
|
return buildErr
|
|
}
|
|
|
|
// To standard output, and everything else to standard error, so the caller can read this
|
|
// without having to separate it from progress.
|
|
out := onceResult{
|
|
Module: built.Manifest.Module,
|
|
Commit: built.Commit,
|
|
Repository: repository,
|
|
Path: *path,
|
|
Ref: *ref,
|
|
Manifest: built.Manifest,
|
|
Against: built.Against,
|
|
Source: built.Source,
|
|
}
|
|
for _, r := range built.Read {
|
|
out.Read = append(out.Read, readRepository{Repository: r.Repository, Ref: r.Ref})
|
|
}
|
|
for _, made := range built.Built {
|
|
out.Made = append(out.Made, madeArtifact{Name: made.Name, Kind: made.Kind, Reference: made.Reference})
|
|
}
|
|
body, err := json.MarshalIndent(out, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println(string(body))
|
|
fmt.Fprintf(os.Stderr, " built %s from %s\n", built.Manifest.Module, short(built.Commit))
|
|
return nil
|
|
}
|
|
|
|
// onceResult is what one build reports to whoever started it.
|
|
//
|
|
// The same fields the mesh records for a build, so a reader comparing a genesis build against an
|
|
// ordinary one is comparing the same thing said the same way.
|
|
type onceResult struct {
|
|
Module string `json:"module"`
|
|
Commit string `json:"commit"`
|
|
Repository string `json:"repository"`
|
|
Path string `json:"path,omitempty"`
|
|
Ref string `json:"ref,omitempty"`
|
|
Manifest any `json:"manifest"`
|
|
Made []madeArtifact `json:"made"`
|
|
Against []string `json:"against,omitempty"`
|
|
Read []readRepository `json:"read,omitempty"`
|
|
Source string `json:"source-fingerprint,omitempty"`
|
|
}
|
|
|
|
// readRepository is a repository this build read source from besides the module's own.
|
|
type readRepository struct {
|
|
Repository string `json:"repository"`
|
|
Ref string `json:"ref,omitempty"`
|
|
}
|
|
|
|
type madeArtifact struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
Reference string `json:"reference"`
|
|
}
|
|
|
|
// parseAround lets flags appear on either side of the repository, because a person writing this by
|
|
// hand will put them wherever reads best and the standard parser stops at the first thing that is
|
|
// not a flag. The same helper the control plane's commands use, for the same reason.
|
|
func parseAround(set *flag.FlagSet, args []string) ([]string, error) {
|
|
var positionals []string
|
|
rest := args
|
|
for {
|
|
if err := set.Parse(rest); err != nil {
|
|
return nil, err
|
|
}
|
|
rest = set.Args()
|
|
if len(rest) == 0 {
|
|
return positionals, nil
|
|
}
|
|
positionals = append(positionals, rest[0])
|
|
rest = rest[1:]
|
|
}
|
|
}
|
|
|
|
// multiple is a flag that may be given more than once.
|
|
type repeated []string
|
|
|
|
func (r *repeated) String() string { return strings.Join(*r, ", ") }
|
|
func (r *repeated) Set(v string) error { *r = append(*r, v); return nil }
|
|
|
|
func multiple(set *flag.FlagSet, name, usage string) *[]string {
|
|
var values repeated
|
|
set.Var(&values, name, usage)
|
|
return (*[]string)(&values)
|
|
}
|