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 }