Files
mesh-controller/internal/link/build.go
T
jochen 9745c1ab31 An older build request never replaces a newer one's artifact
Builds of one module in flight together finish in any order, and the mesh
took whatever it heard last as what the module is: RegisterModule overwrote
the module's manifest unconditionally, and Held/BuiltAgainst/ReadRepositories
ordered builds by when they were recorded. A postgres build asked before the
mesh-tools runtime fix finished after the one asked after it, and the next
push deployed the stale image (novox/hq issue 219).

A build is now ordered by when it was asked, read from the build-<nanos> id
the controller writes: build.asked and module.built_asked (migration 0055).
A registration from an earlier request than the module's current one is
recorded and refused as superseded. A plan takes as its outcome only a build
asked at or after its own ask, so an earlier plan's leftover build cannot
settle a later plan. Ids of any other shape keep the old order.
2026-10-04 00:22:11 +02:00

163 lines
8.0 KiB
Go

package link
import (
"encoding/json"
"strconv"
"strings"
"time"
)
// Asking a machine to build a module, and hearing what came out.
//
// **A build is work, not state.** Everything else the control plane sends a node is a declaration
// — *this is what you should be* — and the node reconciles toward it forever. A build happens
// once, produces something, and is finished. Putting it in a declaration would mean a machine
// rebuilding on every reconcile, or the declaration carrying "and I already did this", which is
// state about an event rather than about the machine.
//
// So it travels on its own queue, and the reply comes back correlated. That is also why the
// builder is a **separate consumer** rather than the host: the host applies declarations and
// holds no opinion about what they contain, and a host that also built things would be a host
// with a container runtime requirement and a git dependency (novox/hq ADR 0005).
// NewBuildID is the correlation for a build asked at that moment: `build-<unix nanoseconds>`.
//
// **The id carries when the build was asked, and that is read back** (novox/hq 04-ISSUES/219). Builds
// of one module can be in flight together and finish in any order; what a module currently is must
// be the newest *request's* outcome, not the last one heard, and the id is the one thing every
// outcome echoes whichever builder answered it. One place writes the shape and one reads it.
func NewBuildID(asked time.Time) string {
return "build-" + strconv.FormatInt(asked.UnixNano(), 10)
}
// BuildAskedAt is when the build with this id was asked, as NewBuildID wrote it. False for an id
// of any other shape — one written before this was read, or by hand — whose request time the mesh
// does not know.
func BuildAskedAt(id string) (time.Time, bool) {
digits, ok := strings.CutPrefix(id, "build-")
if !ok || digits == "" {
return time.Time{}, false
}
nanos, err := strconv.ParseInt(digits, 10, 64)
// A number too small to be a moment this mesh could have asked at is a name, not a time.
if err != nil || nanos < time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC).UnixNano() {
return time.Time{}, false
}
return time.Unix(0, nanos).UTC(), true
}
// BuildRequest is one module to build.
type BuildRequest struct {
// ID correlates the answer with the asking. Not the module name: two builds of one module can
// be in flight, and the second answer is not the first one's.
ID string `json:"id"`
// Repository is where the source is, as git would clone it.
Repository string `json:"repository"`
// Ref is the branch, tag or commit. Empty means whatever the repository's default is, which
// is the only case where the mesh does not know what it built until it has built it.
Ref string `json:"ref,omitempty"`
// Path is the module's directory inside that repository (novox/hq ADR 0069). Empty means the
// repository root, which is the ordinary case; a repository holding several modules names
// each by its own directory.
Path string `json:"path,omitempty"`
// Held is every artifact this mesh has built, keyed "<module>/<artifact>".
//
// **Sent with the asking rather than fetched by the builder** (novox/hq issue 044). A module
// says which module's artifact its build stands on; only the mesh knows which copy of that
// artifact *this* mesh holds, and the builder is deliberately a thing that clones, runs a
// build and answers — giving it a way to ask the mesh questions would make it something else.
// So the answer travels with the question.
//
// It is everything rather than only what this module needs, because what this module needs is
// written in a manifest the mesh has not read: it is inside the repository, and reading it is
// the build's first act.
Held map[string]string `json:"held,omitempty"`
// Seats is the clone base — `scheme://host:port` — of each seat a recipe's context may name
// (novox/hq ADR 0155): `git` for this mesh's own forge. Sent with the asking for the reason
// Held is: the context is written in a manifest the mesh has not read, and only the mesh knows
// which forge holds the seat here. A builder handed no base for a seat a context names refuses
// the build and says so.
Seats map[string]string `json:"seats,omitempty"`
// Source is the repository as the mesh records it when it lives on a seat's holder — the seat
// and the path on it, never the URL just composed (novox/hq ADR 0111). Carried with the
// asking and echoed in the outcome, so whoever hears the outcome can register the module with
// its true source, whether or not they were the one who asked (novox/hq issue 176).
Source *SourceOnSeat `json:"source,omitempty"`
}
// SourceOnSeat names a repository by the seat whose holder serves it and its path there.
type SourceOnSeat struct {
Seat string `json:"seat"`
Repository string `json:"repository"`
}
// BuildResult is what a builder says back.
//
// **Failure is a result, not an absence.** A build that fails and says nothing is
// indistinguishable from a builder that is not running, and those want completely different
// responses — the same rule the host follows about a service that does not exist.
type BuildResult struct {
ID string `json:"id"`
Repository string `json:"repository"`
// Path is echoed back, so what the mesh records as this module's source is what was actually
// built rather than what the asker meant (novox/hq ADR 0069).
Path string `json:"path,omitempty"`
Ref string `json:"ref,omitempty"`
// On is the machine that did it, so a failure that is about one machine can be told from one
// about the source.
On string `json:"on"`
// Module is what was built, read out of the manifest — the only place it is authoritative, since
// a request names a repository and a path.
//
// **Here because one message now reaches three audiences** (novox/hq ADR 0121). On the bus the
// mesh runs on today the answer and the announcement were two publishes to two topologies, so a
// result needed no module name and the announcement carried one. On the bus being built the
// outcome is the role's own event, and the catalogue reading it needs to know what was built.
// Empty on a failed build, which produced no module version.
Module string `json:"module,omitempty"`
// Commit is what was actually built. The mesh records it, which is what makes "is this
// current?" answerable without building again.
Commit string `json:"commit,omitempty"`
// Manifest is the module as the mesh should hold it, artifacts resolved to digests. Raw,
// because the control plane parses it with the same parser it uses for one handed over by
// hand — a second path would be a second thing to disagree.
Manifest json.RawMessage `json:"manifest,omitempty"`
// Made is each artifact, for reporting.
Made []MadeArtifact `json:"made,omitempty"`
// Against is every pinned image this was built on top of, read out of the build's own inputs
// (novox/hq ADR 0009). The catalogue turns these into edges; nothing else need care.
Against []string `json:"against,omitempty"`
// Read is every repository this build read source from besides the module's own. A module whose
// recipe packages source that lives elsewhere is affected when that repository moves, and the
// manifest the mesh keeps says nothing about it (novox/hq 04-ISSUES/131).
Read []ReadRepository `json:"read,omitempty"`
// Failed is why, when it did.
Failed string `json:"failed,omitempty"`
// Source is the request's, echoed: the seat form of the repository, for whoever registers.
Source *SourceOnSeat `json:"source,omitempty"`
}
// ReadRepository is a repository a build read source from besides the module's own, at the branch,
// tag or commit it read. Spelled here as well as in the catalogue and the inventory, for the reason
// MadeArtifact is: one direction of dependency.
type ReadRepository struct {
Repository string `json:"repository"`
Ref string `json:"ref,omitempty"`
}
// MadeArtifact is one thing a build produced, as a person would want it reported.
type MadeArtifact struct {
Name string `json:"name"`
Kind string `json:"kind"`
Reference string `json:"reference"`
}