From 68dfca1057940ab6dc58289eeff5e8bdaa8fa810 Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 23 Oct 2026 15:57:44 +0200 Subject: [PATCH] Gate the token mint's main-rule write by keep-main-rules too The operator, 2026-10-23: "disable the gitea branch protection reset thing". setUpToolsAccount (run at every token mint) called ensureMainMerges regardless; it now does so only when the setting is true. --- modules/gitea/test/mainrules.test.ts | 21 +++++++++++++++++++++ modules/gitea/test/tools-account.test.ts | 17 ++++++++++++++--- modules/gitea/token.ts | 4 +++- modules/gitea/tools-account.ts | 7 +++++-- 4 files changed, 43 insertions(+), 6 deletions(-) diff --git a/modules/gitea/test/mainrules.test.ts b/modules/gitea/test/mainrules.test.ts index 2a2b18a1..37916cd7 100644 --- a/modules/gitea/test/mainrules.test.ts +++ b/modules/gitea/test/mainrules.test.ts @@ -41,3 +41,24 @@ test("true: every step puts the rules right", async () => { assert.equal(writes, 2); assert.deepEqual(said, ["[gitea] main's rule is right in 2 repositories"]); }); + +test("a token mint's account setup writes no protection while keep-main-rules is false, and does when true", async () => { + const { setUpToolsAccount } = await import("../dist/tools-account.js"); + for (const [keep, want] of [[false, 0], [true, 1]] as const) { + let merges = 0; + const forge = { + ensureUser: async () => {}, ensureTeamWith: async () => 1, addUserToTeam: async () => {}, + ensureMainMerges: async () => { merges++; return []; }, + }; + await setUpToolsAccount(forge, "mesh-tools", () => "pw", keep); + assert.equal(merges, want); + } + // The default comes from the setting, which is false when the config does not say true. + let merges = 0; + const saved = process.env.MESH_GITEA_CONFIG_FILE; + delete process.env.MESH_GITEA_CONFIG_FILE; + try { + await setUpToolsAccount({ ensureUser: async () => {}, ensureTeamWith: async () => 1, addUserToTeam: async () => {}, ensureMainMerges: async () => { merges++; return []; } }, "mesh-tools", () => "pw"); + } finally { if (saved !== undefined) process.env.MESH_GITEA_CONFIG_FILE = saved; } + assert.equal(merges, 0); +}); diff --git a/modules/gitea/test/tools-account.test.ts b/modules/gitea/test/tools-account.test.ts index cf30a57a..6c9b3552 100644 --- a/modules/gitea/test/tools-account.test.ts +++ b/modules/gitea/test/tools-account.test.ts @@ -192,11 +192,14 @@ function fakeForge() { const forge = await fakeForge(); after(() => forge.close()); -async function environment(): Promise { +async function environment(keepMainRules = false): Promise { const dir = await mkdtemp(join(tmpdir(), "gitea-tools-account-")); const passwordFile = join(dir, "admin.secret"); await writeFile(passwordFile, PASSWORD + "\n", { mode: 0o600 }); + const config = join(dir, "config.json"); + await writeFile(config, JSON.stringify({ "keep-main-rules": keepMainRules })); return { + MESH_GITEA_CONFIG_FILE: config, MESH_GITEA_URL: forge.url, MESH_GITEA_ADMIN_USER: ADMIN, MESH_GITEA_ADMIN_PASSWORD_FILE: passwordFile, @@ -351,7 +354,7 @@ test("a team another process made between the look and the create is found, not }); 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 env = await environment(true); const from = forge.calls.length; await fresh(env).listRepos(); const hq = forge.mainRules.get("hq"); @@ -369,7 +372,7 @@ test("setting the tools' account up puts main's rule right in every repository: }); test("a glob rule covering main keeps its statuses when main is given a rule of its own name (issue 948, review round 1)", async () => { - const env = await environment(); + const env = await environment(true); await fresh(env).listRepos(); const rule = forge.mainRules.get("globbed"); assert.ok(rule, "main is given its own rule"); @@ -390,3 +393,11 @@ test("a start with the token kept still puts main's rules right: the module's st await GiteaAdmin.fromEnv(env).ensureMainMerges("novox"); assert.equal(forge.mainRules.get("hq").enable_push, false, "a rule gone is made again with no token minted"); }); + +test("a mint with keep-main-rules false (the default) writes no branch protection", async () => { + const env = await environment(); + const from = forge.calls.length; + await fresh(env).listRepos(); + const writes = forge.calls.slice(from).filter((c: string) => /branch_protections/.test(c) && !/^GET /.test(c) && !/^(GET|HEAD) /.test(c)); + assert.deepEqual(writes, []); +}); diff --git a/modules/gitea/token.ts b/modules/gitea/token.ts index 8d7f7afd..48592f33 100644 --- a/modules/gitea/token.ts +++ b/modules/gitea/token.ts @@ -39,6 +39,8 @@ import { dirname, join } from "node:path"; import { setUpToolsAccount, TOOLS_ACCOUNT, type AdminCall } from "./tools-account.js"; /** The name the token carries in the forge's own list — one per mesh runtime, found by name. */ +import { keepMainRules } from "./mainrules.js"; + export const TOKEN_NAME = "mesh-tools"; /** @@ -181,7 +183,7 @@ export class MintedToken implements TokenSource { // GiteaAdmin is loaded when first used: client.ts imports this file. setUp: async (password) => { const { GiteaAdmin } = await import("./client.js"); - await setUpToolsAccount(new GiteaAdmin(url, admin!, password), account, () => randomBytes(24).toString("base64url")); + await setUpToolsAccount(new GiteaAdmin(url, admin!, password), account, () => randomBytes(24).toString("base64url"), keepMainRules(env)); }, }; } diff --git a/modules/gitea/tools-account.ts b/modules/gitea/tools-account.ts index 087e49d0..8fdd6956 100644 --- a/modules/gitea/tools-account.ts +++ b/modules/gitea/tools-account.ts @@ -19,6 +19,8 @@ // or a membership lost behind the module's back is made again at the next mint. /** The account the tools act as. */ +import { keepMainRules } from "./mainrules.js"; + export const TOOLS_ACCOUNT = "mesh-tools"; /** The organisation whose repositories are the mesh's, and the team through which the tools' account writes there. */ export const TOOLS_ORG = "novox"; @@ -47,11 +49,12 @@ export interface ToolsForge { /** Make the tools' account, plain, a member of its team on every repository of the organisation. Its password is * set afresh each time and kept nowhere: the account is reached only by the tokens the administrator mints. */ -export async function setUpToolsAccount(forge: ToolsForge, account = TOOLS_ACCOUNT, password: () => string): Promise { +export async function setUpToolsAccount(forge: ToolsForge, account = TOOLS_ACCOUNT, password: () => string, keepRules: boolean = keepMainRules()): Promise { await forge.ensureUser(account, password(), `${account}@noreply.localhost`, true); const team = await forge.ensureTeamWith(TOOLS_ORG, TOOLS_TEAM, { name: TOOLS_TEAM, ...TOOLS_TEAM_UNITS }); await forge.addUserToTeam(team, account); // Main takes no push and only this account merges into it (novox/hq ADR 0398 §2, issue 948): put right in every // repository now, by the administrator's login, which the account itself is not. - await forge.ensureMainMerges?.(TOOLS_ORG); + // Only while the setting keep-main-rules is true (mainrules.ts; the operator, 2026-10-23); else no protection is written. + if (keepRules) await forge.ensureMainMerges?.(TOOLS_ORG); }