mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 14:25:02 +00:00
278 lines
9.9 KiB
YAML
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.'
|
|
});
|