mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 16:32:28 +00:00
Merge pull request #1416 from PR0M3TH3AN/feat/automated-pr-review-17818997485755159011
feat: add automated PR review agent script
This commit is contained in:
2 files changed
+302
-126
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# Automated PR Review
|
||||
**Context:** jules-17818997485755159011-27e733a1
|
||||
**Date:** 2026-01-19T23:31:08.588Z
|
||||
|
||||
## ✅ Formatting
|
||||
No formatting issues found.
|
||||
## ⚠️ Linter Warnings/Errors
|
||||
`npm run lint` reported issues:
|
||||
<details><summary>Show Lint Output</summary>
|
||||
|
||||
```
|
||||
|
||||
> [email protected] lint
|
||||
> npm run lint:css && npm run lint:hex && npm run lint:inline-styles && npm run lint:tokens && npm run lint:tailwind-brackets && npm run lint:tailwind-colors
|
||||
|
||||
|
||||
> [email protected] lint:css
|
||||
> stylelint css/tailwind.source.css css/tokens.css
|
||||
|
||||
|
||||
> [email protected] lint:hex
|
||||
> node scripts/check-hex.js
|
||||
|
||||
|
||||
> [email protected] lint:inline-styles
|
||||
> node scripts/check-inline-styles.mjs
|
||||
|
||||
No inline style usage found.
|
||||
|
||||
> [email protected] lint:tokens
|
||||
> node scripts/check-design-tokens.mjs --check=tokens
|
||||
|
||||
|
||||
Design token lint failed: raw measurements detected outside tokens.
|
||||
|
||||
css/tailwind.source.css:5598 → 2rem
|
||||
min-width: 2rem;
|
||||
css/tailwind.source.css:5613 → 8rem
|
||||
min-width: 8rem;
|
||||
|
||||
Move these measurements into css/tokens.css or use the metrics helper.
|
||||
|
||||
```
|
||||
</details>
|
||||
## ❌ Test Timeout
|
||||
`npm run test:unit` timed out after 5 minutes.
|
||||
+256
-126
@@ -11,7 +11,7 @@ const SENSITIVE_PATTERNS = [
|
||||
];
|
||||
|
||||
function log(message) {
|
||||
console.log(message);
|
||||
console.log(`[ReviewAgent] ${message}`);
|
||||
}
|
||||
|
||||
function runCommand(command, args = [], options = {}) {
|
||||
@@ -19,130 +19,191 @@ function runCommand(command, args = [], options = {}) {
|
||||
return spawnSync(command, args, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
shell: true,
|
||||
shell: false, // Turn off shell for security unless needed, but spawnSync defaults to false.
|
||||
// However, if I used shell: true before, args might need care.
|
||||
// If shell is false, args are safe.
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
function checkGhAvailability() {
|
||||
try {
|
||||
const res = spawnSync('gh', ['--version'], { encoding: 'utf-8' });
|
||||
return res.status === 0;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const res = spawnSync('git', ['diff', '--name-only', `${baseBranch}...HEAD`], { encoding: 'utf-8' });
|
||||
if (res.status !== 0) throw new Error(res.stderr);
|
||||
return res.stdout.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);
|
||||
}
|
||||
}
|
||||
|
||||
async function reviewContext(contextName, options) {
|
||||
const report = [];
|
||||
report.push(`# Automated PR Review`);
|
||||
report.push(`**Branch:** ${branchArg || 'Current'}`);
|
||||
report.push(`**Context:** ${contextName}`);
|
||||
report.push(`**Date:** ${new Date().toISOString()}`);
|
||||
report.push('');
|
||||
|
||||
let failed = false;
|
||||
|
||||
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
|
||||
// 1. Install dependencies
|
||||
if (!options.noInstall) {
|
||||
log('Installing dependencies (npm ci)...');
|
||||
// On Windows, npm is a cmd. Using shell: true might be needed for 'npm' command,
|
||||
// or use 'npm.cmd'. Linux is fine.
|
||||
// Safe to use shell: true for static commands.
|
||||
const ciResult = spawnSync('npm', ['ci'], { encoding: 'utf-8', shell: true });
|
||||
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 ? ciResult.stderr.slice(-1000) : 'Unknown error');
|
||||
report.push('```');
|
||||
failed = true;
|
||||
}
|
||||
} else {
|
||||
log('Skipping dependency installation (--no-install).');
|
||||
}
|
||||
|
||||
// 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 (!failed) {
|
||||
log('Running formatter...');
|
||||
spawnSync('npm', ['run', 'format'], { shell: true });
|
||||
const gitStatus = spawnSync('git', ['status', '--porcelain'], { encoding: 'utf-8' });
|
||||
const formattedFiles = gitStatus.stdout.split('\n')
|
||||
.filter(line => line.match(/^ M/)) // Modified files
|
||||
.map(line => line.substring(3).trim());
|
||||
|
||||
if (formattedFiles.length > 0) {
|
||||
report.push(`## 🎨 Formatting Suggestions`);
|
||||
report.push(`The following files need formatting. Run \`npm run format\` to fix.`);
|
||||
report.push('<ul>');
|
||||
formattedFiles.forEach(f => report.push(`<li>${f}</li>`));
|
||||
report.push('</ul>');
|
||||
report.push('');
|
||||
if (formattedFiles.length > 0) {
|
||||
report.push(`## 🎨 Formatting Suggestions`);
|
||||
report.push(`The following files need formatting. Run \`npm run format\` to fix.`);
|
||||
report.push('<ul>');
|
||||
formattedFiles.forEach(f => report.push(`<li>${f}</li>`));
|
||||
report.push('</ul>');
|
||||
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}`);
|
||||
// Fast-path suggestion
|
||||
report.push('**Suggestion:** Apply `npm run format` and re-run tests.');
|
||||
|
||||
if (options.applyFixes) {
|
||||
try {
|
||||
log('Applying formatting fixes...');
|
||||
runCommand('git', ['add', '.']);
|
||||
runCommand('git', ['commit', '-m', 'fix(ai): formatting (agent)']);
|
||||
|
||||
log('Pushing changes...');
|
||||
const pushResult = runCommand('git', ['push']);
|
||||
|
||||
if (pushResult.status === 0) {
|
||||
report.push('**Action:** Applied and pushed formatting fixes (agent).');
|
||||
} else {
|
||||
log(`Push failed: ${pushResult.stderr}`);
|
||||
report.push('**Action:** Applied formatting fixes locally, but failed to push.');
|
||||
report.push(`**Error:** ${pushResult.stderr}`);
|
||||
|
||||
// Attempt fallback: Create new branch?
|
||||
// If we are in a detached HEAD or locked branch, pushing might fail.
|
||||
// "open a follow-up PR if you could not directly update the PR branch"
|
||||
// If we have GH CLI...
|
||||
if (checkGhAvailability()) {
|
||||
const newBranch = `fix/ai-formatting-${Date.now()}`;
|
||||
log(`Attempting fallback: creating new branch ${newBranch}...`);
|
||||
runCommand('git', ['checkout', '-b', newBranch]);
|
||||
const pushNew = runCommand('git', ['push', '-u', 'origin', newBranch]);
|
||||
if (pushNew.status === 0) {
|
||||
// Create PR
|
||||
log('Creating fallback PR...');
|
||||
// We need to target the original branch? Or main?
|
||||
// Let's target the current context if possible.
|
||||
// But we don't know the original target easily without GH CLI.
|
||||
// We'll just create a PR against default.
|
||||
const prRes = spawnSync('gh', ['pr', 'create', '--title', 'fix(ai): formatting (agent)', '--body', 'Automated formatting fixes.', '--fill'], { encoding: 'utf-8' });
|
||||
if (prRes.status === 0) {
|
||||
report.push(`**Action:** Created follow-up PR: ${prRes.stdout.trim()}`);
|
||||
} else {
|
||||
report.push(`**Error:** Failed to create follow-up PR: ${prRes.stderr}`);
|
||||
}
|
||||
} else {
|
||||
report.push(`**Error:** Failed to push fallback branch: ${pushNew.stderr}`);
|
||||
}
|
||||
|
||||
// Switch back?
|
||||
// reviewContext usually assumes we stay on the branch or restore.
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
log(`Error applying fixes: ${e.message}`);
|
||||
report.push(`**Error:** Failed to apply formatting fixes: ${e.message}`);
|
||||
}
|
||||
} else {
|
||||
// Revert changes so checking other things is clean?
|
||||
// If we don't apply fixes, we should revert.
|
||||
}
|
||||
} else {
|
||||
report.push(`## ✅ Formatting`);
|
||||
report.push(`No formatting issues found.`);
|
||||
}
|
||||
} 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('<details><summary>Show Lint Output</summary>');
|
||||
report.push('');
|
||||
report.push('```');
|
||||
report.push(lintResult.stdout);
|
||||
report.push(lintResult.stderr);
|
||||
report.push('```');
|
||||
report.push('</details>');
|
||||
} else {
|
||||
report.push(`## ✅ Linter`);
|
||||
report.push(`All lint checks passed.`);
|
||||
if (!failed) {
|
||||
log('Running linter...');
|
||||
const lintResult = spawnSync('npm', ['run', 'lint'], { encoding: 'utf-8', shell: true });
|
||||
if (lintResult.status !== 0) {
|
||||
report.push(`## ⚠️ Linter Warnings/Errors`);
|
||||
report.push('`npm run lint` reported issues:');
|
||||
report.push('<details><summary>Show Lint Output</summary>');
|
||||
report.push('');
|
||||
report.push('```');
|
||||
report.push(lintResult.stdout);
|
||||
report.push(lintResult.stderr);
|
||||
report.push('```');
|
||||
report.push('</details>');
|
||||
|
||||
if (lintResult.stdout && lintResult.stdout.includes('Design token lint failed')) {
|
||||
report.push('**Note:** Design token violations detected. Please check `css/tokens.css`.');
|
||||
}
|
||||
} 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('<details><summary>Show Test Output</summary>');
|
||||
report.push('');
|
||||
report.push('```');
|
||||
report.push(testResult.stdout.slice(-5000)); // Last 5000 chars
|
||||
report.push(testResult.stderr.slice(-5000));
|
||||
report.push('```');
|
||||
report.push('</details>');
|
||||
} else {
|
||||
report.push(`## ✅ Tests`);
|
||||
report.push(`Unit tests passed.`);
|
||||
if (!failed) {
|
||||
log('Running unit tests...');
|
||||
const testResult = spawnSync('npm', ['run', 'test:unit'], { encoding: 'utf-8', shell: true, timeout: 300000 });
|
||||
if (testResult.error && testResult.error.code === 'ETIMEDOUT') {
|
||||
report.push(`## ❌ Test Timeout`);
|
||||
report.push('`npm run test:unit` timed out after 5 minutes.');
|
||||
} else if (testResult.status !== 0) {
|
||||
report.push(`## ❌ Test Failures`);
|
||||
report.push('`npm run test:unit` failed. Inspect the stack traces below.');
|
||||
report.push('<details><summary>Show Test Output</summary>');
|
||||
report.push('');
|
||||
report.push('```');
|
||||
report.push(testResult.stdout ? testResult.stdout.slice(-5000) : 'No stdout');
|
||||
report.push(testResult.stderr ? testResult.stderr.slice(-5000) : 'No stderr');
|
||||
report.push('```');
|
||||
report.push('</details>');
|
||||
} else {
|
||||
report.push(`## ✅ Tests`);
|
||||
report.push(`Unit tests passed.`);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Sensitive Files Check
|
||||
// 5. Sensitive Files
|
||||
const changedFiles = getChangedFiles();
|
||||
const sensitiveChanges = changedFiles.filter(file =>
|
||||
SENSITIVE_PATTERNS.some(pattern => file.match(pattern))
|
||||
@@ -156,44 +217,113 @@ async function main() {
|
||||
report.push('</ul>');
|
||||
}
|
||||
|
||||
// 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.');
|
||||
// Cleanup formatting if not applied
|
||||
if (!options.applyFixes) {
|
||||
try {
|
||||
spawnSync('git', ['checkout', '.'], { stdio: 'ignore' });
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
return report.join('\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('An error occurred during review:', error);
|
||||
if (originalBranch) execSync(`git checkout ${originalBranch}`);
|
||||
process.exit(1);
|
||||
log(`Error reviewing ${contextName}: ${error.message}`);
|
||||
return `Error reviewing ${contextName}: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const options = {
|
||||
noInstall: args.includes('--no-install'),
|
||||
applyFixes: args.includes('--apply-fixes'),
|
||||
all: args.includes('--all'),
|
||||
pr: args.indexOf('--pr') > -1 ? args[args.indexOf('--pr') + 1] : null,
|
||||
};
|
||||
|
||||
const branchArg = args.find(a => !a.startsWith('--') && args.indexOf(a) !== args.indexOf('--pr') + 1);
|
||||
|
||||
const ghAvailable = checkGhAvailability();
|
||||
|
||||
if (options.all) {
|
||||
if (!ghAvailable) {
|
||||
console.error('Error: `gh` CLI not found. Cannot list open PRs.');
|
||||
process.exit(1);
|
||||
}
|
||||
// Fetch PRs
|
||||
log('Fetching open PRs...');
|
||||
try {
|
||||
const prsJson = spawnSync('gh', ['pr', 'list', '--json', 'number,headRefName,url'], { encoding: 'utf-8' });
|
||||
if (prsJson.status !== 0) throw new Error(prsJson.stderr);
|
||||
const prs = JSON.parse(prsJson.stdout);
|
||||
log(`Found ${prs.length} open PRs.`);
|
||||
|
||||
for (const pr of prs) {
|
||||
log(`Checking out PR #${pr.number} (${pr.headRefName})...`);
|
||||
try {
|
||||
spawnSync('gh', ['pr', 'checkout', pr.number]);
|
||||
const report = await reviewContext(`PR #${pr.number}`, options);
|
||||
|
||||
// Post comment
|
||||
const tempFile = `pr_review_${pr.number}.md`;
|
||||
fs.writeFileSync(tempFile, report);
|
||||
log(`Posting comment to PR #${pr.number}...`);
|
||||
spawnSync('gh', ['pr', 'comment', pr.number, '-F', tempFile]);
|
||||
fs.unlinkSync(tempFile);
|
||||
|
||||
} catch (e) {
|
||||
log(`Failed to process PR #${pr.number}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error listing/processing PRs:', e);
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (options.pr) {
|
||||
if (!ghAvailable) {
|
||||
console.error('Error: `gh` CLI not found.');
|
||||
process.exit(1);
|
||||
}
|
||||
log(`Checking out PR #${options.pr}...`);
|
||||
try {
|
||||
const res = spawnSync('gh', ['pr', 'checkout', options.pr], { encoding: 'utf-8' });
|
||||
if (res.status !== 0) throw new Error(res.stderr);
|
||||
|
||||
const report = await reviewContext(`PR #${options.pr}`, options);
|
||||
console.log(report);
|
||||
|
||||
fs.writeFileSync(LOG_FILE, report);
|
||||
log(`Report saved to ${LOG_FILE}`);
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
}
|
||||
} else {
|
||||
// Current branch or specified branch
|
||||
let originalBranch;
|
||||
if (branchArg) {
|
||||
try {
|
||||
originalBranch = spawnSync('git', ['branch', '--show-current'], { encoding: 'utf-8' }).stdout.trim();
|
||||
const res = spawnSync('git', ['checkout', branchArg]);
|
||||
if (res.status !== 0) throw new Error(`Could not checkout ${branchArg}`);
|
||||
} catch(e) {
|
||||
console.error(e.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const currentBranch = spawnSync('git', ['branch', '--show-current'], { encoding: 'utf-8' }).stdout.trim();
|
||||
const report = await reviewContext(currentBranch, options);
|
||||
|
||||
console.log('\n--- REPORT ---\n');
|
||||
console.log(report);
|
||||
fs.writeFileSync(LOG_FILE, report);
|
||||
log(`Report saved to ${LOG_FILE}`);
|
||||
|
||||
if (originalBranch) {
|
||||
spawnSync('git', ['checkout', originalBranch]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user