113 lines
4.1 KiB
JavaScript
113 lines
4.1 KiB
JavaScript
import http from 'node:http';
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const run = promisify(execFile);
|
|
const { GITEA_URL, GITEA_TOKEN } = process.env;
|
|
const OWNER = 'selimaj-dev';
|
|
|
|
const gitEnv = {
|
|
...process.env,
|
|
GIT_CONFIG_COUNT: '1',
|
|
GIT_CONFIG_KEY_0: 'http.extraHeader',
|
|
GIT_CONFIG_VALUE_0: `Authorization: token ${GITEA_TOKEN}`,
|
|
};
|
|
const claudeEnv = { ...process.env };
|
|
delete claudeEnv.GITEA_TOKEN;
|
|
|
|
const api = async (method, p, body) => {
|
|
const r = await fetch(`${GITEA_URL}/api/v1${p}`, {
|
|
method,
|
|
headers: { Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' },
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
if (!r.ok) throw new Error(`${method} ${p} -> ${r.status}`);
|
|
return r.status === 204 ? null : r.json();
|
|
};
|
|
const git = (dir, ...args) => run('git', args, { cwd: dir, env: gitEnv });
|
|
const busy = new Set();
|
|
|
|
async function handle(ev) {
|
|
const repo = ev.repository.full_name;
|
|
const n = ev.issue.number;
|
|
const key = `${repo}#${n}`;
|
|
if (busy.has(key)) return;
|
|
busy.add(key);
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-'));
|
|
try {
|
|
const isPR = !!ev.issue.pull_request;
|
|
const base = ev.repository.default_branch;
|
|
const ref = isPR ? (await api('GET', `/repos/${repo}/pulls/${n}`)).head.ref : base;
|
|
const branch = isPR ? ref : `claude/issue-${n}`;
|
|
|
|
await run('git', ['clone', '--depth', '50', '--branch', ref, `${GITEA_URL}/${repo}.git`, dir], { env: gitEnv });
|
|
if (!isPR) {
|
|
try {
|
|
await git(dir, 'fetch', 'origin', branch);
|
|
await git(dir, 'checkout', '-B', branch, 'FETCH_HEAD');
|
|
} catch {
|
|
await git(dir, 'checkout', '-B', branch);
|
|
}
|
|
}
|
|
|
|
const prompt = [
|
|
`Repository: ${repo}, #${n}: ${ev.issue.title}`,
|
|
ev.issue.body || '',
|
|
`Request from the maintainer:\n${ev.comment.body}`,
|
|
'Make the requested changes by editing files. Do not run git commands; the service commits for you. End with a short plain-text summary of what you changed, or your answer if no change was needed.',
|
|
].join('\n\n');
|
|
|
|
const { stdout } = await run(
|
|
'claude',
|
|
['-p', prompt, '--permission-mode', 'acceptEdits', '--model', 'sonnet', '--max-turns', '25'],
|
|
{ cwd: dir, env: claudeEnv, timeout: 20 * 60_000, maxBuffer: 20 * 1024 * 1024 },
|
|
);
|
|
|
|
let reply = stdout.trim() || '(no output)';
|
|
const { stdout: status } = await git(dir, 'status', '--porcelain');
|
|
if (status.trim()) {
|
|
await git(dir, 'add', '-A');
|
|
await git(dir, '-c', 'user.name=claude', '-c', '[email protected]',
|
|
'commit', '-m', `Claude: address #${n}`);
|
|
await git(dir, 'push', 'origin', `HEAD:${branch}`);
|
|
if (!isPR) {
|
|
const open = await api('GET', `/repos/${repo}/pulls?state=open`);
|
|
let pr = open.find((p) => p.head.ref === branch);
|
|
if (!pr) {
|
|
pr = await api('POST', `/repos/${repo}/pulls`, {
|
|
title: `Claude: ${ev.issue.title}`, head: branch, base,
|
|
body: `Closes #${n}\n\n${reply}`,
|
|
});
|
|
}
|
|
reply += `\n\nPR: ${pr.html_url}`;
|
|
}
|
|
}
|
|
await api('POST', `/repos/${repo}/issues/${n}/comments`, { body: reply });
|
|
} catch (e) {
|
|
console.error(key, e.message);
|
|
await api('POST', `/repos/${repo}/issues/${n}/comments`,
|
|
{ body: `Claude run failed: ${e.message.slice(0, 300)}` }).catch(() => {});
|
|
} finally {
|
|
busy.delete(key);
|
|
await fs.rm(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
http.createServer((req, res) => {
|
|
const chunks = [];
|
|
req.on('data', (c) => chunks.push(c));
|
|
req.on('end', () => {
|
|
res.writeHead(200).end('ok');
|
|
try {
|
|
if (req.headers['x-gitea-event'] !== 'issue_comment') return;
|
|
const ev = JSON.parse(Buffer.concat(chunks));
|
|
if (ev.action !== 'created') return;
|
|
if (ev.comment.user.login !== OWNER) return;
|
|
if (!ev.comment.body.includes('@claude')) return;
|
|
handle(ev);
|
|
} catch (e) { console.error(e.message); }
|
|
});
|
|
}).listen(3000); |