Files
mesh-catalog/modules/gitea/checks.ts
T
jochen 6327f297ea
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 head of the same pull request
Cover a held merge only when the forge says a later delivered merge contains it (review of #150)
Merge order on a protected trunk was the whole proof; the review asked for ancestry. The gitea module
gains gitea_contains, a read-only compare of two commits (total_commits 0 is contained), and mesh-delivery
asks it once per pair, off the lock like its plan ask, keeping the answer on the delivery. A forge that
cannot say leaves the merge held, not covered. Merge order stays the cheap first filter.

A later delivery now carries a module only when its own check passed (so a merge held without a passing
check is closed only by one that passed), its walk is of its own merge commit on the same trunk, and its
steps say the module reached every machine running it, not one. The words say what is known: a later
merge that contains it was delivered.
2026-10-09 02:57:18 +02:00

101 lines
4.8 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 };
}
/** A commit, by its sha or the start of one: what `gitea_contains` takes, so a branch name or an expression
* never reaches the compare's path. */
export function isCommit(ref: string): boolean {
return /^[0-9a-f]{7,40}$/i.test(ref);
}
/** Whether a commit is contained in another — an ancestor of it, or it (novox/hq ADR 0270) — from the forge's
* compare of `in...commit`: the commits on `commit` that `in` lacks. None is contained. An answer that does not
* say how many is no answer. */
export function containedFrom(compare: unknown): { contains: boolean; ahead: number } {
const total = (compare as Record<string, unknown> | null)?.total_commits;
if (typeof total !== "number" || !Number.isInteger(total) || total < 0) {
throw new Error("the forge's compare said no total_commits");
}
return { contains: total === 0, ahead: total };
}