103 lines
5.7 KiB
TypeScript
103 lines
5.7 KiB
TypeScript
// 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 <head12> on <trunk12> …". */
|
|
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 <machine> 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<string, string>;
|
|
|
|
/** 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";
|