mirror of
https://github.com/lightningcell/flask-2fa-auth.git
synced 2026-05-26 07:08:07 +00:00
Add AI-powered code review GitHub Action
Introduces a workflow that uses an OpenAI model to analyze code changes on the development branch. If an issue is detected in the diff, the script automatically creates a GitHub issue with relevant details, labels, and assignees.
This commit is contained in:
169
.github/scripts/ai-review.js
vendored
Normal file
169
.github/scripts/ai-review.js
vendored
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/usr/bin/env node
|
||||
const { execSync } = require('child_process');
|
||||
const https = require('https');
|
||||
|
||||
function run(cmd) {
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf8' });
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const GITHUB_SHA = process.env.GITHUB_SHA || run('git rev-parse HEAD').trim();
|
||||
const REPO = process.env.GITHUB_REPOSITORY || (run('git remote get-url origin').trim().replace(/.*github.com[:\/]/, '').replace(/\.git$/, ''));
|
||||
const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
|
||||
const OPENAI_API_KEY = process.env.OPENAI_API_KEY;
|
||||
const MAX_DIFF_CHARS = parseInt(process.env.MAX_DIFF_CHARS || '20000', 10);
|
||||
const DEFAULT_ASSIGNEE = process.env.DEFAULT_ASSIGNEE || '';
|
||||
const DEFAULT_LABELS = (process.env.DEFAULT_LABELS || 'ai-review').split(',').map(s => s.trim()).filter(Boolean);
|
||||
const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-4o-mini';
|
||||
|
||||
if (!GITHUB_TOKEN) {
|
||||
console.error('GITHUB_TOKEN not provided');
|
||||
process.exit(1);
|
||||
}
|
||||
if (!OPENAI_API_KEY) {
|
||||
console.error('OPENAI_API_KEY not provided');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Compute diff vs previous commit (if exists)
|
||||
let diff = '';
|
||||
try {
|
||||
execSync(`git rev-parse ${GITHUB_SHA}^`, { stdio: 'ignore' });
|
||||
diff = run(`git diff --no-prefix --unified=0 ${GITHUB_SHA}^ ${GITHUB_SHA}`);
|
||||
} catch (e) {
|
||||
diff = run(`git show ${GITHUB_SHA}`);
|
||||
}
|
||||
if (!diff) {
|
||||
console.log('No diff to analyze.');
|
||||
process.exit(0);
|
||||
}
|
||||
if (diff.length > MAX_DIFF_CHARS) diff = diff.slice(0, MAX_DIFF_CHARS) + '\n...[truncated]';
|
||||
|
||||
// Build prompt requesting ONLY JSON output
|
||||
const prompt = `
|
||||
You are an automated code reviewer. Analyze the following git diff and determine whether there is a problem that requires opening a GitHub issue.
|
||||
Return ONLY a single JSON object (no extra text, no surrounding backticks) with these fields:
|
||||
- problem_found: true or false
|
||||
- title: short issue title (string) — present if problem_found is true
|
||||
- body: detailed issue body (string) — present if problem_found is true
|
||||
- labels: array of label strings (can be empty)
|
||||
- assignees: array of usernames to assign (can be empty)
|
||||
|
||||
Diff:
|
||||
${diff}
|
||||
`;
|
||||
|
||||
// Call OpenAI Chat Completions API (v1/chat/completions)
|
||||
function openaiChat(prompt, model) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const payload = JSON.stringify({
|
||||
model,
|
||||
messages: [
|
||||
{ role: 'system', content: 'You are a precise code-review assistant. Answer only with JSON as requested.' },
|
||||
{ role: 'user', content: prompt }
|
||||
],
|
||||
max_tokens: 800,
|
||||
temperature: 0.0
|
||||
});
|
||||
|
||||
const req = https.request({
|
||||
hostname: 'api.openai.com',
|
||||
path: '/v1/chat/completions',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(payload),
|
||||
'Authorization': `Bearer ${OPENAI_API_KEY}`
|
||||
}
|
||||
}, res => {
|
||||
let data = '';
|
||||
res.on('data', d => data += d);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
const content = (parsed.choices && parsed.choices[0] && (parsed.choices[0].message?.content || parsed.choices[0].text)) || '';
|
||||
resolve(content);
|
||||
} catch (err) {
|
||||
reject(new Error('OpenAI response parse error: ' + err.message + '\nRaw:' + data));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(payload);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function createIssue(title, body, labels = [], assignees = []) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const post = JSON.stringify({ title, body, labels, assignees });
|
||||
const [owner, repo] = REPO.split('/');
|
||||
const options = {
|
||||
hostname: 'api.github.com',
|
||||
path: `/repos/${owner}/${repo}/issues`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'User-Agent': 'ai-code-review-action',
|
||||
'Accept': 'application/vnd.github+json',
|
||||
'Authorization': `Bearer ${GITHUB_TOKEN}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(post)
|
||||
}
|
||||
};
|
||||
const req = https.request(options, res => {
|
||||
let data = '';
|
||||
res.on('data', d => data += d);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
resolve(parsed);
|
||||
} catch (err) {
|
||||
reject(new Error('GitHub response parse error: ' + err.message + '\nRaw:' + data));
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(post);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
console.log('Calling OpenAI to analyze diff (truncated to %d chars)', MAX_DIFF_CHARS);
|
||||
const aiReply = await openaiChat(prompt, OPENAI_MODEL);
|
||||
console.log('AI raw reply snippet:', aiReply.slice(0, 1000));
|
||||
|
||||
// Try to extract JSON object from AI reply
|
||||
const jsonMatch = aiReply.match(/\{[\s\S]*\}$/);
|
||||
const jsonText = jsonMatch ? jsonMatch[0] : aiReply;
|
||||
let aiObj = null;
|
||||
try {
|
||||
aiObj = JSON.parse(jsonText);
|
||||
} catch (err) {
|
||||
console.error('Could not parse AI response as JSON:', err.message);
|
||||
console.error('Full AI reply:', aiReply);
|
||||
process.exit(0); // assume no problem to avoid false positives
|
||||
}
|
||||
|
||||
if (!aiObj.problem_found) {
|
||||
console.log('AI determined no problem.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const title = aiObj.title || 'Automated code review found an issue';
|
||||
const body = aiObj.body || 'AI reported an issue. Please review the diff and details.';
|
||||
const labels = (Array.isArray(aiObj.labels) && aiObj.labels.length) ? aiObj.labels : DEFAULT_LABELS;
|
||||
const assignees = (Array.isArray(aiObj.assignees) && aiObj.assignees.length) ? aiObj.assignees : (DEFAULT_ASSIGNEE ? [DEFAULT_ASSIGNEE] : []);
|
||||
|
||||
console.log('Creating issue:', title);
|
||||
const created = await createIssue(title, body, labels, assignees);
|
||||
console.log('Issue created:', created.html_url || created.url);
|
||||
} catch (err) {
|
||||
console.error('Error in AI review script:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
41
.github/workflows/ai-code-review.yml
vendored
Normal file
41
.github/workflows/ai-code-review.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
name: AI Code Review
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- development
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
env:
|
||||
DEFAULT_ASSIGNEE: "lightningcell"
|
||||
DEFAULT_LABELS: "ai-review"
|
||||
MAX_DIFF_CHARS: "20000"
|
||||
OPENAI_MODEL: "gpt-4o-mini"
|
||||
|
||||
jobs:
|
||||
ai-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "18"
|
||||
|
||||
- name: Run AI review script
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||
DEFAULT_ASSIGNEE: ${{ env.DEFAULT_ASSIGNEE }}
|
||||
DEFAULT_LABELS: ${{ env.DEFAULT_LABELS }}
|
||||
MAX_DIFF_CHARS: ${{ env.MAX_DIFF_CHARS }}
|
||||
OPENAI_MODEL: ${{ env.OPENAI_MODEL }}
|
||||
run: |
|
||||
node .github/scripts/ai-review.js
|
||||
Reference in New Issue
Block a user