From d279963d2f0387a2476503bdcae8cdbd24432208 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 18 Jan 2026 23:35:53 +0000 Subject: [PATCH] feat(agent): add automated PR review script --- scripts/agent/review-pr.mjs | 200 ++++++++++++++++++++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 scripts/agent/review-pr.mjs diff --git a/scripts/agent/review-pr.mjs b/scripts/agent/review-pr.mjs new file mode 100644 index 00000000..85fac75f --- /dev/null +++ b/scripts/agent/review-pr.mjs @@ -0,0 +1,200 @@ +import { execSync, spawnSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +const LOG_FILE = 'pr_review_report.md'; +const SENSITIVE_PATTERNS = [ + /^js\/nostr\//, + /^js\/storage\//, + /crypto/i, + /nip44/i +]; + +function log(message) { + console.log(message); +} + +function runCommand(command, args = [], options = {}) { + log(`> Running: ${command} ${args.join(' ')}`); + return spawnSync(command, args, { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'pipe'], + shell: true, + ...options + }); +} + +function getChangedFiles(baseBranch = 'origin/main') { + try { + const output = execSync(`git diff --name-only ${baseBranch}...HEAD`, { + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'] + }); + return output.trim().split('\n').filter(Boolean); + } catch (error) { + log(`Warning: Could not determine changed files against ${baseBranch}. Using local changes if any.`); + return []; + } +} + +async function main() { + const branchArg = process.argv[2]; + let originalBranch; + + if (branchArg && !branchArg.startsWith('--')) { + try { + originalBranch = execSync('git branch --show-current', { encoding: 'utf-8' }).trim(); + log(`Switching to branch: ${branchArg}`); + execSync(`git checkout ${branchArg}`); + } catch (e) { + log(`Error: Could not checkout branch ${branchArg}. ${e.message}`); + process.exit(1); + } + } + + const report = []; + report.push(`# Automated PR Review`); + report.push(`**Branch:** ${branchArg || 'Current'}`); + report.push(`**Date:** ${new Date().toISOString()}`); + report.push(''); + + try { + // 1. npm ci + log('Installing dependencies...'); + const ciResult = runCommand('npm', ['ci']); + if (ciResult.status !== 0) { + report.push(`## 🚨 Dependency Installation Failed`); + report.push('`npm ci` failed. Please check `package-lock.json`.'); + report.push('```'); + report.push(ciResult.stderr.slice(-1000)); // Last 1000 chars + report.push('```'); + // If ci fails, we can't really continue safely + } + + // 2. Format + log('Running formatter...'); + runCommand('npm', ['run', 'format']); + const gitStatus = execSync('git status --porcelain', { encoding: 'utf-8' }); + const formattedFiles = gitStatus.split('\n') + .filter(line => line.match(/^ M/)) // Modified files + .map(line => line.substring(3)); + + if (formattedFiles.length > 0) { + report.push(`## 🎨 Formatting Suggestions`); + report.push(`The following files need formatting. Run \`npm run format\` to fix.`); + report.push(''); + report.push(''); + + if (process.argv.includes('--apply-fixes')) { + try { + log('Applying formatting fixes...'); + execSync('git add .'); + execSync('git commit -m "fix(ai): formatting (agent)"'); + report.push('**Action:** Applied formatting fixes.'); + // Clear formattedFiles list since they are now committed (and presumably fixed in the working tree) + // But wait, if we commit them, the next steps run on clean working tree but with new commit. + // That is fine. + } catch (e) { + log(`Error applying fixes: ${e.message}`); + report.push(`**Error:** Failed to apply formatting fixes: ${e.message}`); + } + } + } else { + report.push(`## ✅ Formatting`); + report.push(`No formatting issues found.`); + } + + // 3. Lint + log('Running linter...'); + const lintResult = runCommand('npm', ['run', 'lint']); + if (lintResult.status !== 0) { + report.push(`## ⚠️ Linter Warnings/Errors`); + report.push('`npm run lint` reported issues:'); + report.push('
Show Lint Output'); + report.push(''); + report.push('```'); + report.push(lintResult.stdout); + report.push(lintResult.stderr); + report.push('```'); + report.push('
'); + } else { + report.push(`## ✅ Linter`); + report.push(`All lint checks passed.`); + } + + // 4. Unit Tests + log('Running unit tests...'); + const testResult = runCommand('npm', ['run', 'test:unit']); + if (testResult.status !== 0) { + report.push(`## ❌ Test Failures`); + report.push('`npm run test:unit` failed. Inspect the stack traces below.'); + report.push('
Show Test Output'); + report.push(''); + report.push('```'); + report.push(testResult.stdout.slice(-5000)); // Last 5000 chars + report.push(testResult.stderr.slice(-5000)); + report.push('```'); + report.push('
'); + } else { + report.push(`## ✅ Tests`); + report.push(`Unit tests passed.`); + } + + // 5. Sensitive Files Check + const changedFiles = getChangedFiles(); + const sensitiveChanges = changedFiles.filter(file => + SENSITIVE_PATTERNS.some(pattern => file.match(pattern)) + ); + + if (sensitiveChanges.length > 0) { + report.push(`## 🔒 Security Review Required`); + report.push(`This PR modifies sensitive files. **requires-security-review**, **requires-protocol-review**`); + report.push(''); + } + + // Summary Recommendation + report.push('## Summary'); + if (formattedFiles.length > 0 && lintResult.status === 0 && testResult.status === 0) { + report.push('**Suggestion:** Apply `npm run format` and re-run tests.'); + report.push('Changes are only formatting related.'); + } else if (lintResult.status !== 0 || testResult.status !== 0) { + report.push('**Status:** Changes requested. Please fix linter/test failures.'); + } else { + report.push('**Status:** Checks passed. Ready for review.'); + } + + // Output + fs.writeFileSync(LOG_FILE, report.join('\n')); + log(`Report generated at ${LOG_FILE}`); + console.log('\n--- REPORT PREVIEW ---\n'); + console.log(report.join('\n')); + + // Cleanup changes if we don't want to keep them? + // The instructions say "offer to auto-commit". + // I won't cleanup automatically. I'll leave it to the user. + // But if I switched branches, I should switch back? + // If I switched branches, the changes are now in that branch's working tree. + // Switching back with dirty tree might fail. + + if (formattedFiles.length > 0 && !process.argv.includes('--apply-fixes')) { + log('\nRunning: git checkout . (reverting formatting changes for safety)'); + execSync('git checkout .'); + } + + if (originalBranch) { + log(`Switching back to original branch: ${originalBranch}`); + execSync(`git checkout ${originalBranch}`); + } + + } catch (error) { + console.error('An error occurred during review:', error); + if (originalBranch) execSync(`git checkout ${originalBranch}`); + process.exit(1); + } +} + +main();