Files
mesh-catalog/modules/mesh-delivery/cmd/mesh-delivery/effects.go
T
jochen 6ae112d299
mesh/merge-gate pass: builds 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 delivered
Say which order rule won, and how to end a contradiction (hq issue 309)
A group whose order rules disagreed was refused as "its order contradicts
itself" with only the member ids: nobody could see which rules clashed or
what to write to resolve it. The controller now resolves rules by
precedence (hq ADR 0249) and answers, per pair, the rules it won over, and
per contradiction no precedence resolves, both rules and how to declare
the order. mesh-delivery keeps both, lists them in groups and the plan
note, and refuses a group by those words; a cycle through more members
names the pairs in it. An older controller's answer reads as before.
2026-10-08 10:43:21 +02:00

299 lines
8.4 KiB
Go

package main
import (
"encoding/json"
"fmt"
"sort"
"strings"
"time"
)
// What a transition owes outside the state (novox/hq ADR 0239 decision 5), each by its owner: the event on
// the bus (said by this module), a line on the commit's note and the pull request's view and statuses (by
// the forge's holder, asked through its tools). Owed, never lost: kept with the delivery before anything is
// tried, and tried again until done.
// transitionEvent is what `mesh-delivery.transition` says: no secret, no address.
func transitionEvent(d *Delivery, t Transition) map[string]any {
e := map[string]any{"id": d.ID, "repository": d.Repository, "commit": d.Commit, "from": t.From, "to": t.To,
"event": t.Event, "why": t.Why, "at": t.At}
if t.By != "" {
e["by"] = t.By
}
if d.Number > 0 {
e["number"] = d.Number
}
if d.Group != "" {
e["group"] = d.Group
}
if d.MergedAs != "" {
e["merged_as"] = d.MergedAs
}
if d.Walk != nil {
e["walk"] = d.Walk.ID
}
if d.Plan != nil {
e["plan"] = d.Plan.Summary
}
return e
}
// noteLine is one transition as the commit's note keeps it.
func noteLine(d *Delivery, t Transition) string {
by := ""
if t.By != "" {
by = " by " + t.By
}
line := fmt.Sprintf("%s delivery %s: %s -> %s (%s%s): %s", t.At.UTC().Format(time.RFC3339), d.ID, orNothing(t.From),
t.To, t.Event, by, t.Why)
if t.To == Proposed && d.Plan != nil {
line += "; predicted: " + d.Plan.Summary
}
if d.Group != "" && (t.To == Proposed || t.To == Published) {
line += "; group " + d.Group
}
return oneLine(line)
}
// executedLine is what a delivery did, written on the commit it landed as when it ends.
func executedLine(d *Delivery) string {
var parts []string
for _, s := range d.Steps {
parts = append(parts, fmt.Sprintf("%s on %s %s", s.Module, s.Machine, s.State))
}
walk := ""
if d.Walk != nil {
walk = " (walk " + d.Walk.ID + ")"
}
return oneLine(fmt.Sprintf("%s delivery %s executed%s: %s", time.Now().UTC().Format(time.RFC3339), d.ID, walk,
strings.Join(parts, "; ")))
}
func oneLine(s string) string { return strings.Join(strings.Fields(s), " ") }
// statusOf is the commit status `mesh/delivery`: where the delivery stands, in the forge's words.
func statusOf(d *Delivery) (string, string) {
why := ""
if len(d.Transitions) > 0 {
why = d.Transitions[len(d.Transitions)-1].Why
}
switch d.State {
case Proposed, Checked:
return "pending", "checking"
case Ready:
return "success", "ready: it delivers once merged"
case Rejected:
return "failure", clip("rejected: "+why, 140)
case Published:
return "pending", "published: its walk waits for its turn"
case Held:
return "pending", clip("held for a person: "+d.HeldWhy, 140)
case Delivering:
passed := 0
for _, s := range d.Steps {
if s.State == StepPassed {
passed++
}
}
return "pending", fmt.Sprintf("delivering: %d machine step(s) passed", passed)
case Delivered:
return "success", "delivered"
case Failed:
return "failure", clip("failed: "+why, 140)
case Superseded:
return "warning", clip("superseded: "+why, 140)
case Stopped:
return "error", clip("stopped: "+why, 140)
}
return "pending", string(d.State)
}
// viewMarker is how the forge's holder finds the one view comment of a pull request.
const viewMarker = "<!-- mesh-delivery:view -->"
// ViewBody is a delivery's view on its pull request, written from the delivery whole.
func ViewBody(d *Delivery, g *Group) string {
var b strings.Builder
fmt.Fprintf(&b, "%s\n**Delivery** `%s` — **%s** since %s\n\n", viewMarker, d.ID, d.State, d.Since.UTC().Format(time.RFC3339))
if d.HeldWhy != "" && d.State == Held {
fmt.Fprintf(&b, "Held for a person: %s — `mesh-delivery.release` with why lets it go on.\n\n", d.HeldWhy)
}
if d.Plan != nil {
fmt.Fprintf(&b, "**Delivery plan** — %s\n", d.Plan.Summary)
for i, tier := range d.Plan.Tiers {
fmt.Fprintf(&b, "- tier %d: %s\n", i, strings.Join(tier, ", "))
}
for _, m := range d.Plan.Machines {
var parts []string
if len(m.Receives) > 0 {
parts = append(parts, "receives "+strings.Join(m.Receives, ", "))
}
if len(m.Waits) > 0 {
parts = append(parts, "waits for a person: "+strings.Join(m.Waits, ", "))
}
fmt.Fprintf(&b, "- %s: %s\n", m.Machine, strings.Join(parts, "; "))
}
for _, s := range d.Plan.Steps {
fmt.Fprintf(&b, "- %s\n", s)
}
b.WriteString("\n")
}
if g != nil {
fmt.Fprintf(&b, "**Group** `%s`, in order: %s\n", g.ID, strings.Join(g.Order, " → "))
for _, p := range g.Pairs {
fmt.Fprintf(&b, "- %s\n", p.orderSaid())
}
for _, c := range g.Contradictions {
fmt.Fprintf(&b, "- **refused**: %s\n", c.Said)
}
if g.Check != nil && g.Check.Verdict != "" {
fmt.Fprintf(&b, "- composed together: %s — %s\n", g.Check.Verdict, g.Check.Summary)
}
b.WriteString("\n")
}
if len(d.Steps) > 0 {
b.WriteString("**Machines**\n")
for _, s := range d.Steps {
fmt.Fprintf(&b, "- %s on %s: %s", s.Module, s.Machine, s.State)
if s.Why != "" {
fmt.Fprintf(&b, " — %s", s.Why)
}
b.WriteString("\n")
}
b.WriteString("\n")
}
b.WriteString("**Transitions**\n")
from := 0
if len(d.Transitions) > 12 {
from = len(d.Transitions) - 12
}
for _, t := range d.Transitions[from:] {
fmt.Fprintf(&b, "- %s %s → %s (%s): %s\n", t.At.UTC().Format("2006-01-02 15:04"), orNothing(t.From), t.To, t.Event, t.Why)
}
b.WriteString("\nThe commit's note under `refs/notes/mesh-plan` keeps every transition: `git log --notes=mesh-plan`.\n")
return b.String()
}
// Flush does what is owed, kept deliveries first: a delivery not yet kept says nothing.
func (h *Holder) Flush() {
type job struct {
id string
group bool
e Effect
body string
repo [2]string
num int
url string
}
h.mu.Lock()
for id := range h.dirty {
if d := h.deliveries[id]; d != nil {
h.keep(d)
}
}
for id := range h.dirtyGroup {
if g := h.groups[id]; g != nil {
h.keepGroup(g)
}
}
var jobs []job
ids := make([]string, 0, len(h.deliveries))
for id := range h.deliveries {
ids = append(ids, id)
}
sort.Strings(ids)
for _, id := range ids {
d := h.deliveries[id]
if h.dirty[id] {
continue
}
for _, e := range d.Owed {
j := job{id: id, e: e, repo: [2]string{d.Owner(), d.Repo()}, num: d.Number, url: d.HTMLURL}
if e.Kind == EffectView {
j.body = ViewBody(d, h.groups[d.Group])
}
jobs = append(jobs, j)
}
}
for id, g := range h.groups {
if h.dirtyGroup[id] {
continue
}
for _, e := range g.Owed {
jobs = append(jobs, job{id: id, group: true, e: e})
}
}
h.mu.Unlock()
for _, j := range jobs {
err := h.do(j.e, j.repo, j.num, j.body, j.url, j.group)
h.mu.Lock()
owed := func(list []Effect) []Effect {
for i, o := range list {
if o.Kind == j.e.Kind && o.Commit == j.e.Commit && o.Context == j.e.Context && o.Line == j.e.Line &&
o.Since.Equal(j.e.Since) {
if err == nil {
return append(list[:i:i], list[i+1:]...)
}
list[i].Tries++
list[i].Last = err.Error()
return list
}
}
return list
}
if j.group {
if g := h.groups[j.id]; g != nil {
g.Owed = owed(g.Owed)
h.keepGroup(g)
}
} else if d := h.deliveries[j.id]; d != nil {
d.Owed = owed(d.Owed)
h.keep(d)
}
h.mu.Unlock()
if err != nil && j.e.Tries%10 == 0 {
h.Logf("[mesh-delivery] %s: %s not done yet (%v); tried again", j.id, j.e.Kind, err)
}
}
}
// do is one owed effect.
func (h *Holder) do(e Effect, repo [2]string, number int, body, url string, group bool) error {
switch e.Kind {
case EffectEmit:
var payload any
if err := json.Unmarshal([]byte(e.Line), &payload); err != nil {
return nil // nothing sayable: dropped
}
event := "transition"
if group {
event = "group"
}
return h.Emit(event, payload)
case EffectNote:
if h.Forge == nil {
return fmt.Errorf("no forge")
}
return h.Forge.Note(repo[0], repo[1], e.Commit, e.Line)
case EffectView:
if h.Forge == nil || number == 0 {
return nil
}
return h.Forge.View(repo[0], repo[1], number, body)
case EffectStatus:
if h.Forge == nil {
return fmt.Errorf("no forge")
}
if group {
// A group's status names its member's repository and page in Last.
where, page, _ := strings.Cut(e.Last, " ")
ownerRepo, _, _ := strings.Cut(where, "#")
owner, name, _ := strings.Cut(ownerRepo, "/")
return h.Forge.Status(owner, name, e.Commit, e.Context, e.State, e.Line, page)
}
return h.Forge.Status(repo[0], repo[1], e.Commit, e.Context, e.State, e.Line, url)
}
return nil
}