mesh-issues: an issue says who works on it, and resolves only from fixing (hq issue 399)
mesh/delivery-group group feat/399-fixing-status checking: 0 of 2 member(s) ready
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 superseded: a newer head of the same pull request
mesh/delivery-group group feat/399-fixing-status checking: 0 of 2 member(s) ready
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 superseded: a newer head of the same pull request
Two agents could start on one issue without seeing each other, and an issue could be resolved without anyone having said they started its fix. A move to diagnosing or fixing now names by, the worker as who@node; get and list show the worker with since and stale; another worker is refused while it is current unless take-over says why (noted); a worker untouched for worker-stale-after (24h) may be taken over from without a reason. Resolved is reached only from fixing. The moved event carries by, since and caller. Fixing counts as not closed for the triage and the duplicate check. hq ADR 0283.
This commit is contained in:
@@ -313,18 +313,29 @@ type MoveAsk struct {
|
||||
Replay string
|
||||
ReplayNone string
|
||||
Reason string
|
||||
// By is who works on it, who@node: what a move to diagnosing or fixing needs (ADR 0283 §1).
|
||||
By string
|
||||
// TakeOver is why the issue is taken over from another worker (ADR 0283 §1).
|
||||
TakeOver string
|
||||
}
|
||||
|
||||
// Moved is the answer to a move.
|
||||
type Moved struct {
|
||||
Number uint64 `json:"number"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Changed []string `json:"changed,omitempty"`
|
||||
By string `json:"by"`
|
||||
Said string `json:"said"`
|
||||
Announced bool `json:"announced"`
|
||||
Warning string `json:"warning,omitempty"`
|
||||
Number uint64 `json:"number"`
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Changed []string `json:"changed,omitempty"`
|
||||
// By is who works on it, as the move named it; for a move that names nobody, who acted.
|
||||
By string `json:"by"`
|
||||
// Since is when By started on it, for a move to diagnosing or fixing.
|
||||
Since *time.Time `json:"since,omitempty"`
|
||||
// TookOverFrom is the worker a take-over took the issue over from.
|
||||
TookOverFrom string `json:"took-over-from,omitempty"`
|
||||
// Released is the worker whose work on it the move ended.
|
||||
Released string `json:"released,omitempty"`
|
||||
Said string `json:"said"`
|
||||
Announced bool `json:"announced"`
|
||||
Warning string `json:"warning,omitempty"`
|
||||
}
|
||||
|
||||
func known(status string) bool {
|
||||
@@ -391,12 +402,36 @@ func (t *Tracker) move(by string, a MoveAsk) (*Moved, error) {
|
||||
if reason != "" && to != StatusWontfix {
|
||||
return nil, fmt.Errorf("reason is what wontfix needs; this moves to %s", to)
|
||||
}
|
||||
// Who works on it (ADR 0283 §1): named on a move to diagnosing or fixing, and on no other.
|
||||
var worker, takeOver string
|
||||
if worked(to) {
|
||||
if strings.TrimSpace(a.By) == "" {
|
||||
return nil, fmt.Errorf("a move to %s needs by: who works on it and its node, as who@node, such as "+
|
||||
"fix-399@g14 or jochen@shanks", to)
|
||||
}
|
||||
if worker, err = checkWorker(a.By); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if strings.TrimSpace(a.By) != "" {
|
||||
return nil, fmt.Errorf("by names who works on an issue, which only diagnosing and fixing hold; this moves to %s", to)
|
||||
}
|
||||
if strings.TrimSpace(a.TakeOver) != "" {
|
||||
if !worked(to) {
|
||||
return nil, fmt.Errorf("take-over is for taking over who works on an issue, at diagnosing or fixing; this moves to %s", to)
|
||||
}
|
||||
if takeOver, err = multiLine("take-over", a.TakeOver, MostReason); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
staleAfter := t.workerStaleAfter()
|
||||
|
||||
var from string
|
||||
var changed []string
|
||||
var work workOutcome
|
||||
is, err := t.change(a.Number, func(is *Issue) error {
|
||||
changed = nil
|
||||
from = is.Status
|
||||
at := t.now()
|
||||
// A field counts as changed only when its value does, so a move that changes nothing is refused.
|
||||
set := func(name string, cond bool, apply func()) {
|
||||
if !cond {
|
||||
@@ -414,17 +449,32 @@ func (t *Tracker) move(by string, a MoveAsk) (*Moved, error) {
|
||||
set("replay", replay != "", func() { is.Replay, is.ReplayNone = replay, "" })
|
||||
set("replay-none", replayNone != "", func() { is.ReplayNone, is.Replay = replayNone, "" })
|
||||
set("reason", reason != "", func() { is.Reason = reason })
|
||||
prev := is.Worker
|
||||
if prev != nil {
|
||||
c := *prev
|
||||
prev = &c
|
||||
}
|
||||
h, cerr := workerFor(is, to, worker, takeOver, at, staleAfter)
|
||||
if cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
work = h
|
||||
if work.took {
|
||||
changed = append(changed, "by")
|
||||
}
|
||||
if work.from != "" {
|
||||
is.Notes = append(is.Notes, Note{By: worker, At: at, Text: takeOverNote(work, worker, takeOver, prev)})
|
||||
}
|
||||
// The same status again only to change what it holds: a fix named while the issue stays located, to be
|
||||
// resolved once it is delivered and proven (ADR 0281 §3).
|
||||
if from == to && len(changed) == 0 {
|
||||
return fmt.Errorf("issue %d is already %s", is.Number, to)
|
||||
}
|
||||
if err := cycleRules(is, to); err != nil {
|
||||
if err := cycleRules(is, from, to); err != nil {
|
||||
return err
|
||||
}
|
||||
is.Status = to
|
||||
if (to == StatusResolved || to == StatusWontfix) && from != to {
|
||||
at := t.now()
|
||||
is.Closed = &at
|
||||
} else if to != StatusResolved && to != StatusWontfix {
|
||||
is.Closed = nil
|
||||
@@ -434,9 +484,28 @@ func (t *Tracker) move(by string, a MoveAsk) (*Moved, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := &Moved{Number: is.Number, From: from, To: to, Changed: changed, By: by,
|
||||
out := &Moved{Number: is.Number, From: from, To: to, Changed: changed, By: by, TookOverFrom: work.from, Released: work.released,
|
||||
Said: fmt.Sprintf("Issue %d moved from %s to %s.", is.Number, from, to)}
|
||||
body := map[string]any{"number": is.Number, "from": from, "to": to, "by": out.By, "at": t.now()}
|
||||
// The event's by is who works on it when the move names them, and who acted otherwise; caller is always who
|
||||
// acted, as the runtime proves it (ADR 0283 §4).
|
||||
body := map[string]any{"number": is.Number, "from": from, "to": to, "by": by, "caller": by, "at": t.now()}
|
||||
if is.Worker != nil {
|
||||
since := is.Worker.Since
|
||||
out.By, out.Since = is.Worker.By, &since
|
||||
body["by"], body["since"] = is.Worker.By, since
|
||||
out.Said = fmt.Sprintf("Issue %d moved from %s to %s; %s works on it since %s.", is.Number, from, to,
|
||||
is.Worker.By, since.Format(time.RFC3339))
|
||||
}
|
||||
if work.from != "" {
|
||||
body["took-over-from"] = work.from
|
||||
if takeOver != "" {
|
||||
body["take-over"] = takeOver
|
||||
}
|
||||
out.Said += fmt.Sprintf(" Taken over from %s; noted on the issue.", work.from)
|
||||
}
|
||||
if work.released != "" {
|
||||
body["released"] = work.released
|
||||
}
|
||||
for _, c := range changed {
|
||||
switch c {
|
||||
case "located-in":
|
||||
@@ -457,9 +526,9 @@ func (t *Tracker) move(by string, a MoveAsk) (*Moved, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// cycleRules is what the development cycle asks of an issue at a status (hq cycle.py, playbook 03, to-be 53
|
||||
// §5), said as the rule that refused it.
|
||||
func cycleRules(is *Issue, to string) error {
|
||||
// cycleRules is what the development cycle asks of an issue moving from one status to another (playbook 03,
|
||||
// to-be 53 §5, ADR 0283), said as the rule that refused it.
|
||||
func cycleRules(is *Issue, from, to string) error {
|
||||
switch to {
|
||||
case StatusLocated:
|
||||
if len(is.LocatedIn) == 0 {
|
||||
@@ -467,6 +536,12 @@ func cycleRules(is *Issue, to string) error {
|
||||
"(a repository, or \"mesh-catalog modules/<module>\")", is.Number)
|
||||
}
|
||||
case StatusResolved:
|
||||
// Resolved only from fixing: whoever fixed it said so when they started (ADR 0283 §3). A resolved issue
|
||||
// may still have what it holds corrected.
|
||||
if from != StatusFixing && from != StatusResolved {
|
||||
return fmt.Errorf("issue %d is %s, and an issue is resolved only from fixing: move it to fixing with by, "+
|
||||
"who works on it (who@node), when the fix starts, then resolve it", is.Number, from)
|
||||
}
|
||||
if len(is.LocatedIn) == 0 {
|
||||
return fmt.Errorf("issue %d cannot be resolved without located-in: name the owner that was fixed", is.Number)
|
||||
}
|
||||
@@ -508,6 +583,7 @@ func (t *Tracker) note(by string, n uint64, text string) (*Noted, error) {
|
||||
note := Note{By: by, At: t.now(), Text: text}
|
||||
is, err := t.change(n, func(is *Issue) error {
|
||||
is.Notes = append(is.Notes, note)
|
||||
touch(is, by, note.At)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -582,6 +658,7 @@ func (t *Tracker) link(by string, a LinkAsk) (*Linked, error) {
|
||||
return fmt.Errorf("issue %d already has the %s link to %s", is.Number, kind, target)
|
||||
}
|
||||
is.Links = append(is.Links[:i:i], is.Links[i+1:]...)
|
||||
touch(is, by, at)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -592,6 +669,7 @@ func (t *Tracker) link(by string, a LinkAsk) (*Linked, error) {
|
||||
return fmt.Errorf("issue %d has %d links, the most one issue keeps", is.Number, MostLinks)
|
||||
}
|
||||
is.Links = append(is.Links, Link{Kind: kind, Target: target, By: by, At: at})
|
||||
touch(is, by, at)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -633,6 +711,11 @@ type Row struct {
|
||||
SuggestedOwner string `json:"suggested-owner,omitempty"`
|
||||
// Delivery is whether its fix is delivered, in words (ADR 0281); empty when fixed-by names no pull request.
|
||||
Delivery string `json:"delivery,omitempty"`
|
||||
// Worker is who works on it and Since since when, while it is diagnosing or fixing; Stale says nobody did
|
||||
// anything on it for worker-stale-after, so it may be taken over without a reason (ADR 0283).
|
||||
Worker string `json:"worker,omitempty"`
|
||||
Since *time.Time `json:"since,omitempty"`
|
||||
Stale bool `json:"stale,omitempty"`
|
||||
}
|
||||
|
||||
// Listed is the answer to a list.
|
||||
@@ -680,6 +763,7 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
now, staleAfter := t.now(), t.workerStaleAfter()
|
||||
var numbers []uint64
|
||||
for n := range all {
|
||||
numbers = append(numbers, n)
|
||||
@@ -710,8 +794,13 @@ func (t *Tracker) List(a ListAsk) (*Listed, error) {
|
||||
}
|
||||
out.Matched++
|
||||
if len(out.Rows) < limit {
|
||||
out.Rows = append(out.Rows, Row{Number: n, Status: is.Status, Owner: strings.Join(is.LocatedIn, "; "), Title: is.Title,
|
||||
Severity: is.Severity, Needs: is.Needs, Ready: ready, SuggestedOwner: is.SuggestedOwner, Delivery: DeliveryLine(is)})
|
||||
row := Row{Number: n, Status: is.Status, Owner: strings.Join(is.LocatedIn, "; "), Title: is.Title,
|
||||
Severity: is.Severity, Needs: is.Needs, Ready: ready, SuggestedOwner: is.SuggestedOwner, Delivery: DeliveryLine(is)}
|
||||
if c := is.Worker; c != nil {
|
||||
since := c.Since
|
||||
row.Worker, row.Since, row.Stale = c.By, &since, c.stale(now, staleAfter)
|
||||
}
|
||||
out.Rows = append(out.Rows, row)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
|
||||
Reference in New Issue
Block a user