fix: review hardening — full push-range diff, fail-closed diff fetch, crash-safe queue, 403 self-heal

- push reviews now fetch ONE compare diff (before...after) instead of
  slicing to the first 20 commits under a head-sha status; new-branch
  pushes (zero before-sha) fall back to the head commit diff
- truncated diffs can no longer yield a clean pass: verdict capped at
  warn and status description prefixed 'partial review (diff truncated)'
- any diff-fetch failure (throw / empty body on a non-empty push or PR)
  short-circuits to status 'warning' ('review unavailable — diff fetch
  failed (not blocking)') with a ledger row; a diffless prompt is never
  sent to the model
- accepted jobs persist to sqlite (queue_jobs) before the pending status
  posts; startup re-enqueues rows that never reached a final state, so
  restarts no longer strand shas at 'pending'
- 403 on status/comment posts self-heals: admin-scoped token (config
  admin_token) adds shre-reviewer as collaborator (write), retries once
- scripts/wire-repos.mjs: idempotent estate-wide webhook + collaborator
  wiring over GET /repos/search
- tests: 32 -> 51 (truncation cap, persisted-queue reconciliation,
  collab-retry stubs, fetchDiff routing)

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Nirav Patel
2026-08-19 09:12:10 -04:00
co-authored by Claude Fable 5
parent f35a75f787
commit e32c600215
14 changed files with 508 additions and 37 deletions
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env node
// Wire EVERY repo on the peer forge for granthi-review:
// - create the push+pull_request webhook -> <hookUrl> (HMAC secret from the
// service config) on every repo that lacks it (idempotent: skipped when a
// hook with that exact URL already exists)
// - add the bot (shre-reviewer) as collaborator (write) so status/comment
// posts don't 403
// Re-runnable at any time. Uses the admin-scoped token (config admin_token).
//
// Usage: node scripts/wire-repos.mjs [--dry-run]
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
const CONFIG_DIR = process.env.GRANTHI_REVIEW_HOME || join(homedir(), '.granthi-review');
const cfg = JSON.parse(readFileSync(join(CONFIG_DIR, 'config.json'), 'utf8'));
const base = (cfg.forgeBaseUrl || 'http://localhost:3030').replace(/\/$/, '');
const adminToken = cfg.admin_token || process.env.GITEA_CLAUDE_SIA_TOKEN;
const secret = cfg.webhookSecret;
const botUsername = cfg.botUsername || 'shre-reviewer';
const hookUrl = process.env.GRANTHI_REVIEW_HOOK_URL || 'http://host.docker.internal:5498/webhook';
const dryRun = process.argv.includes('--dry-run');
if (!adminToken) { console.error('no admin token (config admin_token or $GITEA_CLAUDE_SIA_TOKEN)'); process.exit(1); }
if (!secret) { console.error('no webhookSecret in config'); process.exit(1); }
async function api(method, path, body, ok = [200, 201, 204]) {
const res = await fetch(`${base}${path}`, {
method,
headers: {
Authorization: `token ${adminToken}`,
...(body ? { 'Content-Type': 'application/json' } : {})
},
body: body ? JSON.stringify(body) : undefined
});
if (!ok.includes(res.status)) {
const text = await res.text().catch(() => '');
throw new Error(`${method} ${path} -> ${res.status}: ${text.slice(0, 200)}`);
}
if (res.status === 204) return null;
return res.json().catch(() => null);
}
async function allRepos() {
const repos = [];
for (let page = 1; ; page++) {
const out = await api('GET', `/api/v1/repos/search?limit=50&page=${page}`);
const batch = out?.data || [];
repos.push(...batch);
if (batch.length < 50) break;
}
return repos;
}
const repos = await allRepos();
let hooksCreated = 0, hooksPresent = 0, collabAdded = 0, skipped = 0, errors = 0;
for (const r of repos) {
const full = r.full_name;
const [owner, name] = full.split('/');
try {
if (r.archived || r.empty || r.mirror) { skipped++; console.log(`skip ${full} (${r.archived ? 'archived' : r.mirror ? 'mirror' : 'empty'})`); continue; }
const hooks = await api('GET', `/api/v1/repos/${full}/hooks?limit=50`) || [];
const has = hooks.some(h => h?.config?.url === hookUrl);
if (has) {
hooksPresent++;
} else if (dryRun) {
console.log(`would wire ${full}`);
} else {
await api('POST', `/api/v1/repos/${full}/hooks`, {
type: 'gitea',
active: true,
events: ['push', 'pull_request'],
config: { url: hookUrl, content_type: 'json', secret }
});
hooksCreated++;
console.log(`wired ${full}`);
}
// collaborator write for the bot (idempotent PUT); skip bot-owned repos
if (owner !== botUsername && !dryRun) {
await api('PUT', `/api/v1/repos/${full}/collaborators/${botUsername}`, { permission: 'write' });
collabAdded++;
}
} catch (e) {
errors++;
console.error(`ERROR ${full}: ${e.message}`);
}
}
console.log(`\ntotal repos: ${repos.length}`);
console.log(`hooks created: ${hooksCreated}, already wired: ${hooksPresent}, skipped (archived/empty/mirror): ${skipped}`);
console.log(`collaborator (write) ensured: ${collabAdded}, errors: ${errors}`);
process.exit(errors ? 2 : 0);