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 delivered
The glossary keeps claim for a module's on a seat.
153 lines
6.1 KiB
Go
153 lines
6.1 KiB
Go
package main
|
|
|
|
// Who works on an issue (novox/hq ADR 0284). A move to diagnosing or fixing names, in `by`, the issue's worker:
|
|
// the agent or person and its node, as who@node. The issue keeps its worker while it stays at either status, and
|
|
// `get` and `list` show it with since. A move to either by someone else while the worker is not stale is refused,
|
|
// naming the worker; a take-over gives a reason, and the reason is noted. A worker with no move, note, link or set
|
|
// from a caller for `worker-stale-after` (24 hours) is stale: `list` says so, and it may be taken over without a
|
|
// reason.
|
|
//
|
|
// `by` is what the worker says it is, not who called: who acted is recorded beside it as the runtime proves it
|
|
// (the caller's bus user, hq issue 365, or Unproven), and the worker is a way for agents and people to see each
|
|
// other, not a lock against someone giving a false name.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Worker is who works on an issue, since when, and when a caller last did anything on it.
|
|
type Worker struct {
|
|
By string `json:"by"`
|
|
Since time.Time `json:"since"`
|
|
// Active is the last move, note, link or set on the issue by a caller, not by the tracker itself.
|
|
Active time.Time `json:"active"`
|
|
}
|
|
|
|
// DefaultWorkerStaleAfter is how long a worker stays current with nothing done on its issue: a working day and a
|
|
// night, so an agent that stopped (its session ended, its machine asleep) frees the issue by the next day, and
|
|
// one that works on it keeps it by noting what it found.
|
|
const DefaultWorkerStaleAfter = 24 * time.Hour
|
|
|
|
// MostWorker is the most a worker's name, who@node, may be.
|
|
const MostWorker = 120
|
|
|
|
// workerName is who@node: who is the agent or person (letters, digits and . _ / -, such as fix-399 or jochen),
|
|
// node the machine it works on.
|
|
var workerName = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._/-]*@[a-z0-9][a-z0-9-]*$`)
|
|
|
|
// worked says whether an issue at a status has a worker: at diagnosing and fixing.
|
|
func worked(status string) bool { return status == StatusDiagnosing || status == StatusFixing }
|
|
|
|
// checkWorker is `by` as kept: who@node, in lower case.
|
|
func checkWorker(s string) (string, error) {
|
|
s, err := singleLine("by", s, MostWorker)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
s = strings.ToLower(s)
|
|
if !workerName.MatchString(s) {
|
|
return "", fmt.Errorf("by %q is not who works on it and its node: give who@node, the agent or person and the "+
|
|
"machine it works on, such as fix-399@g14 or jochen@shanks", s)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// ownActor says whether a write is the tracker's own (the triage, the duplicate check, the delivery's notes, the
|
|
// import), which keeps no worker current.
|
|
func ownActor(by string) bool { return by == GroomBy || by == LegacyBy || by == ImportedBy }
|
|
|
|
// touch marks a caller's write on an issue as activity of its worker.
|
|
func touch(is *Issue, by string, at time.Time) {
|
|
if is.Worker != nil && !ownActor(by) {
|
|
is.Worker.Active = at
|
|
}
|
|
}
|
|
|
|
// stale says whether nothing was done on the issue past the bound since the worker last was.
|
|
func (w *Worker) stale(now time.Time, after time.Duration) bool {
|
|
last := w.Active
|
|
if last.Before(w.Since) {
|
|
last = w.Since
|
|
}
|
|
return now.Sub(last) > after
|
|
}
|
|
|
|
// workerStaleAfter is the setting `worker-stale-after`, or DefaultWorkerStaleAfter when it is empty or unreadable.
|
|
func (t *Tracker) workerStaleAfter() time.Duration {
|
|
if s, err := t.Settings(); err == nil && strings.TrimSpace(s.WorkerStaleAfter) != "" {
|
|
if d, err := time.ParseDuration(strings.TrimSpace(s.WorkerStaleAfter)); err == nil && d > 0 {
|
|
return d
|
|
}
|
|
}
|
|
return DefaultWorkerStaleAfter
|
|
}
|
|
|
|
// workOutcome is what a move did to an issue's worker.
|
|
type workOutcome struct {
|
|
took bool // a worker started, or the issue changed hands
|
|
from string // the worker it was taken over from
|
|
wasStale bool // the worker taken over from was stale
|
|
// abandoned is a stale worker with no pull request open: the issue is abandoned (novox/hq issue 483).
|
|
abandoned bool
|
|
released string // the worker whose work ended, on a move to a status that has none
|
|
}
|
|
|
|
// workerFor applies a move's worker to an issue, under ADR 0284 §3 and §4: another worker not stale is refused
|
|
// without a take-over reason; a take-over reason with no other worker to take over from is refused.
|
|
func workerFor(is *Issue, to, by, takeOver string, now time.Time, staleAfter time.Duration) (workOutcome, error) {
|
|
var out workOutcome
|
|
cur := is.Worker
|
|
if !worked(to) {
|
|
if cur != nil {
|
|
out.released = cur.By
|
|
is.Worker = nil
|
|
}
|
|
return out, nil
|
|
}
|
|
if cur != nil && cur.By == by {
|
|
if takeOver != "" {
|
|
return out, fmt.Errorf("%s works on issue %d already: take-over is for taking it over from someone else", by, is.Number)
|
|
}
|
|
cur.Active = now
|
|
return out, nil
|
|
}
|
|
if cur == nil {
|
|
if takeOver != "" {
|
|
return out, fmt.Errorf("nobody works on issue %d: take-over is for taking it over from someone else; move it without one", is.Number)
|
|
}
|
|
is.Worker = &Worker{By: by, Since: now, Active: now}
|
|
out.took = true
|
|
return out, nil
|
|
}
|
|
out.wasStale = cur.stale(now, staleAfter)
|
|
out.abandoned = out.wasStale && !openPullRequest(is)
|
|
if !out.wasStale && takeOver == "" {
|
|
return out, fmt.Errorf("%s works on issue %d (%s) since %s, last active %s. Ask them, or take it over with "+
|
|
"take-over saying why; the reason is noted on the issue. A worker who did nothing on it for %s may be "+
|
|
"taken over from without a reason", cur.By, is.Number, is.Status, cur.Since.Format(time.RFC3339),
|
|
cur.Active.Format(time.RFC3339), staleAfter)
|
|
}
|
|
out.from, out.took = cur.By, true
|
|
is.Worker = &Worker{By: by, Since: now, Active: now}
|
|
return out, nil
|
|
}
|
|
|
|
// takeOverNote is the note a take-over leaves on the issue.
|
|
func takeOverNote(o workOutcome, by, reason string, prev *Worker) string {
|
|
s := fmt.Sprintf("Taken over by %s from %s, who worked on it since %s and was last active %s", by, o.from,
|
|
prev.Since.Format(time.RFC3339), prev.Active.Format(time.RFC3339))
|
|
switch {
|
|
case o.abandoned:
|
|
s += " (stale, and abandoned: no pull request open)"
|
|
case o.wasStale:
|
|
s += " (stale)"
|
|
}
|
|
if reason != "" {
|
|
s += ". Why: " + reason
|
|
}
|
|
return s + ". (novox/hq ADR 0284)"
|
|
}
|