Files
mesh-catalog/modules/mesh-issues/cmd/mesh-issues/tracking.go
T
jschoubben 3aaccba4e2
mesh/delivery superseded: a newer head of the same pull request
mesh/merge-gate pass: builds mesh-issues → 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, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh-issues: read merged from the pull request, not the state, so a closed one is withdrawn (hq issue 483)
mesh-delivery says stopped for a pull request closed unmerged too; read as
merged, it was said stuck to the operator for good. Cite ADR 0284 for the
worker rules, which the comments gave as 0283.
2026-10-11 12:24:45 +02:00

152 lines
4.6 KiB
Go

package main
// Which issues track which conditions (novox/hq issue 400, ADR 0285). An issue names a condition it tracks by the
// condition's key, through a link of kind `condition`, set by whoever opens the issue or works on it. The verb
// `tracking` answers, in one call, every condition key an issue links and, for each, the issues that link it and
// where each stands: its status and what it needs, who works on it and since when, and where its fix is. The
// router of the operator's channel asks it and words the line a condition's message says; the tracker says no
// words for the operator itself, and the controller learns nothing of issues.
import (
"fmt"
"sort"
"strings"
"time"
)
// MostTrackingPerCondition is the most issues the answer gives for one condition key, those not closed first.
const MostTrackingPerCondition = 10
// TrackingIssue is one issue that tracks a condition, as `tracking` answers it.
type TrackingIssue struct {
Number uint64 `json:"number"`
Status string `json:"status"`
Severity string `json:"severity,omitempty"`
Needs string `json:"needs,omitempty"`
// Worker, Since, Active and Stale are who works on it, while it is diagnosing or fixing (ADR 0284).
Worker string `json:"worker,omitempty"`
Since *time.Time `json:"since,omitempty"`
Active *time.Time `json:"active,omitempty"`
Stale bool `json:"stale,omitempty"`
// Fix is where its fix stands, as the delivery last said it (ADR 0281): see FixState.
Fix string `json:"fix,omitempty"`
}
// Tracked is the answer to `tracking`.
type Tracked struct {
// Conditions is, by condition key, the issues that link it.
Conditions map[string][]TrackingIssue `json:"conditions"`
Said string `json:"said"`
}
// FixState is where an issue's fix stands, one word, from the same facts as DeliveryLine and in its order: empty
// when fixed-by names no pull request; failed, held, merged (not delivered yet), not-merged, unheard (nothing
// heard from the delivery), or delivered (every pull request it names).
func FixState(is *Issue) string {
prs := liveFixes(is)
if len(prs) == 0 {
if len(withdrawnFixes(is)) > 0 {
return "withdrawn"
}
return ""
}
var failed, held, merged, open, unheard bool
for _, pr := range prs {
f := fixOf(is, pr)
switch {
case contains(is.Delivered, pr):
case f == nil:
unheard = true
case f.State == "held":
held = true
case f.State == "failed" || f.State == "stopped":
failed = true
case f.Merged:
merged = true
default:
open = true
}
}
switch {
case failed:
return "failed"
case held:
return "held"
case merged:
return "merged"
case open:
return "not-merged"
case unheard:
return "unheard"
}
return "delivered"
}
// Tracking answers which issues track which conditions: only the keys asked, or every key an issue links.
func (t *Tracker) Tracking(keys []string) (*Tracked, error) {
want := map[string]bool{}
for _, k := range keys {
k, err := checkTarget(LinkCondition, k)
if err != nil {
return nil, err
}
want[k] = true
}
all, err := t.allIssues()
if err != nil {
return nil, err
}
now, staleAfter := t.now(), t.workerStaleAfter()
out := &Tracked{Conditions: map[string][]TrackingIssue{}}
for _, is := range all {
for _, l := range is.Links {
if l.Kind != LinkCondition || (len(want) > 0 && !want[l.Target]) {
continue
}
ti := TrackingIssue{Number: is.Number, Status: is.Status, Severity: is.Severity, Needs: is.Needs, Fix: FixState(is)}
if w := is.Worker; w != nil {
since, active := w.Since, w.Active
ti.Worker, ti.Since, ti.Active, ti.Stale = w.By, &since, &active, w.stale(now, staleAfter)
}
out.Conditions[l.Target] = append(out.Conditions[l.Target], ti)
}
}
n := 0
for k, list := range out.Conditions {
sort.Slice(list, func(i, j int) bool {
ci, cj := closedStatus(list[i].Status), closedStatus(list[j].Status)
if ci != cj {
return !ci
}
return list[i].Number < list[j].Number
})
if len(list) > MostTrackingPerCondition {
list = list[:MostTrackingPerCondition]
}
out.Conditions[k] = list
n++
}
switch {
case n == 0 && len(want) > 0:
out.Said = "No issue tracks " + strings.Join(sortedKeys(want), ", ") + "."
case n == 0:
out.Said = "No issue tracks a condition."
case n == 1:
out.Said = "Issues track 1 condition."
default:
out.Said = fmt.Sprintf("Issues track %d conditions.", n)
}
return out, nil
}
func closedStatus(s string) bool { return s == StatusResolved || s == StatusWontfix }
func sortedKeys(m map[string]bool) []string {
var out []string
for k := range m {
out = append(out, k)
}
sort.Strings(out)
return out
}