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.
333 lines
17 KiB
Go
333 lines
17 KiB
Go
package link
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// Emitting a module event from Go.
|
|
//
|
|
// **Every event rides one topic exchange** (novox/hq ADR 0042), which is not the direct exchange
|
|
// nodes and the control plane speak over. A module that announces something publishes here, and
|
|
// consumers bind their own durable queue to a pattern over it.
|
|
//
|
|
// This exists because the builder is a module written in Go while every other emitter is
|
|
// TypeScript on the sdk. The envelope is the sdk's, reproduced exactly: the body is the payload
|
|
// alone and everything about the event travels as headers. A second shape would be a second thing
|
|
// for consumers to handle, and they are written against the first.
|
|
const (
|
|
// EventsExchange is where every event rides. Named here rather than imported from the broker
|
|
// package for the same reason BuildQueueName is duplicated there — one direction of dependency.
|
|
EventsExchange = "mesh.events"
|
|
)
|
|
|
|
// EmitEvent publishes one module event, in the envelope the sdk's consumers expect.
|
|
//
|
|
// The envelope is the transport's to write (bus.go) and this is only what goes in it, which is
|
|
// what lets one conformance fixture hold both implementations to the same headers.
|
|
func EmitEvent(ctx context.Context, bus Bus, eventType, source, node string, body any) error {
|
|
payload, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot serialise a %s event: %w", eventType, err)
|
|
}
|
|
// The publish is not confirmed by the caller: it has already done the work the event
|
|
// describes, and a build that succeeded must not be reported as failed because saying so
|
|
// failed. Each transport decides what "published" means for it.
|
|
return bus.PublishEvent(ctx, eventType, source, node, payload)
|
|
}
|
|
|
|
// eventID is what a consumer deduplicates on: delivery is at-least-once, so a handler must be able
|
|
// to tell a redelivery from a second event, and only the emitter can say which it is.
|
|
func eventID() (string, error) {
|
|
raw := make([]byte, 16)
|
|
if _, err := rand.Read(raw); err != nil {
|
|
return "", fmt.Errorf("cannot make an event id: %w", err)
|
|
}
|
|
return hex.EncodeToString(raw), nil
|
|
}
|
|
|
|
// MeshControllerSeat is the role the control plane holds, and therefore where its own facts live: a
|
|
// role's events belong to the role, not to whichever container is holding it today (novox/hq ADR 0121,
|
|
// ADR 0129). It is what makes them addressable while the control plane itself is being replaced.
|
|
const MeshControllerSeat = "mesh-controller"
|
|
|
|
// The facts the mesh states about its own work (novox/hq ADR 0134).
|
|
const (
|
|
// KeyApplied: a machine now runs what it was sent.
|
|
KeyApplied = "applied"
|
|
// KeyRefused: a machine did not take what it was sent, and why.
|
|
KeyRefused = "refused"
|
|
// KeyBuiltBefore: a build the mesh already held, for a catalogue that asked what it missed. Not
|
|
// `built` — that is the build machine's, said as it happens, and a replay is neither.
|
|
KeyBuiltBefore = "built-before"
|
|
// KeySecretReplaced: a value given to the mesh by hand was replaced with one it made, after its
|
|
// module's first good start (novox/hq ADR 0228). Never the value.
|
|
KeySecretReplaced = "secret-replaced"
|
|
// KeyHealerActed: a healer acted on a condition — what it did and what came of it, or that its budget
|
|
// is spent and the condition is the operator's (novox/hq to-be 45 §7). Never a person's act: those
|
|
// are the hand-act log's.
|
|
KeyHealerActed = "healer-acted"
|
|
// KeyRolledBack: a build failed its gate on its first machine and was put back there, or could not
|
|
// be (novox/hq ADR 0236, to-be 45 §8); or a witness on a machine put a core component back.
|
|
KeyRolledBack = "rolled-back"
|
|
// KeyChecked: a pull request's merge check was judged (novox/hq to-be 45 §9) — the verdict, its
|
|
// summary and its report, for the forge's holder to set as the pull request's status.
|
|
KeyChecked = "checked"
|
|
// KeyPlanMoved: a walk — the controller's plan of one trunk commit, tier by tier across the machines —
|
|
// was kept in a new state (novox/hq ADR 0239): the plan whole, for the delivery it walks to read its
|
|
// steps from. Said after every save of the serving controller; a delivery's holder also asks for the
|
|
// walks it follows, so a save made elsewhere is found by comparison, never lost.
|
|
KeyPlanMoved = "plan-moved"
|
|
)
|
|
|
|
// Applied is what a machine now runs, as the mesh states it.
|
|
type Applied struct {
|
|
Node string `json:"node"`
|
|
Declared string `json:"declared,omitempty"`
|
|
// Resources is how many the machine applied, not which: the list is the machine's own account
|
|
// of itself and belongs in the records, not in a fact every listener has to read past.
|
|
Resources int `json:"resources"`
|
|
}
|
|
|
|
// Refused is a machine that would not take what it was sent.
|
|
type Refused struct {
|
|
Node string `json:"node"`
|
|
Declared string `json:"declared,omitempty"`
|
|
Refused string `json:"refused,omitempty"`
|
|
Failed map[string]string `json:"failed,omitempty"`
|
|
}
|
|
|
|
// KeyModuleBuilt is what the builder announces when it has built something. The catalogue places
|
|
// it in the module graph; nothing else need care.
|
|
const KeyModuleBuilt = "module.builder.built"
|
|
|
|
// KeyModuleUpgraded is the catalogue saying a module's current version has moved.
|
|
//
|
|
// **The control plane hooks the meaning, not the build.** The builder says what it built; the
|
|
// catalogue decides whether that was an upgrade — a rebuild producing the commit already current
|
|
// is not one — and only this says anything the control plane can act on. Consuming the build
|
|
// directly would make the control plane re-derive a decision another module already made, and the
|
|
// two would eventually disagree (novox/hq ADR 0072).
|
|
const KeyModuleUpgraded = "module.mesh-catalog.upgraded"
|
|
|
|
// KeyCatchingUp is the catalogue saying it has just started and may have missed things.
|
|
//
|
|
// **A durable queue only keeps what arrived after it existed.** The catalogue's own queue is
|
|
// durable, so nothing is lost once it is running — but the modules built before it first ran were
|
|
// announced to a queue that did not exist yet, and on a fresh mesh those are, necessarily, the
|
|
// shared base, the store the catalogue runs on, and the catalogue itself. The graph's foundation
|
|
// is the part it never hears about (novox/hq 04-ISSUES/050).
|
|
//
|
|
// So it asks, and the control plane answers with what it recorded. Asking rather than being told
|
|
// because only the catalogue knows it has a gap; the control plane cannot tell a fresh catalogue
|
|
// from one that is merely quiet.
|
|
const KeyCatchingUp = "module.mesh-catalog.catching-up"
|
|
|
|
// CatchUpQueue is where that lands. Durable, for the same reason the upgrade queue is: a catalogue
|
|
// that started while the control plane was restarting is exactly the one with a gap to fill.
|
|
const CatchUpQueue = "control.catchup"
|
|
|
|
// UpgradeQueue is where those land. Durable and named, not a temporary queue: an upgrade announced
|
|
// while the control plane is restarting is exactly the one that must not be missed.
|
|
const UpgradeQueue = "control.upgrades"
|
|
|
|
// Replayer answers a catalogue that says it has just started.
|
|
//
|
|
// It is handed every build the mesh recorded, oldest first, and re-announces each. The catalogue
|
|
// registers them as history: a replayed build changed nothing in the world, so announcing it as an
|
|
// upgrade would have the mesh act on news that is years old.
|
|
type Replayer interface {
|
|
// Announceable is every build worth re-announcing, oldest first.
|
|
//
|
|
// It hands them back rather than publishing them: the wire belongs to this package, and a
|
|
// replay that built its own announcements could drift from what the builder emits — which is
|
|
// the one thing it must match exactly, because the catalogue has a single handler for both.
|
|
Announceable(ctx context.Context) ([]Announcement, error)
|
|
}
|
|
|
|
// Announcement is a build, in the shape the builder announces one.
|
|
//
|
|
// The field names are the wire's, not Go's, because a catalogue reads these and a rename here is
|
|
// an event nobody handles.
|
|
type Announcement struct {
|
|
Module string `json:"module"`
|
|
Commit string `json:"commit"`
|
|
Repository string `json:"repository"`
|
|
Path string `json:"path"`
|
|
Ref string `json:"ref"`
|
|
Manifest json.RawMessage `json:"manifest,omitempty"`
|
|
Against []string `json:"against,omitempty"`
|
|
Made []MadeArtifact `json:"made,omitempty"`
|
|
// Replay says this is history rather than news: it was built once, and this is the mesh
|
|
// telling a catalogue that missed it. A consumer registers it and announces nothing — an
|
|
// upgrade that happened months ago is not one anything should act on now.
|
|
Replay bool `json:"replay,omitempty"`
|
|
}
|
|
|
|
// Upgraded is what the catalogue says when a module's current version moves.
|
|
// SourceMoved is what the forge announces when a pull request is merged: which repository, into
|
|
// which branch, producing which commit. The mesh matches it against every module's recorded
|
|
// source and builds what moved, bases first.
|
|
type SourceMoved struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Base string `json:"base"`
|
|
Head string `json:"head"`
|
|
Commit string `json:"merge_commit_sha"`
|
|
CloneURL string `json:"clone_url"`
|
|
HTMLURL string `json:"html_url"`
|
|
// MergedAt is when the forge merged it, RFC 3339. What decides whether this is news.
|
|
MergedAt string `json:"merged_at"`
|
|
|
|
// Paths are the files the merge changed, from the repository's root. Empty means the forge said
|
|
// nothing about them, and every module built from the repository is treated as affected.
|
|
Paths []string `json:"paths,omitempty"`
|
|
|
|
// PathsTruncated says the merge changed more files than the forge was asked to list, so Paths is
|
|
// a beginning rather than the whole change — and again, everything is treated as affected. Said
|
|
// rather than inferred from a round number, because "this is all of it" and "this is as much as
|
|
// I asked for" are the difference between rebuilding a module and leaving it stale.
|
|
PathsTruncated bool `json:"paths_truncated,omitempty"`
|
|
|
|
// Removed are the files among Paths the merge deleted. A module whose manifest is among them was
|
|
// deleted at its source: it is forgotten, or said, and never built (novox/hq ADR 0236). Empty from an
|
|
// announcer that does not say which files went, and then a build that finds no manifest says it.
|
|
Removed []string `json:"removed,omitempty"`
|
|
|
|
// ModuleDirs are the directories holding the files the merge changed that hold a `module.json` at
|
|
// the merge commit, from the repository's root (novox/hq issue 278). A changed file inside one of
|
|
// them is that module's business, whether or not the mesh holds the module and whether or not the
|
|
// merge touched its manifest; only a file in no such directory is shared code. Without it, a
|
|
// change to a module the mesh does not hold — whose manifest the merge left alone — read as shared
|
|
// and rebuilt every module built from the repository.
|
|
ModuleDirs []string `json:"module_dirs,omitempty"`
|
|
|
|
// ModuleDirsSaid says the announcer looked, so an empty ModuleDirs means "none of them is a
|
|
// module" rather than "not said". An announcer from before this says nothing, and the old rule
|
|
// stands: a directory is a module only when the merge changed its manifest.
|
|
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
|
|
}
|
|
|
|
// PullUpdated is what the forge announces when an open pull request's head moves — opened, or pushed
|
|
// to (novox/hq to-be 45 §9): what the controller asks the build seat to check before it merges.
|
|
type PullUpdated struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Number int `json:"number"`
|
|
Title string `json:"title,omitempty"`
|
|
Base string `json:"base"`
|
|
Head string `json:"head"`
|
|
Commit string `json:"head_sha"`
|
|
CloneURL string `json:"clone_url"`
|
|
HTMLURL string `json:"html_url,omitempty"`
|
|
// Paths are the files the pull request changes; PathsTruncated says there were more.
|
|
Paths []string `json:"paths,omitempty"`
|
|
PathsTruncated bool `json:"paths_truncated,omitempty"`
|
|
// Removed are the files among Paths the change deletes: a module whose manifest is among them is one
|
|
// the merge would remove.
|
|
Removed []string `json:"removed,omitempty"`
|
|
// ModuleDirs are the directories above the changed files that hold a `module.json` at the head, read
|
|
// as the merge announcer reads them at a merge commit (issue 278); ModuleDirsSaid says it looked.
|
|
// What the controller finds a module the graph does not hold yet with: a directory the change adds a
|
|
// module.json in is a new module, checked before it merges.
|
|
ModuleDirs []string `json:"module_dirs,omitempty"`
|
|
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
|
|
// MergeCheck says the head holds a merge-check.sh at its root — the repository's own tests, the
|
|
// check's second layer; MergeCheckSaid says the announcer looked.
|
|
MergeCheck bool `json:"merge_check,omitempty"`
|
|
MergeCheckSaid bool `json:"merge_check_said,omitempty"`
|
|
}
|
|
|
|
// Checked is a pull request's merge check, judged: what the controller says as `checked`.
|
|
type Checked struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Number int `json:"number,omitempty"`
|
|
Commit string `json:"commit"`
|
|
// Verdict is pass, warning, fail, or error — the check could not be run, which is not the change's
|
|
// fault and is never read as a pass.
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
// Report is the check's own account, bounded.
|
|
Report string `json:"report,omitempty"`
|
|
// ID is the ask, and On the machine that ran it.
|
|
ID string `json:"id"`
|
|
On string `json:"on,omitempty"`
|
|
// Gate and RepoCheck are the check's two layers, each its own status on the pull request (novox/hq
|
|
// ADR 0237 as amended): the mesh's — the modules of the graph the change touches, every machine
|
|
// composed with it — as `mesh/merge-gate`, and the repository's own merge-check.sh as
|
|
// `mesh/repo-check`. Verdict and Summary above are the gate's, for a forge holder that reads no more.
|
|
// RepoCheck is nil when the repository is not the mesh's and touches nothing of it: nothing is said.
|
|
Gate *CheckLayer `json:"gate,omitempty"`
|
|
RepoCheck *CheckLayer `json:"repo-check,omitempty"`
|
|
// Plan is the change plan of the commit checked (novox/hq ADR 0238): what a merge of it would build
|
|
// and send, posted with the verdict.
|
|
Plan *ChangePlan `json:"plan,omitempty"`
|
|
// Group is set on a delivery group's composed check (novox/hq ADR 0239): every member's head composed
|
|
// together as one future state. Members are the heads judged. A forge's holder sets no merge-gate
|
|
// status from it — the group's verdict is its owner's, mesh-delivery, to say on each member's head.
|
|
Group string `json:"group,omitempty"`
|
|
Members []CheckedMember `json:"members,omitempty"`
|
|
}
|
|
|
|
// CheckedMember is one head a group's composed check judged.
|
|
type CheckedMember struct {
|
|
Owner string `json:"owner"`
|
|
Repo string `json:"repo"`
|
|
Number int `json:"number,omitempty"`
|
|
Commit string `json:"commit"`
|
|
}
|
|
|
|
// ChangePlan is what a change does to the mesh, computed from its diffset — a repository, the branch it
|
|
// merges into and the commit at hand — by the planner (novox/hq ADR 0238): **one commit, one plan**, the
|
|
// object a pull request's check posts, the release follows and a person reads.
|
|
type ChangePlan struct {
|
|
Repository string `json:"repository"`
|
|
Base string `json:"base"`
|
|
Head string `json:"head"`
|
|
// Moved are the modules a merge moves itself, Dependents those built after them because they stand
|
|
// on them, New the directories it adds a module in, and Unread the changed files no build reads.
|
|
Moved []string `json:"moved,omitempty"`
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
New []string `json:"new,omitempty"`
|
|
Unread []string `json:"unread,omitempty"`
|
|
// Tiers are the build plan's order: each tier built after the one before.
|
|
Tiers [][]string `json:"tiers,omitempty"`
|
|
// Machines are the deploy plan: what each machine is sent, in the build plan's order, and what waits
|
|
// there for a person.
|
|
Machines []MachinePlan `json:"machines,omitempty"`
|
|
// Steps are what is not an ordinary send: a planned bus step, a provider whose consumers are sent again,
|
|
// a module that waits for a person, a module that keeps data.
|
|
Steps []string `json:"steps,omitempty"`
|
|
// Summary is the plan in one line, for a commit status.
|
|
Summary string `json:"summary"`
|
|
}
|
|
|
|
// MachinePlan is one machine's part of a change plan.
|
|
type MachinePlan struct {
|
|
Machine string `json:"machine"`
|
|
Receives []string `json:"receives,omitempty"`
|
|
Waits []string `json:"waits,omitempty"`
|
|
}
|
|
|
|
// CheckLayer is one layer of a merge check, judged.
|
|
type CheckLayer struct {
|
|
// Verdict is pass, warning, fail or error; error is never a pass.
|
|
Verdict string `json:"verdict"`
|
|
Summary string `json:"summary"`
|
|
// Modules are, for the gate, the modules of the mesh's graph the change touches — `new:<dir>` for a
|
|
// module the graph does not hold yet; Dependents those a merge would build after them because they
|
|
// stand on them — the planner's own answer.
|
|
Modules []string `json:"modules,omitempty"`
|
|
Dependents []string `json:"dependents,omitempty"`
|
|
}
|
|
|
|
type Upgraded struct {
|
|
Module string `json:"module"`
|
|
Commit string `json:"commit"`
|
|
Previous string `json:"previous"`
|
|
}
|