// A pull request's merge check (novox/hq to-be 45 §9): what the forge's announcer says when a pull // request's head moves, and the status contexts the check is kept under. // // **Before merge, never after.** Each new head of an open pull request is announced as `pull.updated`, and the mesh // checks it against every machine of its facts before it merges. The verdict is the checker's `mesh/check`, which this // module writes from the checker's owed state (check.ts); the controller's `checked`, which once set the two older // statuses, is no event any more (ADR 0571 clause 13). // // Pure functions here, so they are tested without a forge. import type { GiteaPull, GiteaRepo } from "./client.js"; import { movedSince } from "./client.js"; /** The status context a merge check's gate is kept under: one per commit, the newest replacing the last. */ export const CHECK_CONTEXT = "mesh/merge-gate"; /** The status context of the repository's own merge-check.sh, the check's second layer (novox/hq ADR 0237). */ export const REPO_CHECK_CONTEXT = "mesh/repo-check"; /** The one status main requires (novox/hq ADR 0447 §1, issue 1301): the checker's verdict, written by this module from the * checker's owed state (check.ts, ADR 0521). Its line names the head and the trunk commit it judged: "pass on …". */ export const CHECK_STATUS = "mesh/check"; /** The statuses a branch's rule requires, from the rule the forge applies to it: none when it is unprotected or checks * no status; undefined when the rule could not be read. */ export function requiredOf(rule: { enable_status_check?: boolean; status_check_contexts?: string[] | null } | null | undefined): string[] | undefined { if (rule === undefined) return undefined; if (!rule || !rule.enable_status_check) return []; return (rule.status_check_contexts ?? []).filter(Boolean); } /** * **The controller's statuses are written only where a base still requires them** (issue 1301; ADR 0503 §3, §5): of * mesh/merge-gate and mesh/repo-check, those the base requires — hq's mesh/repo-check alone once the forge requires * mesh/check everywhere else, so no mesh/merge-gate stands on a head that nothing will set. A base whose rule could not * be read keeps both, as before: a status nothing requires blocks nothing, and one that is required is never left out. */ export function maintained(required: string[] | undefined): string[] { const both = [CHECK_CONTEXT, REPO_CHECK_CONTEXT]; return required === undefined ? both : both.filter((c) => required.includes(c)); } /** Whether mesh/merge-gate is written on a head of this base (maintained). */ export function writesGate(required: string[] | undefined): boolean { return maintained(required).includes(CHECK_CONTEXT); } /** * The pending words a head's merge check is announced with (novox/hq issue 446): where the check is, and since when, * in the time the operator reads. They stood as "the mesh is mapping this head onto its module graph" for the whole * check — the few seconds the controller maps the head and then the queue wait and the build — and readers took the * forge for lagging (issue 423). The controller maps and queues the check in seconds, so from the announcement it waits * in the build queue; mesh-delivery says "building on since …" when a build machine takes it. */ export function pendingWords(announced: Date): string { const two = (n: number) => String(n).padStart(2, "0"); return `waiting in the build queue since ${two(announced.getHours())}:${two(announced.getMinutes())}`; } /** The heads already announced, keyed by `headKey`, so a restart announces nothing twice. */ export type Announced = Record; /** The key a pull request's announced head is kept under: `owner/repo#number@base`. The base is in it because a pull * request retargeted keeps its head, and a head judged into one base is not judged into another (issue 1422, de-spiegel #6). */ export function headKey(full: string, number: number, base?: string): string { return `${full}#${number}@${base ?? ""}`; } /** Which open pull requests have a head not yet announced. A pull request merged or closed is not * open and is never asked about. */ export function headsToAnnounce(full: string, pulls: GiteaPull[], announced: Announced): GiteaPull[] { return pulls.filter((p) => p.state === "open" && !!p.head_sha && announced[headKey(full, p.number, p.base)] !== p.head_sha); } /** A pull request the forge found updated, by its repository: what `searchOpenPulls` answers. */ export interface PullSeen { owner: string; name: string; updated_at?: string; } /** **The repositories a pass looks at** (novox/hq issue 347): every one that moved since the floor, every one * holding a pull request updated since the floor — opening, reopening or retargeting a pull request moves its own * update time and not its repository's — and every one on a sweep or the first pass. */ export function reposToLook(repos: GiteaRepo[], floor: string, pulls: PullSeen[], sweep: boolean): GiteaRepo[] { if (sweep || !floor) return repos; const moved = new Set(movedSince(repos, floor).map((r) => r.full_name)); const at = Date.parse(floor); for (const p of pulls) { if (!p.updated_at || !(Date.parse(p.updated_at) < at)) moved.add(`${p.owner}/${p.name}`); } return repos.filter((r) => moved.has(r.full_name)); } export function clipped(d: string): string { d = d.replace(/\s+/g, " ").trim(); return d.length > 140 ? d.slice(0, 139) + "…" : d; } /** What a group member's gate says while the group's composed check is the one that judges it: the member's own check can say * nothing more of it, so asking that check again brings no verdict that names a trunk (novox/hq issue 1106). */ export const WAITS_FOR_COMPOSED = "waits for its group's composed check";