gitea: give the controller what it maps a pull request onto the graph with; set both check statuses; protect a branch by tool (hq ADR 0237)
The controller now decides what a pull request's check runs from the mesh's module graph, so the announcement carries the changed directories that hold a module at the head and whether the head has a merge-check.sh. A verdict sets mesh/merge-gate (the gate, with the modules it judged) and mesh/repo-check (the repository's own tests). gitea_branch_protection_get/set let the operator's agent make those statuses required. The catalogue's merge-check.sh leaves the gate to the build seat and keeps its own layer: every manifest through module check, and the touched Go modules' tests.
This commit is contained in:
+52
-13
@@ -11,9 +11,19 @@
|
||||
|
||||
import type { CommitStatus, GiteaPull } from "./client.js";
|
||||
|
||||
/** The status context a merge check is kept under: one per commit, the newest replacing the last. */
|
||||
/** 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;
|
||||
modules?: string[];
|
||||
}
|
||||
|
||||
/** What the controller says as `checked` (mesh-controller internal/link, Checked). */
|
||||
export interface Checked {
|
||||
owner: string;
|
||||
@@ -25,6 +35,10 @@ export interface Checked {
|
||||
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. */
|
||||
@@ -36,21 +50,46 @@ export function headsToAnnounce(full: string, pulls: GiteaPull[], announced: Ann
|
||||
return pulls.filter((p) => p.state === "open" && !!p.head_sha && announced[`${full}#${p.number}`] !== p.head_sha);
|
||||
}
|
||||
|
||||
/** The forge's status for a verdict: an error is the forge's `error`, never a success. */
|
||||
export function statusFor(c: Checked): CommitStatus {
|
||||
const state: CommitStatus["state"] =
|
||||
c.verdict === "pass" ? "success" : c.verdict === "warning" ? "warning" : c.verdict === "fail" ? "failure" : "error";
|
||||
// The forge keeps a short description; the rest is the comment's.
|
||||
let description = `${c.verdict}: ${c.summary}`.replace(/\s+/g, " ").trim();
|
||||
if (description.length > 140) description = description.slice(0, 139) + "…";
|
||||
return { state, context: CHECK_CONTEXT, description };
|
||||
/** 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";
|
||||
}
|
||||
|
||||
/** The comment a verdict that is not a pass leaves on its pull request: the check's own account. */
|
||||
function described(verdict: string, summary: string, modules?: 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(", ")}]`;
|
||||
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) };
|
||||
}
|
||||
|
||||
/** 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 {
|
||||
if (c.verdict === "pass") return null;
|
||||
const head = `**Merge check: ${c.verdict.toUpperCase()}** at \`${c.commit.slice(0, 8)}\` — ${c.summary}`;
|
||||
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(", ")})` : ""));
|
||||
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 head + ran + report;
|
||||
return lines.join("\n") + ran + report;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user