Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/delivery.go
T
jochen 983d434623
mesh/merge-gate fail: builds mesh-issues → novox; no bus step; a manifest the change touches fails the module check: modules/mesh-issues/module.json: this …
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 25
mesh/delivery superseded: a newer head of the same pull request
mesh-issues: keep every issue in the log changes and the bucket as its fold, so the record outlives the bucket's cap and a week of events (hq ADR 0300, issues 501, 502)
Every write appends one operation at the issue's last sequence before the
bucket is written; a refused append writes nothing. The start carries what
the bucket holds, refolds what is behind and indexes every issue in memory,
so list answers from memory with a ranked search and a before cursor.
2026-10-12 02:41:54 +02:00

531 lines
19 KiB
Go

package main
// Whether an issue's fix is delivered (novox/hq ADR 0281). The tracker hears mesh-delivery's `transition` event
// for every pull request an issue's fixed-by names and keeps, per pull request, the state the delivery last said
// — merged or not, held and why, covered by a later merge, delivered. From that it derives one line for `get` and
// `list`: a measured fact, never set by a verb. Delivered is not resolved: when every fix of an issue not yet
// resolved is delivered, the issue gains a note and the event `ready-to-verify`, and an agent proves its
// acceptance live before resolving it. A fix merged and not delivered past a bound is said on the issue and to
// the operator's channel.
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// EventReadyToVerify is said once when every fix of an issue not yet resolved is delivered.
const EventReadyToVerify = "ready-to-verify"
// DefaultStuckAfter is how long a fix may be merged and not delivered before it is said stuck: a walk takes
// minutes, a gate ten at most, and a hold waits for a person; two hours is past every ordinary delivery and
// still the same working day.
const DefaultStuckAfter = 2 * time.Hour
// Fix is one pull request of an issue's fixed-by, as the delivery last said it.
type Fix struct {
PullRequest string `json:"pull-request"`
State string `json:"state"`
Why string `json:"why,omitempty"`
Merged bool `json:"merged,omitempty"`
MergedAt *time.Time `json:"merged-at,omitempty"`
Since time.Time `json:"since"`
// StuckSaid says it was said stuck, on the issue and to the operator's channel.
StuckSaid bool `json:"stuck-said,omitempty"`
}
// reached says whether a delivery's state says the change reached the machines (mesh-delivery's Reached).
func reached(state string) bool {
return state == "delivered" || state == "covered" || state == "retired"
}
// Whether a pull request is merged is what the delivery says of the pull request itself — `merged_as` on a
// transition, the pull request's `merged` in checks — never read from the state: `stopped` is also what a pull
// request closed without merging becomes (mesh-delivery's `closed` event), and read as merged it was said stuck to
// the operator for good (novox/hq issue 483).
// withdrawn says whether a fix is a pull request closed without merging: no fix at all, neither awaiting delivery,
// delivered, nor open.
func withdrawn(f *Fix) bool { return f != nil && f.State == "stopped" && !f.Merged }
// liveFixes is an issue's fixes less those withdrawn: what its delivery, readiness to verify, the stuck sweep and a
// abandoned issue are measured on. fixPullRequests still names the withdrawn ones, so a pull request reopened is heard.
func liveFixes(is *Issue) []string {
var out []string
for _, pr := range fixPullRequests(is) {
if !withdrawn(fixOf(is, pr)) {
out = append(out, pr)
}
}
return out
}
// withdrawnFixes is the pull requests of an issue closed without merging.
func withdrawnFixes(is *Issue) []string {
var out []string
for _, pr := range fixPullRequests(is) {
if withdrawn(fixOf(is, pr)) {
out = append(out, pr)
}
}
return out
}
func fixOf(is *Issue, pr string) *Fix {
for i := range is.Fixes {
if is.Fixes[i].PullRequest == pr {
return &is.Fixes[i]
}
}
return nil
}
// allDelivered says whether an issue names a pull request and every one is delivered.
func allDelivered(is *Issue) bool {
prs := liveFixes(is)
if len(prs) == 0 {
return false
}
for _, pr := range prs {
if !contains(is.Delivered, pr) {
return false
}
}
return true
}
// DeliveryLine is the derived delivery state of an issue's fix, in words; empty when fixed-by names no pull
// request.
func DeliveryLine(is *Issue) string {
prs := liveFixes(is)
if len(prs) == 0 {
if w := withdrawnFixes(is); len(w) > 0 {
return "fix withdrawn: " + strings.Join(w, ", ") + " closed without merging"
}
return ""
}
var held, failed, merged, open, unheard []string
covered := false
for _, pr := range prs {
f := fixOf(is, pr)
switch {
case contains(is.Delivered, pr):
covered = covered || (f != nil && f.State == "covered")
case f == nil:
unheard = append(unheard, pr)
case f.State == "held":
held = append(held, strings.TrimSpace(pr+": "+f.Why))
case f.State == "failed" || f.State == "stopped":
failed = append(failed, strings.TrimSpace(pr+" "+f.State+": "+f.Why))
case f.Merged:
merged = append(merged, pr)
default:
open = append(open, pr)
}
}
switch {
case len(failed) > 0:
return "fix not delivered: " + strings.Join(failed, "; ")
case len(held) > 0:
return "held: " + strings.Join(held, "; ")
case len(merged) > 0:
return "fix merged, not delivered: " + strings.Join(merged, ", ")
case len(open) > 0:
return "fix not merged: " + strings.Join(open, ", ")
case len(unheard) > 0:
return "nothing heard from the delivery of " + strings.Join(unheard, ", ")
case covered:
return "delivered to every machine it should reach (by a later merge that contains it)"
}
return "delivered to every machine it should reach"
}
// recordFix keeps what the delivery said of a pull request on every issue whose fixed-by names it (only, when
// given, on that one issue), and answers the issues it changed.
func (t *Tracker) recordFix(pr, state, why string, merged bool, at time.Time, only uint64) ([]uint64, error) {
all, err := t.allIssues()
if err != nil {
return nil, err
}
var changed []uint64
for n, is := range all {
if (only != 0 && n != only) || !contains(fixPullRequests(is), pr) {
continue
}
kept := cut(why, MostReasonKept)
if f := fixOf(is, pr); f != nil && (at.Before(f.Since) || (f.State == state && f.Why == kept && f.Merged == (merged || f.Merged))) {
// Older than what is kept, or nothing new: transitions can arrive out of order.
continue
}
if contains(is.Delivered, pr) && !reached(state) {
// Delivered stays delivered: a later state that has not reached the machines is a newer delivery of
// the same pull request at most, and an older one is history.
continue
}
if _, err := t.change(n, Act{Op: "delivery", By: Seat}, func(i *Issue) error {
f := fixOf(i, pr)
if f == nil {
i.Fixes = append(i.Fixes, Fix{PullRequest: pr})
f = &i.Fixes[len(i.Fixes)-1]
}
f.State, f.Why, f.Since = state, kept, at
if merged && !f.Merged {
f.Merged = true
m := at
f.MergedAt = &m
}
if reached(state) && !contains(i.Delivered, pr) {
i.Delivered = append(i.Delivered, pr)
}
return nil
}); err != nil {
return changed, err
}
changed = append(changed, n)
}
sort.Slice(changed, func(i, j int) bool { return changed[i] < changed[j] })
return changed, nil
}
// afterFix is what follows a change of an issue's fixes: ready to verify, once, when every fix is delivered and
// the issue is not yet resolved; a stuck fix delivered at last cleared on the operator's channel; and what
// waited on it let go.
func (t *Tracker) afterFix(n uint64) error {
is, _, err := read(t.Store, n)
if err != nil {
return err
}
if err := t.clearStuck(is); err != nil {
t.Logf("[mesh-issues] the stuck fix of %d could not be cleared on the operator's channel (tried again at the next sweep): %v", n, err)
}
if err := t.clearNotResolved(is); err != nil {
t.Logf("[mesh-issues] the warning that %d is delivered and not resolved could not be cleared on the operator's channel (tried again at the next sweep): %v", n, err)
}
if allDelivered(is) {
if unresolved(is) {
prs := liveFixes(is)
sort.Strings(prs)
if strings.Join(is.VerifyFor, ",") != strings.Join(prs, ",") {
// The note first, kept once by its words; then the claim; then the event. A failure before the claim
// is tried again at the next offer, and the note is not written twice. It names when each fix was
// delivered and who works on the issue: no channel of the tracker reaches a worker, so the note
// and the event are how the worker hears it (novox/hq issue 483).
var when []string
for _, pr := range prs {
if f := fixOf(is, pr); f != nil {
when = append(when, pr+" at "+f.Since.Format(time.RFC3339))
} else {
when = append(when, pr)
}
}
prefix := fmt.Sprintf("Ready to verify: every fix of this issue is delivered (%s).", strings.Join(when, ", "))
worker := ""
if is.Worker != nil {
worker = is.Worker.By
}
if !noted(is, prefix) {
text := prefix
if worker != "" {
text += fmt.Sprintf(" %s works on it: prove the acceptance live and resolve it, or say what is left. "+
"In fixing and not resolved %s after its delivery, it is said to the operator.", worker,
t.deliveredNotResolvedAfter())
}
text += " Delivered is not resolved: prove the acceptance live, then resolve it, from fixing: move it " +
"to fixing with by first if it is not there. (novox/hq ADR 0281, ADR 0284, issue 483)"
if _, err := t.note(GroomBy, n, text); err != nil {
return err
}
}
claimed := false
if _, err := t.change(n, Act{Op: EventReadyToVerify, By: Seat}, func(i *Issue) error {
claimed = false
if strings.Join(i.VerifyFor, ",") == strings.Join(prs, ",") {
return nil
}
i.VerifyFor, claimed = prs, true
return nil
}); err != nil {
return err
}
if claimed {
body := map[string]any{"number": n, "fixed-by": prs, "at": t.now(), "delivered-at": deliveredAt(is)}
if worker != "" {
body["worker"] = worker
}
t.emit(EventReadyToVerify, body)
}
}
}
}
return t.letGo(n)
}
// stillStuck says whether an issue has a fix said stuck that is still named, not delivered, and the issue not
// closed as wontfix.
func stillStuck(is *Issue) bool {
if is.Status == StatusWontfix {
return false
}
named := liveFixes(is)
for _, f := range is.Fixes {
if f.StuckSaid && contains(named, f.PullRequest) && !contains(is.Delivered, f.PullRequest) {
return true
}
}
return false
}
// clearStuck clears the warning said for an issue on the operator's channel once nothing it was said for is
// stuck — delivered, no longer named, or the issue closed as wontfix; one that fails is tried again at the next
// sweep.
func (t *Tracker) clearStuck(is *Issue) error {
if !is.StuckOpen || stillStuck(is) || t.Notify == nil {
return nil
}
if err := t.Notify(stuckKey(is.Number), "", "", "", "", "the fix of issue "+keyOf(is.Number)+" is delivered or no longer waited for", true); err != nil {
return err
}
_, err := t.change(is.Number, Act{Op: "warned", By: Seat}, func(i *Issue) error { i.StuckOpen = false; return nil })
return err
}
func stuckKey(n uint64) string { return "issue-tracker.fix-stuck." + keyOf(n) }
func prKey(repository string, number int) string {
_, repo, ok := strings.Cut(repository, "/")
if !ok {
repo = repository
}
return fmt.Sprintf("%s#%d", strings.ToLower(repo), number)
}
// OnTransition is the handler of mesh-delivery's `transition` event: every state a pull request's delivery moves
// to is kept on each issue whose fixed-by names it.
func (t *Tracker) OnTransition(e stdio.Envelope) error {
var body struct {
Repository string `json:"repository"`
Number int `json:"number"`
To string `json:"to"`
Why string `json:"why"`
MergedAs string `json:"merged_as"`
At time.Time `json:"at"`
}
if err := json.Unmarshal(e.Body, &body); err != nil || body.To == "" || body.Number <= 0 {
return nil
}
at := body.At.UTC()
if at.IsZero() {
at = t.now()
}
merged := body.MergedAs != ""
changed, err := t.recordFix(prKey(body.Repository, body.Number), body.To, body.Why, merged, at, 0)
if err != nil {
return err
}
for _, n := range changed {
if err := t.afterFix(n); err != nil {
return err
}
}
return nil
}
// Delivery is a pull request's delivery as mesh-delivery's `checks` answers it; a test stubs the asking.
type Delivery struct {
State string
Why string
// Since is when the delivery entered its state, as it says; zero when it does not.
Since time.Time
// Merged is the pull request's own merged flag, as the forge says it.
Merged bool
}
// DeliveryState asks mesh-delivery for one pull request's delivery.
type DeliveryState func(repository string, number string) (Delivery, error)
// busDeliveryState asks mesh-delivery's checks over the bus.
func busDeliveryState(owner string) DeliveryState {
return func(repository, number string) (Delivery, error) {
raw, err := stdio.Ask("seat:mesh-delivery.checks", map[string]any{"repository": owner + "/" + repository, "number": number})
if err != nil {
return Delivery{}, err
}
var out struct {
Delivery struct {
State string `json:"state"`
Waits string `json:"waits"`
Since time.Time `json:"since"`
} `json:"delivery"`
Pull struct {
Merged bool `json:"merged"`
} `json:"pull"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return Delivery{}, fmt.Errorf("mesh-delivery's checks answered no delivery: %.200s", raw)
}
return Delivery{State: out.Delivery.State, Why: out.Delivery.Waits, Since: out.Delivery.Since.UTC(), Merged: out.Pull.Merged}, nil
}
}
// OnMoved is the handler of the tracker's own `moved` event: each pull request of the issue's fix not heard of
// yet is asked of mesh-delivery, at most a few per handling, for a delivery that moved before the issue named
// it; then what follows its fixes, and an issue closed lets go what waited on it.
func (t *Tracker) OnMoved(e stdio.Envelope) error {
var body struct {
Number uint64 `json:"number"`
}
if err := json.Unmarshal(e.Body, &body); err != nil || body.Number == 0 {
return nil
}
is, _, err := read(t.Store, body.Number)
if errors.Is(err, ErrNoIssue) {
return nil
}
if err != nil {
return err
}
if t.Deliveries != nil {
// Each ask up to the bus's 30 seconds, under the runtime's two minutes; one that fails, or more left to
// ask, has the event offered again, so a delivery never asked ends in max-deliveries, not in silence.
asked := 0
for _, pr := range fixPullRequests(is) {
if fixOf(is, pr) != nil {
continue
}
if asked == MostDeliveryAsks {
return fmt.Errorf("issue %d names more pull requests than are asked at once; the rest at the next offer", body.Number)
}
asked++
repo, number, _ := strings.Cut(pr, "#")
d, err := t.Deliveries(repo, number)
if err != nil {
return fmt.Errorf("the delivery of %s (issue %d) could not be asked: %v", pr, body.Number, err)
}
state := d.State
if state == "" {
// The delivery knows nothing of it yet: not kept, so it is asked again at the next move, and its own
// events say it when they come.
continue
}
// The delivery's own time, so a transition stamped the same moment is not taken as older.
since := d.Since
if since.IsZero() {
since = t.now()
}
if _, err := t.recordFix(pr, state, d.Why, d.Merged, since, body.Number); err != nil {
return err
}
}
}
return t.afterFix(body.Number)
}
// Notifier tells the operator's channel something (operator-channel.notify), or clears it.
type Notifier func(key, severity, summary, headline, explanation, resolved string, clear bool) error
// busNotifier asks the operator's channel over the bus.
func busNotifier() Notifier {
return func(key, severity, summary, headline, explanation, resolved string, clear bool) error {
body := map[string]any{"key": key, "subject": "the issue tracker"}
if clear {
body["clear"] = true
body["resolved"] = resolved
} else {
body["severity"], body["summary"], body["headline"], body["explanation"] = severity, summary, headline, explanation
}
_, err := stdio.Ask("seat:operator-channel.notify", body)
return err
}
}
// stuckAfter is the setting's bound, or the default.
func (t *Tracker) stuckAfter() time.Duration {
if s, err := t.Settings(); err == nil && strings.TrimSpace(s.StuckAfter) != "" {
if d, err := time.ParseDuration(strings.TrimSpace(s.StuckAfter)); err == nil && d > 0 {
return d
}
}
return DefaultStuckAfter
}
// Sweep says every fix merged and not delivered past the bound: a note on its issue and a warning on the
// operator's channel, once; and every issue in fixing whose fixes were all delivered past its own bound and is not
// resolved (novox/hq issue 483). First it asks the delivery of a few fixes never heard of. It answers how many
// warnings it said. Run every few minutes.
func (t *Tracker) Sweep() (int, error) {
if err := t.askUnheard(); err != nil {
t.Logf("[mesh-issues] the sweep could not ask a fix's delivery (asked again at the next sweep): %v", err)
}
all, err := t.allIssues()
if err != nil {
return 0, err
}
bound, now := t.stuckAfter(), t.now()
said := 0
for n, is := range all {
ok, err := t.sayNotResolved(is, now)
if err != nil {
t.Logf("[mesh-issues] that %d is delivered and not resolved could not be said on the operator's channel (said at the next sweep): %v", n, err)
} else if ok {
said++
}
if is.Status == StatusWontfix {
if err := t.clearStuck(is); err != nil {
t.Logf("[mesh-issues] the stuck fix of %d could not be cleared on the operator's channel: %v", n, err)
}
continue
}
if err := t.clearStuck(is); err != nil {
t.Logf("[mesh-issues] the stuck fix of %d could not be cleared on the operator's channel: %v", n, err)
}
named := liveFixes(is)
for _, f := range is.Fixes {
if !contains(named, f.PullRequest) || contains(is.Delivered, f.PullRequest) || !f.Merged || f.MergedAt == nil ||
reached(f.State) || f.StuckSaid || now.Sub(*f.MergedAt) < bound {
continue
}
state := f.State
if f.Why != "" {
state += ": " + f.Why
}
prefix := fmt.Sprintf("Stuck: the fix %s", f.PullRequest)
if !noted(is, prefix) {
if _, err := t.note(GroomBy, n, fmt.Sprintf("%s was merged %s ago and is not delivered (%s). A merged fix "+
"that does not reach the machines is said on the operator's channel. (novox/hq ADR 0281)", prefix,
now.Sub(*f.MergedAt).Round(time.Minute), state)); err != nil {
return said, err
}
}
if t.Notify != nil {
if err := t.Notify(stuckKey(n), "warning",
fmt.Sprintf("issue %d: fix %s merged %s ago, not delivered", n, f.PullRequest, now.Sub(*f.MergedAt).Round(time.Minute)),
fmt.Sprintf("A fix is merged and not delivered (issue %d)", n),
fmt.Sprintf("The fix of issue %d was merged %s ago and has not reached the machines: %s. Look at its "+
"delivery; nothing else is needed from you unless it waits for a person.", n,
now.Sub(*f.MergedAt).Round(time.Minute), state), "", false); err != nil {
// Tried again at the next sweep: the note is not written twice.
t.Logf("[mesh-issues] the stuck fix of %d could not be said on the operator's channel: %v", n, err)
continue
}
}
pr := f.PullRequest
if _, err := t.change(n, Act{Op: "warned", By: Seat}, func(i *Issue) error {
if g := fixOf(i, pr); g != nil {
g.StuckSaid = true
}
i.StuckOpen = t.Notify != nil
return nil
}); err != nil {
return said, err
}
said++
}
}
return said, nil
}