gitea: give the controller what it maps a pull request onto the graph with; set both check statuses; protect a branch by tool (hq ADR 0237)
The controller now decides what a pull request's check runs from the mesh's module graph, so the announcement carries the changed directories that hold a module at the head and whether the head has a merge-check.sh. A verdict sets mesh/merge-gate (the gate, with the modules it judged) and mesh/repo-check (the repository's own tests). gitea_branch_protection_get/set let the operator's agent make those statuses required. The catalogue's merge-check.sh leaves the gate to the build seat and keeps its own layer: every manifest through module check, and the touched Go modules' tests.
This commit is contained in:
@@ -5,6 +5,8 @@
|
||||
|
||||
import { readFileSync } from "node:fs";
|
||||
import { ConfiguredToken, MintedToken, type TokenSource } from "./token.js";
|
||||
import { protectionBody, type BranchProtection, type ProtectionWanted } from "./protection.js";
|
||||
export type { BranchProtection, ProtectionWanted } from "./protection.js";
|
||||
|
||||
/** A repository, trimmed to what the mesh cares about. */
|
||||
export interface GiteaRepo {
|
||||
@@ -311,6 +313,11 @@ export class GiteaClient {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Whether a repository holds a file at a commit: true, false for a 404, thrown when the forge cannot say. */
|
||||
async holdsFile(owner: string, repo: string, sha: string, file: string): Promise<boolean> {
|
||||
return this.exists(`/repos/${owner}/${repo}/contents/${encodePath(file)}?ref=${encodeURIComponent(sha)}`);
|
||||
}
|
||||
|
||||
/** Whether the forge has something at a path: true for an answer, false for a 404, thrown otherwise. */
|
||||
private async exists(path: string): Promise<boolean> {
|
||||
let token = await this.tokens.current();
|
||||
@@ -331,6 +338,34 @@ export class GiteaClient {
|
||||
await this.request(`/repos/${owner}/${repo}/statuses/${sha}`, { method: "POST", body: JSON.stringify(status) });
|
||||
}
|
||||
|
||||
// ---- Branch protection ----
|
||||
|
||||
/** A repository's branch protection rules. */
|
||||
async branchProtections(owner: string, repo: string): Promise<BranchProtection[]> {
|
||||
return (await this.request<BranchProtection[]>(`/repos/${owner}/${repo}/branch_protections`)) ?? [];
|
||||
}
|
||||
|
||||
/** The rule named for a branch, null when it has none. */
|
||||
async branchProtection(owner: string, repo: string, rule: string): Promise<BranchProtection | null> {
|
||||
return (await this.branchProtections(owner, repo)).find((p) => (p.rule_name ?? p.branch_name) === rule) ?? null;
|
||||
}
|
||||
|
||||
/** Make a branch require these commit statuses to merge (novox/hq ADR 0237): the rule named for the
|
||||
* branch is edited — its required statuses replaced by these, everything else it says kept unless
|
||||
* asked — or created, refusing direct pushes unless asked otherwise. Answers the rule as it is now. */
|
||||
async setBranchProtection(owner: string, repo: string, branch: string, want: ProtectionWanted): Promise<{ created: boolean; rule: BranchProtection }> {
|
||||
const had = await this.branchProtection(owner, repo, branch);
|
||||
const body = protectionBody(want, !had);
|
||||
if (had) {
|
||||
const rule = await this.request<BranchProtection>(`/repos/${owner}/${repo}/branch_protections/${encodeURIComponent(branch)}`,
|
||||
{ method: "PATCH", body: JSON.stringify(body) });
|
||||
return { created: false, rule };
|
||||
}
|
||||
const rule = await this.request<BranchProtection>(`/repos/${owner}/${repo}/branch_protections`,
|
||||
{ method: "POST", body: JSON.stringify({ rule_name: branch, ...body }) });
|
||||
return { created: true, rule };
|
||||
}
|
||||
|
||||
async createPullRequest(
|
||||
owner: string,
|
||||
repo: string,
|
||||
|
||||
Reference in New Issue
Block a user