The controller asks its planner what a pull request reaches, as if merged: a module whose manifest the change deletes is one the merge removes, and the plan's dependents are said on the gate's status beside the modules it moves.
103 lines
5.2 KiB
TypeScript
103 lines
5.2 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 what it sets as the pull request's status when the mesh says the verdict.
|
|
//
|
|
// **Before merge, never after.** Every check the mesh had ran after a merge, on a machine: a manifest the
|
|
// node-engine refuses (issue 236), an identity a real machine's name made too long (263). So each new head
|
|
// of an open pull request is announced as `pull.updated`; the controller asks the build seat to check it
|
|
// against every machine of the mesh's facts; and the verdict comes back as the controller's `checked`,
|
|
// which this sets as the head commit's status — and, when it is not a pass, as a comment saying why.
|
|
//
|
|
// Pure functions here, so they are tested without a forge; index.ts does the asking and the setting.
|
|
|
|
import type { CommitStatus, GiteaPull } 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";
|
|
|
|
/** One layer of a check, judged. */
|
|
export interface Layer {
|
|
verdict: string;
|
|
summary: string;
|
|
/** The modules a merge of the change would move, as the controller's planner reckons it… */
|
|
modules?: string[];
|
|
/** …and those it would build after them because they stand on them. */
|
|
dependents?: string[];
|
|
}
|
|
|
|
/** What the controller says as `checked` (mesh-controller internal/link, Checked). */
|
|
export interface Checked {
|
|
owner: string;
|
|
repo: string;
|
|
number?: number;
|
|
commit: string;
|
|
verdict: string;
|
|
summary: string;
|
|
report?: string;
|
|
id: string;
|
|
on?: string;
|
|
/** The gate — the modules of the mesh's graph the change touches — and the repository's own check. A
|
|
* controller from before the layers says neither, and its verdict is the gate's. */
|
|
gate?: Layer;
|
|
"repo-check"?: Layer;
|
|
}
|
|
|
|
/** The heads already announced, keyed `owner/repo#number`, so a restart announces nothing twice. */
|
|
export type Announced = Record<string, string>;
|
|
|
|
/** 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[`${full}#${p.number}`] !== p.head_sha);
|
|
}
|
|
|
|
/** The forge's state for a verdict: an error is the forge's `error`, never a success. */
|
|
function stateOf(verdict: string): CommitStatus["state"] {
|
|
return verdict === "pass" ? "success" : verdict === "warning" ? "warning" : verdict === "fail" ? "failure" : "error";
|
|
}
|
|
|
|
function described(verdict: string, summary: string, modules?: string[], dependents?: string[]): string {
|
|
// The forge keeps a short description; the rest is the comment's.
|
|
let d = `${verdict || "error"}: ${summary}`;
|
|
if (modules?.length) d += ` [${modules.join(", ")}${dependents?.length ? ` +${dependents.length} dependent(s)` : ""}]`;
|
|
d = d.replace(/\s+/g, " ").trim();
|
|
return d.length > 140 ? d.slice(0, 139) + "…" : d;
|
|
}
|
|
|
|
/** The forge's status for the gate. */
|
|
export function statusFor(c: Checked): CommitStatus {
|
|
const gate = c.gate ?? { verdict: c.verdict, summary: c.summary };
|
|
return {
|
|
state: stateOf(gate.verdict), context: CHECK_CONTEXT,
|
|
description: described(gate.verdict, gate.summary, gate.modules, gate.dependents),
|
|
};
|
|
}
|
|
|
|
/** Every status a verdict sets: the gate's, and the repository's own check's when it was said. */
|
|
export function statusesFor(c: Checked): CommitStatus[] {
|
|
const out = [statusFor(c)];
|
|
const repo = c["repo-check"];
|
|
if (repo) out.push({ state: stateOf(repo.verdict), context: REPO_CHECK_CONTEXT, description: described(repo.verdict, repo.summary) });
|
|
return out;
|
|
}
|
|
|
|
/** The comment a verdict leaves on its pull request, with the check's own account: when the gate is not a
|
|
* pass, or the repository's own check failed or could not run. A repository with no merge-check.sh is
|
|
* said by its status alone, not by a comment on every push. */
|
|
export function commentFor(c: Checked): string | null {
|
|
const gate = c.gate ?? { verdict: c.verdict, summary: c.summary };
|
|
const repo = c["repo-check"];
|
|
const repoWrong = !!repo && repo.verdict !== "pass" && repo.verdict !== "warning";
|
|
if (gate.verdict === "pass" && !repoWrong) return null;
|
|
const lines = [`**Merge check** at \`${c.commit.slice(0, 8)}\``, ""];
|
|
lines.push(`- \`${CHECK_CONTEXT}\`: **${(gate.verdict || "error").toUpperCase()}** — ${gate.summary}` +
|
|
(gate.modules?.length ? ` (modules: ${gate.modules.join(", ")}` +
|
|
(gate.dependents?.length ? `; built after them: ${gate.dependents.join(", ")}` : "") + ")" : ""));
|
|
if (repo) lines.push(`- \`${REPO_CHECK_CONTEXT}\`: **${(repo.verdict || "error").toUpperCase()}** — ${repo.summary}`);
|
|
const ran = c.on ? `\n\nRun by the build seat on ${c.on} as \`${c.id}\` (\`builds --log ${c.id}\`).` : "";
|
|
const report = c.report ? `\n\n\`\`\`\n${c.report.replace(/```/g, "'''")}\n\`\`\`` : "";
|
|
return lines.join("\n") + ran + report;
|
|
}
|