From 02ccf31a7117ecf6ca2d51c99730448c9320018f Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 25 Sep 2026 21:42:38 +0200 Subject: [PATCH] gitea: a consumer's user is actually created, and a failed create says why MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The builder's package-registry grant — the first this provider ever received — retried for a day saying only that an edit 404'd. Two faults under it: the API refuses an email without a dotted domain, so `@localhost` failed validation at create (the CLI that made mesh-admin accepts it, which is why the admin exists and no consumer did); and ensureUser read that 422 as 'already exists' and went on to edit a user that was never made, burying the create's own message. The address is now gitea's own hidden-address shape, and the edit path is taken only for a user that is actually there. --- modules/gitea/client.ts | 23 +++++++++++++++-------- modules/gitea/provisioner/index.ts | 7 ++++++- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/modules/gitea/client.ts b/modules/gitea/client.ts index 983186e..62ddb74 100644 --- a/modules/gitea/client.ts +++ b/modules/gitea/client.ts @@ -387,7 +387,11 @@ export class GiteaAdmin { } /** Ensure a user exists with exactly this password. Created if absent; if already there, its - * password is patched — so the mesh minting a new secret takes on the next reconcile. */ + * password is patched — so the mesh minting a new secret takes on the next reconcile. + * + * The edit path is taken only when the user actually exists. A 422 from the create is also what + * a plain validation failure returns, and reading it as "already there" made the follow-up edit + * 404 — burying the create's own message, which is the one that says what is actually wrong. */ async ensureUser(username: string, password: string, email: string): Promise { const res = await this.request("/admin/users", { method: "POST", @@ -395,13 +399,16 @@ export class GiteaAdmin { }); if (res.status === 201) return; if (res.status === 422 || res.status === 409) { - const patch = await this.request(`/admin/users/${encodeURIComponent(username)}`, { - method: "PATCH", - // login_name is required by the admin edit endpoint; for a local user it is the username. - body: JSON.stringify({ login_name: username, password, must_change_password: false }), - }); - if (patch.status === 200) return; - GiteaAdmin.fail(`/admin/users/${username}`, patch); + const seen = await this.request(`/users/${encodeURIComponent(username)}`); + if (seen.status === 200) { + const patch = await this.request(`/admin/users/${encodeURIComponent(username)}`, { + method: "PATCH", + // login_name is required by the admin edit endpoint; for a local user it is the username. + body: JSON.stringify({ login_name: username, password, must_change_password: false }), + }); + if (patch.status === 200) return; + GiteaAdmin.fail(`/admin/users/${username}`, patch); + } } GiteaAdmin.fail("/admin/users", res); } diff --git a/modules/gitea/provisioner/index.ts b/modules/gitea/provisioner/index.ts index 36d66e2..577487b 100644 --- a/modules/gitea/provisioner/index.ts +++ b/modules/gitea/provisioner/index.ts @@ -34,7 +34,12 @@ runProvisioner("package-registry", { const teamId = await gitea.ensureTeam(ORG, PACKAGE_TEAM, true); // The user carries the consumer's login and the mesh's minted password, set every run so a // rotation takes. Membership of the package team is what grants read+write on packages. - await gitea.ensureUser(p.as, p.password, `${p.as}@localhost`); + // + // The address is gitea's own convention for one that is not real: its email validation + // requires a dotted domain, so `@localhost` was refused at create — the fault that had this + // grant retrying for a day — while `@noreply.localhost` is the shape gitea itself gives + // hidden addresses. + await gitea.ensureUser(p.as, p.password, `${p.as}@noreply.localhost`); await gitea.addUserToTeam(teamId, p.as); }, -- 2.54.0