feat: add AI code reviewer using Gemini API
swadhinbiswas committed 3 months ago
8afe9a8
1 parent 59efa59
commit 8afe9a8
2 files changed
+153 -0
Showing 2 changed files+153-0
▼.github/workflows/ai-review.yml
+34-0
| @@ -0,0 +1,34 @@ | ||
1 | +name: AI Code Reviewer | |
2 | + | |
3 | +on: | |
4 | + pull_request: | |
5 | + types: [opened, synchronize] | |
6 | + | |
7 | +permissions: | |
8 | + pull-requests: write | |
9 | + contents: read | |
10 | + | |
11 | +jobs: | |
12 | + review: | |
13 | + name: Gemini AI Review | |
14 | + runs-on: ubuntu-latest | |
15 | + timeout-minutes: 5 | |
16 | + steps: | |
17 | + - uses: actions/checkout@v4 | |
18 | + with: | |
19 | + fetch-depth: 0 | |
20 | + | |
21 | + - uses: oven-sh/setup-bun@v2 | |
22 | + with: | |
23 | + bun-version: latest | |
24 | + | |
25 | + - name: Run AI Review | |
26 | + env: | |
27 | + # Use user's PAT to count towards their GitHub contribution graph, fallback to standard bot token | |
28 | + GITHUB_TOKEN: ${{ secrets.GH_PAT || secrets.GITHUB_TOKEN }} | |
29 | + GEMINI_API_KEY: ${{ secrets.GEMINIKEY }} | |
30 | + PR_NUMBER: ${{ github.event.pull_request.number }} | |
31 | + REPO: ${{ github.repository }} | |
32 | + BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
33 | + HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
34 | + run: bun run scripts/ai-review.ts | |
0 | 35 | |
▼scripts/ai-review.ts
+119-0
| @@ -0,0 +1,119 @@ | ||
1 | +import { execSync } from 'child_process'; | |
2 | + | |
3 | +const GEMINI_API_KEY = process.env.GEMINI_API_KEY; | |
4 | +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; | |
5 | +const PR_NUMBER = process.env.PR_NUMBER; | |
6 | +const REPO = process.env.REPO; | |
7 | +const BASE_SHA = process.env.BASE_SHA; | |
8 | +const HEAD_SHA = process.env.HEAD_SHA; | |
9 | + | |
10 | +if (!GEMINI_API_KEY) { | |
11 | + console.log("GEMINI_API_KEY is not set. Skipping AI review."); | |
12 | + process.exit(0); | |
13 | +} | |
14 | + | |
15 | +if (!GITHUB_TOKEN) { | |
16 | + console.log("GITHUB_TOKEN is not set. Skipping AI review."); | |
17 | + process.exit(0); | |
18 | +} | |
19 | + | |
20 | +async function run() { | |
21 | + try { | |
22 | + // 1. Get the diff | |
23 | + console.log(`Getting diff between ${BASE_SHA} and ${HEAD_SHA}`); | |
24 | + const diff = execSync(`git diff ${BASE_SHA} ${HEAD_SHA}`).toString(); | |
25 | + | |
26 | + if (!diff.trim()) { | |
27 | + console.log("No diff found. Skipping review."); | |
28 | + return; | |
29 | + } | |
30 | + | |
31 | + if (diff.length > 50000) { | |
32 | + console.log("Diff is too large for AI review (max 50,000 characters). Skipping."); | |
33 | + return; | |
34 | + } | |
35 | + | |
36 | + // 2. Call Gemini API | |
37 | + console.log("Requesting review from Gemini 1.5 Flash..."); | |
38 | + const prompt = `You are a senior software engineer conducting a code review on a pull request. | |
39 | +Review the following git diff and provide constructive, human-like, and professional feedback. | |
40 | +Focus on identifying logic errors, security issues, performance bottlenecks, and best practice violations. | |
41 | +If the code looks perfect, say so in a friendly way. | |
42 | + | |
43 | +Important constraints: | |
44 | +- Do NOT output any internal reasoning, thoughts, or <think> tags. | |
45 | +- Output ONLY the final review in markdown format. | |
46 | +- Be concise but thorough. | |
47 | + | |
48 | +Diff: | |
49 | +\`\`\`diff | |
50 | +${diff} | |
51 | +\`\`\` | |
52 | +`; | |
53 | + | |
54 | + // We use the same API endpoint format from your curl command | |
55 | + const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${GEMINI_API_KEY}`, { | |
56 | + method: 'POST', | |
57 | + headers: { | |
58 | + 'Content-Type': 'application/json' | |
59 | + }, | |
60 | + body: JSON.stringify({ | |
61 | + contents: [ | |
62 | + { | |
63 | + parts: [ | |
64 | + { | |
65 | + text: prompt | |
66 | + } | |
67 | + ] | |
68 | + } | |
69 | + ] | |
70 | + }) | |
71 | + }); | |
72 | + | |
73 | + if (!response.ok) { | |
74 | + const errorText = await response.text(); | |
75 | + console.error(`Gemini API Error: ${response.status} - ${errorText}`); | |
76 | + process.exit(1); | |
77 | + } | |
78 | + | |
79 | + const data = await response.json(); | |
80 | + let reviewComment = data.candidates?.[0]?.content?.parts?.[0]?.text; | |
81 | + | |
82 | + if (!reviewComment) { | |
83 | + console.error("No content received from Gemini."); | |
84 | + process.exit(1); | |
85 | + } | |
86 | + | |
87 | + // Add a signature | |
88 | + reviewComment += "\n\n— *AI Code Review (Powered by Gemini)*"; | |
89 | + | |
90 | + // 3. Post to GitHub | |
91 | + console.log("Posting review to GitHub PR..."); | |
92 | + const ghResponse = await fetch(`https://api.github.com/repos/${REPO}/pulls/${PR_NUMBER}/reviews`, { | |
93 | + method: 'POST', | |
94 | + headers: { | |
95 | + 'Authorization': `Bearer ${GITHUB_TOKEN}`, | |
96 | + 'Accept': 'application/vnd.github.v3+json', | |
97 | + 'X-GitHub-Api-Version': '2022-11-28' | |
98 | + }, | |
99 | + body: JSON.stringify({ | |
100 | + body: reviewComment, | |
101 | + event: 'COMMENT' | |
102 | + }) | |
103 | + }); | |
104 | + | |
105 | + if (!ghResponse.ok) { | |
106 | + const ghError = await ghResponse.text(); | |
107 | + console.error(`GitHub API Error: ${ghResponse.status} - ${ghError}`); | |
108 | + process.exit(1); | |
109 | + } | |
110 | + | |
111 | + console.log("Review successfully posted!"); | |
112 | + | |
113 | + } catch (error) { | |
114 | + console.error("Error during AI review:", error); | |
115 | + process.exit(1); | |
116 | + } | |
117 | +} | |
118 | + | |
119 | +run(); | |
0 | 120 | |