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:
@@ -0,0 +1,21 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
// A branch's protection (novox/hq ADR 0237): what the operator's agent sets so a pull request waits for the
|
||||
// mesh's merge check before it merges.
|
||||
|
||||
test("the statuses asked for replace the rule's, and an empty list requires none", async () => {
|
||||
const { protectionBody } = await import("../protection.ts");
|
||||
assert.deepEqual(protectionBody({ statusChecks: ["mesh/merge-gate", " mesh/repo-check", "", "mesh/merge-gate"] }, false),
|
||||
{ enable_status_check: true, status_check_contexts: ["mesh/merge-gate", "mesh/repo-check"] },
|
||||
"an edited rule keeps its pushes as they are, and each status once");
|
||||
assert.deepEqual(protectionBody({ statusChecks: [] }, false), { enable_status_check: false, status_check_contexts: [] });
|
||||
});
|
||||
|
||||
test("a new rule refuses direct pushes unless asked; an administrator's override only when said", async () => {
|
||||
const { protectionBody } = await import("../protection.ts");
|
||||
assert.equal(protectionBody({ statusChecks: ["mesh/merge-gate"] }, true).enable_push, false);
|
||||
assert.equal(protectionBody({ statusChecks: ["mesh/merge-gate"], push: true }, true).enable_push, true);
|
||||
assert.equal(protectionBody({ statusChecks: ["mesh/merge-gate"] }, true).block_admin_merge_override, undefined);
|
||||
assert.equal(protectionBody({ statusChecks: ["mesh/merge-gate"], blockAdminOverride: true }, false).block_admin_merge_override, true);
|
||||
});
|
||||
@@ -30,12 +30,38 @@ test("a verdict is the head commit's status; an error is the forge's error, neve
|
||||
assert.ok(long.description.length <= 140 && long.context === CHECK_CONTEXT);
|
||||
assert.equal(commentFor({ ...base, verdict: "pass", summary: "fine" }), null, "a pass leaves no comment");
|
||||
const said = commentFor({ ...base, verdict: "fail", summary: "lemurs refused", report: "fails:\n - ```x```" }) ?? "";
|
||||
assert.match(said, /Merge check: FAIL/);
|
||||
assert.match(said, /mesh\/merge-gate`: \*\*FAIL\*\*/);
|
||||
assert.match(said, /01234567/);
|
||||
assert.match(said, /builds --log build-1/);
|
||||
assert.ok(!said.slice(said.indexOf("```") + 3, said.lastIndexOf("```")).includes("```"), "the report cannot close its own block");
|
||||
});
|
||||
|
||||
test("each layer is its own status: the gate with the modules it judged, the repository's own check beside it", async () => {
|
||||
const { statusesFor, commentFor, CHECK_CONTEXT, REPO_CHECK_CONTEXT } = await import("../pulls.ts");
|
||||
const base = { owner: "novox", repo: "mesh-catalog", number: 7, commit: "0123456789abcdef", id: "build-1" };
|
||||
const both = statusesFor({ ...base, verdict: "pass", summary: "every machine composes",
|
||||
gate: { verdict: "pass", summary: "every machine composes", modules: ["gitea", "keycloak"] },
|
||||
"repo-check": { verdict: "fail", summary: "its merge-check.sh failed: FAIL x" } });
|
||||
assert.deepEqual(both.map((s) => [s.context, s.state]), [[CHECK_CONTEXT, "success"], [REPO_CHECK_CONTEXT, "failure"]]);
|
||||
assert.match(both[0].description, /gitea, keycloak/);
|
||||
assert.match(commentFor({ ...base, verdict: "pass", summary: "", gate: { verdict: "pass", summary: "" },
|
||||
"repo-check": { verdict: "fail", summary: "its merge-check.sh failed" } }) ?? "", /mesh\/repo-check`: \*\*FAIL/);
|
||||
|
||||
// Nothing of the graph touched, no script: a pass and a warning, and no comment on every push.
|
||||
const quiet = { ...base, verdict: "pass", summary: "the change touches no module of the mesh's graph",
|
||||
gate: { verdict: "pass", summary: "the change touches no module of the mesh's graph" },
|
||||
"repo-check": { verdict: "warning", summary: "the repository declares no merge-check.sh" } };
|
||||
assert.deepEqual(statusesFor(quiet).map((s) => s.state), ["success", "warning"]);
|
||||
assert.equal(commentFor(quiet), null);
|
||||
|
||||
// A repository outside the mesh, touching nothing: the gate alone, a pass.
|
||||
assert.equal(statusesFor({ ...base, verdict: "pass", summary: "x", gate: { verdict: "pass", summary: "x" } }).length, 1);
|
||||
|
||||
// A controller from before the layers: its verdict is the gate's.
|
||||
assert.deepEqual(statusesFor({ ...base, verdict: "warning", summary: "wide" }).map((s) => [s.context, s.state]),
|
||||
[[CHECK_CONTEXT, "warning"]]);
|
||||
});
|
||||
|
||||
test("a commit status is set on the commit, under the merge check's context", async () => {
|
||||
const { GiteaClient } = await import("../client.ts");
|
||||
let seen: { path: string; body: any } | null = null;
|
||||
|
||||
Reference in New Issue
Block a user