Files
mesh-catalog/modules/mesh-delivery/cmd/mesh-delivery/model.go
T
jochen 6327f297ea
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request
Cover a held merge only when the forge says a later delivered merge contains it (review of #150)
Merge order on a protected trunk was the whole proof; the review asked for ancestry. The gitea module
gains gitea_contains, a read-only compare of two commits (total_commits 0 is contained), and mesh-delivery
asks it once per pair, off the lock like its plan ask, keeping the answer on the delivery. A forge that
cannot say leaves the merge held, not covered. Merge order stays the cheap first filter.

A later delivery now carries a module only when its own check passed (so a merge held without a passing
check is closed only by one that passed), its walk is of its own merge commit on the same trunk, and its
steps say the module reached every machine running it, not one. The words say what is known: a later
merge that contains it was delivered.
2026-10-09 02:57:18 +02:00

519 lines
19 KiB
Go

package main
import (
"encoding/json"
"fmt"
"regexp"
"sort"
"strings"
"time"
)
// What mesh-delivery owns (novox/hq ADR 0239): the delivery — one commit in one repository — and the
// delivery group above it. Each is kept whole, one key of the module's own state per delivery and per
// group, written only by this holder.
// Delivery is one commit in one repository, from its pull request's head to every machine.
type Delivery struct {
// ID is owner/repository@<twelve characters of its commit>.
ID string `json:"id"`
Repository string `json:"repository"`
Commit string `json:"commit"`
// The pull request, when there is one: its number, title, base, head branch and page.
Number int `json:"number,omitempty"`
Title string `json:"title,omitempty"`
Base string `json:"base,omitempty"`
Branch string `json:"branch,omitempty"`
HTMLURL string `json:"html_url,omitempty"`
CloneURL string `json:"clone_url,omitempty"`
// The diffset, as the forge said it: what the planner maps onto modules.
Paths []string `json:"paths,omitempty"`
PathsTruncated bool `json:"paths_truncated,omitempty"`
Removed []string `json:"removed,omitempty"`
ModuleDirs []string `json:"module_dirs,omitempty"`
ModuleDirsSaid bool `json:"module_dirs_said,omitempty"`
// After are the repositories its pull request says it goes after (`after: <repository>` lines).
After []string `json:"after,omitempty"`
// Group is the delivery group it is in: its head branch name. Empty for a lone delivery.
Group string `json:"group,omitempty"`
State State `json:"state"`
Since time.Time `json:"since"`
Created time.Time `json:"created"`
// Plan is its delivery plan, as the controller's planner computed it (the build plan, the deploy plan,
// the steps that are not an ordinary send).
Plan *DeliveryPlan `json:"plan,omitempty"`
// Check is its own check's verdict.
Check *Verdict `json:"check,omitempty"`
// CheckAsk is this owner's own ask of its check, when the forge's announcement did not bring one: a head
// adopted while it already waited, or one whose announcement the controller missed.
CheckAsk *CheckAsk `json:"check_ask,omitempty"`
// The facts the table's guards read. NewerHead is the head that superseded it; ClosedUnmerged the pull
// request closed; MergedAs the commit it landed on the trunk as; HeldWhy what holds it for a person.
NewerHead string `json:"newer_head,omitempty"`
ClosedUnmerged bool `json:"closed_unmerged,omitempty"`
MergedAs string `json:"merged_as,omitempty"`
MergedAt time.Time `json:"merged_at"`
HeldWhy string `json:"held_why,omitempty"`
// FromForge is a delivery made from the forge's word at its merge: its head was never heard by this owner.
// History is why it is history, once it is (novox/hq issue 313).
FromForge bool `json:"from_forge,omitempty"`
History string `json:"history,omitempty"`
// CoveredBy says which later merges into the same trunk delivered every module it moves, once they did
// (novox/hq issue 319); Lifted why what held it for a person no longer holds (issue 316).
CoveredBy string `json:"covered_by,omitempty"`
Lifted string `json:"lifted,omitempty"`
// ContainedIn is the forge's answer, by a later merge commit, to whether that commit contains this one's
// merge (gitea_contains): asked once per pair, and kept (ADR 0270).
ContainedIn map[string]bool `json:"contained_in,omitempty"`
// Walk is the controller's walk of its trunk commit, as last read; Steps the machines it reached.
Walk *WalkSeen `json:"walk,omitempty"`
Steps []Step `json:"steps,omitempty"`
// LetGo is when this owner asked the controller to start its walk, and why.
LetGo time.Time `json:"let_go"`
LetGoWhy string `json:"let_go_why,omitempty"`
Transitions []Transition `json:"transitions,omitempty"`
// Owed are the forge's and the bus's effects of its transitions not yet done: said only after the
// delivery is kept, retried until done.
Owed []Effect `json:"owed,omitempty"`
}
// Transition is one move of the table, kept.
type Transition struct {
At time.Time `json:"at"`
From State `json:"from"`
To State `json:"to"`
Event Event `json:"event"`
Why string `json:"why"`
By string `json:"by,omitempty"`
}
// Verdict is a check's verdict: the gate's and the repository's own. ID is the build seat's ask that ran
// it, On the machine it ran on, At when this owner heard it; Report is the check's own account, as bounded
// as the controller says it. All of it is what `checks` answers, whole, beside the forge's short statuses.
type Verdict struct {
ID string `json:"id,omitempty"`
On string `json:"on,omitempty"`
At time.Time `json:"at"`
Gate string `json:"gate"`
Summary string `json:"summary"`
Modules []string `json:"modules,omitempty"`
Dependents []string `json:"dependents,omitempty"`
Repo string `json:"repo,omitempty"`
RepoSaid string `json:"repo_summary,omitempty"`
Report string `json:"report,omitempty"`
}
// CheckAsk is a head's own check, asked of the controller by this owner (or re-asked by healer H2).
type CheckAsk struct {
// ID is the ask's id as the controller answered it.
ID string `json:"id"`
At time.Time `json:"at"`
By string `json:"by"`
Why string `json:"why"`
// Reasked is how many times H2 asked it again: once at most, then the operator's.
Reasked int `json:"reasked,omitempty"`
}
// Passes is whether the verdict lets the delivery be ready: the gate passed or warned, and the
// repository's own check did not fail or error.
func (v *Verdict) Passes() bool {
if v == nil {
return false
}
gate := v.Gate == "pass" || v.Gate == "warning"
repo := v.Repo == "" || v.Repo == "pass" || v.Repo == "warning"
return gate && repo
}
// DeliveryPlan is a delivery plan as the controller's planner says it (mesh-controller link.ChangePlan).
type DeliveryPlan struct {
Repository string `json:"repository"`
Base string `json:"base"`
Head string `json:"head"`
Moved []string `json:"moved,omitempty"`
Dependents []string `json:"dependents,omitempty"`
New []string `json:"new,omitempty"`
Unread []string `json:"unread,omitempty"`
Tiers [][]string `json:"tiers,omitempty"`
Machines []MachinePlan `json:"machines,omitempty"`
Steps []string `json:"steps,omitempty"`
Summary string `json:"summary"`
}
// MachinePlan is one machine's part of a plan.
type MachinePlan struct {
Machine string `json:"machine"`
Receives []string `json:"receives,omitempty"`
Waits []string `json:"waits,omitempty"`
}
// MovesNothing is whether the delivery's plan moves no module the mesh holds: nothing for a walk to do. A
// module it adds is not moved: the controller builds and registers it and sends it nowhere, and opens no walk
// for it (novox/hq issues 300 and 319). A delivery with no plan known is not said to move nothing.
func (d *Delivery) MovesNothing() bool {
return d.Plan != nil && len(d.Plan.Moved) == 0
}
// Step is one machine's part of a delivering walk.
type Step struct {
Machine string `json:"machine"`
Module string `json:"module"`
Build string `json:"build,omitempty"`
State StepState `json:"state"`
Since time.Time `json:"since"`
Why string `json:"why,omitempty"`
}
// Effect is one thing owed outside the state: an event said, a line appended to a commit's note, the view
// on the pull request refreshed, a status set.
type Effect struct {
Kind string `json:"kind"`
Commit string `json:"commit,omitempty"`
Line string `json:"line,omitempty"`
Context string `json:"context,omitempty"`
State string `json:"state,omitempty"`
Tries int `json:"tries,omitempty"`
Last string `json:"last,omitempty"`
Since time.Time `json:"since"`
// Next is when a failed effect is tried again: later after every failure, up to retryCap.
Next *time.Time `json:"next,omitempty"`
}
// The kinds of effect.
const (
EffectEmit = "emit"
EffectNote = "note"
EffectView = "view"
EffectStatus = "status"
)
// FromForgesWord is whether the delivery was made from the forge's word at its merge: said by FromForge, or —
// for one made before FromForge was kept — by its first transition.
func (d *Delivery) FromForgesWord() bool {
if d.FromForge {
return true
}
return len(d.Transitions) > 0 && d.Transitions[0].Event == EvAnnounced &&
strings.Contains(d.Transitions[0].Why, "so it is made from the forge's word")
}
// shownOnItsPullRequest is whether its view and status are kept on its pull request: not for history, whose
// pull request the forge may no longer hold — or holds under its number another one.
func (d *Delivery) shownOnItsPullRequest() bool {
return d.Number > 0 && d.History == "" && d.State != Retired
}
// Owner and Repo split the repository.
func (d *Delivery) Owner() string { o, _, _ := strings.Cut(d.Repository, "/"); return o }
func (d *Delivery) Repo() string { _, r, _ := strings.Cut(d.Repository, "/"); return r }
// IDOf is a delivery's id: the repository and twelve characters of its commit.
func IDOf(repository, commit string) string {
return strings.ToLower(repository) + "@" + shortOf(commit, 12)
}
func shortOf(commit string, n int) string {
if len(commit) > n {
return commit[:n]
}
return commit
}
// sameCommit is whether two commits are one, however either is abbreviated.
func sameCommit(a, b string) bool {
if a == "" || b == "" {
return false
}
if len(a) > len(b) {
a, b = b, a
}
return len(a) >= 7 && strings.HasPrefix(strings.ToLower(b), strings.ToLower(a))
}
// afterLines are the `after: <repository>` lines of a pull request's description.
var afterLines = regexp.MustCompile(`(?im)^\s*after:\s*([A-Za-z0-9_.\-/]+)\s*$`)
// AfterIn reads them.
func AfterIn(body string) []string {
var out []string
for _, m := range afterLines.FindAllStringSubmatch(body, -1) {
out = append(out, strings.TrimSuffix(m[1], ".git"))
}
sort.Strings(out)
return out
}
// The walk: the controller's plan of one trunk commit, as its `plan-moved` and `delivery-walks` say it
// (mesh-controller inventory.Plan). Read, never written here.
// The states of a walk as the controller keeps them.
const (
walkBuilding = "building"
walkRolling = "rolling"
walkDone = "done"
walkFailed = "failed"
walkSuperseded = "superseded"
)
// Walk is a walk whole, as the controller says it.
type Walk struct {
ID string `json:"id"`
Repository string `json:"repository"`
Branch string `json:"branch,omitempty"`
Commit string `json:"commit"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
State string `json:"state"`
Tier int `json:"tier"`
Tiers [][]string `json:"tiers"`
Modules map[string]*WalkModule `json:"modules"`
Note string `json:"note,omitempty"`
Revision int64 `json:"revision"`
Release *WalkRelease `json:"release,omitempty"`
Delivery *WalkDelivery `json:"delivery,omitempty"`
}
// WalkModule is one module of a walk.
type WalkModule struct {
State string `json:"state,omitempty"`
SentAt *time.Time `json:"sent_at,omitempty"`
First []string `json:"first,omitempty"`
FirstAt *time.Time `json:"first_at,omitempty"`
Commit string `json:"commit,omitempty"`
Why string `json:"why,omitempty"`
Build string `json:"build,omitempty"`
Gate *WalkGate `json:"gate,omitempty"`
GatedBy string `json:"gated_by,omitempty"`
}
// WalkGate is a module's judging on its first machine.
type WalkGate struct {
Machines []string `json:"machines"`
From string `json:"from,omitempty"`
To string `json:"to,omitempty"`
Since *time.Time `json:"since,omitempty"`
Passes int `json:"passes,omitempty"`
Verdict string `json:"verdict,omitempty"`
Why string `json:"why,omitempty"`
JudgedAt *time.Time `json:"judged_at,omitempty"`
Rollback string `json:"rollback,omitempty"`
Carried []WalkCarry `json:"carried,omitempty"`
}
// WalkCarry is one build a gated send carried.
type WalkCarry struct {
Module string `json:"module"`
Node string `json:"node"`
From string `json:"from,omitempty"`
To string `json:"to"`
Build string `json:"build,omitempty"`
}
// WalkRelease is a backlog walk's way through the machines.
type WalkRelease struct {
Order []string `json:"order"`
Next int `json:"next"`
Gate *WalkGate `json:"gate,omitempty"`
Done []string `json:"done,omitempty"`
By string `json:"by,omitempty"`
}
// WalkDelivery is what a walk waits for.
type WalkDelivery struct {
Awaits string `json:"awaits"`
Go *time.Time `json:"go,omitempty"`
By string `json:"by,omitempty"`
Why string `json:"why,omitempty"`
Stopped string `json:"stopped,omitempty"`
StoppedWhy string `json:"stopped_why,omitempty"`
}
// WalkSeen is what a delivery keeps of its walk.
type WalkSeen struct {
ID string `json:"id"`
Commit string `json:"commit"`
State string `json:"state"`
Note string `json:"note,omitempty"`
Revision int64 `json:"revision"`
Waits bool `json:"waits,omitempty"`
WaitedFor string `json:"waited_for,omitempty"`
GoBy string `json:"go_by,omitempty"`
StoppedBy string `json:"stopped_by,omitempty"`
Tier int `json:"tier"`
Tiers int `json:"tiers"`
Updated time.Time `json:"updated"`
// Branch is the branch the walk's commit was merged into; empty in a record kept before it was read.
Branch string `json:"branch,omitempty"`
}
// Seen is a walk as a delivery keeps it.
func (w Walk) Seen() *WalkSeen {
s := &WalkSeen{ID: w.ID, Commit: w.Commit, State: w.State, Note: w.Note, Revision: w.Revision, Tier: w.Tier,
Tiers: len(w.Tiers), Updated: w.Updated}
if d := w.Delivery; d != nil {
s.WaitedFor = d.Awaits
s.Waits = d.Awaits != "" && d.Go == nil
s.GoBy = d.By
s.StoppedBy = d.Stopped
}
s.Branch = w.Branch
return s
}
// Started is whether the walk is past its wait: it waited for nobody, or was let go — whatever came of it
// since. A walk superseded or stopped while it waited never started.
func (s *WalkSeen) Started() bool { return s != nil && !s.Waits && (s.WaitedFor == "" || s.GoBy != "") }
// Waited is whether it waited for a word at all.
func (s *WalkSeen) Waited() bool { return s != nil && s.WaitedFor != "" }
// LetGoByAPerson is whether a person's `plans go` started it.
func (s *WalkSeen) LetGoByAPerson() bool { return s != nil && strings.HasPrefix(s.GoBy, "a person") }
// Open is whether the controller still works it.
func (w Walk) Open() bool { return w.State == walkBuilding || w.State == walkRolling }
// The steps that say a module reached every machine running it: sent to all at once, or to the rest after its
// first machines passed.
const (
stepEveryMachine = "every machine running it"
stepTheRest = "the rest running it"
)
// StepsOf are the machine steps a walk's record says, for one delivery: every module it moved, on its first
// machines from its gate, and the rest once sent.
func StepsOf(w Walk) []Step {
var out []Step
names := make([]string, 0, len(w.Modules))
for n := range w.Modules {
names = append(names, n)
}
sort.Strings(names)
for _, name := range names {
m := w.Modules[name]
if m == nil || m.FirstAt == nil {
if m != nil && m.SentAt != nil && m.FirstAt == nil && m.Why == "" {
out = append(out, Step{Machine: stepEveryMachine, Module: name, Build: m.Build,
State: StepSent, Since: *m.SentAt})
}
continue
}
gate := m.Gate
state, why, since := StepSent, "", *m.FirstAt
if gate != nil {
switch {
case gate.Rollback == "rolled-back":
state, why = StepRolledBack, gate.Why
case gate.Verdict == "failed":
state, why = StepFailed, gate.Why
case gate.Verdict == "passed":
state, why = StepPassed, gate.Why
case gate.Since != nil:
state = StepJudging
why = fmt.Sprintf("%d healthy judging(s) so far", gate.Passes)
}
if gate.JudgedAt != nil {
since = *gate.JudgedAt
}
}
for _, machine := range m.First {
out = append(out, Step{Machine: machine, Module: name, Build: m.Build, State: state, Since: since, Why: why})
}
if m.SentAt != nil && state == StepPassed {
out = append(out, Step{Machine: stepTheRest, Module: name, Build: m.Build, State: StepSent,
Since: *m.SentAt})
}
}
return out
}
// Group is a delivery group: two or more deliveries sharing a head branch name, one level only.
type Group struct {
ID string `json:"id"`
Members []string `json:"members"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
// Order is the members in the order they are delivered, Pairs why, Cycle the members whose order
// contradicts itself; OrderOf the member commits the order was worked out for.
Order []string `json:"order,omitempty"`
Pairs []Pair `json:"pairs,omitempty"`
Cycle []string `json:"cycle,omitempty"`
// Contradictions is each two members whose rules order them both ways at one rank, said with how a
// person declares the order (novox/hq ADR 0249).
Contradictions []Contradiction `json:"contradictions,omitempty"`
OrderOf map[string]string `json:"order_of,omitempty"`
// Check is its composed check: asked of the controller for these member commits, and its verdict.
Check *GroupCheck `json:"check,omitempty"`
// Closed is set once a member reached the trunk: a group delivering takes no new member.
Closed bool `json:"closed,omitempty"`
// Said is the derived state last said, so a change of it is said once.
Said string `json:"said,omitempty"`
Owed []Effect `json:"owed,omitempty"`
}
// Pair is one "before" among a group's members: the rule that won, and the rules that ordered it the other
// way and lost by precedence (novox/hq ADR 0249).
type Pair struct {
Before string `json:"before"`
After string `json:"after"`
Why string `json:"why"`
Over []string `json:"over,omitempty"`
}
// Contradiction is two members that rules of one rank order both ways, which no precedence resolves: the
// rules, and Said, which names them and says how to declare the order.
type Contradiction struct {
Members []string `json:"members"`
Rules []Pair `json:"rules"`
Said string `json:"said"`
}
// orderSaid is a pair as a person reads it: what won, and over what.
func (p Pair) orderSaid() string {
s := p.Before + " before " + p.After + ": " + p.Why
if len(p.Over) > 0 {
s += ", over " + strings.Join(p.Over, ", ")
}
return s
}
// GroupCheck is a group's composed check.
type GroupCheck struct {
Asked string `json:"asked"`
At time.Time `json:"at"`
Commits map[string]string `json:"commits"`
Verdict string `json:"verdict,omitempty"`
Summary string `json:"summary,omitempty"`
}
// Commits is each member's commit, by id.
func commitsOf(ds []*Delivery) map[string]string {
out := map[string]string{}
for _, d := range ds {
out[d.ID] = d.Commit
}
return out
}
// sameMembers is whether two member→commit maps are one.
func sameMembers(a, b map[string]string) bool {
if len(a) != len(b) {
return false
}
for k, v := range a {
if b[k] != v {
return false
}
}
return true
}
func mustJSON(v any) json.RawMessage {
raw, _ := json.Marshal(v)
return raw
}