gitea: the merging account writes every repository that builds a registered module, read from the indexer's modules (issue 1945)
car-hunter #2 was approved and its commit status refused 403: mesh-tools only read the repositories of a user. It is now a write collaborator on each repository the indexer's modules state names (watched over the bus: at the start and on each registration), read back and said as gitea.repo.writable; the operator's other repositories stay read-only. gitea_grant_write and gitea_unwritable_repos do and show it by hand; gitea_commit_statuses answers merge.writes.
This commit is contained in:
@@ -206,7 +206,10 @@ function fakeForge() {
|
||||
if (m) {
|
||||
const repo = forgeRepos.find((r) => r.owner === decodeURIComponent(m![1]) && r.name === decodeURIComponent(m![2]));
|
||||
if (!repo || permissionOf(me, repo) === "none") return json(res, 404, { message: "repository does not exist" });
|
||||
if (req.method === "GET" && !m[3]) return json(res, 200, repoJSON(repo));
|
||||
if (req.method === "GET" && !m[3]) {
|
||||
const mine = permissionOf(me, repo);
|
||||
return json(res, 200, { ...repoJSON(repo), permissions: { pull: true, push: ["write", "admin", "owner"].includes(mine), admin: ["admin", "owner"].includes(mine) } });
|
||||
}
|
||||
const user = decodeURIComponent(m[3] ?? "");
|
||||
if (req.method === "GET" && m[4]) {
|
||||
if (repoFaults.permission.has(repo.full)) return json(res, 500, { message: "the forge is not well" });
|
||||
@@ -576,3 +579,67 @@ test("on the forge's events: a repository made is granted read at once, a known
|
||||
repoFaults.refuse.clear();
|
||||
repoFaults.permission.clear();
|
||||
});
|
||||
|
||||
// The merging account writes every repository that builds a registered module, and no other (writes.ts; the operator,
|
||||
// 2026-10-26; issue 1945): car-hunter #2 was approved and its commit status refused 403, mesh-tools reading only there.
|
||||
|
||||
test("the merging account is given write on each repository the indexer's modules name, read back and said; the operator's other repositories stay read-only; the tools say both", async () => {
|
||||
const env = await environment();
|
||||
const client = fresh(env);
|
||||
await client.listRepos(); // the account and its team exist, as after any mint
|
||||
forgeRepos.splice(0);
|
||||
addRepo("novox/hq", true);
|
||||
for (const full of ["jschoubben/car-hunter", "jschoubben/snake", "jschoubben/dotfiles"]) addRepo(full, true).collaborators.set(TOOLS_ACCOUNT, "read");
|
||||
const { GiteaAdmin } = await import("../dist/client.js");
|
||||
const { DeliveredWrites, writeTools, WRITABLE_EVENT } = await import("../dist/writes.js");
|
||||
const lines: string[] = [];
|
||||
const said: { type: string; body: any }[] = [];
|
||||
const writes = new DeliveredWrites(GiteaAdmin.fromEnv(env), TOOLS_ACCOUNT, "novox", (l: string) => lines.push(l),
|
||||
async (type: string, body: unknown) => { said.push({ type, body }); });
|
||||
const [unwritableTool, grantTool] = writeTools(() => writes);
|
||||
await assert.rejects(grantTool.run({}), /not known yet/, "nothing is granted before the indexer's modules were read");
|
||||
assert.equal((await client.getRepo("jschoubben", "car-hunter")).writes, false, "the statuses' answer says the account cannot write");
|
||||
|
||||
// 1945/AC1: the watch hands the indexer's modules; a user's repository a module builds from is made writable, once.
|
||||
await writes.take({ key: "car-hunter", op: "put", value: { v: 1, repository: "jschoubben/car-hunter", commit: "c" }, current: true });
|
||||
await writes.take({ key: "gitea", op: "put", value: { v: 1, repository: "novox/hq", commit: "c" }, current: true });
|
||||
await writes.take({ key: "broken", op: "put", value: { v: 1, repository: "no-slash" }, current: true });
|
||||
const from = forge.calls.length;
|
||||
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), "write");
|
||||
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 } }]);
|
||||
assert.match(lines.join("\n"), /mesh-tools was given write on jschoubben\/car-hunter, which builds the registered module car-hunter/);
|
||||
assert.match(lines.join("\n"), /broken names no repository/);
|
||||
|
||||
// 1945/AC2: a delivered repository the forge refuses is said, in its words, by the listing and the grant; the operator's
|
||||
// repository no module builds from is left read-only, named with why, and a grant naming it asks the forge nothing.
|
||||
repoFaults.refuse.add("jschoubben/snake");
|
||||
await writes.take({ key: "snake", op: "put", value: { v: 1, repository: "jschoubben/snake" } });
|
||||
assert.match(lines.join("\n"), /could not be given write on jschoubben\/snake, which builds snake — .*422/);
|
||||
const listed = await unwritableTool.run({}) as any;
|
||||
assert.deepEqual(listed.unwritable.map((u: any) => [u.repository, u.permission]), [["jschoubben/snake", "read"]]);
|
||||
assert.match(listed.unwritable[0].why, /builds snake.*gitea_grant_write/);
|
||||
const all = await grantTool.run({}) as any;
|
||||
assert.deepEqual(all.delivered, ["jschoubben/car-hunter", "jschoubben/snake", "novox/hq"]);
|
||||
assert.deepEqual(all.already, ["jschoubben/car-hunter", "novox/hq"]);
|
||||
assert.deepEqual(all.granted, []);
|
||||
assert.match(all.refused.find((x: any) => x.repository === "jschoubben/snake").why, /422.*would not have it/);
|
||||
assert.deepEqual(all.left_read_only.map((x: any) => x.repository), ["jschoubben/dotfiles"]);
|
||||
assert.match(all.left_read_only[0].why, /builds no registered module/);
|
||||
const puts = forge.calls.length;
|
||||
const named = await grantTool.run({ repositories: ["jschoubben/dotfiles"] }) as any;
|
||||
assert.deepEqual([named.granted, named.left_read_only.map((x: any) => x.repository)], [[], ["jschoubben/dotfiles"]]);
|
||||
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();
|
||||
const after = await grantTool.run({ repositories: ["jschoubben/snake"] }) as any;
|
||||
assert.deepEqual(after.granted, ["jschoubben/snake"]);
|
||||
assert.deepEqual(((await unwritableTool.run({})) as any).unwritable, []);
|
||||
// A module gone from the indexer leaves its repository out of the delivered list.
|
||||
await writes.take({ key: "snake", op: "delete" });
|
||||
assert.deepEqual(writes.delivered(), ["jschoubben/car-hunter", "novox/hq"]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user