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-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.
152 lines
4.6 KiB
Go
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
|
|
}
|