granthi-review v1: estate-wide AI code review service
Webhook-driven central review service for Granthi forges: HMAC-verified push/pull_request webhooks, per-repo serialization with global LLM concurrency 1, shre-router powered rubric review (8 axes, strict JSON, tolerant extractor + retry), commit statuses + PR scorecard comments, sqlite attribution ledger with Co-Authored-By trailer parsing, per-repo HTML history dashboard, nightly per-agent digests, fail-open-with- visibility when the router is unavailable. Posture: BLOCK on confirmed critical/high correctness+security findings; admin merge is the human override. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
// Minimal Gitea API client (fetch-based, no deps).
|
||||
|
||||
export class GiteaClient {
|
||||
constructor({ baseUrl, token, timeoutMs = 30000 }) {
|
||||
this.baseUrl = baseUrl.replace(/\/$/, '');
|
||||
this.token = token;
|
||||
this.timeoutMs = timeoutMs;
|
||||
}
|
||||
|
||||
async req(method, path, { body, raw = false, ok = [200, 201] } = {}) {
|
||||
const res = await fetch(`${this.baseUrl}${path}`, {
|
||||
method,
|
||||
headers: {
|
||||
Authorization: `token ${this.token}`,
|
||||
...(body ? { 'Content-Type': 'application/json' } : {})
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
signal: AbortSignal.timeout(this.timeoutMs)
|
||||
});
|
||||
if (!ok.includes(res.status)) {
|
||||
const text = await res.text().catch(() => '');
|
||||
const err = new Error(`gitea ${method} ${path} -> ${res.status}: ${text.slice(0, 300)}`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return raw ? res.text() : res.json().catch(() => ({}));
|
||||
}
|
||||
|
||||
// PR diff
|
||||
getPrDiff(owner, repo, index) {
|
||||
return this.req('GET', `/api/v1/repos/${owner}/${repo}/pulls/${index}.diff`, { raw: true });
|
||||
}
|
||||
|
||||
// Single-commit diff
|
||||
getCommitDiff(owner, repo, sha) {
|
||||
return this.req('GET', `/api/v1/repos/${owner}/${repo}/git/commits/${sha}.diff`, { raw: true });
|
||||
}
|
||||
|
||||
// Compare (commit list between two shas)
|
||||
compare(owner, repo, before, after) {
|
||||
return this.req('GET', `/api/v1/repos/${owner}/${repo}/compare/${before}...${after}`);
|
||||
}
|
||||
|
||||
// File content at HEAD (returns null when absent)
|
||||
async getFileHead(owner, repo, path, maxBytes = 8192) {
|
||||
try {
|
||||
const txt = await this.req('GET',
|
||||
`/api/v1/repos/${owner}/${repo}/raw/${encodeURIComponent(path)}`, { raw: true });
|
||||
return txt.slice(0, maxBytes);
|
||||
} catch (e) {
|
||||
if (e.status === 404) return null;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
postStatus(owner, repo, sha, { state, context, description, target_url }) {
|
||||
return this.req('POST', `/api/v1/repos/${owner}/${repo}/statuses/${sha}`, {
|
||||
body: { state, context, description: (description || '').slice(0, 255), target_url }
|
||||
});
|
||||
}
|
||||
|
||||
postIssueComment(owner, repo, index, body) {
|
||||
return this.req('POST', `/api/v1/repos/${owner}/${repo}/issues/${index}/comments`, {
|
||||
body: { body }
|
||||
});
|
||||
}
|
||||
|
||||
getCommit(owner, repo, sha) {
|
||||
return this.req('GET', `/api/v1/repos/${owner}/${repo}/git/commits/${sha}?stat=false&verification=false&files=false`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user