Files
metabuilder/.github/workflows/pr/code-review.yml

278 lines
9.9 KiB
YAML

name: Automated Code Review
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
checks: read
jobs:
automated-review:
name: AI-Assisted Code Review
runs-on: ubuntu-latest
defaults:
run:
working-directory: frontends/nextjs
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: '1.3.4'
- name: Cache Bun dependencies
uses: actions/cache@v4
with:
key: bun-deps-${{ runner.os }}-${{ hashFiles('bun.lock') }}
path: |
frontends/nextjs/node_modules
~/.bun
restore-keys: bun-deps-${{ runner.os }}-
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Generate Prisma Client
run: bun run db:generate
env:
DATABASE_URL: file:./dev.db
- name: Run linter for review
id: lint
run: |
bun run lint > lint-output.txt 2>&1 || echo "LINT_FAILED=true" >> $GITHUB_OUTPUT
cat lint-output.txt
continue-on-error: true
- name: Analyze code changes
id: analyze
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
// Get PR diff
const { data: files } = await github.rest.pulls.listFiles({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
});
let issues = [];
let warnings = [];
let suggestions = [];
// Analyze each file
for (const file of files) {
const patch = file.patch || '';
const filename = file.filename;
// Check for security issues
if (patch.match(/eval\s*\(/)) {
issues.push(`⚠️ **Security**: Use of \`eval()\` found in ${filename}`);
}
if (patch.match(/innerHTML\s*=/)) {
warnings.push(`⚠️ **Security**: Direct \`innerHTML\` usage in ${filename}. Consider using safer alternatives.`);
}
if (patch.match(/dangerouslySetInnerHTML/)) {
warnings.push(`⚠️ **Security**: \`dangerouslySetInnerHTML\` usage in ${filename}. Ensure content is sanitized.`);
}
// Check for code quality
if (patch.match(/console\.(log|debug|info)/)) {
warnings.push(`🔍 **Code Quality**: Console statements found in ${filename}. Remove before merging.`);
}
if (patch.match(/debugger/)) {
issues.push(`🐛 **Debug Code**: Debugger statement found in ${filename}. Remove before merging.`);
}
if (patch.match(/(:\s*any\b|\bany\s*[>;,\)])/)) {
suggestions.push(`💡 **Type Safety**: Consider replacing \`any\` types with specific types in ${filename}`);
}
// Check for best practices
if (filename.endsWith('.tsx') || filename.endsWith('.jsx')) {
if (patch.match(/useEffect.*\[\]/) && !patch.includes('// eslint-disable')) {
suggestions.push(`💡 **React**: Empty dependency array in useEffect in ${filename}. Verify if intentional.`);
}
}
// Check for large files
if (file.additions > 500) {
warnings.push(`📏 **File Size**: ${filename} has ${file.additions} additions. Consider breaking into smaller files.`);
}
}
// Read lint output if exists
let lintIssues = '';
try {
lintIssues = fs.readFileSync('lint-output.txt', 'utf8');
} catch (e) {
// File doesn't exist
}
// Determine if auto-approve is appropriate
const hasBlockingIssues = issues.length > 0 || lintIssues.includes('error');
return {
issues,
warnings,
suggestions,
lintIssues,
hasBlockingIssues,
fileCount: files.length,
totalAdditions: files.reduce((sum, f) => sum + f.additions, 0),
totalDeletions: files.reduce((sum, f) => sum + f.deletions, 0)
};
- name: Post review comment
uses: actions/github-script@v7
with:
script: |
const analysis = JSON.parse('${{ steps.analyze.outputs.result }}');
let comment = '## 🤖 Automated Code Review\n\n';
comment += `**Changes Summary:**\n`;
comment += `- Files changed: ${analysis.fileCount}\n`;
comment += `- Lines added: ${analysis.totalAdditions}\n`;
comment += `- Lines deleted: ${analysis.totalDeletions}\n\n`;
if (analysis.issues.length > 0) {
comment += '### ❌ Blocking Issues\n\n';
analysis.issues.forEach(issue => comment += `- ${issue}\n`);
comment += '\n';
}
if (analysis.warnings.length > 0) {
comment += '### ⚠️ Warnings\n\n';
analysis.warnings.forEach(warning => comment += `- ${warning}\n`);
comment += '\n';
}
if (analysis.suggestions.length > 0) {
comment += '### 💡 Suggestions\n\n';
analysis.suggestions.forEach(suggestion => comment += `- ${suggestion}\n`);
comment += '\n';
}
if (analysis.lintIssues && analysis.lintIssues.includes('error')) {
comment += '### 🔴 Linting Errors\n\n';
comment += '```\n' + analysis.lintIssues + '\n```\n\n';
}
if (analysis.hasBlockingIssues) {
comment += '---\n';
comment += '### ❌ Review Status: **CHANGES REQUESTED**\n\n';
comment += 'Please address the blocking issues above before this PR can be approved.\n';
} else {
comment += '---\n';
comment += '### ✅ Review Status: **APPROVED**\n\n';
comment += 'No blocking issues found! This PR looks good to merge after CI checks pass.\n';
}
// Check if we already commented
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Automated Code Review')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: comment
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: comment
});
}
- name: Add labels based on review
uses: actions/github-script@v7
with:
script: |
const analysis = JSON.parse('${{ steps.analyze.outputs.result }}');
let labels = [];
if (analysis.hasBlockingIssues) {
labels.push('needs-changes');
} else {
labels.push('ready-for-review');
}
if (analysis.warnings.length > 0) {
labels.push('has-warnings');
}
if (analysis.totalAdditions > 500) {
labels.push('large-pr');
}
// Remove conflicting labels first
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'needs-changes'
});
} catch (e) {
// Label doesn't exist
}
try {
await github.rest.issues.removeLabel({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
name: 'ready-for-review'
});
} catch (e) {
// Label doesn't exist
}
// Add new labels
for (const label of labels) {
try {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: [label]
});
} catch (e) {
console.log(`Label ${label} might not exist, skipping...`);
}
}
- name: Auto-approve if no issues
if: steps.analyze.outputs.result && !fromJSON(steps.analyze.outputs.result).hasBlockingIssues
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
event: 'APPROVE',
body: '✅ Automated review passed! No blocking issues found. This PR is approved pending successful CI checks.'
});