diff --git a/cmd/mesh-controller/delivery_conditions.go b/cmd/mesh-controller/delivery_conditions.go index 74b14082..d0a13dc5 100644 --- a/cmd/mesh-controller/delivery_conditions.go +++ b/cmd/mesh-controller/delivery_conditions.go @@ -58,8 +58,17 @@ type stalledLine struct { // Severity is "urgent" for a group held or half merged and a refused member (novox/hq issues 1275, 1278); empty // or anything else is a warning, as before. Severity string `json:"severity,omitempty"` + // Needs is "new-head" for a delivery whose cure is its author's new head (novox/hq ADR 0505 §5), Author that author + // as the operator reads it ("the author of feat/x", with the session that pushed it when its pull request names one) + // and Branch its branch. A mesh-delivery from before says none, and the words are as before. + Needs string `json:"needs,omitempty"` + Author string `json:"author,omitempty"` + Branch string `json:"branch,omitempty"` } +// needsNewHead is what a stalled line needs when its cure is its author's new head (ADR 0505 §5). +const needsNewHead = "new-head" + // severity is the condition's severity a stalled line asks: urgent when it says so, a warning otherwise. func (l stalledLine) severity() conditions.Severity { if l.Severity == string(conditions.Urgent) { diff --git a/cmd/mesh-controller/new_head_words_test.go b/cmd/mesh-controller/new_head_words_test.go new file mode 100644 index 00000000..86c8a959 --- /dev/null +++ b/cmd/mesh-controller/new_head_words_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "strings" + "testing" + + "github.com/novox/mesh-controller/internal/conditions" +) + +// novox/hq ADR 0505 §5: a delivery whose cure is its author's new head reaches the operator in words that name its +// author and say a new head is needed, offer no Stop and never call it the operator's; the line as mesh-delivery-gate +// says it (ready.go readyLines: needs "new-head", author, branch). +func TestADeliveryThatNeedsANewHeadNamesItsAuthorAndOffersNoStop(t *testing.T) { + l := stalledLine{ID: "novox/mesh-controller@499ff5c21861", Number: 432, State: "rejected", For: "2h1m0s", Bound: "2h0m0s", + H2: "none: a new head from its author, or a recheck that passes", Needs: needsNewHead, + Author: "the author of feat/quiet-deliveries (quiet-deliveries on shanks)", Branch: "feat/quiet-deliveries", + Says: "novox/mesh-controller#432 passed its checks at 499ff5c21861 and is not merged: a conflict"} + o := stalledObservations([]stalledLine{l})[0] + for _, words := range []string{o.Headline, o.Explanation, o.Resolved, o.Needs} { + if why, ok := conditions.Plain(words); !ok { + t.Errorf("%q is not plain: %s", words, why) + } + for _, never := range []string{"stop it", "operator's"} { + if strings.Contains(words, never) { + t.Errorf("%q says %q", words, never) + } + } + } + if o.Headline != "Pull request 432 of mesh-controller needs a new head from its author" || + !strings.Contains(o.Explanation, "until the author of feat/quiet-deliveries (quiet-deliveries on shanks) pushes a new head") || + !strings.Contains(o.Explanation, "for 2 hours") || len(o.Actions) != 0 || + o.Needs != "ask the author of feat/quiet-deliveries (quiet-deliveries on shanks) for a new head." { + t.Fatalf("words: %q / %q / %q / %v", o.Headline, o.Explanation, o.Needs, o.Actions) + } + // A branch in code is not said; the author is then "its author". + l.Author = "the author of fix/a1b2c3d4e5f6" + if o := stalledObservations([]stalledLine{l})[0]; !strings.Contains(o.Needs, "ask its author") { + t.Fatalf("a dotted branch was said: %q", o.Needs) + } +} diff --git a/cmd/mesh-controller/plain_words.go b/cmd/mesh-controller/plain_words.go index fce97830..1a954534 100644 --- a/cmd/mesh-controller/plain_words.go +++ b/cmd/mesh-controller/plain_words.go @@ -966,6 +966,9 @@ func stalledWords(l stalledLine, o conditions.Observation) (headline, explanatio if l.State == "unanswered" { return unansweredWords(l, o) } + if l.Needs == needsNewHead { + return newHeadWords(l, name) + } held := l.State if held == "" { held = "held" @@ -999,6 +1002,29 @@ func stalledWords(l stalledLine, o conditions.Observation) (headline, explanatio fmt.Sprintf("Delivery of %s is no longer %s", name, held), needs, actions } +// newHeadWords are the words of a delivery whose cure is its author's new head (novox/hq ADR 0505 §5): it names the +// author and says a new head is needed; it offers no Stop and never calls it the operator's, because the operator +// hears of it only once the author's own watch has failed, after two hours with no new head. +func newHeadWords(l stalledLine, name string) (headline, explanation, resolved, needs string, actions []conditions.Action) { + pull := "A pull request of " + name + if l.Number > 0 { + pull = fmt.Sprintf("Pull request %d of %s", l.Number, name) + } + author := l.Author + if _, plain := conditions.Plain(author); !plain { + author = "its author" // a branch named in code (a dotted name, a number) is not said + } + long := "for a long time" + if d, err := time.ParseDuration(l.For); err == nil { + long = "for " + humanDuration(d) + } + return pull + " needs a new head from its author", + fmt.Sprintf("%s cannot merge until %s pushes a new head: its check failed after its Approve, and no new head "+ + "came %s.", pull, author, long), + pull + " has a new head, or is closed", + "ask " + author + " for a new head.", nil +} + // stoppedNeeds is what ends a delivery that is already stopped, from the stalled line's healer words ("none: a // merge that delivers its modules ends it — the operator's"): the part that says what ends it. func stoppedNeeds(l stalledLine) string {