ISSUES: mesh-issues reads git.pull, git.contains and the runner's list and show, not mesh-delivery.checks and the forge's tools

Pull request facts come from seat git's pull, ancestry from contains, the review time from the runner's review step,
and get shows one pipeline row per module (no run where the runner's list answers none).

Builds: design 47 G §654, A-75

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-11 23:05:28 +01:00
parent e4c5108306
commit c854373840
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",