Files
bitvid/scripts/automated-pr-review.mjs
T
google-labs-jules[bot] 7ecae5d7a2 feat(ci): enhance automated pr review with robust fix fallback and release channel checks
- Add sensitive path checks for storage/crypto services
- Add checks for modifications to critical config when targeting main
- Implement fallback for micro-fixes: create follow-up PR if push fails
- Add Audit Log to review comment
2026-01-23 23:16:40 +00:00

241 lines
9.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { spawnSync, execSync } from 'node:child_process';
import { readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
// Configurations
const SENSITIVE_PATHS = [
/^js\/nostr\//,
/^js\/storage\//,
/^js\/dmDecryptor\.js$/,
/^js\/services\/storageService\.js$/,
/^js\/services\/attachmentService\.js$/,
/^js\/services\/hashtagPreferencesService\.js$/
];
function runCommand(command, args = [], options = {}) {
console.log(`\n> ${command} ${args.join(' ')}`);
return spawnSync(command, args, { stdio: 'pipe', encoding: 'utf-8', ...options });
}
function getChangedFiles() {
try {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const baseUrl = process.env.GITHUB_BASE_REPO_URL;
let remote = 'origin';
if (baseUrl) {
remote = 'upstream';
try {
execSync(`git remote add upstream ${baseUrl}`, { stdio: 'ignore' });
} catch (e) {
// Ignore if remote already exists
}
console.log(`Fetching ${remote}/${baseRef} from ${baseUrl}...`);
try {
execSync(`git fetch ${remote} ${baseRef}`, { stdio: 'ignore' });
} catch (e) {
console.error(`Failed to fetch ${remote}/${baseRef}:`, e.message);
}
} else {
// Fallback for local testing or if env var missing
try {
execSync(`git fetch origin ${baseRef} --depth=1`, { stdio: 'ignore' });
} catch (e) {
// Ignore
}
}
// We want the files changed in this PR.
// Use 3-dot diff to compare HEAD against the merge base with upstream
const cmd = `git diff --name-only ${remote}/${baseRef}...HEAD`;
console.log(`Running diff: ${cmd}`);
const output = execSync(cmd, { encoding: 'utf-8' });
return output.split('\n').filter(Boolean).map(f => f.trim());
} catch (error) {
console.error('Error getting changed files:', error.message);
// Fallback: try 2-dot diff if 3-dot failed (e.g. shallow clone issues)
try {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const remote = process.env.GITHUB_BASE_REPO_URL ? 'upstream' : 'origin';
const cmd = `git diff --name-only ${remote}/${baseRef} HEAD`;
console.log(`Retrying with 2-dot diff: ${cmd}`);
const output = execSync(cmd, { encoding: 'utf-8' });
return output.split('\n').filter(Boolean).map(f => f.trim());
} catch (e) {
console.error('Fallback diff failed:', e.message);
return [];
}
}
}
function checkReleaseChannel(changedFiles) {
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const warnings = [];
if (baseRef === 'main') {
const criticalFiles = ['js/constants.js', 'config/instance-config.js'];
const modifiedCriticalFiles = changedFiles.filter(f => criticalFiles.includes(f));
if (modifiedCriticalFiles.length > 0) {
warnings.push(`⚠️ **Release Channel Warning**: This PR targets \`main\` but modifies the following critical configuration files: ${modifiedCriticalFiles.map(f => `\`${f}\``).join(', ')}.`);
warnings.push(`> **Guidance:** \`main\` is the production track. Ensure feature flags in \`js/constants.js\` are set to safe defaults (usually \`false\`) and \`config/instance-config.js\` has production-appropriate settings (e.g. \`IS_DEV_MODE\`).`);
}
}
return warnings;
}
async function main() {
console.log('Starting Automated PR Review...');
let commentBody = '## 🤖 Automated PR Review\n\n';
let hasFailures = false;
let hasFormatChanges = false;
let microFixesStatus = '';
// 1. Install Dependencies
if (process.env.CI) {
runCommand('npm', ['ci']);
}
// 2. Format
console.log('Checking formatting...');
const formatRes = runCommand('npm', ['run', 'format']);
try {
const diff = execSync('git diff --name-only', { encoding: 'utf-8' });
if (diff.trim().length > 0) {
hasFormatChanges = true;
commentBody += '### 🎨 Formatting\n\n';
commentBody += 'Formatting changes detected.\n\n';
}
} catch (e) {
console.error('Error checking git diff for format:', e);
}
// 3. Lint
console.log('Running lint...');
const lintRes = runCommand('npm', ['run', 'lint']);
if (lintRes.status !== 0) {
hasFailures = true;
commentBody += '### ⚠️ Lint Warnings/Errors\n\n';
commentBody += '```\n' + lintRes.stdout + '\n' + lintRes.stderr + '\n```\n\n';
}
// 4. Unit Tests
console.log('Running unit tests...');
const testRes = runCommand('npm', ['run', 'test:unit']);
if (testRes.status !== 0) {
hasFailures = true;
commentBody += '### ❌ Test Failures\n\n';
commentBody += '```\n' + testRes.stdout + '\n' + testRes.stderr + '\n```\n\n';
commentBody += '> **Suggestion:** Inspect the stack trace above. Run `npm run test:unit` locally to reproduce.\n\n';
} else {
commentBody += '### ✅ Tests Passed\n\nAll unit tests passed.\n\n';
}
// 5. Security/Protocol Review Flags
const changedFiles = getChangedFiles();
const sensitiveChanges = changedFiles.filter(file =>
SENSITIVE_PATHS.some(pattern => pattern.test(file))
);
// Release Channel Check
const channelWarnings = checkReleaseChannel(changedFiles);
if (channelWarnings.length > 0) {
commentBody += '### 🚀 Release Channel Checks\n\n';
channelWarnings.forEach(w => commentBody += `${w}\n\n`);
}
if (sensitiveChanges.length > 0) {
commentBody += '### 🛡️ Guardrails\n\n';
commentBody += 'This PR modifies sensitive files. **Security and Protocol Review Required.**\n';
commentBody += '`requires-security-review` `requires-protocol-review`\n\n';
commentBody += '<details><summary>Sensitive Files Touched</summary>\n\n';
sensitiveChanges.forEach(f => commentBody += `- ${f}\n`);
commentBody += '\n</details>\n\n';
}
// Audit Log
commentBody += '### 📝 Audit Log\n\n';
commentBody += '| Check | Status | Details |\n';
commentBody += '| :--- | :--- | :--- |\n';
commentBody += `| **Formatting** | ${hasFormatChanges ? '⚠️ Changes' : '✅ Pass'} | ${hasFormatChanges ? 'Fixes applied/suggested' : 'No issues'} |\n`;
commentBody += `| **Lint** | ${lintRes.status === 0 ? '✅ Pass' : '⚠️ Warnings'} | ${lintRes.status === 0 ? '-' : 'See output above'} |\n`;
commentBody += `| **Unit Tests** | ${testRes.status === 0 ? '✅ Pass' : '❌ Fail'} | ${testRes.status === 0 ? '-' : 'See failures above'} |\n`;
commentBody += `| **Security** | ${sensitiveChanges.length > 0 ? '🛡️ Review Req' : '✅ Pass'} | ${sensitiveChanges.length > 0 ? `${sensitiveChanges.length} sensitive files` : 'No sensitive files'} |\n`;
commentBody += `| **Release Channel** | ${channelWarnings.length > 0 ? '⚠️ Warning' : '✅ Pass'} | ${channelWarnings.length > 0 ? 'Critical config modified' : 'Safe'} |\n`;
commentBody += '\n';
// 6. Post Comment (if in CI)
if (process.env.GITHUB_TOKEN && process.env.PR_NUMBER) {
// Attempt Micro-fixes
if (hasFormatChanges) {
console.log('Attempting to commit formatting fixes...');
try {
execSync('git config user.name "bitvid-agent"');
execSync('git config user.email "[email protected]"');
execSync('git add .');
execSync('git commit -m "fix(ai): formatting (agent)"');
execSync('git push');
commentBody += '> **✅ Micro-fixes applied:** Formatting fixes have been committed to this branch.\n\n';
} catch (e) {
console.error('Failed to push to PR branch:', e.message);
// Fallback: Create a new branch and open a PR
try {
console.log('Attempting to create a follow-up PR with fixes...');
const prNumber = process.env.PR_NUMBER;
const fixBranchName = `agent-fix/pr-${prNumber}-${Date.now()}`;
// Push the current HEAD (with fixes) to a new branch on origin
execSync(`git push origin HEAD:refs/heads/${fixBranchName}`);
const baseRef = process.env.GITHUB_BASE_REF || 'main';
const prTitle = `fix(ai): formatting fixes for PR #${prNumber}`;
const prBody = `This PR applies automated formatting fixes for PR #${prNumber}.`;
const ghCmd = ['pr', 'create', '--base', baseRef, '--head', fixBranchName, '--title', prTitle, '--body', prBody];
const ghRes = spawnSync('gh', ghCmd, { encoding: 'utf-8' });
if (ghRes.status === 0) {
const newPrUrl = ghRes.stdout.trim();
commentBody += `> **ℹ️ Micro-fixes available:** I could not push to your branch, but I have created a follow-up PR with the fixes: ${newPrUrl}\n\n`;
} else {
throw new Error(`gh pr create failed: ${ghRes.stderr}`);
}
} catch (fallbackError) {
console.error('Failed to create follow-up PR:', fallbackError.message);
commentBody += '> **ℹ️ Micro-fixes available:** Formatting issues were found. Please run `npm run format` locally and push.\n\n';
}
}
}
console.log('Posting comment to PR #' + process.env.PR_NUMBER);
try {
const gh = spawnSync('gh', ['pr', 'comment', process.env.PR_NUMBER, '-F', '-'], {
input: commentBody,
stdio: ['pipe', 'inherit', 'inherit']
});
if (gh.status !== 0) {
console.error('Failed to post comment via gh CLI');
}
} catch (e) {
console.error('Error posting comment:', e);
}
} else {
console.log('Not in CI or missing tokens. Printing report to stdout:');
console.log('---------------------------------------------------');
console.log(commentBody);
console.log('---------------------------------------------------');
}
if (hasFailures) {
process.exit(1);
}
}
main();