While the mesh-delivery seat has a holder on record, a merge that moves no core module opens its walk and asks nothing until mesh-delivery or a person says go; nothing of it is registered before its turn, so no other send carries it. The controller keeps the planner, the gate, sending and the walk, and gains the verbs the owner asks with: delivery-plan, -order, -check (a group composed as one future state), deliver, delivery-stop, delivery-walks; every walk kept is said as plan-moved.
268 lines
14 KiB
Go
268 lines
14 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"`
|
|
// DryRun says the asker wants the outcome to look at and nothing else (novox/hq issue 240): the
|
|
// builder echoes it, and whoever hears the outcome takes nothing in — no record, no registration,
|
|
// no plan, nothing a push could send.
|
|
DryRun bool `json:"dry-run,omitempty"`
|
|
// Check makes this ask a pull request's merge check rather than a build (novox/hq to-be 45 §9): the
|
|
// builder checks the repository out at Ref with the repositories it is checked beside, reads the
|
|
// facts snapshot, raises the throwaway stores the check needs, runs the repository's own
|
|
// merge-check.sh and answers its verdict. Nothing is built, published or registered.
|
|
Check *CheckRequest `json:"check,omitempty"`
|
|
}
|
|
|
|
// CheckRequest is what a merge check needs beyond the repository and its head.
|
|
type CheckRequest struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Number int `json:"number,omitempty"`
|
|
Base string `json:"base,omitempty"`
|
|
// Paths are the files the pull request changes, for the width of its rebuild.
|
|
Paths []string `json:"paths,omitempty"`
|
|
// Beside are the repositories the check reads next to this one, each cloned at the ref given — the
|
|
// controller the mesh runs, the catalogue it holds, the host it runs — keyed by the directory name the
|
|
// check finds it under.
|
|
Beside map[string]CheckedOut `json:"beside,omitempty"`
|
|
// Modules are the modules of the mesh's graph the change touches, and New the directories it adds a
|
|
// module in that the graph does not hold (novox/hq ADR 0237 as amended): **the graph decides whether
|
|
// the gate runs**, not the repository. With neither, only the repository's own merge-check.sh runs.
|
|
Modules []string `json:"modules,omitempty"`
|
|
New []string `json:"new,omitempty"`
|
|
// Dependents are the modules a merge would build after Modules because they stand on them: the
|
|
// planner's dependency walk, said on the pull request.
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
// Plan is the change plan of the commit checked, computed by the controller and echoed with the outcome.
|
|
Plan *ChangePlan `json:"plan,omitempty"`
|
|
// Manifests are the touched modules' manifests in the change's tree, by path from its root: what the
|
|
// gate puts through `module check`.
|
|
Manifests []string `json:"manifests,omitempty"`
|
|
// Judge is who judges the gate: empty for the controller the mesh runs; JudgeSelf for a change to
|
|
// the controller, judged by itself; JudgeValidator for a change to the node-engine, judged by the
|
|
// running controller built with the change's validator in place of the one it vendors.
|
|
Judge string `json:"judge,omitempty"`
|
|
// Group names a delivery group whose members' heads are composed together with this one (novox/hq ADR
|
|
// 0239), and Members are those other heads, each cloned beside it and laid over the mesh in turn. The
|
|
// repository's own check is not run for a group: each member's pull request runs its own.
|
|
Group string `json:"group,omitempty"`
|
|
Members []GroupMember `json:"members,omitempty"`
|
|
}
|
|
|
|
// GroupMember is one other head of a delivery group's composed check.
|
|
type GroupMember struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Number int `json:"number,omitempty"`
|
|
Repository string `json:"repository"`
|
|
Ref string `json:"ref"`
|
|
Paths []string `json:"paths,omitempty"`
|
|
}
|
|
|
|
// Who judges a merge check's gate.
|
|
const (
|
|
JudgeSelf = "self"
|
|
JudgeValidator = "validator"
|
|
)
|
|
|
|
// CheckedOut is a repository cloned beside a check, at a ref.
|
|
type CheckedOut struct {
|
|
Repository string `json:"repository"`
|
|
Ref string `json:"ref,omitempty"`
|
|
}
|
|
|
|
// CheckOutcome is a merge check's verdict.
|
|
type CheckOutcome struct {
|
|
// Verdict is pass, warning, fail, or error: the check could not run, which is never a pass.
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
// Report is the check's own account, its last lines, bounded.
|
|
Report string `json:"report,omitempty"`
|
|
// Took is how long it ran.
|
|
Took string `json:"took,omitempty"`
|
|
// Gate and RepoCheck are its two layers; Verdict and Summary above are the gate's.
|
|
Gate *CheckLayer `json:"gate,omitempty"`
|
|
RepoCheck *CheckLayer `json:"repo-check,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"`
|
|
|
|
// Trunk is the repository's default branch at the build, and OnTrunk whether the commit built is on it
|
|
// (novox/hq ADR 0238): **only a commit on the trunk is published** — the controller refuses to register
|
|
// a build of one off it. Empty Trunk is a build seat that could not say, or predates the rule.
|
|
Trunk string `json:"trunk,omitempty"`
|
|
OnTrunk bool `json:"on-trunk,omitempty"`
|
|
// Branches are every branch the commit is on: the trunk of a module that follows a branch other than
|
|
// the repository's default is the branch it follows.
|
|
Branches []string `json:"branches,omitempty"`
|
|
|
|
// SourceFingerprint is what the build was made from, hashed (novox/hq issue 280): the module's
|
|
// tree at the commit, the trees of the contexts it read, its bases and toolchains by digest. Two
|
|
// builds with one fingerprint are one build, however their digests differ — an image is not
|
|
// byte-reproducible. Empty from a builder that predates it, or where the source does not pin the
|
|
// build; then only identical artifacts make a rebuild no move.
|
|
SourceFingerprint string `json:"source-fingerprint,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"`
|
|
|
|
// DryRun is the request's, echoed: an outcome nobody may take in (novox/hq issue 240).
|
|
DryRun bool `json:"dry-run,omitempty"`
|
|
|
|
// Check is a merge check's verdict, for an ask that was one; the request's Check is echoed in
|
|
// Checked so whoever hears it knows which pull request it judged.
|
|
Check *CheckOutcome `json:"check-outcome,omitempty"`
|
|
Checked *CheckRequest `json:"check,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"`
|
|
}
|