Feature 57 (config and provisioning): modules declare config and provisions, their files rendered from templates; synced with main (issues of feature 57, 874)

This commit is contained in:
2026-10-16 07:37:43 +00:00
parent 1a2c917abf
commit 9652b2d5ce
771 changed files with 52063 additions and 1774 deletions
+42 -4
View File
@@ -9,8 +9,9 @@
//
// Pure functions here, so they are tested without a forge; index.ts does the asking and the setting.
import type { CommitStatus, GiteaPull, GiteaRepo } from "./client.js";
import type { CommitStatus, GiteaClient, GiteaPull, GiteaRepo } from "./client.js";
import { movedSince } from "./client.js";
import { UNKNOWN_MARK, besideWords, siblingsOf } from "./siblings.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";
@@ -65,6 +66,13 @@ export interface Checked {
trunk?: string;
beside?: Record<string, string>;
took?: string;
/** The other members of its delivery group the check read beside the head, each at its head merged onto its base
* commit (novox/hq ADR 0382 §4); absent when it read none, and from a controller from before the rule. */
siblings?: { repository: string; number: number; head: string; base: string; base_commit: string }[];
/** Why the siblings could not be learned (mesh-delivery did not answer), when that is so. */
siblings_unknown?: string;
/** Where the judge came from, when it came from the group (ADR 0382 §3): said in the account. */
judge_from?: string;
}
/** A change plan, as the controller says it (mesh-controller internal/link, ChangePlan). */
@@ -163,7 +171,7 @@ function described(verdict: string, summary: string, modules?: string[], depende
return clipped(d);
}
function clipped(d: string): string {
export function clipped(d: string): string {
d = d.replace(/\s+/g, " ").trim();
return d.length > 140 ? d.slice(0, 139) + "…" : d;
}
@@ -176,6 +184,24 @@ export interface RepoCheckFacts {
required?: boolean;
}
// **What only the forge knows of a repository check said as a warning** (novox/hq issue 293): whether the
// head holds a merge-check.sh at all, and — when it does not — whether the base branch's protection
// requires mesh/repo-check. Asked only for a warning; unknown is left undefined, which statusesFor reads
// as possibly required: a failure on a status nothing requires blocks nothing, a success on one that is
// required would let an untested repository merge. The base's protection is the one the forge applies, read
// from the branch: the rules' own list is a repository administrator's, which the tools' account is not
// (novox/hq ADR 0365).
export async function repoCheckFacts(
forge: Pick<GiteaClient, "holdsFile" | "appliedProtection">, c: Checked, base: string | undefined): Promise<RepoCheckFacts> {
if (c["repo-check"]?.verdict !== "warning") return {};
const defined = await forge.holdsFile(c.owner, c.repo, c.commit, "merge-check.sh").catch(() => undefined);
if (defined !== false) return { defined };
const rule = await forge.appliedProtection(c.owner, c.repo, base || "main").catch(() => undefined);
if (rule === undefined) return { defined };
const required = !!rule?.enable_status_check && (rule.status_check_contexts ?? []).includes(REPO_CHECK_CONTEXT);
return { defined, required };
}
/** The description of a required repository check that is not defined. */
export const UNDEFINED_REQUIRED = "fail: mesh/repo-check is required here and this head defines no merge-check.sh — " +
"a person adds one, or lifts the requirement from the branch's protection";
@@ -220,8 +246,12 @@ export function statusesFor(c: Checked, facts: RepoCheckFacts = {}): CommitStatu
* 7a8b9c0d1e2f — pass: …", so the forge's 140 characters never clip it away; the trunk's move is weighed by it. A
* verdict that names no trunk commit is said as before. */
function judgedFirst(c: Checked, status: CommitStatus): CommitStatus {
if (!c.trunk) return status;
return { ...status, description: clipped(`head ${c.commit.slice(0, 12)} merged into trunk ${c.trunk.slice(0, 12)} — ${status.description}`) };
// The siblings it read, named after the commits (ADR 0382 §4), so the clip never takes them either.
const siblings = siblingsOf(c.siblings);
// A verdict that could not learn them says so on the status too (clause 1: it is still checked, and merges).
const mark = siblings.length ? besideWords(siblings) : c.siblings_unknown ? UNKNOWN_MARK : "";
if (!c.trunk) return mark ? { ...status, description: clipped(`${mark} — ${status.description}`) } : status;
return { ...status, description: clipped(`head ${c.commit.slice(0, 12)} merged into trunk ${c.trunk.slice(0, 12)}${mark ? " " + mark : ""} — ${status.description}`) };
}
/** The comment a verdict leaves on its pull request, with the check's own account: the change plan of a
@@ -240,6 +270,14 @@ export function commentFor(c: Checked, facts: RepoCheckFacts = {}): string | nul
lines.splice(1, 0, `Judged: head \`${c.commit.slice(0, 12)}\` merged into trunk \`${c.trunk.slice(0, 12)}\`` +
(beside ? `; read beside it: ${beside}` : ""));
}
const siblings = siblingsOf(c.siblings);
if (siblings.length) {
lines.splice(c.trunk ? 2 : 1, 0, `Siblings read beside it: ` + siblings.map((s) =>
`${s.repository}#${s.number} \`${s.head.slice(0, 12)}\` merged onto \`${s.base_commit.slice(0, 12)}\` (${s.base})`).join(", ") +
(c.judge_from ? `; judged by ${c.judge_from}` : ""));
} else if (c.siblings_unknown) {
lines.splice(c.trunk ? 2 : 1, 0, `Siblings could not be learned: ${c.siblings_unknown}`);
}
lines.push(`- \`${CHECK_CONTEXT}\`: **${saidAs(gate.verdict).toUpperCase()}** — ${gate.summary}` +
(gate.modules?.length ? ` (modules: ${gate.modules.join(", ")}` +
(gate.dependents?.length ? `; built after them: ${gate.dependents.join(", ")}` : "") + ")" : ""));