96 lines
3.7 KiB
JavaScript
96 lines
3.7 KiB
JavaScript
#!/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);
|