import { spawnSync } from 'child_process'; import fs from 'fs'; import path from 'path'; import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js'; const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`; function makeRunner(spawn) { return function run(args, cwd, env) { const opts = { cwd, encoding: 'utf8' }; if (env) opts.env = env; const result = spawn('git', args, opts); if (result.error) throw result.error; if (result.status !== 0) throw new Error((result.stderr || result.stdout || '').trim()); return (result.stdout || '').trim(); }; } function withAskpass(workspace, fn) { const askpassScript = path.join(workspace, '.git-askpass.sh'); fs.writeFileSync(askpassScript, '#!/bin/sh\necho "$GIT_TOKEN"\n', { mode: 0o700 }); const credEnv = { ...process.env, GIT_ASKPASS: askpassScript, GIT_USERNAME: 'x-token', GIT_TOKEN: GITEA_TOKEN }; try { return fn(credEnv); } finally { try { fs.unlinkSync(askpassScript); } catch {} } } /** * Clone PR head branch to workspace/repo (idempotent) */ export function cloneRepo(workspace, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); const repoDir = path.join(workspace, 'repo'); return withAskpass(workspace, credEnv => { if (!fs.existsSync(repoDir)) { run(['clone', '--depth=1', '--branch', PR_HEAD_BRANCH, remoteUrl, repoDir], workspace, credEnv); console.log(` ✅ repo cloned to ${repoDir}`); } else { run(['fetch', 'origin', PR_HEAD_BRANCH], repoDir, credEnv); run(['checkout', PR_HEAD_BRANCH], repoDir); console.log(` ✅ repo already exists, fetched latest`); } return repoDir; }); } export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync) { const run = makeRunner(_spawnSync); try { await withAskpass(workspace, async credEnv => { run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir); run(['config', 'user.name', 'AI Review Bot'], repoDir); const srcFindings = path.join(workspace, FINDINGS_PATH); const destFindings = path.join(repoDir, FINDINGS_PATH); fs.mkdirSync(path.dirname(destFindings), { recursive: true }); fs.copyFileSync(srcFindings, destFindings); run(['add', FINDINGS_PATH], repoDir); const status = run(['status', '--porcelain'], repoDir); if (!status) { console.log(' findings.json 無變更,跳過 commit'); return; } const out = run(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir); const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown'; run(['push', remoteUrl, PR_HEAD_BRANCH], repoDir, credEnv); console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`); }); } catch (e) { console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`); } }