Files
metabuilder/.github/workflows/code-review.yml
johndoe6345789 e328aa37a6 Refactor CI/CD workflows to set working directory and cache paths for Next.js frontend
Add AuthProvider component for user authentication management
Implement users API route with DBAL integration
Create layout component for application structure and metadata
Add Level1Client component for navigation handling
2025-12-25 13:46:55 +00:00

271 lines
9.7 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@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontends/nextjs/package-lock.json
- name: Install dependencies
run: npm ci
- name: Generate Prisma Client
run: npm run db:generate
env:
DATABASE_URL: file:./dev.db
- name: Run linter for review
id: lint
run: |
npm 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.'
});