mesh/merge-gate head 8d8dcd5ebb86 merged into trunk cd239283207a — pass: builds gitea, lab → novox, shanks; no bus step; every machine composes with the ch…
mesh/repo-check head 8d8dcd5ebb86 merged into trunk cd239283207a — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The lab's credential came from the package team, which also writes the @novox packages, and the branches a run clones are not reviewed yet. The lab now contributes {"access": "read"} to its npm-package-registry requirement, and gitea's provisioner puts such a consumer on a team of its own, readers: code read on the org's repositories, package read, no write, and off the package team. The build agent, which publishes, stays on packages. A stop's SIGTERM to the run's process group killed the wrapper before it removed the credential file. The run's script now removes it as soon as its clones are done and traps every way it ends, and a run's start and lab_check remove what a crash or a reboot left.
65 lines
3.5 KiB
TypeScript
65 lines
3.5 KiB
TypeScript
// Which team of the forge's npm owner a consumer of `npm-package-registry` joins, and the per-consumer
|
|
// half of the provisioner around it — pure of the forge's address, so it is tested with a stand-in.
|
|
//
|
|
// **Two teams, by what the consumer said it needs** (novox/hq issue 624). A consumer that publishes —
|
|
// the build agent, whose builds publish the SDK and every TypeScript module's packages — joins `packages`:
|
|
// code read on the org's repositories and write on its packages. A consumer that contributes
|
|
// `{"access": "read"}` joins `readers`: code read and package read, and no write. The lab is the first: it
|
|
// clones branches that are not yet reviewed and runs their code beside its credential, and code that is
|
|
// not reviewed must not be able to publish the mesh's packages with it.
|
|
|
|
/** The npm registry's owner: the gitea org every consumer's packages live under (ADR 0076). */
|
|
export const ORG = "novox";
|
|
/** The team whose members read and write the org's packages. */
|
|
export const PACKAGE_TEAM = "packages";
|
|
/** The team whose members read the org's code and packages, and write nothing. */
|
|
export const READ_TEAM = "readers";
|
|
|
|
/** The team a consumer joins, from what it contributed; an access it may not ask for is refused. */
|
|
export function teamFor(values: Readonly<Record<string, unknown>> | undefined): { team: string; packageWrite: boolean } {
|
|
const access = values?.access;
|
|
if (access === undefined || access === "write") return { team: PACKAGE_TEAM, packageWrite: true };
|
|
if (access === "read") return { team: READ_TEAM, packageWrite: false };
|
|
throw new Error(`npm-package-registry: access ${JSON.stringify(access)} is not "read" or "write"`);
|
|
}
|
|
|
|
/** What the adapter asks of the forge's admin API (client.ts GiteaAdmin). */
|
|
export interface Forge {
|
|
ensureOrg(org: string): Promise<void>;
|
|
ensureTeam(org: string, team: string, packageWrite: boolean): Promise<number>;
|
|
ensureUser(username: string, password: string, email: string): Promise<void>;
|
|
addUserToTeam(teamId: number, username: string): Promise<void>;
|
|
removeUserFromTeam(teamId: number, username: string): Promise<void>;
|
|
holdsTeamMember(org: string, team: string, username: string, password: string): Promise<boolean>;
|
|
}
|
|
|
|
export interface Consumer {
|
|
readonly as: string;
|
|
readonly password: string;
|
|
readonly values: Readonly<Record<string, unknown>>;
|
|
}
|
|
|
|
/**
|
|
* grant makes the consumer's user, with exactly the mesh's password, a member of its one team. A consumer
|
|
* that asks for read only is also taken out of the package team, so one that asked for write before and
|
|
* read now loses the write at once.
|
|
*/
|
|
export async function grant(forge: Forge, p: Consumer): Promise<void> {
|
|
const { team, packageWrite } = teamFor(p.values);
|
|
await forge.ensureOrg(ORG);
|
|
const teamId = await forge.ensureTeam(ORG, team, packageWrite);
|
|
// 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, while `@noreply.localhost` is the shape gitea
|
|
// itself gives hidden addresses.
|
|
await forge.ensureUser(p.as, p.password, `${p.as}@noreply.localhost`);
|
|
await forge.addUserToTeam(teamId, p.as);
|
|
if (!packageWrite) {
|
|
await forge.removeUserFromTeam(await forge.ensureTeam(ORG, PACKAGE_TEAM, true), p.as);
|
|
}
|
|
}
|
|
|
|
/** holds is whether the forge still holds the consumer as the mesh gave it: its login, in its team. */
|
|
export async function holds(forge: Forge, p: Consumer): Promise<boolean> {
|
|
return forge.holdsTeamMember(ORG, teamFor(p.values).team, p.as, p.password);
|
|
}
|