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
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
84 lines
3.9 KiB
TypeScript
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 };
|
|
}
|