Let a walk wait for its delivery's word, and serve the delivery's owner (hq ADR 0239)
mesh/merge-gate pass: every machine composes with the change as it did without (0 of 4 compose)
mesh/delivery delivered
mesh/delivery-group group feat/mesh-delivery delivered: every member is delivered

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.
This commit is contained in:
jochen
2026-10-07 00:01:42 +02:00
parent ba26ba2772
commit 487aa040de
29 changed files with 1562 additions and 39 deletions
+1 -1
View File
@@ -162,7 +162,7 @@ func TestTheCataloguesDerivedSubjectsMeet(t *testing.T) {
for _, want := range c.Consumes {
emitter, event, named := strings.Cut(want, ".")
if named {
if s, isASeat := byName[emitter]; isASeat {
if s, isASeat := byName[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
principal.Watches = append(principal.Watches,
Seat{Name: s.Name, Emits: []string{event}})
continue
+2
View File
@@ -32,6 +32,8 @@ func TestTheFactsTheGrantPermitsAreTheFactsTheMeshStates(t *testing.T) {
states = append(states, link.KeyRolledBack)
// And a pull request's merge check, judged (novox/hq to-be 45 §9).
states = append(states, link.KeyChecked)
// And a walk kept in a new state, for the delivery it walks (novox/hq ADR 0239).
states = append(states, link.KeyPlanMoved)
for _, event := range states {
if !slices.Contains(broker.ControllerStates, event) {
t.Errorf("the mesh states %q and its account may not publish it", event)
+3 -1
View File
@@ -218,7 +218,9 @@ var ControllerStates = []string{"applied", "refused", "built-before",
"rolled-back",
// And a pull request's merge check, judged (novox/hq to-be 45 §9): what the forge's holder sets as
// the pull request's status, and anybody else may read.
"checked"}
"checked",
// And a walk kept in a new state (novox/hq ADR 0239): what the delivery it walks reads its steps from.
"plan-moved"}
// BusAdvisories are what the bus server says about the mesh's own account that the controller
// reads (novox/hq to-be 45 §3, S9): a durable consumer that handed a message over as often as it
+1 -1
View File
@@ -24,7 +24,7 @@ accounts {
jetstream: enabled
users = [
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.assignment.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*"] }
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.assignment.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*"] }
subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built"] }
allow_responses: { max: 1, ttl: "1m" }
} }
+49 -1
View File
@@ -81,6 +81,19 @@ type CheckSpec struct {
// Toolchains is every toolchain the mesh holds, by language, for a script that declares another.
Toolchain string
Toolchains map[string]string
// Group are a delivery group's other heads (novox/hq ADR 0239), each cloned beside this one at its head
// and composed with it by the gate as one future state. The repository's own check is not run for a
// group: each member's pull request runs its own.
Group []GroupHead
}
// GroupHead is one other head of a delivery group's composed check.
type GroupHead struct {
Owner string
Repo string
Repository string
Ref string
Paths []string
}
// Gated is whether the change touches the mesh's graph, and so whether the gate runs.
@@ -141,6 +154,9 @@ const (
EnvVerdict = "MESH_CHECK_VERDICT"
EnvBeside = "MESH_CHECK_BESIDE"
EnvModules = "MESH_CHECK_MODULES"
// EnvGroup is a delivery group's other heads, as JSON, for the gate (novox/hq ADR 0239); empty for a
// pull request checked alone.
EnvGroup = "MESH_CHECK_GROUP"
)
// CheckTimeout bounds one check; a check that runs past it is an error, not a pass.
@@ -234,6 +250,32 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
say("check", "beside it %s at %s", dir, short(b.Ref))
}
// A delivery group's other heads (novox/hq ADR 0239), each at its head, for the gate to compose with this.
groupFile := ""
if len(spec.Group) > 0 {
var heads []map[string]any
for i, g := range spec.Group {
dir := fmt.Sprintf("group-%d", i)
if g.Repo != "" && safeName.MatchString(g.Repo) {
dir = "group-" + g.Repo
}
if err := clone(g.Repository, g.Ref, dir); err != nil {
return CheckVerdict{}, fmt.Errorf("a head of the group, %w", err)
}
say("check", "with it, of its group, %s/%s at %s", g.Owner, g.Repo, short(g.Ref))
heads = append(heads, map[string]any{"repository": g.Owner + "/" + g.Repo,
"tree": filepath.Join(root, dir), "changed": g.Paths})
}
raw, err := json.Marshal(heads)
if err != nil {
return CheckVerdict{}, err
}
groupFile = filepath.Join(root, "group.json")
if err := os.WriteFile(groupFile, raw, 0o644); err != nil {
return CheckVerdict{}, err
}
}
// The facts, and the versions they say the mesh runs.
if registry == "" {
return CheckVerdict{}, errors.New("no artifact store to read the facts snapshot from")
@@ -316,6 +358,7 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
EnvTestStore + "=" + storeURL, EnvTestBus + "=nats://" + bus, EnvRepository + "=" + spec.Owner + "/" + spec.Repo,
EnvChanged + "=" + strings.Join(spec.Paths, ","), EnvBeside + "=" + root,
EnvModules + "=" + strings.Join(append(append([]string{}, spec.Modules...), spec.New...), ","),
EnvGroup + "=" + groupFile,
"GOCACHE=" + filepath.Join(workspace, "go-cache"), "GOMODCACHE=" + filepath.Join(workspace, "go-modules")}
v := CheckVerdict{}
@@ -346,6 +389,10 @@ func Check(ctx context.Context, run Runner, spec CheckSpec, workspace, registry
// **The repository's own check**, in the toolchain it declares.
switch {
case len(spec.Group) > 0:
// A group's composed check judges the heads together; each member's own tests are its pull request's.
v.Repo = &Layer{Verdict: "pass", Summary: "a group's composed check: each member's own " + CheckScript +
" runs on its pull request"}
case !hasScript:
v.Repo = &Layer{Verdict: "warning", Summary: noScript}
case timedOut():
@@ -447,7 +494,8 @@ func gateLayer(ctx context.Context, spec CheckSpec, tree, root, gate, verdictFil
// 2. Every machine composed with the change.
if err := running(inToolchain(tree, append(env, EnvVerdict+"="+verdictFile), "sh", "-c",
`"$MESH_GATE" merge-gate --facts "$MESH_FACTS" --store "$MESH_GATE_POSTGRES" `+
`--repository "$MESH_CHECK_REPOSITORY" --tree . --changed "$MESH_CHECK_CHANGED" --json > "$MESH_CHECK_VERDICT"`)); err != nil {
`--repository "$MESH_CHECK_REPOSITORY" --tree . --changed "$MESH_CHECK_CHANGED" `+
`${MESH_CHECK_GROUP:+--group "$MESH_CHECK_GROUP"} --json > "$MESH_CHECK_VERDICT"`)); err != nil {
if ctx.Err() != nil {
return "error", "the check was ended during the merge gate"
}
+40
View File
@@ -232,6 +232,7 @@ var aJudge = map[string]string{
"cmd/mesh-controller/main.go": `package main
import (
"encoding/json"
"fmt"
"os"
"strings"
@@ -252,6 +253,24 @@ func main() {
fmt.Println(m + ": ok")
}
case os.Args[1] == "merge-gate":
for i, a := range os.Args {
if a == "--group" && i+1 < len(os.Args) {
body, _ := os.ReadFile(os.Args[i+1])
var heads []struct {
Repository string ` + "`json:\"repository\"`" + `
Tree string ` + "`json:\"tree\"`" + `
}
_ = json.Unmarshal(body, &heads)
var said []string
for _, h := range heads {
if _, err := os.Stat(h.Tree + "/module.json"); err == nil {
said = append(said, h.Repository)
}
}
fmt.Printf("{\"verdict\":\"pass\",\"summary\":\"composed with %s\"}\n", strings.Join(said, ","))
return
}
}
fmt.Println(` + "`" + `{"verdict":"warning","summary":"a merge rebuilds 14 module(s)"}` + "`" + `)
}
}
@@ -323,6 +342,27 @@ func TestTheGateRunsWhenTheGraphIsTouchedBesideTheRepositorysOwnCheck(t *testing
t.Fatalf("a fault the base already had failed the change: %+v\n%s", v.Gate, v.Report)
}
// A delivery group (novox/hq ADR 0239): the other heads cloned beside it at their heads and handed to the
// gate to compose with this one; the repository's own check is each pull request's, not the group's.
app, appHead := aCheckedRepository(t, map[string]string{"module.json": `{"module":"app"}`})
repo2, head2 := aCheckedRepository(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`,
CheckScript: "exit 1\n"})
gid := fmt.Sprintf("check-group-%d", time.Now().UnixNano())
v, err = Check(t.Context(), Command, CheckSpec{ID: gid, Repository: repo2, Ref: head2, Owner: "novox",
Repo: "mesh-catalog", Paths: []string{"modules/gitea/module.json"}, Beside: beside, Modules: []string{"gitea"},
Manifests: []string{"modules/gitea/module.json"}, Toolchain: goToolchain,
Group: []GroupHead{{Owner: "novox", Repo: "app", Repository: app, Ref: appHead, Paths: []string{"module.json"}}}},
t.TempDir(), registry, GitCredential{}, nil)
if err != nil {
t.Fatal(err)
}
if v.Gate.Verdict != "pass" || v.Gate.Summary != "composed with novox/app" {
t.Fatalf("the group's other head was not composed: %+v\n%s", v.Gate, v.Report)
}
if v.Repo == nil || v.Repo.Verdict != "pass" || !strings.Contains(v.Repo.Summary, "group") {
t.Fatalf("a group's check ran a member's own script: %+v", v.Repo)
}
// A change to the controller judges itself: one that does not build fails its own gate.
v = check(map[string]string{"go.mod": "module x\n\ngo 1.22\n", "cmd/mesh-controller/main.go": "package main\nfunc main() { nope }\n",
"modules/gitea/module.json": "{}"}, "self")
+98
View File
@@ -0,0 +1,98 @@
package catalogue
// The delivery's owner (novox/hq ADR 0239, to-be 47): one holder for the mesh, the module mesh-delivery,
// which owns a delivery — one commit in one repository, from its pull request's head to every machine —
// and a delivery group, and asks the controller for every act. A seat of the mesh's own, so the role has
// one name whoever holds it, and the controller can tell whether anything holds it: while nothing does,
// the controller starts every walk itself, as it did before there was a delivery to own.
// DeliverySeat is the seat mesh-delivery holds.
const DeliverySeat = "mesh-delivery"
// deliveryVerbs are the delivery seat's tools: five that read, and the acts of a person and of healer H2.
func deliveryVerbs() []Verb {
return []Verb{
{Name: "deliveries", Description: "Every delivery not final, and those that ended in the last day, one line " +
"each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed " +
"by state, repository or group.",
Input: schema(map[string]string{"state": "one state, e.g. delivering or held",
"repository": "owner/repository", "group": "a group's id (its branch name)",
"all": "\"true\": the final ones of the last thirty days too"}, nil, "all")},
{Name: "show", Description: "One delivery or group whole: its delivery plan (what it builds, what each " +
"machine receives, what is not an ordinary send), every transition with when and why, the machine " +
"steps of its walk, its group and its order.",
Input: schema(map[string]string{"id": "a delivery's id, or a group's"}, []string{"id"})},
{Name: "groups", Description: "Every delivery group: its members in order, why each pair is ordered " +
"(declared, built by, version skew, engine before controller, by name), its composed check and its " +
"state, derived from its members.",
Input: schema(map[string]string{"all": "\"true\": the groups that ended too"}, nil, "all")},
{Name: "what-if", Description: "The delivery plan a change would have, asked of the controller's planner " +
"and kept nowhere: the modules it moves and their dependents in tiers, what each machine would receive, " +
"the steps that are not an ordinary send.",
Input: schema(map[string]string{"repository": "owner/repository",
"paths": "the files it changes, comma-separated, from the repository's root",
"base": "the branch it merges into (default main)"}, []string{"repository", "paths"})},
{Name: "table", Description: "The state table every delivery runs by: each transition with its guard, " +
"each state's bound and what healer H2 may do once it has passed; and the machine steps' table.",
Input: schema(map[string]string{}, nil)},
{Name: "stalled", Description: "Every delivery held past its state's bound, with the transition the table " +
"lets healer H2 take for it, or none.",
Input: schema(map[string]string{}, nil)},
{Name: "recheck", Description: "Check a rejected or ready delivery again: it goes back to proposed and the " +
"controller is asked for its check. With why.",
Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"},
[]string{"id", "why"})},
{Name: "release", Description: "A person's word that a held delivery goes on: it starts delivering. With why.",
Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"},
[]string{"id", "why"})},
{Name: "stop", Description: "Stop a delivery that is not final, with why; its walk is ended through the " +
"controller, and in a group every member after it is stopped too, naming it.",
Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"},
[]string{"id", "why"})},
{Name: "close", Description: "Healer H2's verb: take the one transition the table names for a delivery held " +
"past its bound. Refused for any other.",
Input: schema(map[string]string{"id": "the delivery's id", "why": "the condition it answers"},
[]string{"id", "why"})},
}
}
// SeatSays is whether a consumed `<seat>.<event>` names one of the seat's own events: the seat emits it,
// exactly or by one of its patterns (`log.*`). A consumed name whose emitter is a seat and whose event the
// seat does not emit is the event of the module of that name (novox/hq ADR 0239): mesh-delivery is a seat
// and the module holding it, and what the module says it says as itself, through the runtime that launches
// it — read as the seat's, a consumer would subscribe a subject nothing may publish.
func SeatSays(emits []string, event string) bool {
for _, e := range emits {
if e == event || topicMatches(e, event) {
return true
}
}
return false
}
// topicMatches is whether an event's name matches a declared pattern, `*` one dot-separated segment and
// `>` or `**` the rest.
func topicMatches(pattern, event string) bool {
p, e := splitDots(pattern), splitDots(event)
for i, part := range p {
if part == ">" || part == "**" {
return len(e) > i
}
if i >= len(e) || (part != "*" && part != e[i]) {
return false
}
}
return len(p) == len(e)
}
func splitDots(s string) []string {
var out []string
start := 0
for i := 0; i < len(s); i++ {
if s[i] == '.' {
out = append(out, s[start:i])
start = i + 1
}
}
return append(out, s[start:])
}
+7 -1
View File
@@ -93,7 +93,9 @@ var defaultSeats = append([]Seat{
// A build put back after its gate failed (novox/hq ADR 0236, to-be 45 §8).
"rolled-back",
// A pull request's merge check, judged (novox/hq to-be 45 §9).
"checked"},
"checked",
// A walk kept in a new state, for the delivery it walks (novox/hq ADR 0239).
"plan-moved"},
Serves: ControllerVerbs},
// The store's first verbs (novox/hq ADR 0159): the smallest set that makes the store askable,
// served by whichever module holds the seat with tools of these names.
@@ -121,6 +123,10 @@ var defaultSeats = append([]Seat{
// image registry.
{Name: "mesh-artifact-store", Scope: ScopeMesh, Delivers: "artifact-store", Decision: "novox/hq ADR 0075"},
{Name: "mesh-catalog", Scope: ScopeMesh, Decision: "novox/hq ADR 0121"},
// The delivery's owner (novox/hq ADR 0239): one commit's journey from its pull request's head to every
// machine, and a group of them, recorded and ordered by its holder, which asks the controller for each act.
// What it says it says as its module (`mesh-delivery.transition`), through the runtime that launches it.
{Name: DeliverySeat, Scope: ScopeMesh, Serves: deliveryVerbs(), Decision: "novox/hq ADR 0239"},
// Deferred renames (novox/hq ADR 0121): these deliver a provision, so renaming them is a
// delivering-seat migration with a mesh-wide cascade if a holder stops resolving mid-flight.
// They keep their names until that migration is done deliberately, apart from the node-* pass.
+4 -3
View File
@@ -46,7 +46,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
delivered[s.Delivers] = s.Name
}
}
// Thirty-six since node-resolver-config retired into node-uplink (novox/hq ADR 0223); thirty-seven
// Thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
// into node-uplink (novox/hq ADR 0223); thirty-seven
// since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with
// node-backup (novox/hq ADR 0214); thirty-seven with node-message-bus (novox/hq ADR 0215);
// thirty-six with mesh-dns-resolver (novox/hq ADR 0194) and node-hosts-file (ADR 0199, now
@@ -56,8 +57,8 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
// ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired
// mesh-build-machine row goes, when no registered manifest claims it.
if len(Seats()) != 36 {
t.Errorf("the mesh defines %d seats rather than 36; the set is closed, so a change here is "+
if len(Seats()) != 37 {
t.Errorf("the mesh defines %d seats rather than 37; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
}
}
+42 -6
View File
@@ -102,19 +102,55 @@ var ControllerVerbs = []Verb{
{Name: "plans", Description: "What the last merges produced and where each stands (novox/hq ADR 0162): " +
"the tiers, the tier a plan is at, what it waits for and since when; one plan whole, given its id.",
Input: schema(map[string]string{
"id": "a plan's id (as `plans` lists them): that plan, tier by tier",
"stop": "a plan's id: stop it — what was asked still builds, nothing further is asked",
"close": "a plan's id: close a plan that will not move again, as failed by hand (novox/hq issue 254)",
"retry": "a failed plan's id: ask its failed builds again under new ids, and carry the plan on from that tier (novox/hq ADR 0219)",
"id": "a plan's id (as `plans` lists them): that plan, tier by tier",
"stop": "a plan's id: stop it — what was asked still builds, nothing further is asked",
"close": "a plan's id: close a plan that will not move again, as failed by hand (novox/hq issue 254)",
"retry": "a failed plan's id: ask its failed builds again under new ids, and carry the plan on from that tier (novox/hq ADR 0219)",
"go": "a walk's id that waits for its delivery's word: start it by hand, in place of mesh-delivery — with why, " +
"recorded in the hand-act log (novox/hq ADR 0239)",
"repository": "owner/repository: the plan a merge there would produce, saving nothing (what-if); with paths or modules",
"paths": "with repository: the files the merge would change, comma-separated, from the repository's root",
"modules": "with repository: or the modules it would change, comma-separated",
"module-dirs": "with repository and paths: the directories holding a module.json at the commit, comma-separated, " +
"as the forge's announcer says them (novox/hq issue 278); without it, a directory is a module only when the paths hold its manifest",
"limit": "how many plans to list (default 10); only when listing",
"why": "with stop or close: why it is ended by hand — required, and recorded in the hand-act log (novox/hq to-be 45 §7)",
"cause": "with stop or close: the cause in a word, or a condition's kind (optional)",
"why": "with stop, close or go: why it is done by hand — required, and recorded in the hand-act log (novox/hq to-be 45 §7)",
"cause": "with stop, close or go: the cause in a word, or a condition's kind (optional)",
}, nil)},
// The verbs the delivery's owner asks with (novox/hq ADR 0239): the planner's answer, a group's order and
// composed check, and the start and end of a walk. Callable by a person too; mesh-delivery is who needs them.
{Name: "delivery-plan", Description: "The delivery plan of a diffset, as the planner computes it and keeping " +
"nothing (novox/hq ADR 0239): what it moves and builds after them in tiers, what each machine receives and " +
"what waits there, the steps that are not an ordinary send, and whether the gate would run.",
Input: schema(map[string]string{"repository": "owner/repository", "head": "the commit at hand",
"base": "the branch it merges into (default main)",
"paths": "the files it changes, comma-separated, from the repository's root",
"module-dirs": "the directories holding a module.json at the head, comma-separated, as the forge says them",
"removed": "the files among paths it deletes, comma-separated"}, []string{"repository", "paths"})},
{Name: "delivery-order", Description: "A delivery group's order (novox/hq ADR 0239): its members in the order " +
"they are delivered, every pair and why — declared, built by, version skew, engine before controller — and " +
"the cycle when the pairs contradict each other.",
Input: schema(map[string]string{"members": "the members, as JSON: [{id, repository, base, head, number, paths, " +
"module_dirs, module_dirs_said, removed, after}]"}, []string{"members"})},
{Name: "delivery-check", Description: "Ask the build seat for a delivery group's composed check (novox/hq ADR " +
"0239): every member's head laid over the mesh in turn and judged as one future state. Answers the ask's id; " +
"the verdict is said as `checked` with the group's id. With one member and no group, that head is checked " +
"again as its pull request is.",
Input: schema(map[string]string{"group": "the group's id (none for one head checked again)",
"members": "the members, as JSON, as delivery-order takes them"}, []string{"members"})},
{Name: "deliver", Description: "The delivery's word that a walk waiting for it may start (novox/hq ADR 0239): its " +
"first tier is asked at the controller's next pass. Refused for a walk that waits for nobody.",
Input: schema(map[string]string{"plan": "the walk's id", "why": "what lets it go: its turn, a person's release"},
[]string{"plan"})},
{Name: "delivery-stop", Description: "End a walk on its delivery's word (novox/hq ADR 0239): failed, said as stopped " +
"by whom and why; what it asked still builds and registers, nothing further is asked or sent.",
Input: schema(map[string]string{"plan": "the walk's id", "why": "why", "by": "who stopped the delivery"},
[]string{"plan", "why"})},
{Name: "delivery-walks", Description: "The walks the controller keeps (novox/hq ADR 0239): every open one and the " +
"last ended ones, each whole — its tiers, each module's state, first machines and gate — and whether the " +
"delivery seat has a holder on record. Given a plan, that one.",
Input: schema(map[string]string{"plan": "one walk's id", "limit": "how many ended walks beside the open ones (default 50)"},
nil)},
{Name: "plan", Description: "What one machine would run, and why: the declaration the mesh would send it — " +
"or, with files, the files it would be given.",
Input: schema(map[string]string{
+3 -1
View File
@@ -114,7 +114,9 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
for _, c := range m.Consumes {
emitter, event, named := strings.Cut(c, ".")
if named {
if s, isASeat := seats[emitter]; isASeat {
// A seat's event when the seat says it; else the event of the module of that name — a seat and
// the module holding it may share a name (mesh-delivery, novox/hq ADR 0239).
if s, isASeat := seats[emitter]; isASeat && catalogue.SeatSays(s.Emits, event) {
watches = append(watches, broker.Seat{Name: s.Name, Emits: []string{event}})
continue
}
@@ -0,0 +1,9 @@
-- A walk waits for its delivery's word (novox/hq ADR 0239).
--
-- A plan is from now on the walk of one delivery's trunk commit across the machines: tier by tier, one
-- machine first and judged at the gate. While the mesh-delivery seat has a holder on record, a walk that
-- moves no core module is opened by the merge and waits — nothing asked, nothing registered, nothing sent —
-- until the delivery's owner says it may start, or a person does (`plans go`). Kept with the plan, as one
-- document: who it waits for, and when, by whom and why it was let go or stopped. Null for every plan
-- before this and for a walk on the controller's own path, which waits for nobody.
alter table release_plan add column delivery jsonb;
+49 -8
View File
@@ -41,8 +41,35 @@ type Plan struct {
// Release is set on a release plan (novox/hq ADR 0236): not a merge's, but the builds waiting for a
// gate, walked through the machines one at a time.
Release *PlanRelease `json:"release,omitempty"`
// Delivery is set on a walk that waits for its delivery's word (novox/hq ADR 0239): nil for a walk on
// the controller's own path, which starts at the merge as every plan did before.
Delivery *PlanDelivery `json:"delivery,omitempty"`
}
// PlanDelivery is what a walk waits for and what came of the wait (novox/hq ADR 0239).
type PlanDelivery struct {
// Awaits is who must say the walk may start: the seat whose holder owns the delivery.
Awaits string `json:"awaits"`
// Go is when it was let go, By by whom (the seat's holder, or a person's name) and Why.
Go *time.Time `json:"go,omitempty"`
By string `json:"by,omitempty"`
Why string `json:"why,omitempty"`
// Stopped is who ended the walk through the delivery's owner, and StoppedWhy why: the walk is failed,
// and the delivery reads it as stopped, not as a build that failed.
Stopped string `json:"stopped,omitempty"`
StoppedWhy string `json:"stopped_why,omitempty"`
}
// Waiting is whether the walk waits for its delivery's word.
func (p Plan) Waiting() bool {
return p.Delivery != nil && p.Delivery.Awaits != "" && p.Delivery.Go == nil
}
// PlanSaved is told every plan this process kept, after it is kept (novox/hq ADR 0239): the serving
// controller says it on the bus as `plan-moved`. Nil in a command, which says nothing; whoever follows a
// walk also asks for it, so a save made by a command is found by comparison.
var PlanSaved func(Plan)
// ErrPlanMoved is a save against a plan written by somebody else since it was read.
var ErrPlanMoved = errors.New("the plan was written by somebody else since it was read")
@@ -174,12 +201,17 @@ func (i *Inventory) SavePlan(ctx context.Context, p *Plan) error {
if err != nil {
return err
}
var release []byte
var release, delivery []byte
if p.Release != nil {
if release, err = json.Marshal(p.Release); err != nil {
return err
}
}
if p.Delivery != nil {
if delivery, err = json.Marshal(p.Delivery); err != nil {
return err
}
}
// **And how long the tier it left took** (novox/hq to-be 45 Phase 0): measured here, where the
// plan moves, in the same transaction as the move, so no save can move a tier unmeasured or
// measure one twice.
@@ -195,16 +227,16 @@ func (i *Inventory) SavePlan(ctx context.Context, p *Plan) error {
var revision int64
err = tx.QueryRow(ctx,
`insert into release_plan (id, repository, commit_hash, created, updated, state, tier, tiers, modules, note,
branch, tier_entered, revision, epoch, release)
values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13, $14)
branch, tier_entered, revision, epoch, release, delivery)
values ($1, $2, $3, $4, now(), $5, $6, $7, $8, $9, $10, $11, 1, $13, $14, $15)
on conflict (id) do update set updated = now(), state = excluded.state, tier = excluded.tier,
tiers = excluded.tiers, modules = excluded.modules, note = excluded.note, branch = excluded.branch,
tier_entered = excluded.tier_entered, revision = release_plan.revision + 1, epoch = excluded.epoch,
release = excluded.release
release = excluded.release, delivery = excluded.delivery
where release_plan.revision = $12
returning revision`,
p.ID, p.Repository, p.Commit, p.Created, p.State, p.Tier, tiers, modules, p.Note, p.Branch, entered,
p.Revision, epoch, release).Scan(&revision)
p.Revision, epoch, release, delivery).Scan(&revision)
if errors.Is(err, pgx.ErrNoRows) {
// The row is there and at another revision — moved since this was read, or there already
// when this one is new: either way not this writer's to overwrite. (A plan saved before plans
@@ -223,6 +255,9 @@ func (i *Inventory) SavePlan(ctx context.Context, p *Plan) error {
} else {
p.Epoch = 0
}
if PlanSaved != nil {
PlanSaved(*p)
}
return nil
}
@@ -259,7 +294,7 @@ func (i *Inventory) PlanByID(ctx context.Context, id string) (Plan, error) {
func (i *Inventory) plans(ctx context.Context, tail string) ([]Plan, error) {
rows, err := i.store.Pool().Query(ctx,
`select id, repository, commit_hash, created, updated, state, tier, tiers, modules, note, branch,
coalesce(tier_entered, created), revision, coalesce(epoch, 0), release
coalesce(tier_entered, created), revision, coalesce(epoch, 0), release, delivery
from release_plan `+tail)
if err != nil {
return nil, err
@@ -268,10 +303,11 @@ func (i *Inventory) plans(ctx context.Context, tail string) ([]Plan, error) {
var out []Plan
for rows.Next() {
var p Plan
var tiers, modules, release []byte
var tiers, modules, release, delivery []byte
var epoch int64
if err := rows.Scan(&p.ID, &p.Repository, &p.Commit, &p.Created, &p.Updated, &p.State,
&p.Tier, &tiers, &modules, &p.Note, &p.Branch, &p.TierEntered, &p.Revision, &epoch, &release); err != nil {
&p.Tier, &tiers, &modules, &p.Note, &p.Branch, &p.TierEntered, &p.Revision, &epoch, &release,
&delivery); err != nil {
return nil, err
}
if len(release) > 0 {
@@ -279,6 +315,11 @@ func (i *Inventory) plans(ctx context.Context, tail string) ([]Plan, error) {
return nil, err
}
}
if len(delivery) > 0 {
if err := json.Unmarshal(delivery, &p.Delivery); err != nil {
return nil, err
}
}
p.Epoch = uint64(epoch)
if err := json.Unmarshal(tiers, &p.Tiers); err != nil {
return nil, err
+15
View File
@@ -123,6 +123,21 @@ type CheckRequest struct {
// 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.
+18
View File
@@ -76,6 +76,11 @@ const (
// 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.
@@ -261,6 +266,19 @@ type Checked struct {
// 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