// Gitea API client. Minimal surface — only what the auditor needs: // list open PRs, get commits + files for a PR, fetch a diff, post a // commit status, post a review. // // Auth: reads PAT from ~/.git-credentials (set up by the credential // helper flow in 2026-04-22 session). Gitea's "token" auth scheme // matches what `git fetch` is already using. import { readFile } from "node:fs/promises"; import type { PrSnapshot } from "./types.ts"; const HOST = process.env.GITEA_HOST ?? "https://git.agentview.dev"; const OWNER = "profit"; const REPO = "lakehouse"; const CRED_FILE = "/home/profit/.git-credentials"; let cachedPat: string | null = null; async function getPat(): Promise { if (cachedPat) return cachedPat; const raw = await readFile(CRED_FILE, "utf8"); for (const line of raw.split("\n")) { const m = line.match(/^https:\/\/[^:]+:([^@]+)@git\.agentview\.dev/); if (m) { cachedPat = m[1]; return m[1]; } } throw new Error(`no Gitea PAT in ${CRED_FILE}`); } async function giteaFetch(path: string, init: RequestInit = {}): Promise { const pat = await getPat(); const url = `${HOST}/api/v1${path}`; const headers = new Headers(init.headers); headers.set("Authorization", `token ${pat}`); if (init.body && !headers.has("content-type")) { headers.set("content-type", "application/json"); } return fetch(url, { ...init, headers, signal: AbortSignal.timeout(20000) }); } export async function listOpenPrs(): Promise { const r = await giteaFetch(`/repos/${OWNER}/${REPO}/pulls?state=open&page=1&limit=50`); if (!r.ok) throw new Error(`listOpenPrs ${r.status}: ${await r.text()}`); const rows = (await r.json()) as any[]; return Promise.all(rows.map(row => snapshotFromPr(row))); } export async function getPrSnapshot(num: number): Promise { const r = await giteaFetch(`/repos/${OWNER}/${REPO}/pulls/${num}`); if (!r.ok) throw new Error(`getPr ${num} ${r.status}: ${await r.text()}`); return snapshotFromPr((await r.json()) as any); } async function snapshotFromPr(row: any): Promise { const num = row.number; const commitsResp = await giteaFetch(`/repos/${OWNER}/${REPO}/pulls/${num}/commits`); const commits = commitsResp.ok ? ((await commitsResp.json()) as any[]) : []; const filesResp = await giteaFetch(`/repos/${OWNER}/${REPO}/pulls/${num}/files`); const files = filesResp.ok ? ((await filesResp.json()) as any[]) : []; return { number: num, head_sha: row.head?.sha ?? "", base_sha: row.base?.sha ?? "", title: row.title ?? "", body: row.body ?? "", state: row.state === "open" ? "open" : (row.merged ? "merged" : "closed"), author: row.user?.login ?? "", commits: commits.map(c => ({ sha: (c.sha ?? "").slice(0, 12), message: c.commit?.message ?? "", author: c.commit?.author?.name ?? "", })), files: files.map(f => ({ path: f.filename ?? "", additions: f.additions ?? 0, deletions: f.deletions ?? 0, })), }; } /// Returns the unified diff text of the PR. Used by static checks. export async function getPrDiff(num: number): Promise { const r = await giteaFetch(`/repos/${OWNER}/${REPO}/pulls/${num}.diff`); if (!r.ok) throw new Error(`getDiff ${num} ${r.status}: ${await r.text()}`); return await r.text(); } /// Hard-block mechanism: post a failing commit status on the PR head /// SHA. Branch protection (if enabled on `main`) treats this as a /// required-check fail and prevents merge. The description is shown /// in the Gitea UI next to the red X. export async function postCommitStatus(args: { sha: string; state: "success" | "pending" | "failure" | "error"; context: string; description: string; target_url?: string; }): Promise { const r = await giteaFetch(`/repos/${OWNER}/${REPO}/statuses/${args.sha}`, { method: "POST", body: JSON.stringify({ state: args.state, context: args.context, description: args.description.slice(0, 140), target_url: args.target_url ?? "", }), }); if (!r.ok) throw new Error(`postCommitStatus ${r.status}: ${await r.text()}`); } /// Post a review comment. Type: "REQUEST_CHANGES" for block, /// "COMMENT" for non-blocking, "APPROVE" for green. export async function postReview(args: { pr_number: number; commit_id: string; body: string; event: "APPROVE" | "REQUEST_CHANGES" | "COMMENT"; }): Promise { const r = await giteaFetch(`/repos/${OWNER}/${REPO}/pulls/${args.pr_number}/reviews`, { method: "POST", body: JSON.stringify({ commit_id: args.commit_id, body: args.body, event: args.event, }), }); if (!r.ok) throw new Error(`postReview ${r.status}: ${await r.text()}`); }