#!/usr/bin/env node /** * e2e-gitops-validate.js — GitOps 全流程端到端验证 * * 在本地模拟完整的 GitOps 流程(无需远程服务器),验证: * 1. pre-task.sh — Git 凭证注入 + 分支创建 * 2. (模拟) — Agent 在工作区修改文件 * 3. post-task-success.sh — Git add + commit + push * 4. 验证 — bare repo 收到正确的 commit * * 用法:node scripts/e2e-gitops-validate.js */ const { execSync, spawnSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const os = require('os'); let passed = 0; let failed = 0; function assert(condition, msg) { if (!condition) { console.error(` \x1b[31m❌ FAIL:\x1b[0m ${msg}`); failed++; return false; } console.log(` \x1b[32m✅ PASS:\x1b[0m ${msg}`); passed++; return true; } function step(n, msg) { console.log(`\n\x1b[36m[${n}]\x1b[0m ${msg}`); } function git(args, opts = {}) { return execSync(`git ${args}`, { encoding: 'utf-8', stdio: 'pipe', ...opts, }).trim(); } function runHook(script, env, cwd) { const result = spawnSync('bash', [script], { cwd, env: { ...process.env, ...env }, encoding: 'utf-8', stdio: 'pipe', }); if (result.stdout) { result.stdout.split('\n').forEach(l => l && console.log(` ${l}`)); } if (result.stderr) { result.stderr.split('\n').forEach(l => l && console.log(` \x1b[33m${l}\x1b[0m`)); } return result; } // ─── 临时目录 ─── const TMP = fs.mkdtempSync(path.join(os.tmpdir(), 'gitops-e2e-')); const BARE_REPO = path.join(TMP, 'origin.git'); const WORKSPACE = path.join(TMP, 'workspace'); const HOOKS_DIR = path.join(__dirname, '../agent-bridge/hooks'); // 检查 hooks 存在 if (!fs.existsSync(path.join(HOOKS_DIR, 'pre-task.sh'))) { console.error('❌ 找不到 agent-bridge/hooks/pre-task.sh'); process.exit(1); } console.log(` ╔═══════════════════════════════════════╗ ║ GitOps E2E 全流程验证 ║ ╚═══════════════════════════════════════╝ 临时目录: ${TMP} Hooks: ${HOOKS_DIR} `); try { // ═══════════════════════════════════ step(1, '创建 bare repo + 初始 commit'); // ═══════════════════════════════════ execSync(`git init --bare ${BARE_REPO}`, { stdio: 'pipe' }); // 创建临时 clone 做初始提交 const initDir = path.join(TMP, 'init'); git(`clone ${BARE_REPO} ${initDir}`); fs.writeFileSync(path.join(initDir, 'README.md'), '# Test Project\n'); git('add -A', { cwd: initDir }); git('config user.email "test@e2e.local"', { cwd: initDir }); git('config user.name "E2E Test"', { cwd: initDir }); git('commit -m "initial commit"', { cwd: initDir }); git('push origin main', { cwd: initDir }); fs.rmSync(initDir, { recursive: true }); assert(true, 'bare repo 创建完成 + 初始 commit'); // ═══════════════════════════════════ step(2, '克隆工作区'); // ═══════════════════════════════════ git(`clone ${BARE_REPO} ${WORKSPACE}`); git('config user.email "agent@e2e.local"', { cwd: WORKSPACE }); git('config user.name "E2E Agent"', { cwd: WORKSPACE }); assert(fs.existsSync(path.join(WORKSPACE, '.git')), '工作区克隆成功'); // ═══════════════════════════════════ step(3, '运行 pre-task.sh'); // ═══════════════════════════════════ const preTaskEnv = { TASK_ID: 'e2e-001', AGENT_NAME: 'test-coder', GIT_REMOTE: 'origin', HOME: os.homedir(), PATH: process.env.PATH, }; const preResult = runHook(path.join(HOOKS_DIR, 'pre-task.sh'), preTaskEnv, WORKSPACE); assert(preResult.status === 0, 'pre-task.sh 退出码 = 0'); const branch = git('branch --show-current', { cwd: WORKSPACE }); assert(branch === 'feat/task-e2e-001', `分支名正确: ${branch}`); // ═══════════════════════════════════ step(4, '模拟 Agent 修改文件'); // ═══════════════════════════════════ const agentFile = path.join(WORKSPACE, 'solution.js'); fs.writeFileSync(agentFile, ` // Auto-generated by test-coder agent // Task: e2e-001 function fibonacci(n) { if (n <= 1) return n; return fibonacci(n - 1) + fibonacci(n - 2); } module.exports = { fibonacci }; `); assert(fs.existsSync(agentFile), 'Agent 生成了 solution.js'); // ═══════════════════════════════════ step(5, '运行 post-task-success.sh'); // ═══════════════════════════════════ const postEnv = { TASK_ID: 'e2e-001', AGENT_NAME: 'test-coder', EXIT_CODE: '0', GIT_REMOTE: 'origin', HOME: os.homedir(), PATH: process.env.PATH, }; const postResult = runHook(path.join(HOOKS_DIR, 'post-task-success.sh'), postEnv, WORKSPACE); assert(postResult.status === 0, 'post-task-success.sh 退出码 = 0'); // ═══════════════════════════════════ step(6, '验证 bare repo'); // ═══════════════════════════════════ // 检查分支是否推送成功 const branches = git(`--git-dir=${BARE_REPO} branch`); assert(branches.includes('feat/task-e2e-001'), `bare repo 包含 feat/task-e2e-001 分支`); // 检查 commit 消息 const log = git(`--git-dir=${BARE_REPO} log feat/task-e2e-001 --oneline -1`); assert(log.includes('e2e-001'), `commit 消息包含 task ID: ${log}`); assert(log.includes('test-coder'), `commit 消息包含 agent name: ${log}`); // 检查文件内容 const content = git(`--git-dir=${BARE_REPO} show feat/task-e2e-001:solution.js`); assert(content.includes('fibonacci'), 'bare repo 中 solution.js 包含 fibonacci'); // ═══════════════════════════════════ step(7, '验证 GIT_TOKEN 凭证注入(dry run)'); // ═══════════════════════════════════ // 在干净目录中测试凭证注入(不写入真实 ~/.git-credentials) const credDir = path.join(TMP, 'cred-test'); fs.mkdirSync(credDir); git('init', { cwd: credDir }); git('config user.email "x@x.x"', { cwd: credDir }); git('config user.name "x"', { cwd: credDir }); git(`remote add origin ${BARE_REPO}`, { cwd: credDir }); // 添加 remote 使 fetch 不报错 // 创建初始 commit(pre-task.sh 需要 HEAD 存在才能 checkout 分支) fs.writeFileSync(path.join(credDir, '.gitkeep'), ''); git('add .', { cwd: credDir }); git('commit -m "init"', { cwd: credDir }); const credEnv = { TASK_ID: 'cred-test', AGENT_NAME: 'test', GIT_TOKEN: 'ghp_test123', GIT_HOST: 'github.example.com', HOME: credDir, // 隔离——写到临时目录 PATH: process.env.PATH, }; const credResult = runHook(path.join(HOOKS_DIR, 'pre-task.sh'), credEnv, credDir); assert(credResult.status === 0, 'pre-task.sh(带 GIT_TOKEN)退出码 = 0'); const credFile = path.join(credDir, '.git-credentials'); if (fs.existsSync(credFile)) { const credContent = fs.readFileSync(credFile, 'utf-8'); assert(credContent.includes('ghp_test123'), '.git-credentials 包含 token'); assert(credContent.includes('github.example.com'), '.git-credentials 包含自定义 host'); const stat = fs.statSync(credFile); const mode = '0' + (stat.mode & 0o777).toString(8); assert(mode === '0600', `.git-credentials 权限 = ${mode}`); } else { assert(false, '.git-credentials 文件存在'); } // ═══════════════════════════════════ step(8, '清理'); // ═══════════════════════════════════ fs.rmSync(TMP, { recursive: true, force: true }); assert(true, '临时目录已清理'); } catch (e) { console.error(`\n💥 异常: ${e.message}`); console.error(e.stack); // 清理 try { fs.rmSync(TMP, { recursive: true, force: true }); } catch { } } // ═══════════════════════════════════ console.log(` ══════════════════════════════════ 总计: ${passed + failed} 通过: ${passed} 失败: ${failed} ══════════════════════════════════`); if (failed > 0) { console.error('💥 有测试失败!'); process.exit(1); } else { console.log('🎉 GitOps E2E 全流程验证通过!'); process.exit(0); }