Merge pull request 'Answer what the mesh's checks said of a pull request: mesh-delivery's checks verb (hq ADR 0239)' (#109) from feat/delivery-checks-verb into main

This commit was merged in pull request #109.
This commit is contained in:
2026-10-07 19:04:10 +00:00
14 changed files with 919 additions and 15 deletions
+83
View File
@@ -0,0 +1,83 @@
// What the forge says of a commit's checks (novox/hq ADR 0239): every status it keeps for the commit, the
// newest of each context, and whether the branch it merges into would let it merge — every status that
// branch's protection requires being a success. Read by mesh-delivery's `checks` verb, which joins it to the
// verdict it keeps; the forge's statuses are what the merge button reads, so they are asked of the forge.
//
// Pure functions, so they are tested without a forge; the tool does the asking.
import type { BranchProtection } from "./protection.js";
/** One status of a commit as the forge keeps it. */
export interface StatusRead {
context: string;
state: string;
description: string;
target_url?: string;
created_at?: string;
updated_at?: string;
creator?: string;
}
/** The forge's statuses of a commit, the newest of each context, in context order. The forge answers the
* newest first; a status it says without a context is not one anything can require, and is left out. */
export function newestByContext(raw: unknown[]): StatusRead[] {
const seen = new Map<string, StatusRead>();
const stamp = (s: StatusRead) => s.updated_at || s.created_at || "";
for (const r of raw ?? []) {
const s = r as Record<string, any>;
const context = String(s?.context ?? "").trim();
if (!context) continue;
const read: StatusRead = {
context,
state: String(s.status ?? s.state ?? ""),
description: String(s.description ?? ""),
};
if (s.target_url) read.target_url = String(s.target_url);
if (s.created_at) read.created_at = String(s.created_at);
if (s.updated_at) read.updated_at = String(s.updated_at);
const creator = s.creator?.login ?? s.creator?.username;
if (creator) read.creator = String(creator);
const before = seen.get(context);
if (!before || stamp(read) > stamp(before)) seen.set(context, read);
}
return [...seen.values()].sort((a, b) => a.context.localeCompare(b.context));
}
/** Whether a commit may merge into a branch, as its protection reads the statuses. */
export interface MergeableRead {
/** The branch the protection is read for. */
branch: string;
/** Whether the branch has a rule at all, and whether it requires statuses. */
protected: boolean;
required: string[];
/** Every required status that is not a success, with its state ("missing" when the commit has none). */
blocking: { context: string; state: string }[];
/** True when every required status is a success; null when the protection could not be read. */
mergeable: boolean | null;
/** In words. */
says: string;
}
/** Whether the statuses let the commit merge under the branch's rule. A rule that cannot be read says
* nothing, never yes. A warning blocks: the forge combines it as a failure (novox/hq issue 293). */
export function mergeableUnder(branch: string, rule: BranchProtection | null | undefined, statuses: StatusRead[],
unreadable?: string): MergeableRead {
if (unreadable !== undefined) {
return { branch, protected: false, required: [], blocking: [], mergeable: null,
says: `the protection of ${branch} could not be read: ${unreadable}` };
}
const required = rule && rule.enable_status_check ? [...new Set((rule.status_check_contexts ?? []).filter(Boolean))] : [];
const by = new Map(statuses.map((s) => [s.context, s.state]));
const blocking = required
.filter((c) => by.get(c) !== "success")
.map((c) => ({ context: c, state: by.get(c) ?? "missing" }));
const says = !rule
? `${branch} has no protection: nothing is required`
: required.length === 0
? `${branch} requires no status`
: blocking.length === 0
? `every status ${branch} requires is a success: ${required.join(", ")}`
: `${branch} requires ${required.join(", ")}; not a success: ` +
blocking.map((b) => `${b.context} (${b.state})`).join(", ");
return { branch, protected: !!rule, required, blocking, mergeable: blocking.length === 0, says };
}
+7
View File
@@ -348,6 +348,13 @@ export class GiteaClient {
return out;
}
/** A commit's statuses whole, as the forge combines them for a ref (a sha, or the start of one): the commit
* it resolved to and every status with its description, link, times and setter (novox/hq ADR 0239). */
async combinedStatus(owner: string, repo: string, ref: string): Promise<{ sha: string; state: string; statuses: unknown[] }> {
const raw = await this.request<any>(`/repos/${owner}/${repo}/commits/${encodeURIComponent(ref)}/status?limit=50`);
return { sha: String(raw?.sha ?? ""), state: String(raw?.state ?? ""), statuses: Array.isArray(raw?.statuses) ? raw.statuses : [] };
}
/** Replace a comment's body: the delivery's view, kept current in place (novox/hq ADR 0239). */
async editComment(owner: string, repo: string, id: number, body: string): Promise<{ id: number; html_url: string }> {
const c = await this.request<any>(`/repos/${owner}/${repo}/issues/comments/${id}`, { method: "PATCH", body: JSON.stringify({ body }) });
+48
View File
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import { test } from "node:test";
// What the forge says of a commit's checks (novox/hq ADR 0239): read by mesh-delivery's `checks` verb.
test("the newest status of each context is kept, with what it said, when and by whom", async () => {
const { newestByContext } = await import("../checks.ts");
const read = newestByContext([
{ context: "mesh/merge-gate", status: "success", description: "pass: every machine composes", updated_at: "2026-10-07T10:00:00Z",
creator: { login: "mesh-admin" }, target_url: "https://forge.example/o/r/pulls/1" },
{ context: "mesh/merge-gate", status: "failure", description: "fail: 0 of 4 compose", updated_at: "2026-10-07T09:00:00Z" },
{ context: "mesh/repo-check", state: "error", description: "error: the toolchain was not there", created_at: "2026-10-07T10:01:00Z" },
{ status: "success", description: "a status with no context" },
]);
assert.deepEqual(read.map((s) => [s.context, s.state]), [["mesh/merge-gate", "success"], ["mesh/repo-check", "error"]]);
assert.equal(read[0].creator, "mesh-admin");
assert.equal(read[0].description, "pass: every machine composes");
assert.equal(read[1].created_at, "2026-10-07T10:01:00Z");
});
test("a commit merges only when every required status is a success; a warning blocks", async () => {
const { mergeableUnder } = await import("../checks.ts");
const rule = { rule_name: "main", enable_status_check: true, status_check_contexts: ["mesh/merge-gate", "mesh/repo-check"] };
const ok = mergeableUnder("main", rule, [
{ context: "mesh/merge-gate", state: "success", description: "" },
{ context: "mesh/repo-check", state: "success", description: "" },
]);
assert.equal(ok.mergeable, true);
assert.deepEqual(ok.blocking, []);
const not = mergeableUnder("main", rule, [{ context: "mesh/merge-gate", state: "warning", description: "" }]);
assert.equal(not.mergeable, false);
assert.deepEqual(not.blocking, [{ context: "mesh/merge-gate", state: "warning" }, { context: "mesh/repo-check", state: "missing" }]);
assert.match(not.says, /mesh\/repo-check \(missing\)/);
});
test("no rule requires nothing; a rule that cannot be read says nothing, never yes", async () => {
const { mergeableUnder } = await import("../checks.ts");
const none = mergeableUnder("main", null, []);
assert.equal(none.mergeable, true);
assert.equal(none.protected, false);
const off = mergeableUnder("main", { rule_name: "main", enable_status_check: false, status_check_contexts: ["mesh/merge-gate"] }, []);
assert.equal(off.mergeable, true);
assert.deepEqual(off.required, []);
const unread = mergeableUnder("main", undefined, [], "Gitea API: 500");
assert.equal(unread.mergeable, null);
assert.match(unread.says, /could not be read/);
});
+42
View File
@@ -12,6 +12,7 @@ import { registerModuleTools, type ToolDefinition } from "@novox/mesh-sdk/tools"
import { emit } from "@novox/mesh-sdk/events";
import { GiteaClient, type BranchProtection } from "../client.js";
import { appendNote, deliveryStatus, run, viewBody, viewComment } from "../delivery.js";
import { mergeableUnder, newestByContext } from "../checks.js";
/** The forge's container, where its repositories and git are: the note is written there (novox/hq ADR 0239). */
const forgeContainer = process.env.MESH_GITEA_CONTAINER || "gitea";
@@ -450,6 +451,47 @@ export function getGiteaTools(gitea: GiteaClient): ToolDefinition[] {
return { created: await gitea.addComment(owner, repo, number, body) };
},
},
{
name: "gitea_commit_statuses",
description: "Read a commit's statuses — or a pull request's head's — as the forge keeps them: the newest of each context (mesh/merge-gate, mesh/repo-check, mesh/delivery, …) with its state, description, link, when it was set and by whom; and whether the branch it merges into would let it merge, every status that branch's protection requires being a success. Reads only.",
input: {
owner: { type: "string", description: "the repository owner" },
repo: { type: "string", description: "the repository name" },
number: { type: "number", description: "a pull request's number: its head is read, and its base's protection" },
sha: { type: "string", description: "a commit, or the start of one, instead of a pull request" },
base: { type: "string", description: "with sha: the branch whose protection is read (default main)" },
},
run: async (args) => {
const owner = String(args.owner ?? "").trim(), repo = String(args.repo ?? "").trim();
const number = args.number === undefined || args.number === "" ? 0 : Number(args.number);
let ref = String(args.sha ?? "").trim();
let base = String(args.base ?? "").trim();
if (!owner || !repo) throw new Error("name the repository: owner and repo");
if (!Number.isInteger(number) || number < 0) throw new Error(`${args.number} is not a pull request's number`);
let pull: { number: number; title: string; state: string; merged: boolean; base?: string; head?: string;
head_sha?: string; merge_commit_sha?: string; html_url: string } | undefined;
if (number > 0) {
const p = await gitea.getPullRequest(owner, repo, number);
pull = { number: p.number, title: p.title, state: p.state, merged: p.merged, base: p.base, head: p.head,
head_sha: p.head_sha, merge_commit_sha: p.merge_commit_sha, html_url: p.html_url };
if (!ref) ref = p.head_sha ?? "";
if (!base) base = p.base ?? "";
if (!ref) throw new Error(`${owner}/${repo}#${number} names no head commit`);
}
if (!ref) throw new Error("name a pull request's number or a commit");
const combined = await gitea.combinedStatus(owner, repo, ref);
const statuses = newestByContext(combined.statuses);
base = base || "main";
let rule: BranchProtection | null = null, unreadable: string | undefined;
try {
rule = await gitea.branchProtection(owner, repo, base);
} catch (err) {
unreadable = err instanceof Error ? err.message : String(err);
}
return { commit: combined.sha || ref, pull, combined: combined.state, statuses,
merge: mergeableUnder(base, rule, statuses, unreadable) };
},
},
{
name: "gitea_commit_status",
description: "Set one of the mesh's statuses on a commit (mesh/delivery, mesh/delivery-group): pending, success, error, failure or warning, a short description, and the page it links to.",
@@ -0,0 +1,274 @@
package main
import (
"errors"
"fmt"
"sort"
"strings"
"time"
)
// The `checks` verb (novox/hq ADR 0239): what the mesh's checks said of a pull request's head, or of one
// commit. Two records answer it, and neither alone is enough. The forge keeps each status — the newest of
// each context, a description clipped to what it shows, and the protection that decides whether the merge
// button works. This owner keeps the verdict whole as the controller said it: each layer's summary
// unclipped ("0 of 4 compose" is not lost to a 140-character description), the build seat's ask that ran
// it, the machine it ran on, and its report. So the forge is asked for its statuses and this owner joins
// its own verdict to the two statuses that verdict set. Nothing is written: a read, by anyone.
// Context names of the statuses this owner can say more of than the forge keeps.
const (
ctxGate = "mesh/merge-gate"
ctxRepo = "mesh/repo-check"
ctxDelivery = "mesh/delivery"
ctxGroup = "mesh/delivery-group"
)
// CheckStatus is one status of the commit, with what this owner knows of it beyond the forge.
type CheckStatus struct {
Name string `json:"name"`
State string `json:"state"`
Description string `json:"description,omitempty"`
SetAt string `json:"set_at,omitempty"`
SetBy string `json:"set_by,omitempty"`
Link string `json:"link,omitempty"`
// From the verdict that set it, when this owner holds it: the build seat's ask, the machine that ran it,
// when it was heard, and the layer's verdict in full.
Build string `json:"build,omitempty"`
CheckedOn string `json:"checked_on,omitempty"`
CheckedAt string `json:"checked_at,omitempty"`
Verdict string `json:"verdict,omitempty"`
// Source says where the status was read: the forge, or — the forge unreachable — this owner's verdict.
Source string `json:"source"`
}
// ChecksAnswer is the `checks` verb's answer.
type ChecksAnswer struct {
Repository string `json:"repository"`
Commit string `json:"commit"`
Pull *ForgePull `json:"pull,omitempty"`
Delivery *ChecksOf `json:"delivery,omitempty"`
Statuses []CheckStatus `json:"statuses"`
// Verdict is the merge check's verdict whole, as this owner heard it for this commit.
Verdict *Verdict `json:"verdict,omitempty"`
// Merge is whether the branch's protection lets the commit merge; Mergeable nil when it is not known.
Merge ForgeMerge `json:"merge"`
// Forge is why the forge could not be read, when it could not.
Forge string `json:"forge,omitempty"`
Says string `json:"says"`
}
// ChecksOf is the delivery of the commit, in brief: `show` has it whole.
type ChecksOf struct {
ID string `json:"id"`
State State `json:"state"`
Since string `json:"since"`
Waits string `json:"waits,omitempty"`
Group string `json:"group,omitempty"`
}
// Checks is the `checks` verb: a repository and a pull request's number, or a repository and a commit.
func (h *Holder) Checks(repository string, number int, commit string) (*ChecksAnswer, error) {
repository = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(repository), ".git"))
owner, repo, ok := strings.Cut(repository, "/")
if !ok || owner == "" || repo == "" || strings.Contains(repo, "/") {
return nil, fmt.Errorf("%q is not owner/repository", repository)
}
commit = strings.ToLower(strings.TrimSpace(commit))
if number < 0 {
return nil, fmt.Errorf("%d is not a pull request's number", number)
}
if number == 0 && commit == "" {
return nil, errors.New("name a pull request's number or a commit")
}
if commit != "" && (len(commit) < 7 || strings.Trim(commit, "0123456789abcdef") != "") {
return nil, fmt.Errorf("%q is not a commit: seven or more hexadecimal characters", commit)
}
// What this owner knows first, for the base whose protection is read; the forge then says the head.
base := ""
if d := h.deliveryOf(repository, number, commit); d != nil {
base = d.Base
}
a := &ChecksAnswer{Repository: repository, Commit: commit}
fc, err := h.Forge.Statuses(owner, repo, number, commit, base)
if err != nil {
a.Forge = err.Error()
} else {
a.Commit, a.Pull = fc.Commit, fc.Pull
}
d := h.deliveryOf(repository, number, a.Commit)
if a.Commit == "" && d != nil {
a.Commit = d.Commit // the forge away: the newest head this owner holds for the pull request
}
if d != nil {
a.Delivery = &ChecksOf{ID: d.ID, State: d.State, Since: d.Since.UTC().Format(time.RFC3339), Waits: d.waits,
Group: d.Group}
if d.Check != nil && sameCommit(d.Commit, a.Commit) {
a.Verdict = d.Check
}
}
if fc != nil {
for _, s := range fc.Statuses {
a.Statuses = append(a.Statuses, CheckStatus{Name: s.Context, State: s.State, Description: s.Description,
SetAt: firstOf(s.UpdatedAt, s.CreatedAt), SetBy: s.Creator, Link: s.TargetURL, Source: "the forge"})
}
a.Merge = fc.Merge
} else {
// The forge away: the statuses the verdict set, as the forge holder sets them from it, said as such.
if v := a.Verdict; v != nil {
a.Statuses = append(a.Statuses, CheckStatus{Name: ctxGate, State: forgeStateOf(v.Gate), Source: "mesh-delivery's verdict; the forge could not be read"})
if v.Repo != "" {
a.Statuses = append(a.Statuses, CheckStatus{Name: ctxRepo, State: forgeStateOf(v.Repo), Source: "mesh-delivery's verdict; the forge could not be read"})
}
}
a.Merge = ForgeMerge{Branch: firstOf(base, "main"), Says: "not known: the forge could not be read, and only it holds the protection"}
}
for i := range a.Statuses {
h.addWhatIsKnown(&a.Statuses[i], a.Verdict, d)
}
sort.SliceStable(a.Statuses, func(i, j int) bool { return a.Statuses[i].Name < a.Statuses[j].Name })
a.Says = a.says()
return a, nil
}
// checksDelivery is the part of a delivery `checks` reads, copied under the lock.
type checksDelivery struct {
Delivery
waits string
groupSaid string
}
// deliveryOf is the delivery of a commit — its head, or the commit it merged as — else, for a pull request,
// its newest head that was not superseded, else its newest.
func (h *Holder) deliveryOf(repository string, number int, commit string) *checksDelivery {
h.mu.Lock()
defer h.mu.Unlock()
var found *Delivery
if commit != "" {
for _, d := range h.deliveries {
if strings.EqualFold(d.Repository, repository) && (sameCommit(d.Commit, commit) || sameCommit(d.MergedAs, commit)) {
found = d
break
}
}
} else if number > 0 {
for _, d := range h.deliveries {
if !strings.EqualFold(d.Repository, repository) || d.Number != number {
continue
}
if found == nil || newerHead(d, found) {
found = d
}
}
}
if found == nil {
return nil
}
c := &checksDelivery{Delivery: *found, waits: h.waitsFor(found)}
if found.Check != nil {
v := *found.Check
c.Check = &v
}
if g := h.groups[found.Group]; g != nil {
state, why := GroupState(g, h.membersOf(g))
c.groupSaid = state + ": " + why
if g.Check != nil && g.Check.Verdict != "" {
c.groupSaid += "; composed check " + g.Check.Verdict + " — " + g.Check.Summary
}
}
return c
}
// newerHead is whether d is a pull request's head to read before e: one not superseded first, then the newest.
func newerHead(d, e *Delivery) bool {
if (d.State == Superseded) != (e.State == Superseded) {
return e.State == Superseded
}
return d.Created.After(e.Created)
}
// addWhatIsKnown joins this owner's verdict to the status it set, and the delivery to its own statuses.
func (h *Holder) addWhatIsKnown(s *CheckStatus, v *Verdict, d *checksDelivery) {
switch s.Name {
case ctxGate, ctxRepo:
if v == nil {
return
}
s.Build, s.CheckedOn = v.ID, v.On
if !v.At.IsZero() {
s.CheckedAt = v.At.UTC().Format(time.RFC3339)
}
if s.Name == ctxGate {
s.Verdict = strings.ToUpper(firstOf(v.Gate, "error")) + " — " + v.Summary
if len(v.Modules) > 0 {
s.Verdict += " (modules: " + strings.Join(v.Modules, ", ")
if len(v.Dependents) > 0 {
s.Verdict += "; built after them: " + strings.Join(v.Dependents, ", ")
}
s.Verdict += ")"
}
} else if v.Repo != "" {
s.Verdict = strings.ToUpper(v.Repo) + " — " + v.RepoSaid
}
case ctxDelivery:
if d != nil {
s.Verdict = string(d.State)
if d.waits != "" {
s.Verdict += ": waits for " + d.waits
}
}
case ctxGroup:
if d != nil && d.groupSaid != "" {
s.Verdict = d.groupSaid
}
}
}
// forgeStateOf is the forge state the forge's holder sets for a verdict (the gitea module's pulls.ts): a
// warning is a pass with a note, an error never a success.
func forgeStateOf(verdict string) string {
switch verdict {
case "pass", "warning":
return "success"
case "fail":
return "failure"
}
return "error"
}
func (a *ChecksAnswer) says() string {
what := a.Repository + "@" + shortOf(a.Commit, 8)
if a.Pull != nil {
what = fmt.Sprintf("%s#%d at %s", a.Repository, a.Pull.Number, shortOf(a.Commit, 8))
}
var parts []string
for _, s := range a.Statuses {
parts = append(parts, s.Name+" "+s.State)
}
statuses := "no status"
if len(parts) > 0 {
statuses = strings.Join(parts, ", ")
}
merge := a.Merge.Says
switch {
case a.Merge.Mergeable == nil:
merge = "mergeable: not known — " + merge
case *a.Merge.Mergeable:
merge = "mergeable — " + merge
default:
merge = "NOT mergeable — " + merge
}
return what + ": " + statuses + "; " + merge
}
func firstOf(s ...string) string {
for _, x := range s {
if x != "" {
return x
}
}
return ""
}
@@ -0,0 +1,242 @@
package main
import (
"encoding/json"
"strings"
"testing"
)
// The `checks` verb (novox/hq ADR 0239): what the mesh's checks said of a pull request's head or a commit —
// the forge's statuses, the verdict whole as the controller said it, and whether the base's protection lets
// it merge.
// checkedAs is the controller's `checked` as it comes off the bus, both layers said.
func checkedAs(t *testing.T, repo string, number int, sha, gate, gateSaid, repoVerdict, repoSaid string) CheckedEvent {
t.Helper()
owner, name, _ := strings.Cut(repo, "/")
raw := map[string]any{"owner": owner, "repo": name, "number": number, "commit": sha, "verdict": gate,
"summary": gateSaid, "id": "build-" + sha[:8], "on": "the-build-node", "report": "== compose\n" + gateSaid,
"gate": map[string]any{"verdict": gate, "summary": gateSaid, "modules": []string{"app"}, "dependents": []string{"web"}},
"repo-check": map[string]any{"verdict": repoVerdict, "summary": repoSaid}}
var c CheckedEvent
if err := json.Unmarshal(mustJSON(raw), &c); err != nil {
t.Fatal(err)
}
return c
}
// statusesAsTheForgeSets puts the two statuses the forge's holder sets from a verdict: clipped, as it keeps them.
func statusesAsTheForgeSets(w *world, repo, sha string, c CheckedEvent) {
owner, name, _ := strings.Cut(repo, "/")
_ = w.forge.Status(owner, name, sha, ctxGate, forgeStateOf(c.Gate.Verdict), clip(c.Gate.Verdict+": "+c.Gate.Summary, 40), "https://forge.invalid/pr")
_ = w.forge.Status(owner, name, sha, ctxRepo, forgeStateOf(c.RepoCheck.Verdict), clip(c.RepoCheck.Verdict+": "+c.RepoCheck.Summary, 40), "https://forge.invalid/pr")
}
func statusNamed(a *ChecksAnswer, name string) *CheckStatus {
for i := range a.Statuses {
if a.Statuses[i].Name == name {
return &a.Statuses[i]
}
}
return nil
}
func TestChecksOfAPullRequestSayEveryStatusTheVerdictWholeAndWhetherItMerges(t *testing.T) {
w := newWorld(t)
w.forge.required = []string{ctxGate, ctxRepo}
w.forge.pulls["novox/app#7"] = ForgePull{Number: 7, Title: "a change", State: "open", Base: "main", Head: "feat/x", HeadSHA: head}
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x", "src/a.go"))
said := "0 of 4 compose: anchor refuses app's manifest — a provision nothing provides, and three more machines alike"
c := checkedAs(t, "novox/app", 7, head, "fail", said, "pass", "its merge-check.sh passed")
w.h.Checked(c)
w.settleAll()
statusesAsTheForgeSets(w, "novox/app", head, c)
a, err := w.h.Checks("novox/app", 7, "")
if err != nil {
t.Fatal(err)
}
if a.Commit != head || a.Pull == nil || a.Pull.Number != 7 || a.Forge != "" {
t.Fatalf("the head read is %q, pull %+v, forge %q", a.Commit, a.Pull, a.Forge)
}
gate := statusNamed(a, ctxGate)
if gate == nil || gate.State != "failure" || gate.Source != "the forge" || gate.SetBy != "mesh-admin" {
t.Fatalf("the gate's status is %+v", gate)
}
// The forge clipped it; the verdict says it whole, with the machine, the build and the modules.
if strings.Contains(gate.Description, "three more machines") {
t.Fatalf("the fake forge did not clip: %q", gate.Description)
}
if !strings.Contains(gate.Verdict, "FAIL — 0 of 4 compose") || !strings.Contains(gate.Verdict, "three more machines") ||
!strings.Contains(gate.Verdict, "modules: app; built after them: web") {
t.Fatalf("the gate's verdict is %q", gate.Verdict)
}
if gate.Build != "build-"+head[:8] || gate.CheckedOn != "the-build-node" || gate.CheckedAt == "" {
t.Fatalf("the gate was checked as %q on %q at %q", gate.Build, gate.CheckedOn, gate.CheckedAt)
}
repo := statusNamed(a, ctxRepo)
if repo == nil || repo.State != "success" || repo.Verdict != "PASS — its merge-check.sh passed" || repo.CheckedOn != "the-build-node" {
t.Fatalf("the repository check's status is %+v", repo)
}
if d := statusNamed(a, ctxDelivery); d == nil || !strings.HasPrefix(d.Verdict, "rejected: waits for a new head") {
t.Fatalf("the delivery's status is %+v", d)
}
if a.Verdict == nil || !strings.Contains(a.Verdict.Report, "== compose") {
t.Fatalf("the verdict whole is %+v", a.Verdict)
}
if a.Delivery == nil || a.Delivery.ID != IDOf("novox/app", head) || a.Delivery.State != Rejected {
t.Fatalf("the delivery is %+v", a.Delivery)
}
if a.Merge.Mergeable == nil || *a.Merge.Mergeable || len(a.Merge.Blocking) != 1 || a.Merge.Blocking[0].Context != ctxGate {
t.Fatalf("the merge is %+v", a.Merge)
}
if !strings.Contains(a.Says, "novox/app#7 at aaaaaaaa") || !strings.Contains(a.Says, "NOT mergeable") {
t.Fatalf("it says %q", a.Says)
}
// The forge was asked for the pull request, with the base this owner knew.
if len(w.forge.asked) != 1 || w.forge.asked[0] != "novox/app#7@ base=main" {
t.Fatalf("the forge was asked %v", w.forge.asked)
}
}
func TestANewHeadIsReadWithItsOwnVerdictNeverTheOlderOnes(t *testing.T) {
w := newWorld(t)
w.forge.required = []string{ctxGate, ctxRepo}
newer := "bbbbbbbbbbbb2222"
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x"))
w.h.Checked(checkedAs(t, "novox/app", 7, head, "fail", "0 of 4 compose", "pass", "passed"))
w.h.PullUpdated(pr("novox/app", 7, newer, "feat/x"))
w.forge.pulls["novox/app#7"] = ForgePull{Number: 7, State: "open", Base: "main", HeadSHA: newer}
a, err := w.h.Checks("novox/app", 7, "")
if err != nil {
t.Fatal(err)
}
if a.Commit != newer || a.Delivery == nil || a.Delivery.ID != IDOf("novox/app", newer) || a.Delivery.State != Proposed {
t.Fatalf("the new head reads as %q, %+v", a.Commit, a.Delivery)
}
if a.Verdict != nil {
t.Fatalf("the older head's verdict was given for the newer: %+v", a.Verdict)
}
if a.Merge.Mergeable == nil || *a.Merge.Mergeable || len(a.Merge.Blocking) != 2 || a.Merge.Blocking[0].State != "missing" {
t.Fatalf("a head not checked yet merges as %+v", a.Merge)
}
// Passed, it merges.
c := checkedAs(t, "novox/app", 7, newer, "pass", "every machine composes", "pass", "its merge-check.sh passed")
w.h.Checked(c)
statusesAsTheForgeSets(w, "novox/app", newer, c)
a, _ = w.h.Checks("novox/app", 7, "")
if a.Merge.Mergeable == nil || !*a.Merge.Mergeable || !strings.Contains(a.Says, "mergeable") || strings.Contains(a.Says, "NOT") {
t.Fatalf("a head that passed: %+v, says %q", a.Merge, a.Says)
}
if g := statusNamed(a, ctxGate); g == nil || g.Verdict != "PASS — every machine composes (modules: app; built after them: web)" {
t.Fatalf("its gate: %+v", g)
}
}
func TestACommitIsReadByTheStartOfItsShaAndAMergeByItsDelivery(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x"))
c := checkedAs(t, "novox/app", 7, head, "pass", "every machine composes", "pass", "passed")
w.h.Checked(c)
statusesAsTheForgeSets(w, "novox/app", head, c)
a, err := w.h.Checks("Novox/App.git", 0, head[:8])
if err != nil {
t.Fatal(err)
}
if a.Commit != head || a.Verdict == nil || a.Delivery == nil || a.Pull != nil {
t.Fatalf("by the start of its sha: %q, %+v, %+v", a.Commit, a.Verdict, a.Delivery)
}
// No protection: nothing is required, so it merges.
if a.Merge.Mergeable == nil || !*a.Merge.Mergeable {
t.Fatalf("with no protection: %+v", a.Merge)
}
mergedAs := "dddddddddddd9999"
w.h.PullMerged(merged("novox/app", 7, head, mergedAs))
_ = w.forge.Status("novox", "app", mergedAs, ctxDelivery, "pending", "published", "")
a, err = w.h.Checks("novox/app", 0, mergedAs)
if err != nil {
t.Fatal(err)
}
if a.Delivery == nil || a.Delivery.ID != IDOf("novox/app", head) {
t.Fatalf("the merge commit's delivery is %+v", a.Delivery)
}
if a.Verdict != nil {
t.Fatalf("the head's verdict was given for the merge commit: %+v", a.Verdict)
}
if s := statusNamed(a, ctxDelivery); s == nil || s.State != "pending" || s.Verdict == "" {
t.Fatalf("the merge's delivery status: %+v", s)
}
}
func TestWithTheForgeAwayTheVerdictIsSaidAndWhetherItMergesIsNotKnown(t *testing.T) {
w := newWorld(t)
w.h.PullUpdated(pr("novox/app", 7, head, "feat/x"))
w.h.Checked(checkedAs(t, "novox/app", 7, head, "warning", "every machine composes, with a note", "error", "the toolchain was not there"))
w.forge.down = true
a, err := w.h.Checks("novox/app", 7, "")
if err != nil {
t.Fatal(err)
}
if a.Forge == "" || a.Commit != head || a.Verdict == nil {
t.Fatalf("with the forge away: forge %q, commit %q, verdict %+v", a.Forge, a.Commit, a.Verdict)
}
gate, repo := statusNamed(a, ctxGate), statusNamed(a, ctxRepo)
if gate == nil || gate.State != "success" || !strings.Contains(gate.Source, "could not be read") ||
repo == nil || repo.State != "error" || repo.Verdict != "ERROR — the toolchain was not there" {
t.Fatalf("the statuses from the verdict: %+v %+v", gate, repo)
}
if a.Merge.Mergeable != nil || !strings.Contains(a.Says, "mergeable: not known") {
t.Fatalf("whether it merges, with the forge away: %+v, %q", a.Merge, a.Says)
}
// The protection unreadable: the statuses are said, and whether it merges is not known.
w.forge.down, w.forge.protectionDown = false, true
w.forge.pulls["novox/app#7"] = ForgePull{Number: 7, Base: "main", HeadSHA: head}
a, _ = w.h.Checks("novox/app", 7, "")
if a.Merge.Mergeable != nil || a.Forge != "" {
t.Fatalf("with the protection unreadable: %+v", a.Merge)
}
}
func TestChecksRefuseWhatNamesNoCommit(t *testing.T) {
w := newWorld(t)
for _, c := range []struct {
repo string
number int
commit string
says string
}{
{"novox/app", 0, "", "number or a commit"},
{"app", 7, "", "owner/repository"},
{"novox/app/x", 7, "", "owner/repository"},
{"novox/app", 0, "abc", "not a commit"},
{"novox/app", 0, "zzzzzzzzzz", "not a commit"},
{"novox/app", -1, "", "not a pull request's number"},
} {
if _, err := w.h.Checks(c.repo, c.number, c.commit); err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s #%d %q: %v", c.repo, c.number, c.commit, err)
}
}
if len(w.forge.asked) != 0 {
t.Fatalf("the forge was asked for what names nothing: %v", w.forge.asked)
}
}
func TestANumberIsTakenAsANumberOrAsText(t *testing.T) {
for _, c := range []struct {
in any
want int
ok bool
}{{float64(168), 168, true}, {"168", 168, true}, {"#168", 168, true}, {nil, 0, true}, {"", 0, true},
{"x", 0, false}, {1.5, 0, false}} {
got, err := intArg(map[string]any{"number": c.in}, "number")
if (err == nil) != c.ok || got != c.want {
t.Errorf("%v → %d %v", c.in, got, err)
}
}
}
@@ -215,10 +215,17 @@ type fakeForge struct {
views map[string]string
statuses map[string]string
down bool
// What Statuses reads: the pull requests by owner/repo#number, the base's required statuses, whether the
// protection cannot be read, and what it was asked.
pulls map[string]ForgePull
required []string
protectionDown bool
asked []string
}
func newFakeForge() *fakeForge {
return &fakeForge{notes: map[string][]string{}, views: map[string]string{}, statuses: map[string]string{}}
return &fakeForge{notes: map[string][]string{}, views: map[string]string{}, statuses: map[string]string{},
pulls: map[string]ForgePull{}}
}
func (f *fakeForge) Note(owner, repo, commit, line string) error {
@@ -255,6 +262,73 @@ func (f *fakeForge) Status(owner, repo, commit, context, state, description, tar
return nil
}
// Statuses answers what the fake was told: a pull request's head from pulls, the statuses set on the commit
// (those Status set too, as the forge would keep them), and the base's required statuses from required.
func (f *fakeForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.down {
return nil, errors.New("the forge is away")
}
f.asked = append(f.asked, fmt.Sprintf("%s/%s#%d@%s base=%s", owner, repo, number, commit, base))
out := &ForgeChecks{}
if number > 0 {
p, ok := f.pulls[fmt.Sprintf("%s/%s#%d", owner, repo, number)]
if !ok {
return nil, fmt.Errorf("no pull request %d", number)
}
out.Pull = &p
if commit == "" {
commit = p.HeadSHA
}
if base == "" {
base = p.Base
}
}
for key, said := range f.statuses {
at, context, _ := strings.Cut(key, " ")
if !strings.HasPrefix(at, owner+"/"+repo+"@") || !sameCommit(strings.TrimPrefix(at, owner+"/"+repo+"@"), commit) {
continue
}
if len(strings.TrimPrefix(at, owner+"/"+repo+"@")) > len(commit) {
commit = strings.TrimPrefix(at, owner+"/"+repo+"@")
}
state, rest, _ := strings.Cut(said, " ")
description, link, _ := strings.Cut(rest, " → ")
out.Statuses = append(out.Statuses, ForgeStatus{Context: context, State: state, Description: description,
TargetURL: link, UpdatedAt: "2026-10-06T12:00:00Z", Creator: "mesh-admin"})
}
sort.Slice(out.Statuses, func(i, j int) bool { return out.Statuses[i].Context < out.Statuses[j].Context })
out.Commit = commit
if base == "" {
base = "main"
}
out.Merge = ForgeMerge{Branch: base, Protected: f.required != nil, Required: f.required}
if f.protectionDown {
out.Merge.Says = "the protection of " + base + " could not be read"
return out, nil
}
ok := true
for _, r := range f.required {
state := "missing"
for _, st := range out.Statuses {
if st.Context == r {
state = st.State
}
}
if state != "success" {
ok = false
out.Merge.Blocking = append(out.Merge.Blocking, struct {
Context string `json:"context"`
State string `json:"state"`
}{r, state})
}
}
out.Merge.Mergeable = &ok
out.Merge.Says = fmt.Sprintf("%s requires %v", base, f.required)
return out, nil
}
func (f *fakeForge) notesOn(commit string) []string {
f.mu.Lock()
defer f.mu.Unlock()
@@ -456,9 +456,13 @@ type CheckedEvent struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
ID string `json:"id"`
On string `json:"on,omitempty"`
Report string `json:"report,omitempty"`
Gate *struct {
Verdict string `json:"verdict"`
Summary string `json:"summary"`
Verdict string `json:"verdict"`
Summary string `json:"summary"`
Modules []string `json:"modules,omitempty"`
Dependents []string `json:"dependents,omitempty"`
} `json:"gate,omitempty"`
RepoCheck *struct {
Verdict string `json:"verdict"`
@@ -509,9 +513,10 @@ func (h *Holder) Checked(c CheckedEvent) {
h.Logf("[mesh-delivery] %s is %s: a verdict for it now is history (%s)", d.ID, d.State, c.Verdict)
return
}
v := &Verdict{ID: c.ID, At: now, Gate: c.Verdict, Summary: c.Summary}
v := &Verdict{ID: c.ID, On: c.On, At: now, Gate: c.Verdict, Summary: c.Summary, Report: c.Report}
if c.Gate != nil {
v.Gate, v.Summary = c.Gate.Verdict, c.Gate.Summary
v.Modules, v.Dependents = c.Gate.Modules, c.Gate.Dependents
}
if c.RepoCheck != nil {
v.Repo, v.RepoSaid = c.RepoCheck.Verdict, c.RepoCheck.Summary
@@ -10,6 +10,7 @@ import (
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
@@ -137,6 +138,29 @@ func str(description string) map[string]any {
func strArg(a map[string]any, k string) string { s, _ := a[k].(string); return strings.TrimSpace(s) }
// intArg is a whole number given as a number or as text; absent is zero.
func intArg(a map[string]any, k string) (int, error) {
switch v := a[k].(type) {
case nil:
return 0, nil
case float64:
if v == float64(int(v)) {
return int(v), nil
}
case int:
return v, nil
case string:
v = strings.TrimPrefix(strings.TrimSpace(v), "#")
if v == "" {
return 0, nil
}
if n, err := strconv.Atoi(v); err == nil {
return n, nil
}
}
return 0, fmt.Errorf("%q is not a whole number: %v", k, a[k])
}
func boolArg(a map[string]any, k string) bool {
switch v := a[k].(type) {
case bool:
@@ -195,6 +219,26 @@ func tools(h *Holder, l *listening) []stdio.Tool {
}
return h.WhatIf(strArg(a, "repository"), strArg(a, "base"), strings.Split(strArg(a, "paths"), ","))
}},
{Name: seat + "checks",
Description: "What the mesh's checks said of a pull request's head or of one commit: each of the commit's " +
"statuses (mesh/merge-gate, mesh/repo-check, mesh/delivery, …) with its state, description and when it " +
"was set; the merge check's full verdict as the controller said it — each layer's summary, the machine " +
"that ran it, when, its build id and its report; and whether the branch's protection would let it " +
"merge, every required status being success.",
Input: map[string]any{"type": "object", "properties": map[string]any{"repository": str("owner/repository"),
"number": str("a pull request's number: its head is read"),
"commit": str("a commit's sha, or the start of one, instead of a pull request")},
"required": []string{"repository"}},
Run: func(a map[string]any) (any, error) {
if err := need(a, "repository"); err != nil {
return nil, err
}
number, err := intArg(a, "number")
if err != nil {
return nil, err
}
return h.Checks(strArg(a, "repository"), number, strArg(a, "commit"))
}},
{Name: seat + "table",
Description: "The state table every delivery runs by: each transition with its guard, each state's bound " +
"and what healer H2 may do once it has passed; and the machine steps' table.",
@@ -59,12 +59,13 @@ func TestItClaimsTheDeliverySeatAndFollowsWhatItHandles(t *testing.T) {
if !reflect.DeepEqual(m.State, []string{"deliveries", "groups"}) {
t.Fatalf("state %v", m.State)
}
// It calls exactly what its ports ask: the controller's six verbs and the forge's three tools.
// It calls exactly what its ports ask: the controller's six verbs and the forge's four tools.
want := []string{}
for _, v := range []string{"delivery-plan", "delivery-order", "delivery-check", "deliver", "delivery-stop", "delivery-walks"} {
want = append(want, "seat:"+ControllerSeat+"."+v)
}
want = append(want, "gitea.gitea_note_append", "gitea.gitea_delivery_view", "gitea.gitea_commit_status")
want = append(want, "gitea.gitea_note_append", "gitea.gitea_delivery_view", "gitea.gitea_commit_status",
"gitea.gitea_commit_statuses")
if !reflect.DeepEqual(m.Invokes, want) {
t.Fatalf("invokes %v, wanted %v", m.Invokes, want)
}
@@ -80,14 +80,20 @@ type Transition struct {
By string `json:"by,omitempty"`
}
// Verdict is a check's verdict: the gate's and the repository's own.
// Verdict is a check's verdict: the gate's and the repository's own. ID is the build seat's ask that ran
// it, On the machine it ran on, At when this owner heard it; Report is the check's own account, as bounded
// as the controller says it. All of it is what `checks` answers, whole, beside the forge's short statuses.
type Verdict struct {
ID string `json:"id,omitempty"`
At time.Time `json:"at"`
Gate string `json:"gate"`
Summary string `json:"summary"`
Repo string `json:"repo,omitempty"`
RepoSaid string `json:"repo_summary,omitempty"`
ID string `json:"id,omitempty"`
On string `json:"on,omitempty"`
At time.Time `json:"at"`
Gate string `json:"gate"`
Summary string `json:"summary"`
Modules []string `json:"modules,omitempty"`
Dependents []string `json:"dependents,omitempty"`
Repo string `json:"repo,omitempty"`
RepoSaid string `json:"repo_summary,omitempty"`
Report string `json:"report,omitempty"`
}
// CheckAsk is a head's own check, asked of the controller by this owner (or re-asked by healer H2).
@@ -71,6 +71,56 @@ type Forge interface {
View(owner, repo string, number int, body string) error
// Status sets one status of a commit, linking to the view.
Status(owner, repo, commit, context, state, description, target string) error
// Statuses reads a commit's statuses — a pull request's head's, given its number — and whether the branch
// it merges into would let it merge. Reads only.
Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error)
}
// ForgeChecks is what the forge says of a commit's checks (the gitea module's gitea_commit_statuses).
type ForgeChecks struct {
Commit string `json:"commit"`
Pull *ForgePull `json:"pull,omitempty"`
Combined string `json:"combined,omitempty"`
Statuses []ForgeStatus `json:"statuses"`
Merge ForgeMerge `json:"merge"`
}
// ForgePull is a pull request as the forge says it.
type ForgePull struct {
Number int `json:"number"`
Title string `json:"title,omitempty"`
State string `json:"state,omitempty"`
Merged bool `json:"merged,omitempty"`
Base string `json:"base,omitempty"`
Head string `json:"head,omitempty"`
HeadSHA string `json:"head_sha,omitempty"`
MergeCommit string `json:"merge_commit_sha,omitempty"`
HTMLURL string `json:"html_url,omitempty"`
}
// ForgeStatus is one status of a commit, the newest of its context.
type ForgeStatus struct {
Context string `json:"context"`
State string `json:"state"`
Description string `json:"description"`
TargetURL string `json:"target_url,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
UpdatedAt string `json:"updated_at,omitempty"`
Creator string `json:"creator,omitempty"`
}
// ForgeMerge is whether the branch's protection lets the commit merge: every required status a success.
// Mergeable is nil when the protection could not be read.
type ForgeMerge struct {
Branch string `json:"branch"`
Protected bool `json:"protected"`
Required []string `json:"required"`
Blocking []struct {
Context string `json:"context"`
State string `json:"state"`
} `json:"blocking"`
Mergeable *bool `json:"mergeable"`
Says string `json:"says"`
}
// Store keeps deliveries and groups: one key each, in the module's own state (ADR 0201).
@@ -259,6 +309,32 @@ func (f toolForge) Status(owner, repo, commit, context, state, description, targ
"context": context, "state": state, "description": description, "target_url": target})
}
func (f toolForge) Statuses(owner, repo string, number int, commit, base string) (*ForgeChecks, error) {
args := map[string]any{"owner": owner, "repo": repo}
if number > 0 {
args["number"] = number
}
if commit != "" {
args["sha"] = commit
}
if base != "" {
args["base"] = base
}
raw, err := f.ask("gitea.gitea_commit_statuses", args)
if err != nil {
return nil, err
}
answer, output, ok := answerOf(raw)
if !ok {
return nil, fmt.Errorf("the forge refused gitea_commit_statuses: %s", lastLine(output))
}
var c ForgeChecks
if err := json.Unmarshal(answer, &c); err != nil || c.Commit == "" {
return nil, fmt.Errorf("the forge's gitea_commit_statuses is not readable: %s", clip(string(answer), 200))
}
return &c, nil
}
// kvStore is the module's own state: `deliveries` and `groups`, one key each.
type kvStore struct{}
@@ -8,7 +8,7 @@ import (
"time"
)
// The seat's verbs (novox/hq ADR 0239, the mesh-delivery seat in the controller's set): five that read, and
// The seat's verbs (novox/hq ADR 0239, the mesh-delivery seat in the controller's set): six that read, and
// the acts — a person's recheck, release and stop, and healer H2's close, each only by a transition the table
// holds.
+3 -1
View File
@@ -11,6 +11,7 @@
"show",
"groups",
"what-if",
"checks",
"table",
"stalled",
"recheck",
@@ -40,7 +41,8 @@
"seat:mesh-controller.delivery-walks",
"gitea.gitea_note_append",
"gitea.gitea_delivery_view",
"gitea.gitea_commit_status"
"gitea.gitea_commit_status",
"gitea.gitea_commit_statuses"
],
"state": [
"deliveries",