Skip to content

Commit 630fe51

Browse files
authored
Merge pull request #15 from demandio/main
Add support to optional parameters (context, auto approve, etc)
2 parents 97f0d05 + 7e3070d commit 630fe51

File tree

15 files changed

+394
-95
lines changed

15 files changed

+394
-95
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,12 @@ jobs:
5151
AI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
5252
AI_MODEL: "gpt-4o-mini"
5353

54-
# Optional configurations (currently not used)
55-
# REVIEW_MAX_COMMENTS: 10
56-
# EXCLUDE_PATTERNS: "**/*.md,**/*.json"
57-
# APPROVE_REVIEWS: false
58-
# REVIEW_PROJECT_CONTEXT: "This is a Node.js TypeScript project"
54+
# Optional configurations
55+
APPROVE_REVIEWS: true
56+
MAX_COMMENTS: 10 # 0 to disable
57+
PROJECT_CONTEXT: "This is a Node.js TypeScript project"
58+
CONTEXT_FILES: "package.json,README.md"
59+
EXCLUDE_PATTERNS: "**/*.md,**/*.json"
5960
```
6061
6162
## Configuration
@@ -65,10 +66,11 @@ jobs:
6566
| `AI_PROVIDER` | AI provider to use (`openai`, `anthropic`, `google`) | `openai` |
6667
| `AI_API_KEY` | API key for chosen provider | Required |
6768
| `AI_MODEL` | Model to use (see supported models below) | Provider's default |
68-
| `REVIEW_MAX_COMMENTS` | Maximum number of review comments | `10` |
69+
| `APPROVE_REVIEWS` | Whether to approve PRs automatically | `true` |
70+
| `MAX_COMMENTS` | Maximum number of review comments | `0` |
71+
| `PROJECT_CONTEXT` | Project context for better reviews | `""` |
72+
| `CONTEXT_FILES` | Files to include in review (comma-separated) | `"package.json,README.md"` |
6973
| `EXCLUDE_PATTERNS` | Files to exclude (glob patterns, comma-separated) | `""` |
70-
| `APPROVE_REVIEWS` | Whether to approve PRs automatically | `false` |
71-
| `REVIEW_PROJECT_CONTEXT` | Project context for better reviews | `""` |
7274

7375
### Supported Models
7476

action.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,21 @@ inputs:
1515
AI_API_KEY:
1616
description: "API key for the chosen AI provider"
1717
required: true
18-
REVIEW_BEHAVIOR:
19-
description: "Review behavior (suggest_approval, comment_only, full_review)"
18+
APPROVE_REVIEWS:
19+
description: "Whether to approve/reject PRs automatically"
2020
required: false
21-
default: "full_review"
21+
default: "true"
22+
MAX_COMMENTS:
23+
description: "Maximum number of review comments"
24+
required: false
25+
default: "0"
2226
PROJECT_CONTEXT:
2327
description: "Additional context about the project"
2428
required: false
29+
CONTEXT_FILES:
30+
description: "Files to include in review (comma-separated)"
31+
required: false
32+
default: "package.json,README.md"
2533
EXCLUDE_PATTERNS:
2634
description: "Files to exclude (glob patterns)"
2735
required: false

dist/index.js

Lines changed: 141 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ main().catch(error => {
118118
"use strict";
119119

120120
Object.defineProperty(exports, "__esModule", ({ value: true }));
121-
exports.baseCodeReviewPrompt = exports.outputFormat = void 0;
121+
exports.updateReviewPrompt = exports.baseCodeReviewPrompt = exports.outputFormat = void 0;
122122
exports.outputFormat = `
123123
{
124124
"summary": "",
@@ -162,7 +162,15 @@ For the "comments" field, provide a list of comments. Each comment should have t
162162
- path: The path to the file that the comment is about
163163
- line: The line number in the file that the comment is about
164164
- comment: The comment text
165-
Comments should ONLY be added to lines or blocks of code that have issues.
165+
Other rules for "comments" field:
166+
- Comments should ONLY be added to lines or blocks of code that have issues.
167+
- ONLY use line numbers that appear in the "diff" property of each file
168+
- Each diff line starts with a prefix:
169+
* "normal" for unchanged lines
170+
* "del" for removed lines
171+
* "add" for added lines
172+
- Extract the line number that appears after the prefix
173+
- DO NOT use line number 0 or line numbers not present in the diff
166174

167175
For the "suggestedAction" field, provide a single word that indicates the action to be taken. Options are:
168176
- "approve"
@@ -171,6 +179,15 @@ For the "suggestedAction" field, provide a single word that indicates the action
171179

172180
For the "confidence" field, provide a number between 0 and 100 that indicates the confidence in the verdict.
173181
`;
182+
exports.updateReviewPrompt = `
183+
When reviewing updates to a PR:
184+
1. Focus on the modified sections but consider their context
185+
2. Reference previous comments if they're still relevant
186+
3. Acknowledge fixed issues from previous reviews
187+
4. Only comment on new issues or unresolved previous issues
188+
5. Consider the cumulative impact of changes
189+
6. IMPORTANT: Only use line numbers that appear in the current "diff" field
190+
`;
174191
exports["default"] = exports.baseCodeReviewPrompt;
175192

176193

@@ -256,16 +273,15 @@ class AnthropicProvider {
256273
}
257274
async review(request) {
258275
var _a;
259-
const prompt = this.buildPrompt(request);
260-
core.info(`Sending request to Anthropic with prompt structure: ${JSON.stringify(request, null, 2)}`);
276+
core.debug(`Sending request to Anthropic with prompt structure: ${JSON.stringify(request, null, 2)}`);
261277
const response = await this.client.messages.create({
262278
model: this.config.model,
263279
max_tokens: 4000,
264-
system: prompts_1.baseCodeReviewPrompt,
280+
system: this.buildSystemPrompt(request),
265281
messages: [
266282
{
267283
role: 'user',
268-
content: prompt,
284+
content: this.buildPullRequestPrompt(request),
269285
},
270286
{
271287
role: 'user',
@@ -274,19 +290,35 @@ class AnthropicProvider {
274290
],
275291
temperature: (_a = this.config.temperature) !== null && _a !== void 0 ? _a : 0.3,
276292
});
277-
core.info(`Raw Anthropic response: ${JSON.stringify(response.content[0].text, null, 2)}`);
293+
core.debug(`Raw Anthropic response: ${JSON.stringify(response.content[0].text, null, 2)}`);
278294
const parsedResponse = this.parseResponse(response);
279295
core.info(`Parsed response: ${JSON.stringify(parsedResponse, null, 2)}`);
280296
return parsedResponse;
281297
}
282-
buildPrompt(request) {
298+
buildPullRequestPrompt(request) {
299+
var _a;
283300
return JSON.stringify({
284301
type: 'code_review',
285302
files: request.files,
286303
pr: request.pullRequest,
287304
context: request.context,
305+
previousReviews: (_a = request.previousReviews) === null || _a === void 0 ? void 0 : _a.map(review => ({
306+
summary: review.summary,
307+
lineComments: review.lineComments.map(comment => ({
308+
path: comment.path,
309+
line: comment.line,
310+
comment: comment.comment
311+
}))
312+
}))
288313
});
289314
}
315+
buildSystemPrompt(request) {
316+
const isUpdate = request.context.isUpdate;
317+
return `
318+
${prompts_1.baseCodeReviewPrompt}
319+
${isUpdate ? prompts_1.updateReviewPrompt : ''}
320+
`;
321+
}
290322
parseResponse(response) {
291323
try {
292324
const content = JSON.parse(response.content[0].text);
@@ -368,33 +400,50 @@ class GeminiProvider {
368400
});
369401
}
370402
async review(request) {
371-
const prompt = this.buildPrompt(request);
372-
core.info(`Sending request to Gemini with prompt structure: ${JSON.stringify(request, null, 2)}`);
403+
core.debug(`Sending request to Gemini with prompt structure: ${JSON.stringify(request, null, 2)}`);
373404
const result = await this.model.generateContent({
374-
systemInstruction: prompts_1.baseCodeReviewPrompt,
405+
systemInstruction: this.buildSystemPrompt(request),
375406
contents: [
376407
{
377408
role: 'user',
378409
parts: [
379410
{
380-
text: prompt,
411+
text: this.buildPullRequestPrompt(request),
381412
}
382413
]
383414
}
384415
]
385416
});
386417
const response = result.response;
387-
core.info(`Raw Gemini response: ${JSON.stringify(response.text(), null, 2)}`);
388-
return this.parseResponse(response);
418+
core.debug(`Raw Gemini response: ${JSON.stringify(response.text(), null, 2)}`);
419+
const parsedResponse = this.parseResponse(response);
420+
core.info(`Parsed response: ${JSON.stringify(parsedResponse, null, 2)}`);
421+
return parsedResponse;
389422
}
390-
buildPrompt(request) {
423+
buildPullRequestPrompt(request) {
424+
var _a;
391425
return JSON.stringify({
392426
type: 'code_review',
393427
files: request.files,
394428
pr: request.pullRequest,
395429
context: request.context,
430+
previousReviews: (_a = request.previousReviews) === null || _a === void 0 ? void 0 : _a.map(review => ({
431+
summary: review.summary,
432+
lineComments: review.lineComments.map(comment => ({
433+
path: comment.path,
434+
line: comment.line,
435+
comment: comment.comment
436+
}))
437+
}))
396438
});
397439
}
440+
buildSystemPrompt(request) {
441+
const isUpdate = request.context.isUpdate;
442+
return `
443+
${prompts_1.baseCodeReviewPrompt}
444+
${isUpdate ? prompts_1.updateReviewPrompt : ''}
445+
`;
446+
}
398447
parseResponse(response) {
399448
try {
400449
const content = JSON.parse(response.text());
@@ -474,36 +523,51 @@ class OpenAIProvider {
474523
}
475524
async review(request) {
476525
var _a;
477-
const prompt = this.buildPrompt(request);
478526
core.info(`Sending request to OpenAI with prompt structure: ${JSON.stringify(request, null, 2)}`);
479527
const response = await this.client.chat.completions.create({
480528
model: this.config.model,
481529
messages: [
482530
{
483531
role: 'system',
484-
content: prompts_1.baseCodeReviewPrompt,
532+
content: this.buildSystemPrompt(request),
485533
},
486534
{
487535
role: 'user',
488-
content: prompt,
536+
content: this.buildPullRequestPrompt(request),
489537
},
490538
],
491539
temperature: (_a = this.config.temperature) !== null && _a !== void 0 ? _a : 0.3,
492540
response_format: { type: 'json_object' },
493541
});
494-
core.info(`Raw OpenAI response: ${JSON.stringify(response.choices[0].message.content, null, 2)}`);
542+
core.debug(`Raw OpenAI response: ${JSON.stringify(response.choices[0].message.content, null, 2)}`);
495543
const parsedResponse = this.parseResponse(response);
496544
core.info(`Parsed response: ${JSON.stringify(parsedResponse, null, 2)}`);
497545
return parsedResponse;
498546
}
499-
buildPrompt(request) {
547+
buildPullRequestPrompt(request) {
548+
var _a;
500549
return JSON.stringify({
501550
type: 'code_review',
502551
files: request.files,
503552
pr: request.pullRequest,
504553
context: request.context,
554+
previousReviews: (_a = request.previousReviews) === null || _a === void 0 ? void 0 : _a.map(review => ({
555+
summary: review.summary,
556+
lineComments: review.lineComments.map(comment => ({
557+
path: comment.path,
558+
line: comment.line,
559+
comment: comment.comment
560+
}))
561+
}))
505562
});
506563
}
564+
buildSystemPrompt(request) {
565+
const isUpdate = request.context.isUpdate;
566+
return `
567+
${prompts_1.baseCodeReviewPrompt}
568+
${isUpdate ? prompts_1.updateReviewPrompt : ''}
569+
`;
570+
}
507571
parseResponse(response) {
508572
var _a;
509573
// Implement response parsing
@@ -575,23 +639,26 @@ class DiffService {
575639
.map(p => p.trim());
576640
}
577641
async getRelevantFiles(prDetails, lastReviewedCommit) {
578-
const baseUrl = `https://api.github.com/repos/${prDetails.owner}/${prDetails.repo}/pulls/${prDetails.number}`;
642+
const baseUrl = `https://api.github.com/repos/${prDetails.owner}/${prDetails.repo}`;
579643
const diffUrl = lastReviewedCommit ?
580-
`${baseUrl}/compare/${lastReviewedCommit}...${prDetails.head}.diff` :
581-
`${baseUrl}.diff`;
644+
`${baseUrl}/compare/${lastReviewedCommit}...${prDetails.head}` :
645+
`${baseUrl}/pulls/${prDetails.number}`;
582646
const response = await fetch(diffUrl, {
583647
headers: {
584648
'Authorization': `Bearer ${this.githubToken}`,
585-
'Accept': 'application/vnd.github.v3.diff'
649+
'Accept': 'application/vnd.github.v3.diff',
650+
'X-GitHub-Api-Version': '2022-11-28'
586651
}
587652
});
588653
if (!response.ok) {
589-
core.error(`Failed to fetch diff: ${await response.text()}`);
654+
const errorText = await response.text();
655+
core.error(`Failed to fetch diff from ${diffUrl}: ${errorText}`);
590656
throw new Error(`Failed to fetch diff: ${response.statusText}`);
591657
}
592658
const diffText = await response.text();
593-
core.info(`Diff text length: ${diffText.length}`);
659+
core.debug(`Full diff text length: ${diffText.length}`);
594660
const files = (0, parse_diff_1.default)(diffText);
661+
core.info(`Found ${files.length} files in diff`);
595662
return this.filterRelevantFiles(files);
596663
}
597664
filterRelevantFiles(files) {
@@ -805,6 +872,34 @@ class GitHubService {
805872
});
806873
return (lastCommit === null || lastCommit === void 0 ? void 0 : lastCommit.sha) || null;
807874
}
875+
async getPreviousReviews(prNumber) {
876+
const { data: reviews } = await this.octokit.pulls.listReviews({
877+
owner: this.owner,
878+
repo: this.repo,
879+
pull_number: prNumber,
880+
});
881+
// Filter to bot reviews and fetch their comments
882+
const botReviews = reviews.filter(review => { var _a; return ((_a = review.user) === null || _a === void 0 ? void 0 : _a.login) === 'github-actions[bot]'; });
883+
core.debug(`Found ${botReviews.length} bot reviews`);
884+
const botReviewsWithComments = await Promise.all(botReviews.map(async (review) => {
885+
const { data: comments } = await this.octokit.pulls.listReviewComments({
886+
owner: this.owner,
887+
repo: this.repo,
888+
pull_number: prNumber,
889+
review_id: review.id
890+
});
891+
return {
892+
commit: review.commit_id,
893+
summary: review.body || '',
894+
lineComments: comments.map(comment => ({
895+
path: comment.path,
896+
line: comment.line || 0,
897+
comment: comment.body
898+
}))
899+
};
900+
}));
901+
return botReviewsWithComments;
902+
}
808903
}
809904
exports.GitHubService = GitHubService;
810905

