mesh/delivery merged: source.main moved to novox/mesh-catalog@3feeefc70f7c
mesh/check checking 16ea3824b5ea on 3feeefc70f7c
The operator, 2026-10-28: the gates and trunk_rule apply to every trunk, the branches the mesh installs from, and later there may be more than main. The trunks are read at the call from mesh-delivery-gate's source, in one function (trunksOf), and fall back to main, said, only when it names none. A force-push onto any trunk is refused, and trunk_rule says whether the branch asked is a trunk.
251 lines
16 KiB
TypeScript
251 lines
16 KiB
TypeScript
// gitea_branch_protection_set_full (the operator, 2026-10-28: "just make a generic tool so you can setup any
|
|
// configuration"): any field of a branch protection rule, only the fields given, as the forge's administrator; it
|
|
// answers the rule before and after, and whether the controller's trunk rule (judgedRule, mesh-control
|
|
// build_source.go) now holds. Against a fake forge that keeps the rules and answers them to the administrator alone.
|
|
|
|
import { test, after, beforeEach } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { createServer } from "node:http";
|
|
|
|
import { GiteaAdmin, GiteaClient } from "../dist/client.js";
|
|
import { ConfiguredToken } from "../dist/token.js";
|
|
import { getGiteaTools } from "../dist/tools/index.js";
|
|
import { trunkRule } from "../dist/protectfull.js";
|
|
|
|
const adminBasic = "Basic " + Buffer.from("mesh-admin:secret").toString("base64");
|
|
let rules: any[] = [];
|
|
let repoState: any = {};
|
|
const branches = ["master", "main"];
|
|
let sent: { method: string; path: string; body: any }[] = [];
|
|
beforeEach(() => {
|
|
// de-spiegel's main as found: no push, but the operator alone merges and an administrator merges past it.
|
|
rules = [{ rule_name: "main", enable_push: false, enable_merge_whitelist: true, merge_whitelist_usernames: ["jschoubben"],
|
|
enable_status_check: false, status_check_contexts: [], block_admin_merge_override: false, required_approvals: 0,
|
|
enable_push_whitelist: false, push_whitelist_deploy_keys: false, enable_force_push: false, enable_force_push_allowlist: false,
|
|
force_push_allowlist_deploy_keys: false, merge_whitelist_teams: [] }];
|
|
source = {};
|
|
repoState = { full_name: "novox/de-spiegel", default_branch: "master", description: "", has_wiki: true };
|
|
sent = [];
|
|
});
|
|
|
|
// What gitea 1.27.3's EditBranchProtection does with a body (routers/api/v1/repo/branch.go): the push whitelist flags are
|
|
// read only beside enable_push true and cleared by enable_push false, the force-push allowlist flags likewise with
|
|
// enable_force_push; teams are passed over on a repository no organisation owns (de-spiegel here, for the test).
|
|
function giteaEdit(r: any, body: any) {
|
|
const nested = ["enable_push", "enable_push_whitelist", "push_whitelist_deploy_keys", "enable_force_push",
|
|
"enable_force_push_allowlist", "force_push_allowlist_deploy_keys", "merge_whitelist_teams", "push_whitelist_teams"];
|
|
for (const [k, v] of Object.entries(body)) if (!nested.includes(k) && k !== "rule_name") r[k] = v;
|
|
if (body.enable_push === false) Object.assign(r, { enable_push: false, enable_push_whitelist: false, push_whitelist_deploy_keys: false });
|
|
if (body.enable_push === true) {
|
|
r.enable_push = true;
|
|
if (body.enable_push_whitelist !== undefined) r.enable_push_whitelist = body.enable_push_whitelist;
|
|
if (body.push_whitelist_deploy_keys !== undefined) r.push_whitelist_deploy_keys = body.push_whitelist_deploy_keys;
|
|
}
|
|
if (body.enable_force_push === false) Object.assign(r, { enable_force_push: false, enable_force_push_allowlist: false, force_push_allowlist_deploy_keys: false });
|
|
if (body.enable_force_push === true) {
|
|
r.enable_force_push = true;
|
|
if (body.enable_force_push_allowlist !== undefined) r.enable_force_push_allowlist = body.enable_force_push_allowlist;
|
|
if (body.force_push_allowlist_deploy_keys !== undefined) r.force_push_allowlist_deploy_keys = body.force_push_allowlist_deploy_keys;
|
|
}
|
|
}
|
|
|
|
const server = createServer((req, res) => {
|
|
const path = (req.url ?? "").replace(/^\/api\/v1/, "");
|
|
const send = (status: number, body: unknown) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(body)); };
|
|
if (req.headers.authorization !== adminBasic) return send(403, { message: "not an administrator of the repository" });
|
|
let raw = "";
|
|
req.on("data", (c) => (raw += c));
|
|
req.on("end", () => {
|
|
const body = raw ? JSON.parse(raw) : undefined;
|
|
if (req.method !== "GET") sent.push({ method: req.method!, path, body });
|
|
if (path === "/repos/novox/de-spiegel") {
|
|
if (req.method === "GET") return send(200, repoState);
|
|
if (req.method === "PATCH") {
|
|
// gitea's updateBasicProperties: a default branch that does not exist is passed over, the call still 200.
|
|
// enable_prune is a mirror's, kept beside the repository and never in its answer; mirror_interval is answered in Go's
|
|
// duration form.
|
|
const { default_branch, enable_prune, mirror_interval, ...rest } = body;
|
|
Object.assign(repoState, rest);
|
|
if (mirror_interval !== undefined) repoState.mirror_interval = mirror_interval === "8h" ? "8h0m0s" : mirror_interval;
|
|
if (default_branch !== undefined && branches.includes(default_branch)) repoState.default_branch = default_branch;
|
|
return send(200, repoState);
|
|
}
|
|
}
|
|
const m = path.match(/^\/repos\/novox\/de-spiegel\/branch_protections(?:\/(.+))?$/);
|
|
if (!m) return send(404, { message: "not found" });
|
|
if (req.method === "GET" && !m[1]) return send(200, rules);
|
|
if (req.method === "PATCH" && m[1]) {
|
|
const r = rules.find((x) => x.rule_name === decodeURIComponent(m[1]));
|
|
if (!r) return send(404, { message: "no rule" });
|
|
giteaEdit(r, body);
|
|
return send(200, r);
|
|
}
|
|
if (req.method === "POST" && !m[1]) { const r = { rule_name: body.rule_name }; giteaEdit(r, body); rules.push(r); return send(201, r); }
|
|
send(405, { message: "no" });
|
|
});
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, "127.0.0.1", () => r()));
|
|
after(() => new Promise<void>((r) => server.close(() => r())));
|
|
const { port } = server.address() as { port: number };
|
|
const url = `http://127.0.0.1:${port}`;
|
|
// The gate's source as the tools read it (mesh-delivery-gate.source): one key per trunk, each naming the repositories
|
|
// installed from it. A test sets it; empty is a source that names no trunk of de-spiegel.
|
|
let source: Record<string, { heads: Record<string, string> }> = {};
|
|
const readSource = async () => source;
|
|
const tools = getGiteaTools(new GiteaClient(url, new ConfiguredToken("tools-token")), () => new GiteaAdmin(url, "mesh-admin", "secret"), readSource);
|
|
const setRepo = (args: Record<string, unknown>) =>
|
|
tools.find((t) => t.name === "gitea_repo_settings_set_full")!.run({ owner: "novox", repo: "de-spiegel", why: "L2", ...args });
|
|
const setFull = (args: Record<string, unknown>) =>
|
|
tools.find((t) => t.name === "gitea_branch_protection_set_full")!.run({ owner: "novox", repo: "de-spiegel", branch: "main", why: "L2", ...args });
|
|
|
|
test("it sets the merge whitelist and the administrator's override, only the fields given, and says the trunk rule now holds", async () => {
|
|
const out: any = await setFull({ merge_whitelist_usernames: ["mesh-tools", "jschoubben"], block_admin_merge_override: true });
|
|
assert.deepEqual(sent, [{ method: "PATCH", path: "/repos/novox/de-spiegel/branch_protections/main",
|
|
body: { merge_whitelist_usernames: ["mesh-tools", "jschoubben"], block_admin_merge_override: true } }]);
|
|
assert.deepEqual(out.before.merge_whitelist, ["jschoubben"]);
|
|
assert.equal(out.before.admin_may_override, true);
|
|
assert.deepEqual(out.after.merge_whitelist, ["mesh-tools", "jschoubben"]);
|
|
assert.equal(out.after.admin_may_override, false);
|
|
assert.equal(out.why, "L2");
|
|
assert.equal(out.trunk_rule.holds, true, out.trunk_rule.why);
|
|
});
|
|
|
|
test("it loosens as told too, and says the trunk rule no longer holds and why", async () => {
|
|
const out: any = await setFull({ enable_push: true, status_check_contexts: ["mesh/check"], enable_status_check: true, required_approvals: 1 });
|
|
assert.equal(out.after.push, true);
|
|
assert.deepEqual(out.after.required_statuses, ["mesh/check"]);
|
|
assert.equal(out.after.required_approvals, 1);
|
|
assert.equal(out.trunk_rule.holds, false);
|
|
assert.match(out.trunk_rule.why, /push to main directly/);
|
|
});
|
|
|
|
test("a branch with no rule gets one with the fields given", async () => {
|
|
const out: any = await setFull({ branch: "release/*", enable_push: false, protected_file_patterns: "*.lock" });
|
|
assert.equal(out.created, true);
|
|
assert.deepEqual(sent[0], { method: "POST", path: "/repos/novox/de-spiegel/branch_protections",
|
|
body: { rule_name: "release/*", enable_push: false, protected_file_patterns: "*.lock" } });
|
|
});
|
|
|
|
test("an unknown field, a field of the wrong kind, no field, no reason or a login that is none: refused, nothing sent", async () => {
|
|
await assert.rejects(setFull({ enable_pushes: true }), /no field of a branch protection rule/);
|
|
await assert.rejects(setFull({ enable_push: "yes" }), /true or false/);
|
|
await assert.rejects(setFull({ required_approvals: -1 }), /whole number/);
|
|
await assert.rejects(setFull({ merge_whitelist_usernames: ["../x"] }), /no forge login/);
|
|
await assert.rejects(setFull({}), /at least one field/);
|
|
await assert.rejects(setFull({ why: "", enable_push: false }), /say why/);
|
|
assert.deepEqual(sent, []);
|
|
});
|
|
|
|
// judgedRule's cases (mesh-control cmd/mesh-controller/build_source.go), each in its own words.
|
|
test("the trunk rule is judged as the controller judges it", () => {
|
|
const ok = { rule_name: "main", enable_push: false, enable_merge_whitelist: true, merge_whitelist_usernames: ["mesh-tools"], block_admin_merge_override: true };
|
|
const cases: [any[], string, boolean, RegExp][] = [
|
|
[[], "main", false, /no protection rule covers main/],
|
|
[[{ ...ok, enable_push: true }], "main", false, /push to main directly/],
|
|
[[{ ...ok, enable_merge_whitelist: false }], "main", false, /no merge whitelist/],
|
|
[[{ ...ok, merge_whitelist_usernames: ["jschoubben"] }], "main", false, /and not mesh-tools/],
|
|
[[{ ...ok, merge_whitelist_usernames: ["mesh-tools", "agent-g14"] }], "main", false, /agent's account/],
|
|
[[{ ...ok, block_admin_merge_override: false }], "main", false, /administrator/],
|
|
[[ok], "main", true, /takes no push/],
|
|
[[{ ...ok, rule_name: "ma*" }], "main", true, /ma\*/],
|
|
[[{ ...ok, rule_name: "ma*", enable_push: true }, { ...ok }], "main", true, /rule main/],
|
|
];
|
|
for (const [rs, branch, holds, why] of cases) {
|
|
const got = trunkRule(rs, branch);
|
|
assert.equal(got.holds, holds, `${JSON.stringify(rs)}: ${got.why}`);
|
|
assert.match(got.why, why);
|
|
}
|
|
});
|
|
|
|
// Review round 1 of feat/protect-as-trunk: gitea passes over a dependent field without its parent set true, and answers 200.
|
|
test("a field the forge reads only beside its parent is refused without it; with it, it is applied", async () => {
|
|
await assert.rejects(setFull({ enable_push_whitelist: true }), /only when the same call sets enable_push: true/);
|
|
await assert.rejects(setFull({ force_push_allowlist_deploy_keys: true, enable_force_push: false }), /enable_force_push: true/);
|
|
assert.deepEqual(sent, []);
|
|
const out: any = await setFull({ enable_push: true, enable_push_whitelist: true, push_whitelist_usernames: ["jschoubben"] });
|
|
assert.equal(out.after.fields.enable_push_whitelist, true);
|
|
await setFull({ enable_push: false, enable_push_whitelist: false }); // a false beside a false parent is what the forge does
|
|
});
|
|
|
|
test("a field the forge answered and did not apply fails the call, naming it; what it changed beside the call is said", async () => {
|
|
await assert.rejects(setFull({ merge_whitelist_teams: ["owners"] }), /did not apply merge_whitelist_teams \(sent \["owners"\], the forge holds \[\]\)/);
|
|
rules[0].enable_push = true; rules[0].enable_push_whitelist = true;
|
|
const out: any = await setFull({ enable_push: false });
|
|
assert.deepEqual(out.also_changed, ["enable_push_whitelist"]);
|
|
});
|
|
|
|
test("gitea_repo_settings_set_full sets the default branch, and fails loudly on a branch the forge passed over", async () => {
|
|
const out: any = await setRepo({ default_branch: "main" });
|
|
assert.deepEqual(sent.at(-1), { method: "PATCH", path: "/repos/novox/de-spiegel", body: { default_branch: "main" } });
|
|
assert.equal(out.before.default_branch, "master");
|
|
assert.equal(out.after.default_branch, "main");
|
|
await assert.rejects(setRepo({ default_branch: "trunk" }), /did not apply default_branch/);
|
|
await assert.rejects(setRepo({ name: "x" }), /no field of a repository's settings/);
|
|
await assert.rejects(setRepo({ why: "", has_wiki: false }), /say why/);
|
|
});
|
|
|
|
test("every field gitea 1.27.3 edits on a rule is taken: 32, the bypass allowlist among them", async () => {
|
|
const { PROTECTION_FIELDS } = await import("../dist/protectfull.js");
|
|
assert.equal(Object.keys(PROTECTION_FIELDS).length, 32);
|
|
for (const f of ["enable_bypass_allowlist", "bypass_allowlist_usernames", "bypass_allowlist_teams"]) assert.ok(f in PROTECTION_FIELDS, f);
|
|
});
|
|
|
|
// Review round 2: the settings tool's read-back meets the forge's own answer.
|
|
test("A: enable_prune, which the forge never answers, is said as not read back, never failed", async () => {
|
|
const pruned: any = await setRepo({ enable_prune: true });
|
|
assert.deepEqual(pruned.not_read_back, ["enable_prune"]);
|
|
});
|
|
|
|
test("B: mirror_interval is compared as a duration: 8h and the forge's 8h0m0s are one", async () => {
|
|
const mirror: any = await setRepo({ mirror_interval: "8h" });
|
|
assert.equal(mirror.after.mirror_interval, "8h0m0s");
|
|
await assert.rejects(setRepo({ mirror_interval: "often" }), /duration/);
|
|
});
|
|
|
|
test("C: allow_merge_update, default_update_style and has_code are taken; projects_mode needs has_projects; what is left out is named", async () => {
|
|
const more: any = await setRepo({ allow_merge_update: true, default_update_style: "rebase", has_code: true });
|
|
assert.deepEqual(more.set, { allow_merge_update: true, default_update_style: "rebase", has_code: true });
|
|
await assert.rejects(setRepo({ projects_mode: "repo" }), /only when the same call sets has_projects: true/);
|
|
await setRepo({ has_projects: true, projects_mode: "repo" });
|
|
const desc = tools.find((t) => t.name === "gitea_repo_settings_set_full")!.description;
|
|
for (const w of ["name", "mirror_username", "mirror_password", "mirror_token", "tracker", "wiki"]) assert.match(desc, new RegExp(w));
|
|
});
|
|
|
|
// The operator, 2026-10-28 16:4x: a call waits for their Approve only if it removes branch protection or allows a
|
|
// force-push to main. No tool of this module can ask yet (no TypeScript asker; ADR 0286's mark is unbuilt in the
|
|
// runner), so until one can, such a call is refused, with nothing sent, and says it is the operator's; everything else
|
|
// runs at once.
|
|
test("a force-push onto a rule covering main is refused, nothing sent; on another branch, and tightening, it runs", async () => {
|
|
for (const args of [{ enable_force_push: true }, { enable_force_push: true, enable_force_push_allowlist: true },
|
|
{ force_push_allowlist_usernames: ["jschoubben"] }, { force_push_allowlist_teams: ["owners"] },
|
|
{ branch: "ma*", enable_force_push: true }, { branch: "**", force_push_allowlist_usernames: ["x"] }]) {
|
|
await assert.rejects(setFull(args), /force-push to a trunk is the operator's/, JSON.stringify(args));
|
|
}
|
|
assert.deepEqual(sent, []);
|
|
await setFull({ branch: "release/*", enable_force_push: true });
|
|
const tight: any = await setFull({ enable_force_push: false, force_push_allowlist_usernames: [] });
|
|
assert.equal(tight.trunk_rule.holds, false, "trunk_rule is in the answer");
|
|
});
|
|
|
|
// The operator, 2026-10-28 16:5x: the gates and trunk_rule apply to every trunk, the branches the mesh installs from, as
|
|
// the gate's source names them; "main" only when it names none, and said.
|
|
test("a trunk the gate's source names, beta, is gated and judged as main is; main is then no trunk of the repository", async () => {
|
|
source = { main: { heads: { "novox/okioki": "a1" } }, beta: { heads: { "novox/de-spiegel": "b2" } } };
|
|
await assert.rejects(setFull({ branch: "beta", enable_force_push: true }), /force-push to a trunk is the operator's/);
|
|
await assert.rejects(setFull({ branch: "b*", force_push_allowlist_usernames: ["x"] }), /covers beta/);
|
|
assert.deepEqual(sent, []);
|
|
const main: any = await setFull({ enable_force_push: true });
|
|
assert.deepEqual(main.trunks, { names: ["beta"], from: "mesh-delivery-gate.source" });
|
|
assert.equal(main.trunk_rule.is_trunk, false);
|
|
const beta: any = await setFull({ branch: "beta", enable_push: false, enable_merge_whitelist: true, merge_whitelist_usernames: ["mesh-tools"], block_admin_merge_override: true });
|
|
assert.equal(beta.trunk_rule.is_trunk, true);
|
|
assert.equal(beta.trunk_rule.holds, true, beta.trunk_rule.why);
|
|
});
|
|
|
|
test("a source that names no trunk of the repository falls back to main, and says so", async () => {
|
|
const out: any = await setFull({ block_admin_merge_override: true });
|
|
assert.equal(out.trunks.names[0], "main");
|
|
assert.match(out.trunks.from, /names no trunk of novox\/de-spiegel: main assumed/);
|
|
assert.equal(out.trunk_rule.is_trunk, true);
|
|
});
|