Merge branch 'feat/47-G-issues6' into feat/47-G-join8

This commit is contained in:
2026-11-12 00:14:17 +01:00
12 changed files with 356 additions and 241 deletions
+5 -5
View File
@@ -89,7 +89,7 @@ still read as that issue's verdict.
**Ready.** An issue is ready when it is not resolved or wontfix and every issue it depends on is wontfix, or
resolved with every pull request its `fixed-by` names (`<repository>#<number>`) delivered. The module consumes
`mesh-delivery-gate.transition` and marks a pull request delivered on every issue naming it; at its own `moved` to
resolved it asks `seat:mesh-delivery.checks` once for what it has not heard. An issue made ready gains a note and
resolved it asks `seat:git.pull` once for what it has not heard. An issue made ready gains a note and
the event `ready`. `get` answers `waits-for`, `needed-by` and `ready`; `list` filters on `severity`, `needs` and
`ready` — an agent looking for work asks `severity=minor`, `needs=none`, `ready=true`. `set` changes severity,
needs and suggested owner as a person decides. `link` refuses a `depends-on` that would close a cycle.
@@ -106,8 +106,8 @@ key-value state has no schema to migrate.
## Whether the fix is delivered (hq ADR 0281)
For each pull request an issue's `fixed-by` names, the module keeps what `mesh-delivery-gate.transition` last said —
state, why, merged and when — or asks `seat:mesh-delivery.checks` once (at its own `moved`) for one it has not
heard of. Nothing sets it by hand. `get` and every `list` row carry `delivery`, one line derived from it, worst
state, why, merged and when — or asks `seat:git.pull` once (at its own `moved`) for one it has not
heard of. Nothing sets it by hand. `get` also carries `pipelines`: one row per module's pipeline of each live fix's head commit with its steps, `no run` where the runner's list answers none. `get` and every `list` row carry `delivery`, one line derived from it, worst
first: "fix not delivered: …", "held: …", "fix merged, not delivered: …", "fix not merged: …", "nothing heard from
the delivery of …", "delivered to every machine it should reach" (covered by a later merge says so).
@@ -163,8 +163,8 @@ core one, counted from the move to `fixing`), `over`, the fix delivered last (`f
| phase | ends at | read from |
|---|---|---|
| implementing | its pull request opened | mesh-delivery's `opened` (the pull request's first head heard) |
| review | the first approval at its head | the forge's reviews (`gitea_pull_reviews`): the forge's own approval, or a comment review whose first line is `Verdict: approve at <sha>`, the sha the head it was posted at or a prefix of at least seven characters (the forge refuses an approval of one's own pull request, so the mesh's independent reviews are comments) |
| checks | its merge, required checks and any wait for the merge included | mesh-delivery's `merged_at` |
| review | the first review that passed at its head | the runner: `seat:mesh-pipeline-runner.list` given the head commit `seat:git.pull` answers, and `show` of each pipeline, the review step's `ended`; the forge's own reviews are read nowhere |
| checks | its merge, required checks and any wait for the merge included | the gate's `merged_at`, or `seat:git.pull`'s `merged-at` |
| merge to running | delivered | the delivery's `delivered` (its delivery time) |
A phase whose end came before the end of the phase before it counts zero; a phase whose end is not known (no
+15 -51
View File
@@ -528,28 +528,25 @@ func (t *Tracker) fillMerge(n uint64, f *Fix, pr string) {
}
}
// busContains asks the forge's gitea_contains over the bus: whether commit is an ancestor of in, or it.
// busContains asks the seat `git`'s `contains` over the bus: whether commit is an ancestor of in, or it.
func busContains(owner, repo, commit, in string) (bool, error) {
raw, err := stdio.Ask("gitea.gitea_contains", map[string]any{"owner": owner, "repo": repo, "commit": commit, "in": in})
raw, err := stdio.Ask("seat:git.contains", map[string]any{"repository": owner + "/" + repo, "commit": commit, "in": in})
if err != nil {
return false, err
}
var out struct {
Contains *bool `json:"contains"`
Answer struct {
Contains *bool `json:"contains"`
} `json:"answer"`
return parseContains(raw)
}
func parseContains(raw json.RawMessage) (bool, error) {
m, err := answerOf(raw, "git.contains")
if err != nil {
return false, err
}
if json.Unmarshal(raw, &out) != nil {
return false, errors.New("the forge's gitea_contains is not readable")
var yes *bool
if json.Unmarshal(m["contains"], &yes) != nil || yes == nil {
return false, errors.New("git.contains answered no verdict")
}
switch {
case out.Answer.Contains != nil:
return *out.Answer.Contains, nil
case out.Contains != nil:
return *out.Contains, nil
}
return false, errors.New("the forge's gitea_contains answered no verdict")
return *yes, nil
}
// coveredBy says whether a merged fix's merge is an ancestor of, or equal to, the head a delivered revision holds for its
@@ -612,7 +609,7 @@ func sameCommitIn(a, b string) bool {
return len(a) >= 7 && strings.HasPrefix(b, a)
}
// Delivery is a pull request's delivery as mesh-delivery's `checks` answers it; a test stubs the asking.
// Delivery is a pull request's delivery as the tracker learns it (from the seat `git`'s `pull` and from the gate's events); a test stubs the asking.
type Delivery struct {
State string
Why string
@@ -649,42 +646,9 @@ func nonZero(t *time.Time) *time.Time {
return &u
}
// DeliveryState asks mesh-delivery for one pull request's delivery.
// DeliveryState reads one pull request's facts from the seat `git`.
type DeliveryState func(repository string, number string) (Delivery, error)
// busDeliveryState asks mesh-delivery's checks over the bus.
func busDeliveryState(owner string) DeliveryState {
return func(repository, number string) (Delivery, error) {
raw, err := stdio.Ask("seat:mesh-delivery.checks", map[string]any{"repository": owner + "/" + repository, "number": number})
if err != nil {
return Delivery{}, err
}
var out struct {
Delivery struct {
State string `json:"state"`
Waits string `json:"waits"`
Since time.Time `json:"since"`
Class string `json:"class"`
Opened *time.Time `json:"opened"`
MergedAt *time.Time `json:"merged_at"`
Group string `json:"group"`
MergedAs string `json:"merged_as"`
} `json:"delivery"`
Pull *pullFacts `json:"pull"`
}
if err := json.Unmarshal(raw, &out); err != nil {
return Delivery{}, fmt.Errorf("mesh-delivery's checks answered no delivery: %.200s", raw)
}
d := Delivery{State: out.Delivery.State, Why: out.Delivery.Waits, Since: out.Delivery.Since.UTC(),
Class: out.Delivery.Class, Opened: nonZero(out.Delivery.Opened), MergedAt: nonZero(out.Delivery.MergedAt),
Repository: strings.ToLower(owner + "/" + repository), Group: out.Delivery.Group, MergedAs: out.Delivery.MergedAs}
if out.Pull != nil {
d.fromPull(*out.Pull, owner+"/"+repository)
}
return d, nil
}
}
// OnMoved is the handler of the tracker's own `moved` event: each pull request of the issue's fix not heard of
// yet is asked of mesh-delivery, at most a few per handling, for a delivery that moved before the issue named
// it; then what follows its fixes, and an issue closed lets go what waited on it.
+10 -80
View File
@@ -14,8 +14,6 @@ import (
"sort"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// The classes of a fix (ADR 0282 §1, ADR 0313 §2) and the class of a fix that moves no module.
@@ -76,8 +74,8 @@ type FixPhase struct {
Said string `json:"said,omitempty"`
}
// Approval is when a pull request was first approved at its head, as the forge says it; a test stubs the asking.
// found is false when no approval at its head is on the forge.
// Approval is when a pull request's review step first passed, as the runner says it; a test stubs the asking.
// found is false when no pipeline of its head commit passed its review.
type Approval func(repository string, number int) (at time.Time, found bool, err error)
// classOfFixes is the class of an issue's fixes: that of the slowest-budgeted module they move — core when any
@@ -252,7 +250,7 @@ func (t *Tracker) fixTimeOf(is *Issue) (*FixTime, string) {
why string
}
var approved *time.Time
approvalWhy := "no approval at its head is on the forge: review and checks are counted together"
approvalWhy := "no review of its head commit passed in the runner: review and checks are counted together"
if t.Approval != nil {
repo, number, _ := strings.Cut(last.PullRequest, "#")
var n int
@@ -260,14 +258,14 @@ func (t *Tracker) fixTimeOf(is *Issue) (*FixTime, string) {
at, found, err := t.Approval(repo, n)
switch {
case err != nil:
approvalWhy = "the forge could not be read for its approval (" + cut(err.Error(), 200) + "): review and checks are counted together"
approvalWhy = "the runner could not be read for its review (" + cut(err.Error(), 200) + "): review and checks are counted together"
case found:
a := at.UTC()
approved = &a
}
}
} else {
approvalWhy = "the forge is not asked here: review and checks are counted together"
approvalWhy = "the runner is not asked here: review and checks are counted together"
}
openedWhy := "the delivery did not say when its pull request opened: implementing is counted in review"
mergedWhy := "the delivery did not say when it merged: checks are counted in merge to running"
@@ -570,83 +568,15 @@ func (t *Tracker) FixTimes(days int) (*FixTimes, error) {
return a, nil
}
// firstApproval is when a pull request was first approved at its head among the forge's reviews: the forge's own
// approval, or — the forge refusing an approval of one's own pull request, so the mesh's independent reviews are
// posted as comments by the account that opened it — a comment review whose first line is "Verdict: approve at
// <sha>", the sha the head it was posted at or a prefix of it of at least seven characters. An approval dismissed,
// stale, or of an older head does not count.
func firstApproval(raw json.RawMessage) (time.Time, bool, error) {
var a struct {
Reviews []struct {
State string `json:"state"`
AtHead bool `json:"at_head"`
Commit string `json:"commit"`
Body string `json:"body"`
Submitted string `json:"submitted"`
Stale bool `json:"stale"`
Dismissed bool `json:"dismissed"`
} `json:"reviews"`
}
var s string
if json.Unmarshal(raw, &s) == nil {
raw = json.RawMessage(s)
}
var wrapped struct {
Content []struct {
Text string `json:"text"`
} `json:"content"`
}
if json.Unmarshal(raw, &wrapped) == nil && len(wrapped.Content) > 0 {
raw = json.RawMessage(wrapped.Content[0].Text)
}
if err := json.Unmarshal(raw, &a); err != nil || a.Reviews == nil {
return time.Time{}, false, fmt.Errorf("the forge's gitea_pull_reviews answered no reviews: %.200s", raw)
}
var first time.Time
for _, r := range a.Reviews {
if !r.AtHead || r.Stale || r.Dismissed {
continue
}
switch r.State {
case "APPROVED":
case "COMMENT":
if !verdictApproves(r.Body, r.Commit) {
continue
}
default:
continue
}
at, err := time.Parse(time.RFC3339, r.Submitted)
if err != nil {
continue
}
if first.IsZero() || at.Before(first) {
first = at
}
}
return first, !first.IsZero(), nil
}
// verdictApproves says whether a review's body opens with the mesh's approving verdict on the commit it was posted
// at: "Verdict: approve at <sha>", the sha that commit or a prefix of it of at least seven characters.
func verdictApproves(body, commit string) bool {
line, _, _ := strings.Cut(strings.TrimLeft(body, " \t\r\n"), "\n")
rest, ok := strings.CutPrefix(strings.TrimSpace(line), "Verdict: approve at ")
if !ok {
return false
}
sha := strings.ToLower(strings.TrimRight(strings.TrimSpace(rest), "."))
commit = strings.ToLower(strings.TrimSpace(commit))
return len(sha) >= 7 && commit != "" && strings.Trim(sha, "0123456789abcdef") == "" && strings.HasPrefix(commit, sha)
}
// busApproval asks the forge's reviews of a pull request over the bus (gitea_pull_reviews).
// busApproval is when a pull request's review step first passed among its pipelines, read from the runner (`list` given
// the head commit, `show` of each); the forge's own reviews are read nowhere.
func busApproval(owner func() string) Approval {
return func(repository string, number int) (time.Time, bool, error) {
raw, err := stdio.Ask("gitea.gitea_pull_reviews", map[string]any{"owner": owner(), "repo": repository, "number": number})
rows, err := busPipelines(owner())(repository, fmt.Sprint(number))
if err != nil {
return time.Time{}, false, err
}
return firstApproval(raw)
at, ok := firstReview(rows)
return at, ok, nil
}
}
@@ -441,50 +441,33 @@ func TestAFixWhoseStartCameFromTheLogIsNeverWarnedOn(t *testing.T) {
}
}
// The forge's reviews: the first approval at the head, never one dismissed, stale or of an older head.
func TestTheReviewEndsAtTheFirstApprovalAtItsHead(t *testing.T) {
raw := json.RawMessage(`{"reviews":[
{"state":"APPROVED","at_head":false,"submitted":"2026-10-12T09:01:00Z"},
{"state":"REQUEST_CHANGES","at_head":true,"submitted":"2026-10-12T09:02:00Z"},
{"state":"APPROVED","at_head":true,"dismissed":true,"submitted":"2026-10-12T09:03:00Z"},
{"state":"APPROVED","at_head":true,"submitted":"2026-10-12T09:05:00Z"},
{"state":"APPROVED","at_head":true,"submitted":"2026-10-12T09:04:00Z"}]}`)
at, found, err := firstApproval(raw)
if err != nil || !found || at.Format(time.RFC3339) != "2026-10-12T09:04:00Z" {
t.Fatalf("%s %v %v", at, found, err)
}
// The mesh's own reviews: comments by the account that opened the pull request, whose first line is the verdict.
head := "43df7ebc5aa21c0bceacd1580f01f67d83d3495d"
comments := json.RawMessage(`{"reviews":[
{"state":"COMMENT","at_head":true,"commit":"` + head + `","body":"Verdict: request changes at ` + head + `\n\nBlocking: …","submitted":"2026-10-12T09:01:00Z"},
{"state":"COMMENT","at_head":true,"commit":"` + head + `","body":"Looks fine. Verdict: approve at ` + head + `","submitted":"2026-10-12T09:02:00Z"},
{"state":"COMMENT","at_head":true,"commit":"` + head + `","body":"Verdict: approve at 43df7eb\n\nAll clauses hold.","submitted":"2026-10-12T09:07:00Z"},
{"state":"COMMENT","at_head":true,"commit":"` + head + `","body":"Verdict: approve at ` + head + `\n\nAgain.","submitted":"2026-10-12T09:08:00Z"}]}`)
at, found, err = firstApproval(comments)
if err != nil || !found || at.Format(time.RFC3339) != "2026-10-12T09:07:00Z" {
t.Fatalf("a comment's verdict: %s %v %v", at, found, err)
}
for _, c := range []struct {
body, commit string
approves bool
}{
{"Verdict: approve at " + head, head, true},
{"Verdict: approve at 43df7eb", head, true},
{"Verdict: approve at 43df7e", head, false}, // shorter than seven characters
{"Verdict: approve at 0123456789", head, false}, // another commit
{"Verdict: request changes at " + head, head, false}, // not an approval
{"Not yet.\nVerdict: approve at " + head, head, false}, // not the first line
{"Verdict: approve at 43df7ebc", "", false}, // no commit to hold it to
} {
if got := verdictApproves(c.body, c.commit); got != c.approves {
t.Errorf("%q at %q approves %v, want %v", c.body, c.commit, got, c.approves)
// The runner's review step: the first review that passed among a pull request's pipelines, never one that failed or waits.
func TestTheReviewEndsAtTheFirstPassedReviewStepOfTheRunner(t *testing.T) {
show := func(outcome, ended string) PipelineRow {
step := `{"step":"lint","outcome":"passed"},{"step":"review","outcome":"` + outcome + `"`
if ended != "" {
step += `,"ended":"` + ended + `"`
}
row, err := parsePipelineRow(json.RawMessage(`{"pipeline":{"pipeline":"b.1.m","module":"m","outcome":"passed","steps":[`+step+`}]}}`), "r#1")
if err != nil {
t.Fatal(err)
}
return row
}
if _, found, err := firstApproval(json.RawMessage(`{"reviews":[]}`)); err != nil || found {
t.Fatalf("no reviews: %v %v", found, err)
rows := []PipelineRow{show("failed", "2026-10-12T09:01:00Z"), show("passed", "2026-10-12T09:05:00Z"),
show("accepted", "2026-10-12T09:04:00Z"), show("waiting", "")}
at, found := firstReview(rows)
if !found || at.Format(time.RFC3339) != "2026-10-12T09:04:00Z" {
t.Fatalf("%s %v", at, found)
}
if _, _, err := firstApproval(json.RawMessage(`"nothing"`)); err == nil {
t.Fatal("an answer with no reviews read as none")
if _, found := firstReview(rows[:1]); found {
t.Fatal("a failed review read as passed")
}
if _, found := firstReview([]PipelineRow{{Outcome: NoRun}}); found {
t.Fatal("no run read as reviewed")
}
if _, err := parsePipelineRow(json.RawMessage(`{"error":"no such pipeline"}`), "r#1"); err == nil {
t.Fatal("a failed show read as a pipeline")
}
}
+65 -40
View File
@@ -2,8 +2,7 @@ package main
// A fix whose pull request the tracker keeps no record for (merged before the tracker heard the delivery, or linked after it
// moved) learns its merge here, so the view's `delivered` event can cover it like any recorded fix (issue 1950). The delivery
// is asked first (its `checks` carries the forge's own pull request beside its delivery); the forge is asked when the delivery
// cannot say. One that neither can say is left for the next time and logged; a pull request closed without merging becomes
// is read from the seat `git`'s `pull`. One that neither can say is left for the next time and logged; a pull request closed without merging becomes
// the withdrawn state the tracker already knows.
import (
@@ -16,26 +15,27 @@ import (
stdio "git.novox.be/novox/mesh-sdk/go"
)
// pullFacts reads the forge's pull request block, as gitea_get_pull_request and mesh-delivery's checks both carry it.
// pullFacts reads the seat `git`'s pull request block (the verb `pull`: state open, merged or closed, the merge commit
// and when it merged, and the head commit).
type pullFacts struct {
Merged bool `json:"merged"`
State string `json:"state"`
MergeCommitSHA string `json:"merge_commit_sha"`
MergedAt *time.Time `json:"merged_at"`
State string `json:"state"`
Head string `json:"head"`
MergeCommit string `json:"merge-commit"`
MergedAt *time.Time `json:"merged-at"`
}
// fromPull completes a delivery with what the forge's pull request says: whether it is merged or closed, the commit its
// merge landed as and when.
func (d *Delivery) fromPull(p pullFacts, repository string) {
d.Pulled = true
d.Merged = p.Merged
d.Closed = p.State == "closed" && !p.Merged
d.Merged = p.State == "merged"
d.Closed = p.State == "closed"
if d.Repository == "" {
d.Repository = strings.ToLower(repository)
}
if p.Merged {
if d.Merged {
if d.MergedAs == "" {
d.MergedAs = p.MergeCommitSHA
d.MergedAs = p.MergeCommit
}
if d.MergedAt == nil {
d.MergedAt = nonZero(p.MergedAt)
@@ -43,36 +43,61 @@ func (d *Delivery) fromPull(p pullFacts, repository string) {
}
}
// busPull asks the forge for one pull request (gitea_get_pull_request).
// answerOf is a seat verb's answer body: the object itself, or the one an `answer` field wraps. A verb that failed
// answers `error` (and `error-kind`) alone, which a caller reads as a failure, never as none.
func answerOf(raw json.RawMessage, verb string) (map[string]json.RawMessage, error) {
var m map[string]json.RawMessage
if json.Unmarshal(raw, &m) != nil {
return nil, fmt.Errorf("%s is not readable: %.200s", verb, raw)
}
if w, ok := m["answer"]; ok {
var inner map[string]json.RawMessage
if json.Unmarshal(w, &inner) == nil {
m = inner
}
}
if e, ok := m["error"]; ok {
var why string
_ = json.Unmarshal(e, &why)
return nil, fmt.Errorf("%s failed: %.200s", verb, why)
}
return m, nil
}
// parsePull reads the answer of the seat `git`'s `pull` into a delivery, and answers the pull request's head commit.
func parsePull(raw json.RawMessage, repository string) (Delivery, string, error) {
m, err := answerOf(raw, "git.pull")
if err != nil {
return Delivery{}, "", err
}
var p pullFacts
if json.Unmarshal(m["pull"], &p) != nil || p.State == "" {
return Delivery{}, "", errors.New("git.pull answered no pull request")
}
var d Delivery
d.fromPull(p, repository)
return d, p.Head, nil
}
// askPull asks the seat `git` for one pull request by its repository (owner/name) and number.
func askPull(repository string, number string) (Delivery, string, error) {
var n int
if _, err := fmt.Sscanf(number, "%d", &n); err != nil || n <= 0 {
return Delivery{}, "", fmt.Errorf("%q is not a pull request number", number)
}
raw, err := stdio.Ask("seat:git.pull", map[string]any{"repository": repository, "number": n})
if err != nil {
return Delivery{}, "", err
}
return parsePull(raw, repository)
}
// busPull asks the seat `git` for one pull request (`pull`): what the delivery of a fix is learned from, now that
// the delivery's own `checks` is retired.
func busPull(owner string) DeliveryState {
return func(repository, number string) (Delivery, error) {
var n int
if _, err := fmt.Sscanf(number, "%d", &n); err != nil || n <= 0 {
return Delivery{}, fmt.Errorf("%q is not a pull request number", number)
}
raw, err := stdio.Ask("gitea.gitea_get_pull_request", map[string]any{"owner": owner, "repo": repository, "number": n})
if err != nil {
return Delivery{}, err
}
var out struct {
Pull *pullFacts `json:"pull"`
Answer struct {
Pull *pullFacts `json:"pull"`
} `json:"answer"`
}
if json.Unmarshal(raw, &out) != nil {
return Delivery{}, errors.New("the forge's gitea_get_pull_request is not readable")
}
p := out.Pull
if p == nil {
p = out.Answer.Pull
}
if p == nil {
return Delivery{}, errors.New("the forge's gitea_get_pull_request answered no pull request")
}
var d Delivery
d.fromPull(*p, owner+"/"+repository)
return d, nil
d, _, err := askPull(owner+"/"+repository, number)
return d, err
}
}
@@ -178,5 +203,5 @@ func (t *Tracker) LearnFixes(n uint64) (map[string]any, error) {
out[pr] = f.State
}
}
return map[string]any{"number": n, "fixes": out, "delivery": DeliveryLine(is)}, nil
return map[string]any{"number": n, "fixes": out, "delivery": DeliveryLine(is), "pipelines": t.pipelinesOf(is)}, nil
}
@@ -8,10 +8,10 @@ import (
"time"
)
// The forge's answer for novox/mesh-delivery#5, read from gitea.gitea_get_pull_request on 2026-10-27 (shortened): merged, closed,
// its merge commit and moment. mesh-delivery's checks carries the same `pull` block and no `delivery` block for it.
const forgePullMerged = `{"pull":{"base":"main","head":"feat/delivery-parts","merged":true,"merged_at":"2026-10-19T18:05:16Z",` +
`"merge_commit_sha":"1325c8b452c8fa65c478af6815887870a1f42e7b","number":5,"state":"closed"}}`
// The seat git's answer for novox/mesh-delivery#5 (shortened): merged, its merge commit and moment.
const forgePullMerged = `{"pull":{"repository":"novox/mesh-delivery","number":5,"base":"main","branch":"feat/delivery-parts",` +
`"head":"4444444444444444444444444444444444444444","state":"merged","merged-at":"2026-10-19T18:05:16Z",` +
`"merge-commit":"1325c8b452c8fa65c478af6815887870a1f42e7b"}}`
func TestAPullRequestWithNoFixRecordLearnsItsMerge(t *testing.T) {
mergedAt := time.Date(2026, 10, 19, 18, 5, 16, 0, time.UTC)
@@ -110,19 +110,72 @@ func TestAFixMergedBeforeTheTrackerHeardDeliveriesIsDeliveredByTheNextEventOfIts
}
}
func TestTheForgesRealPullAnswerIsReadAsMerged(t *testing.T) {
var out struct {
Pull pullFacts `json:"pull"`
func TestTheSeatGitsPullAnswerIsReadAsMerged(t *testing.T) {
d, head, err := parsePull(json.RawMessage(forgePullMerged), "novox/mesh-delivery")
if err != nil || head != "4444444444444444444444444444444444444444" {
t.Fatalf("%v %s", err, head)
}
if err := json.Unmarshal([]byte(forgePullMerged), &out); err != nil {
t.Fatal(err)
}
var d Delivery
d.fromPull(out.Pull, "novox/mesh-delivery")
if !d.Pulled || !d.Merged || d.Closed || d.MergedAs != "1325c8b452c8fa65c478af6815887870a1f42e7b" ||
d.MergedAt == nil || d.MergedAt.Format(time.RFC3339) != "2026-10-19T18:05:16Z" || d.Repository != "novox/mesh-delivery" {
t.Fatalf("%+v", d)
}
if d, _, _ := parsePull(json.RawMessage(`{"pull":{"state":"closed","head":"x"}}`), "novox/r"); !d.Closed || d.Merged {
t.Fatalf("closed: %+v", d)
}
if d, _, _ := parsePull(json.RawMessage(`{"pull":{"state":"open","head":"x"}}`), "novox/r"); d.Closed || d.Merged {
t.Fatalf("open: %+v", d)
}
for _, bad := range []string{`{"error":"no such pull request","error-kind":"not-found"}`, `{"pull":{}}`, `nothing`} {
if _, _, err := parsePull(json.RawMessage(bad), "novox/r"); err == nil {
t.Fatalf("%s read as a pull request", bad)
}
}
}
func TestTheSeatGitsContainsAnswerIsReadAndAFailureIsNeverNo(t *testing.T) {
for raw, want := range map[string]bool{`{"commit":"a","in":"b","contains":true}`: true, `{"contains":false,"ahead":2}`: false} {
if got, err := parseContains(json.RawMessage(raw)); err != nil || got != want {
t.Fatalf("%s: %v %v", raw, got, err)
}
}
for _, bad := range []string{`{"error":"forge down","error-kind":"failed"}`, `{}`} {
if _, err := parseContains(json.RawMessage(bad)); err == nil {
t.Fatalf("%s read as an answer", bad)
}
}
}
// design 47 G §654: a repository of two modules shows two rows with their steps; a commit the runner's list answers none for
// shows "no run"; an unreadable runner is said, never read as none.
func TestAPullRequestShowsOnePipelineRowPerModuleAndNoRunWhereThereIsNone(t *testing.T) {
keys, err := parsePipelineKeys(json.RawMessage(`{"pipelines":[{"pipeline":"b.1.a","module":"a"},{"pipeline":"b.1.b","module":"b"}]}`))
if err != nil || len(keys) != 2 {
t.Fatalf("%v %v", keys, err)
}
if keys, err := parsePipelineKeys(json.RawMessage(`{"pipelines":[]}`)); err != nil || len(keys) != 0 {
t.Fatalf("%v %v", keys, err)
}
h := newHarness(t)
n := fixIssue(t, h, "mesh-catalog#50")
h.Pipelines = func(repo, number string) ([]PipelineRow, error) {
return []PipelineRow{
{PullRequest: repo + "#" + number, Module: "a", Outcome: "passed", Steps: []StepRow{{"unit", "passed"}, {"review", "passed"}}},
{PullRequest: repo + "#" + number, Module: "b", Outcome: "failed", Steps: []StepRow{{"unit", "failed"}}}}, nil
}
v, err := h.View(n)
if err != nil || len(v.Pipelines) != 2 || v.Pipelines[1].Module != "b" || len(v.Pipelines[0].Steps) != 2 {
t.Fatalf("%+v %v", v, err)
}
h.Pipelines = func(repo, number string) ([]PipelineRow, error) {
return []PipelineRow{{PullRequest: repo + "#" + number, Outcome: NoRun}}, nil
}
if v, _ := h.View(n); len(v.Pipelines) != 1 || v.Pipelines[0].Outcome != "no run" {
t.Fatalf("%+v", v.Pipelines)
}
h.Pipelines = func(string, string) ([]PipelineRow, error) { return nil, errors.New("runner down") }
if v, _ := h.View(n); len(v.Pipelines) != 1 || !strings.HasPrefix(v.Pipelines[0].Outcome, "not readable") {
t.Fatalf("%+v", v.Pipelines)
}
}
// A merge learned late is kept as of the moment it was learned: the stuck bound runs from then, so an old merge raises no
+3 -3
View File
@@ -103,12 +103,12 @@ func main() {
return "novox"
}
t.Deliveries = func(repository, number string) (Delivery, error) {
return busDeliveryState(forgeOwner())(repository, number)
}
t.Pull = func(repository, number string) (Delivery, error) {
return busPull(forgeOwner())(repository, number)
}
t.Approval = busApproval(forgeOwner)
t.Pipelines = func(repository, number string) ([]PipelineRow, error) {
return busPipelines(forgeOwner())(repository, number)
}
t.Contains = busContains
t.Reached = busReach
if len(os.Args) > 1 && os.Args[1] == "dry-run" {
@@ -111,8 +111,8 @@ func TestItDeclaresAndHoldsTheIssueTrackerSeatOnceForTheMesh(t *testing.T) {
// The duplicate check hears the tracker's own opens and asks the agent module on its node (ADR 0278 §1, §5);
// the import and a reservation read the record through the records module (issue 424).
if !reflect.DeepEqual(m.Consumes, []string{"mesh-issues." + EventOpened, "mesh-issues." + EventMoved, "mesh-delivery-gate.transition", DeliveredEvent}) ||
!reflect.DeepEqual(m.Invokes, []string{"claude-code.claude_code_judge", "seat:mesh-delivery.checks", "seat:operator-channel.notify",
"gitea.gitea_pull_reviews", "gitea.gitea_contains", "gitea.gitea_get_pull_request", "records.records_status", "records.records_list", "records.records_read", "mesh-knowledge.knowledge_highest"}) {
!reflect.DeepEqual(m.Invokes, []string{"claude-code.claude_code_judge", "seat:operator-channel.notify",
"seat:git.pull", "seat:git.contains", "seat:mesh-pipeline-runner.list", "seat:mesh-pipeline-runner.show", "records.records_status", "records.records_list", "records.records_read", "mesh-knowledge.knowledge_highest"}) {
t.Fatalf("consumes %v, invokes %v", m.Consumes, m.Invokes)
}
// The tools bundle, and the archive the build packs files/ into for the templates (novox/hq ADR 0349 §2).
@@ -0,0 +1,155 @@
package main
// A pull request's pipelines, read from the runner (design 47 G §654, A-75): the seat `mesh-pipeline-runner`'s `list`,
// given the pull request's head commit, answers that commit's pipelines, one per module of the repository it builds, and
// `show` of each answers it whole with each step's result, the review step's among them. The forge's own reviews are read
// nowhere: G keeps no review record.
import (
"encoding/json"
"fmt"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
// NoRun is a pull request's head commit the runner's list answers no pipeline for.
const NoRun = "no run"
// StepRow is one step of a pipeline: the step and its outcome.
type StepRow struct {
Step string `json:"step"`
Outcome string `json:"outcome"`
}
// PipelineRow is one module's pipeline of a pull request's head commit, with its steps; a commit with no pipeline is one
// row whose outcome is NoRun.
type PipelineRow struct {
PullRequest string `json:"pull-request"`
Module string `json:"module,omitempty"`
Pipeline string `json:"pipeline,omitempty"`
Outcome string `json:"outcome"`
Steps []StepRow `json:"steps,omitempty"`
// reviewed is when the review step ended in a pass or an acceptance; zero when it did not.
reviewed time.Time
}
// Pipelines reads a pull request's pipeline rows; a test stubs the asking. repository is the name in the forge's owner.
type Pipelines func(repository string, number string) ([]PipelineRow, error)
// parsePipelineKeys reads the keys of the pipelines a `list` answers.
func parsePipelineKeys(raw json.RawMessage) ([]string, error) {
m, err := answerOf(raw, "mesh-pipeline-runner.list")
if err != nil {
return nil, err
}
var l []struct {
Pipeline string `json:"pipeline"`
}
if json.Unmarshal(m["pipelines"], &l) != nil {
return nil, fmt.Errorf("mesh-pipeline-runner.list answered no pipelines: %.200s", raw)
}
var keys []string
for _, p := range l {
if p.Pipeline != "" {
keys = append(keys, p.Pipeline)
}
}
return keys, nil
}
// parsePipelineRow reads the answer of `show` into a row of the pull request.
func parsePipelineRow(raw json.RawMessage, pr string) (PipelineRow, error) {
m, err := answerOf(raw, "mesh-pipeline-runner.show")
if err != nil {
return PipelineRow{}, err
}
var p struct {
Pipeline string `json:"pipeline"`
Module string `json:"module"`
Outcome string `json:"outcome"`
Steps []struct {
Step string `json:"step"`
Outcome string `json:"outcome"`
Ended *time.Time `json:"ended"`
} `json:"steps"`
}
if json.Unmarshal(m["pipeline"], &p) != nil || p.Module == "" {
return PipelineRow{}, fmt.Errorf("mesh-pipeline-runner.show answered no pipeline: %.200s", raw)
}
row := PipelineRow{PullRequest: pr, Module: p.Module, Pipeline: p.Pipeline, Outcome: p.Outcome}
for _, s := range p.Steps {
row.Steps = append(row.Steps, StepRow{Step: s.Step, Outcome: s.Outcome})
if s.Step == "review" && (s.Outcome == "passed" || s.Outcome == "accepted") && s.Ended != nil {
row.reviewed = s.Ended.UTC()
}
}
return row, nil
}
// busPipelines reads a pull request's head from the seat `git`, then its pipelines from the runner: `list` given the head
// commit, `show` of each.
func busPipelines(owner string) Pipelines {
return func(repository, number string) ([]PipelineRow, error) {
pr := repository + "#" + number
_, head, err := askPull(owner+"/"+repository, number)
if err != nil {
return nil, err
}
raw, err := stdio.Ask("seat:mesh-pipeline-runner.list", map[string]any{"commit": head})
if err != nil {
return nil, err
}
keys, err := parsePipelineKeys(raw)
if err != nil {
return nil, err
}
if len(keys) == 0 {
return []PipelineRow{{PullRequest: pr, Outcome: NoRun}}, nil
}
var rows []PipelineRow
for _, k := range keys {
raw, err := stdio.Ask("seat:mesh-pipeline-runner.show", map[string]any{"pipeline": k})
if err != nil {
return nil, err
}
row, err := parsePipelineRow(raw, pr)
if err != nil {
return nil, err
}
rows = append(rows, row)
}
return rows, nil
}
}
// firstReview is when the earliest review step passed among the rows.
func firstReview(rows []PipelineRow) (time.Time, bool) {
var first time.Time
for _, r := range rows {
if !r.reviewed.IsZero() && (first.IsZero() || r.reviewed.Before(first)) {
first = r.reviewed
}
}
return first, !first.IsZero()
}
// pipelinesOf is the rows of every live fix of an issue: one per module's pipeline, with its steps, and NoRun where the
// runner knows none. A fix whose runner could not be read is one row saying so.
func (t *Tracker) pipelinesOf(is *Issue) []PipelineRow {
if t.Pipelines == nil {
return nil
}
var out []PipelineRow
for _, pr := range liveFixes(is) {
repo, number, _ := strings.Cut(pr, "#")
rows, err := t.Pipelines(repo, number)
if err != nil {
out = append(out, PipelineRow{PullRequest: pr, Outcome: "not readable: " + cut(err.Error(), 200)})
continue
}
out = append(out, rows...)
}
return out
}
+4 -1
View File
@@ -298,6 +298,9 @@ type View struct {
Ready bool `json:"ready"`
// Delivery is whether its fix is delivered, in words (ADR 0281).
Delivery string `json:"delivery,omitempty"`
// Pipelines is each live fix's pipelines from the runner: one row per module with its steps, "no run" where the runner
// knows none (design 47 G A-75).
Pipelines []PipelineRow `json:"pipelines,omitempty"`
// WorkerStale says nothing was done on it by its worker, or anyone, for worker-stale-after (ADR 0284 §4).
WorkerStale bool `json:"worker-stale,omitempty"`
// DeliveredNotResolved and Abandoned are list's flags, said of this issue (novox/hq issue 483).
@@ -317,7 +320,7 @@ func (t *Tracker) View(n uint64) (*View, error) {
}
now, staleAfter := t.now(), t.workerStaleAfter()
return &View{Issue: is, NeededBy: neededBy(all, n), WaitsFor: waitsFor(all, is), Ready: isReady(all, is),
Delivery: DeliveryLine(is), WorkerStale: is.Worker != nil && is.Worker.stale(now, staleAfter),
Delivery: DeliveryLine(is), Pipelines: t.pipelinesOf(is), WorkerStale: is.Worker != nil && is.Worker.stale(now, staleAfter),
DeliveredNotResolved: deliveredNotResolved(is), Abandoned: abandoned(is, now, staleAfter)}, nil
}
@@ -37,12 +37,14 @@ type Tracker struct {
Judge Judgement
// Deliveries answers a pull request's delivery (ADR 0279 §3, ADR 0281); nil asks nothing.
Deliveries DeliveryState
// Pull reads one pull request from the forge (gitea_get_pull_request) for a fix the delivery cannot say anything of; nil asks nothing.
// Pull reads one pull request from the seat `git` (pull) for a fix the delivery cannot say anything of; nil asks nothing.
Pull DeliveryState
// askedAt is when each pull request without a record was last asked by the sweep, so the least recently asked goes first.
askedMu sync.Mutex
askedAt map[string]time.Time
// Contains asks the forge whether a commit is contained in another (gitea_contains); nil asks nothing, and a delivered
// Pipelines reads a pull request's pipelines from the runner (design 47 G A-75); nil asks nothing.
Pipelines Pipelines
// Contains asks the seat `git` whether a commit is contained in another (contains); nil asks nothing, and a delivered
// revision then marks only the fixes it names (OnDelivered).
Contains func(owner, repo, commit, in string) (bool, error)
// Reached asks the delivery view what reached the machines of one repository now (view_reach); nil asks nothing. A delivered
+4 -4
View File
@@ -74,11 +74,11 @@
],
"invokes": [
"claude-code.claude_code_judge",
"seat:mesh-delivery.checks",
"seat:operator-channel.notify",
"gitea.gitea_pull_reviews",
"gitea.gitea_contains",
"gitea.gitea_get_pull_request",
"seat:git.pull",
"seat:git.contains",
"seat:mesh-pipeline-runner.list",
"seat:mesh-pipeline-runner.show",
"records.records_status",
"records.records_list",
"records.records_read",