@@ -865,21 +960,33 @@ class ReviewService {
865960
const prDetails = await this.githubService.getPRDetails(prNumber);
866961
core.info(`PR title: ${prDetails.title}`);
867962
// Get modified files from diff
868-
const modifiedFiles = await this.diffService.getRelevantFiles(prDetails);
963+
const lastReviewedCommit = await this.githubService.getLastReviewedCommit(prNumber);
964+
const isUpdate = !!lastReviewedCommit;
965+
// If this is an update, get previous reviews
966+
let previousReviews;
967+
if (isUpdate) {
968+
previousReviews = await this.githubService.getPreviousReviews(prNumber);
969+
core.debug(`Found ${previousReviews.length} previous reviews`);
970+
}
971+
const modifiedFiles = await this.diffService.getRelevantFiles(prDetails, lastReviewedCommit);
869972
core.info(`Modified files length: ${modifiedFiles.length}`);
870973
// Get full content for each modified file
871-
const filesWithContent = await Promise.all(modifiedFiles.map(async (file) => ({
872-
path: file.path,
873-
content: await this.githubService.getFileContent(file.path, prDetails.head),
874-
originalContent: await this.githubService.getFileContent(file.path, prDetails.base),
875-
diff: file.diff,
876-
})));
974+
const filesWithContent = await Promise.all(modifiedFiles.map(async (file) => {
975+
const fullContent = await this.githubService.getFileContent(file.path, prDetails.head);
976+
return {
977+
path: file.path,
978+
content: fullContent,
979+
originalContent: await this.githubService.getFileContent(file.path, prDetails.base),
980+
diff: file.diff,
981+
};
982+
}));
877983
// Get repository context (package.json, readme, etc)
878984
const contextFiles = await this.getRepositoryContext();
879985
// Perform AI review
880986
const review = await this.aiProvider.review({
881987
files: filesWithContent,
882988
contextFiles,
989+
previousReviews,
883990
pullRequest: {
884991
title: prDetails.title,
885992
description: prDetails.description,
@@ -890,6 +997,7 @@ class ReviewService {
890997
repository: (_a = process.env.GITHUB_REPOSITORY) !== null && _a !== void 0 ? _a : '',
891998
owner: (_b = process.env.GITHUB_REPOSITORY_OWNER) !== null && _b !== void 0 ? _b : '',
892999
projectContext: process.env.INPUT_PROJECT_CONTEXT,
1000+
isUpdate,
8931001
},
8941002
});
8951003
// Add model name to summary

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)