gitea: the package team may read code, and its units are reconciled #72

Merged
jschoubben merged 1 commits from fix/gitea-package-team-reads-code into main 2026-09-25 19:59:26 +00:00
+24 -14
View File
@@ -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<number> {
const found = await this.findTeam(org, team);
if (found !== null) return found;
const res = await this.request(`/orgs/${encodeURIComponent(org)}/teams`, {
method: "POST",
body: JSON.stringify({
name: team,
// 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",
// 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" },
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) {
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, ...units }),
});
if (res.status === 201) return Number(res.body?.id);
if (res.status === 422 || res.status === 409) {