Main takes no push and only mesh-tools merges, set by setup and guarded (issue 948, ADR 0398 §2)

This commit is contained in:
fix-forge-awg
2026-10-16 21:28:43 +02:00
parent b0f86671b6
commit 7807135a1b
10 changed files with 187 additions and 12 deletions
+18
View File
@@ -491,6 +491,24 @@ test("the judges, without a forge", () => {
assert.equal(judgeProtectionChange(protectedMain(), { statusChecks: ["mesh/repo-check", "mesh/merge-gate"] }), null);
});
test("main's protection is never weakened in its push or its merge whitelist (issue 948, ADR 0398 §2)", () => {
const strong = () => ({ ...protectedMain(), enable_merge_whitelist: true, merge_whitelist_usernames: ["mesh-tools"] });
const checks = ["mesh/merge-gate", "mesh/repo-check"];
assert.equal(judgeProtectionChange(strong(), { statusChecks: checks }, [], "main"), null);
assert.equal(judgeProtectionChange(strong(), { statusChecks: checks, mergeWhitelist: ["mesh-tools"] }, [], "main"), null);
assert.match(judgeProtectionChange(strong(), { statusChecks: checks, mergeWhitelist: [] }, [], "main") ?? "", /merge whitelist/);
assert.match(judgeProtectionChange(strong(), { statusChecks: checks, mergeWhitelist: null }, [], "main") ?? "", /merge whitelist/);
assert.match(judgeProtectionChange(strong(), { statusChecks: checks, mergeWhitelist: ["mesh-tools", "agent-ace"] }, [], "main") ?? "", /widens/);
assert.match(judgeProtectionChange(strong(), { statusChecks: checks, mergeWhitelist: ["agent-ace"] }, [], "main") ?? "", /widens/);
assert.match(judgeProtectionChange(strong(), { statusChecks: checks, push: true }, [], "main") ?? "", /push/);
// Even a rule that lets a person push already, or has no rule yet, is not given a push through a tool.
assert.match(judgeProtectionChange({ ...strong(), enable_push: true }, { statusChecks: checks, push: true }, [], "main") ?? "", /push/);
assert.match(judgeProtectionChange(null, { statusChecks: checks, push: true }, [], "main") ?? "", /push/);
assert.match(judgeProtectionChange(null, { statusChecks: checks, mergeWhitelist: ["agent-ace"] }, [], "main") ?? "", /widens/);
// Another branch is judged as before.
assert.equal(judgeProtectionChange(null, { statusChecks: checks, push: true }, [], "feat/x"), null);
});
test("gitea_api never acts as another user: sudo, in any case, and any query key it does not send are refused", async () => {
for (const path of ["/user?sudo=jschoubben", "/user?SUDO=jschoubben", "/repos/novox/mesh-catalog/issues?state=open&Sudo=jschoubben",
"/repos/novox/mesh-catalog/issues?token=abc", "/repos/novox/mesh-catalog/issues?access_token=abc"]) {
+30 -2
View File
@@ -5,7 +5,7 @@ import { test } from "node:test";
// 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");
const { protectionBody } = await import("../dist/protection.js");
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");
@@ -13,9 +13,37 @@ test("the statuses asked for replace the rule's, and an empty list requires none
});
test("a new rule refuses direct pushes unless asked; an administrator's override only when said", async () => {
const { protectionBody } = await import("../protection.ts");
const { protectionBody } = await import("../dist/protection.js");
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);
});
// Issue 948 (novox/hq ADR 0398 §2): main takes no push, and only the tools' account merges.
test("main's body always refuses pushes and lets only the tools' account merge, whatever is asked", async () => {
const { protectionBody } = await import("../dist/protection.js");
const { TOOLS_ACCOUNT } = await import("../dist/tools-account.js");
for (const creating of [true, false]) {
for (const want of [{ statusChecks: ["mesh/merge-gate"] }, { statusChecks: [], push: true }, { statusChecks: [], push: false }]) {
const body = protectionBody(want, creating, "main");
assert.equal(body.enable_push, false, "main takes no push, an edit and a push=true included");
assert.equal(body.enable_merge_whitelist, true);
assert.deepEqual(body.merge_whitelist_usernames, [TOOLS_ACCOUNT]);
}
}
assert.deepEqual(TOOLS_ACCOUNT, "mesh-tools");
});
test("another branch's body carries no merge whitelist", async () => {
const { protectionBody } = await import("../dist/protection.js");
const body = protectionBody({ statusChecks: ["mesh/merge-gate"] }, false, "release/1");
assert.equal(body.enable_merge_whitelist, undefined);
assert.equal(body.merge_whitelist_usernames, undefined);
assert.equal(protectionBody({ statusChecks: [] }, false).enable_merge_whitelist, undefined);
});
test("the rule main is put right in the forge's own words: the body a setup sends carries no status checks of its own", async () => {
const { mainRuleBody } = await import("../dist/protection.js");
assert.deepEqual(mainRuleBody(), { enable_push: false, enable_merge_whitelist: true, merge_whitelist_usernames: ["mesh-tools"] });
});
+2
View File
@@ -204,6 +204,8 @@ function fakeForge(): Promise<Forge> {
if (req.method === "GET" && p === "/api/v1/orgs/novox/teams") return json(res, 200, [{ id: 7, name: "mesh-tools" }]);
if (req.method === "PATCH" && p === "/api/v1/teams/7") return json(res, 200, { id: 7 });
if (req.method === "PUT" && p === "/api/v1/teams/7/members/mesh-tools") return json(res, 204, null);
// Main's rule put right in every repository (issue 948): this forge has none.
if (req.method === "GET" && p === "/api/v1/orgs/novox/repos") return json(res, 200, []);
}
const adminUser = url.pathname.match(/^\/api\/v1\/admin\/users\/([^/]+)$/);
if (adminUser && req.method === "PATCH") {
+40 -3
View File
@@ -31,6 +31,10 @@ function fakeForge() {
const tokens: Token[] = [];
const teams: Team[] = [];
const calls: string[] = [];
// The organisation's repositories, and the rule each has for main, which the administrator's setup puts right
// (issue 948, ADR 0398 §2).
const repos = ["hq", "mesh-catalog"];
const mainRules = new Map<string, any>([["hq", { rule_name: "main", enable_status_check: true, status_check_contexts: ["mesh/merge-gate"] }]]);
// What the forge does wrong when a test says so: a token mint or delete it fails, a team another process made
// between this one's look and its create (gitea answers the create 422).
const faults = { mint: false, deleteToken: false, teamRace: false };
@@ -131,6 +135,10 @@ function fakeForge() {
if (req.method === "PATCH") { t.units = await read(req); return json(res, 200, { id: t.id }); }
if (req.method === "PUT" && m[2]) { t.members.add(decodeURIComponent(m[2])); return json(res, 204, null); }
}
if (req.method === "GET" && p === "/orgs/novox/repos") {
if (!isAdmin(me)) return json(res, 403, { message: "must be an owner of the organisation" });
return json(res, 200, url.searchParams.get("page") === "1" ? repos.map((name) => ({ name })) : []);
}
// Repository administration: a site administrator, or a team member whose team administers the repository.
m = p.match(/^\/repos\/novox\/([^/]+)\/branches\/(.+)$/);
if (m && req.method === "GET") {
@@ -140,10 +148,21 @@ function fakeForge() {
return json(res, 404, { message: "branch does not exist" });
}
// gitea's reqAdmin on the whole group: reading the rules is administration too.
m = p.match(/^\/repos\/novox\/([^/]+)\/branch_protections(?:\/.*)?$/);
m = p.match(/^\/repos\/novox\/([^/]+)\/branch_protections(?:\/(.*))?$/);
if (m) {
const admins = isAdmin(me) || teams.some((t) => t.members.has(me) && t.units.permission === "admin");
if (!admins) return json(res, 403, { message: "user should be an owner or a collaborator with admin write of a repository" });
if (req.method === "PATCH" && m[2] === "main") {
const rule = mainRules.get(m[1]);
if (!rule) return json(res, 404, { message: "branch protection does not exist" });
Object.assign(rule, await read(req));
return json(res, 200, rule);
}
if (req.method === "POST" && p.endsWith("/branch_protections")) {
const b = await read(req);
mainRules.set(m[1], b);
return json(res, 201, b);
}
return json(res, 201, { rule_name: "main" });
}
m = p.match(/^\/repos\/novox\/([^/]+)\/statuses\/([0-9a-f]+)$/);
@@ -155,11 +174,11 @@ function fakeForge() {
if (req.method === "GET" && p === "/repos/search") return json(res, 200, { data: [] });
return json(res, 404, { message: "no such route in the fake" });
});
return new Promise<{ url: string; users: typeof users; tokens: typeof tokens; teams: typeof teams; calls: string[];
return new Promise<{ url: string; users: typeof users; tokens: typeof tokens; teams: typeof teams; calls: string[]; mainRules: typeof mainRules;
faults: typeof faults; close(): Promise<void> }>((resolve) => {
server.listen(0, "127.0.0.1", () => {
const { port } = server.address() as { port: number };
resolve({ url: `http://127.0.0.1:${port}`, users, tokens, teams, calls, faults,
resolve({ url: `http://127.0.0.1:${port}`, users, tokens, teams, calls, faults, mainRules,
close: () => new Promise((r) => server.close(() => r())) });
});
});
@@ -330,3 +349,21 @@ test("a team another process made between the look and the create is found, not
const team = forge.teams.find((t) => t.name === "mesh-tools");
assert.ok(team?.members.has(TOOLS_ACCOUNT));
});
test("setting the tools' account up puts main's rule right in every repository: no push, only the tools' account merges (issue 948)", async () => {
const env = await environment();
const from = forge.calls.length;
await fresh(env).listRepos();
const hq = forge.mainRules.get("hq");
assert.equal(hq.enable_push, false);
assert.equal(hq.enable_merge_whitelist, true);
assert.deepEqual(hq.merge_whitelist_usernames, [TOOLS_ACCOUNT]);
assert.deepEqual(hq.status_check_contexts, ["mesh/merge-gate"], "a rule found keeps the statuses it requires");
const catalog = forge.mainRules.get("mesh-catalog");
assert.ok(catalog, "a repository with no rule for main is given one");
assert.equal(catalog.rule_name, "main");
assert.equal(catalog.enable_push, false);
assert.deepEqual(catalog.merge_whitelist_usernames, [TOOLS_ACCOUNT]);
assert.ok(forge.calls.slice(from).filter((c) => /branch_protections/.test(c)).every((c) => c.endsWith(` as ${ADMIN}`)),
"only the administrator's login touches a rule: the tools' account is no repository's administrator");
});