diff --git a/modules/gitea/client.ts b/modules/gitea/client.ts index 62ddb74..0fcc47e 100644 --- a/modules/gitea/client.ts +++ b/modules/gitea/client.ts @@ -352,24 +352,34 @@ export class GiteaAdmin { GiteaAdmin.fail("/orgs", res); } - /** Ensure the org's package team exists, granting read+write on packages, and return its id. The - * team is found by name if it is already there, created otherwise; a lost create race is resolved - * by re-listing. */ + /** Ensure the org's package team exists with exactly these units, and return its id. Found or + * created, the units are applied either way — a team is configuration the reconcile loop owns, + * the same as a user's password, so a unit this code gains reaches a team that already exists + * rather than only the next mesh raised from scratch. A lost create race is resolved by + * re-listing. */ async ensureTeam(org: string, team: string, packageWrite: boolean): Promise { + // The units a consumer needs, and no more. `units_map` is exhaustive — a unit not named is a + // unit the team does not have — so code read must be said here: without it gitea answers a + // member's clone of a private repository with "not found", which is how the builder's first + // credentialed clone failed against a team that named only packages. + const units = { + permission: "read", + units_map: { "repo.code": "read", "repo.packages": packageWrite ? "write" : "read" }, + includes_all_repositories: true, + can_create_org_repo: false, + }; const found = await this.findTeam(org, team); - if (found !== null) return found; + if (found !== null) { + const patch = await this.request(`/teams/${found}`, { + method: "PATCH", + body: JSON.stringify({ name: team, ...units }), + }); + if (patch.status === 200) return found; + GiteaAdmin.fail(`/teams/${found}`, patch); + } const res = await this.request(`/orgs/${encodeURIComponent(org)}/teams`, { method: "POST", - body: JSON.stringify({ - name: team, - permission: "read", - // Package access is a per-unit grant; the team needs write on the packages unit and nothing - // else. includes_all_repositories keeps the team's repo view whole without widening its - // repo permission beyond read. - units_map: { "repo.packages": packageWrite ? "write" : "read" }, - includes_all_repositories: true, - can_create_org_repo: false, - }), + body: JSON.stringify({ name: team, ...units }), }); if (res.status === 201) return Number(res.body?.id); if (res.status === 422 || res.status === 409) {