// 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(); const stamp = (s: StatusRead) => s.updated_at || s.created_at || ""; for (const r of raw ?? []) { const s = r as Record; 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 }; }