Files
mesh-catalog/modules/gitea/checks.ts
T
jochen 13894744a7
mesh/delivery-group group feat/delivery-checks-verb delivering: 0 of 2 delivered
mesh/merge-gate pass: builds gitea, mesh-delivery → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
Answer what the mesh's checks said of a pull request, as mesh-delivery's checks verb (hq ADR 0239)
Reading a pull request's mesh/merge-gate and mesh/repo-check meant scraping the controller's journal:
the forge clips each status to 140 characters and nothing returned the verdict whole. mesh-delivery
already keeps the verdict; it now keeps the machine that ran it, the layers' modules and the report,
and its checks verb joins that to the forge's statuses, read through a new gitea_commit_statuses tool
that also says whether the base's protection lets the commit merge.

after: novox/mesh-controller
2026-10-07 19:36:41 +02:00

84 lines
3.9 KiB
TypeScript

// 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 };
}