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-`. // // **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 "/". // // **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"` }