One commit, one plan: a commit off the trunk — a pull request's head, a branch built by hand, a rebuild or replay of one — is for checking. The build seat reads from its clone which branches hold the commit, and the controller records and never registers a build whose commit is not on the branch the module follows (the repository's default for a new one), so nothing off the trunk can be sent. A pull request's check now carries its change plan, computed by the planner: what a merge would build in which order, what each machine would receive, and what is not an ordinary send — the bus step, a module waiting for a person, a provider's consumers.
253 lines
13 KiB
Go
253 lines
13 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"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|