mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-14 22:02:59 -07:00
🤖 Implement Gemini PR Pre-review Reflex
This PR adds a new reflex action to the Gemini CLI Bot that automatically provides high-level pre-reviews for new pull requests. ### Changes: - Created `tools/gemini-cli-bot/reflexes/scripts/pr_pre_review.ts`: A reflex script that uses Gemini to evaluate PRs based on quality, roadmap alignment, "obvious wins," and repository best practices. - Updated `.github/workflows/gemini-cli-bot-pulse.yml`: Added `GEMINI_API_KEY` and ensure the Gemini CLI is bundled before running reflex actions. ### Impact: - **Time to First Response**: Will decrease as the bot provides initial feedback within 30 minutes of PR submission. - **Maintainer Productivity**: High-level assessments and roadmap alignment checks will help maintainers prioritize reviews. - **Contributor Experience**: Immediate feedback on best practices and roadmap alignment. This addresses the request in issue #26471.
This commit is contained in:
@@ -34,9 +34,14 @@ jobs:
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci'
|
||||
|
||||
- name: 'Build Gemini CLI'
|
||||
run: 'npm run bundle'
|
||||
|
||||
- name: 'Run Reflex Processes'
|
||||
env:
|
||||
GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
GEMINI_API_KEY: '${{ secrets.GEMINI_API_KEY }}'
|
||||
GEMINI_MODEL: 'gemini-3-flash-preview'
|
||||
run: |
|
||||
if [ -d "tools/gemini-cli-bot/reflexes/scripts" ] && [ "$(ls -A tools/gemini-cli-bot/reflexes/scripts)" ]; then
|
||||
for script in tools/gemini-cli-bot/reflexes/scripts/*.ts; do
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { execFileSync, execSync } from 'node:child_process';
|
||||
import { readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const REVIEW_HEADER = '## 🤖 Gemini Bot Pre-review';
|
||||
|
||||
/**
|
||||
* Runs a shell command and returns the output.
|
||||
*/
|
||||
function runCommand(cmd: string, args: string[], options: any = {}): string {
|
||||
try {
|
||||
return execFileSync(cmd, args, {
|
||||
encoding: 'utf-8',
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
...options,
|
||||
}).trim();
|
||||
} catch (err) {
|
||||
// Silence errors for expected failures or missing data
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting Gemini PR Pre-review Reflex...');
|
||||
|
||||
// 1. Get open PRs
|
||||
const prsJson = runCommand('gh', [
|
||||
'pr',
|
||||
'list',
|
||||
'--state',
|
||||
'open',
|
||||
'--limit',
|
||||
'20',
|
||||
'--json',
|
||||
'number,title,body,author,labels',
|
||||
]);
|
||||
|
||||
if (!prsJson) {
|
||||
console.log('No open PRs found or gh command failed.');
|
||||
return;
|
||||
}
|
||||
|
||||
let prs;
|
||||
try {
|
||||
prs = JSON.parse(prsJson);
|
||||
} catch (err) {
|
||||
console.error('Failed to parse PR list JSON.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const pr of prs) {
|
||||
// Skip bot PRs
|
||||
if (pr.author.login.includes('[bot]') || pr.author.login.includes('robot')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Check if already reviewed by the bot
|
||||
const commentsJson = runCommand('gh', [
|
||||
'pr',
|
||||
'view',
|
||||
pr.number.toString(),
|
||||
'--json',
|
||||
'comments',
|
||||
]);
|
||||
if (!commentsJson) continue;
|
||||
|
||||
let comments;
|
||||
try {
|
||||
comments = JSON.parse(commentsJson).comments;
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const alreadyReviewed = comments.some(
|
||||
(c: any) => c.body && c.body.includes(REVIEW_HEADER),
|
||||
);
|
||||
|
||||
if (alreadyReviewed) {
|
||||
console.log(`PR #${pr.number} already has a Gemini pre-review. Skipping.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log(`Pre-reviewing PR #${pr.number}: ${pr.title}`);
|
||||
|
||||
// 3. Get Diff
|
||||
const diff = runCommand('gh', ['pr', 'diff', pr.number.toString()]);
|
||||
if (!diff) {
|
||||
console.log(`Could not get diff for PR #${pr.number}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Prepare Prompt
|
||||
const roadmapPath = join(process.cwd(), 'ROADMAP.md');
|
||||
const roadmap = existsSync(roadmapPath) ? readFileSync(roadmapPath, 'utf-8') : 'No roadmap found.';
|
||||
|
||||
const prompt = `
|
||||
You are the Gemini CLI Bot. Your task is to perform a pre-review of a Pull Request.
|
||||
Provide a high-level assessment based on:
|
||||
1. **Objective measures of quality**: (code structure, tests, documentation, performance).
|
||||
2. **Alignment with the project roadmap**: (roadmap provided below).
|
||||
3. **Identification of "obvious wins"**: (significant improvements, critical bug fixes, "good to go" small PRs).
|
||||
4. **Conformity with TypeScript and repository best practices**: (license headers, type safety, naming conventions).
|
||||
|
||||
Roadmap:
|
||||
${roadmap}
|
||||
|
||||
PR Title: ${pr.title}
|
||||
PR Body: ${pr.body || 'No description provided.'}
|
||||
|
||||
PR Diff:
|
||||
${diff.substring(0, 50000)}
|
||||
|
||||
Output your review in Markdown.
|
||||
Rules:
|
||||
- Start with the header: ${REVIEW_HEADER}
|
||||
- Use sections for the 4 points above.
|
||||
- If you find a "roadmap match", mention it explicitly.
|
||||
- If it's an "obvious win", recommend it for fast-track review.
|
||||
- Suggest 1-3 labels if appropriate (e.g., area/core, status/needs-tests, priority/p2).
|
||||
- Be concise, professional, and encouraging.
|
||||
- Do NOT use tools. Just output the text of the review.
|
||||
`;
|
||||
|
||||
// 5. Run Gemini
|
||||
const promptFile = join(process.cwd(), `pr_prompt_${pr.number}.md`);
|
||||
writeFileSync(promptFile, prompt);
|
||||
|
||||
let review = '';
|
||||
try {
|
||||
const geminiPath = join(process.cwd(), 'bundle', 'gemini.js');
|
||||
if (existsSync(geminiPath)) {
|
||||
review = execFileSync('node', [
|
||||
geminiPath,
|
||||
'--prompt-file', promptFile
|
||||
], {
|
||||
encoding: 'utf-8',
|
||||
env: {
|
||||
...process.env,
|
||||
GEMINI_CLI_TRUST_WORKSPACE: 'true',
|
||||
GEMINI_MODEL: process.env.GEMINI_MODEL || 'gemini-3-flash-preview'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('bundle/gemini.js not found. Cannot perform review.');
|
||||
continue;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error running Gemini for PR #${pr.number}:`, err);
|
||||
continue;
|
||||
} finally {
|
||||
if (existsSync(promptFile)) rmSync(promptFile);
|
||||
}
|
||||
|
||||
if (!review || review.trim().length === 0) {
|
||||
console.log(`Gemini returned an empty review for PR #${pr.number}.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 6. Post Comment
|
||||
const commentFile = join(process.cwd(), `pr_review_${pr.number}.md`);
|
||||
writeFileSync(commentFile, review);
|
||||
|
||||
try {
|
||||
execFileSync('gh', ['pr', 'comment', pr.number.toString(), '--body-file', commentFile]);
|
||||
console.log(`Successfully posted pre-review for PR #${pr.number}.`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to post comment for PR #${pr.number}:`, err);
|
||||
} finally {
|
||||
if (existsSync(commentFile)) rmSync(commentFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Fatal error in PR pre-review reflex:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user