gitea: mesh-tools writes a user's delivered repository and main's rule stays the operator's: only its merge whitelist gets mesh-tools, read back; what else blocks the mesh's merge is said (issue 1945, the operator's answer 2026-10-26 18:30)

This commit is contained in:
2026-10-26 18:34:03 +01:00
parent c85472285d
commit e1e5a8ff0d
3 changed files with 124 additions and 85 deletions
+22 -18
View File
@@ -628,9 +628,8 @@ test("the merging account is given write on each repository the indexer's module
await writes.take({ key: "car-hunter-2", op: "put", value: { v: 1, repository: "jschoubben/car-hunter" } });
assert.equal(forge.calls.length, from, "a repository already written asks nothing more");
const repo = (full: string) => forgeRepos.find((x) => x.full === full)!;
assert.equal(repo("jschoubben/car-hunter").collaborators.get(TOOLS_ACCOUNT), "admin", "a user's repository is administered, not only written");
assert.deepEqual(userRules.get("jschoubben/car-hunter"), { rule_name: "main", enable_push: false, enable_merge_whitelist: true,
merge_whitelist_usernames: [TOOLS_ACCOUNT], enable_status_check: false, status_check_contexts: [], block_admin_merge_override: true });
assert.equal(repo("jschoubben/car-hunter").collaborators.get(TOOLS_ACCOUNT), "write", "a user's repository is written, not administered");
assert.equal(userRules.has("jschoubben/car-hunter"), false, "a repository with no rule for main is given none");
assert.equal(repo("novox/hq").collaborators.size, 0, "the organisation's repository is written through its team, no collaborator");
assert.equal((await client.getRepo("jschoubben", "car-hunter")).writes, true);
assert.deepEqual(said, [{ type: WRITABLE_EVENT, body: { owner: "jschoubben", repo: "car-hunter", account: TOOLS_ACCOUNT } }]);
@@ -658,23 +657,28 @@ test("the merging account is given write on each repository the indexer's module
assert.ok(!forge.calls.slice(puts).some((c) => c.startsWith("PUT ")), "no write is asked on a repository no module builds from");
assert.equal(repo("jschoubben/dotfiles").collaborators.get(TOOLS_ACCOUNT), "read");
repoFaults.refuse.clear();
// A stronger rule the operator set (a whitelist of the tools' account alone, two approvals) is kept, never weakened.
userRules.set("jschoubben/snake", { rule_name: "main", enable_push: true, enable_merge_whitelist: true,
merge_whitelist_usernames: [TOOLS_ACCOUNT], required_approvals: 2, block_admin_merge_override: false });
// The operator's rule is left as set (the operator, 2026-10-26: "Write, rule untouched"): only mesh-tools is added to its
// merge whitelist; a status the mesh does not set and required approvals are said as blocking the mesh's merge, unchanged.
const operators = { rule_name: "main", enable_push: true, enable_merge_whitelist: true, merge_whitelist_usernames: ["jschoubben"],
enable_status_check: true, status_check_contexts: ["mesh/merge-gate", "ci/external"], required_approvals: 2, block_admin_merge_override: false };
userRules.set("jschoubben/snake", structuredClone(operators));
const after = await grantTool.run({ repositories: ["jschoubben/snake"] }) as any;
assert.deepEqual(after.granted, ["jschoubben/snake"]);
assert.deepEqual(after.main_rule.map((x: any) => [x.repository, x.created, x.rule.enable_push, x.rule.required_approvals, x.rule.block_admin_merge_override]),
[["jschoubben/snake", false, false, 2, true]]);
assert.equal(repo("jschoubben/snake").collaborators.get(TOOLS_ACCOUNT), "admin");
assert.deepEqual(((await unwritableTool.run({})) as any).unwritable, []);
// The forge drops a whitelist login that cannot write and still answers 200: main's rule is read back and that fails loud.
const admin = GiteaAdmin.fromEnv(env);
await assert.rejects(admin.ensureMainRule("jschoubben", "dotfiles"),
/main's rule of jschoubben\/dotfiles was written, but the forge holds otherwise: the merge whitelist \[\] leaves out mesh-tools/);
userRules.get("jschoubben/snake").enable_status_check = true;
userRules.get("jschoubben/snake").enable_push = true;
const held = await admin.ensureMainRule("jschoubben", "snake");
assert.deepEqual([held.rule.enable_push, held.rule.enable_status_check, held.rule.merge_whitelist_usernames], [false, false, [TOOLS_ACCOUNT]]);
assert.equal(repo("jschoubben/snake").collaborators.get(TOOLS_ACCOUNT), "write");
assert.deepEqual(userRules.get("jschoubben/snake"), { ...operators, merge_whitelist_usernames: ["jschoubben", TOOLS_ACCOUNT] }, "only the whitelist changed");
assert.deepEqual(after.merge_whitelist, [{ repository: "jschoubben/snake", changed: true, listed: ["jschoubben", TOOLS_ACCOUNT] }]);
const blockedWhy = "writable, but main's rule blocks the mesh's merge: it requires the status ci/external, which the mesh does not set; " +
"it requires 2 approving reviews on the forge, which the gate does not give";
assert.deepEqual(after.merge_blocked, [{ repository: "jschoubben/snake", why: blockedWhy }]);
const again = await unwritableTool.run({}) as any;
assert.deepEqual([again.unwritable, again.merge_blocked], [[], [{ repository: "jschoubben/snake", why: blockedWhy }]]);
const writesBefore = forge.calls.filter((c) => /^PATCH .*branch_protections/.test(c)).length;
assert.deepEqual(((await grantTool.run({ repositories: ["jschoubben/snake"] })) as any).merge_whitelist[0].changed, false);
assert.equal(forge.calls.filter((c) => /^PATCH .*branch_protections/.test(c)).length, writesBefore, "a listed account is not written again");
// The forge drops a whitelist login that cannot write and still answers 200: the whitelist is read back and that fails loud.
userRules.set("jschoubben/dotfiles", { rule_name: "main", enable_merge_whitelist: true, merge_whitelist_usernames: ["jschoubben"] });
await assert.rejects(GiteaAdmin.fromEnv(env).ensureMergerListed("jschoubben", "dotfiles", TOOLS_ACCOUNT),
/mesh-tools was added to the merge whitelist of main's rule main of jschoubben\/dotfiles, but the forge holds \[jschoubben\]: it drops a login that cannot write/);
userRules.delete("jschoubben/dotfiles");
// A module gone from the indexer leaves its repository out of the delivered list.
await writes.take({ key: "snake", op: "delete" });