feat: add review mode for PR code reviews

- Add 'review' as a new execution mode in action.yml
- Use default GitHub Action token (ACTIONS_TOKEN) for review mode
- Create review mode implementation with GitHub MCP tools included by default
- Move review-specific prompt to review mode's generatePrompt method
- Add comprehensive review workflow instructions for inline comments
- Fix type safety with proper mode validation
- Keep agent mode's simple inline prompt handling
This commit is contained in:
km-anthropic
2025-07-30 15:50:57 -07:00
parent 1f6e3225b0
commit bbc8c6d6d5
6 changed files with 312 additions and 9 deletions

View File

@@ -530,6 +530,7 @@ export function generatePrompt(
context: PreparedContext,
githubData: FetchDataResult,
useCommitSigning: boolean,
mode?: Mode,
): string {
if (context.overridePrompt) {
return substitutePromptVariables(
@@ -539,6 +540,14 @@ export function generatePrompt(
);
}
// Delegate to mode-specific prompt generators if available
if (mode?.generatePrompt) {
const modePrompt = mode.generatePrompt(context, githubData);
if (modePrompt) {
return modePrompt;
}
}
const {
contextData,
comments,
@@ -808,9 +817,11 @@ export async function createPrompt(
try {
// Prepare the context for prompt generation
let claudeCommentId: string = "";
if (mode.name === "tag") {
if (mode.name === "tag" || mode.name === "review") {
if (!modeContext.commentId) {
throw new Error("Tag mode requires a comment ID for prompt generation");
throw new Error(
`${mode.name} mode requires a comment ID for prompt generation`,
);
}
claudeCommentId = modeContext.commentId.toString();
}
@@ -831,6 +842,7 @@ export async function createPrompt(
preparedContext,
githubData,
context.inputs.useCommitSigning,
mode,
);
// Log the final prompt to console

View File

@@ -10,13 +10,35 @@ import { setupGitHubToken } from "../github/token";
import { checkWritePermissions } from "../github/validation/permissions";
import { createOctokit } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry";
import { getMode, isValidMode, DEFAULT_MODE } from "../modes/registry";
import type { ModeName } from "../modes/types";
import { prepare } from "../prepare";
async function run() {
try {
// Step 1: Setup GitHub token
const githubToken = await setupGitHubToken();
// Step 1: Get mode first to determine authentication method
const modeInput = process.env.MODE || DEFAULT_MODE;
// Validate mode input
if (!isValidMode(modeInput)) {
throw new Error(`Invalid mode: ${modeInput}`);
}
const validatedMode: ModeName = modeInput;
// Step 2: Setup GitHub token based on mode
let githubToken: string;
if (validatedMode === "review") {
// For review mode, use the default GitHub Action token
githubToken = process.env.ACTIONS_TOKEN || "";
if (!githubToken) {
throw new Error("ACTIONS_TOKEN not found for review mode");
}
console.log("Using default GitHub Action token for review mode");
core.setOutput("GITHUB_TOKEN", githubToken);
} else {
// For other modes, use the existing token exchange
githubToken = await setupGitHubToken();
}
const octokit = createOctokit(githubToken);
// Step 2: Parse GitHub context (once for all operations)
@@ -36,7 +58,7 @@ async function run() {
}
// Step 4: Get mode and check trigger conditions
const mode = getMode(context.inputs.mode, context);
const mode = getMode(validatedMode, context);
const containsTrigger = mode.shouldTrigger(context);
// Set output for action.yml to check

View File

@@ -13,11 +13,12 @@
import type { Mode, ModeName } from "./types";
import { tagMode } from "./tag";
import { agentMode } from "./agent";
import { reviewMode } from "./review";
import type { GitHubContext } from "../github/context";
import { isAutomationContext } from "../github/context";
export const DEFAULT_MODE = "tag" as const;
export const VALID_MODES = ["tag", "agent"] as const;
export const VALID_MODES = ["tag", "agent", "review"] as const;
/**
* All available modes.
@@ -26,6 +27,7 @@ export const VALID_MODES = ["tag", "agent"] as const;
const modes = {
tag: tagMode,
agent: agentMode,
review: reviewMode,
} as const satisfies Record<ModeName, Mode>;
/**

257
src/modes/review/index.ts Normal file
View File

@@ -0,0 +1,257 @@
import * as core from "@actions/core";
import type { Mode, ModeOptions, ModeResult } from "../types";
import { checkContainsTrigger } from "../../github/validation/trigger";
import { createInitialComment } from "../../github/operations/comments/create-initial";
import { setupBranch } from "../../github/operations/branch";
import { configureGitAuth } from "../../github/operations/git-config";
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { fetchGitHubData } from "../../github/data/fetcher";
import type { FetchDataResult } from "../../github/data/fetcher";
import { createPrompt } from "../../create-prompt";
import type { PreparedContext } from "../../create-prompt";
import { isEntityContext } from "../../github/context";
import {
formatContext,
formatBody,
formatComments,
formatReviewComments,
formatChangedFilesWithSHA,
} from "../../github/data/formatter";
/**
* Review mode implementation.
*
* Code review mode that uses the default GitHub Action token
* and focuses on providing inline comments and suggestions.
* Automatically includes GitHub MCP tools for review operations.
*/
export const reviewMode: Mode = {
name: "review",
description: "Code review mode for inline comments and suggestions",
shouldTrigger(context) {
if (!isEntityContext(context)) {
return false;
}
return checkContainsTrigger(context);
},
prepareContext(context, data) {
return {
mode: "review",
githubContext: context,
commentId: data?.commentId,
baseBranch: data?.baseBranch,
claudeBranch: data?.claudeBranch,
};
},
getAllowedTools() {
return ["mcp__github__*"];
},
getDisallowedTools() {
return [];
},
shouldCreateTrackingComment() {
return true;
},
generatePrompt(
context: PreparedContext,
githubData: FetchDataResult,
): string {
const {
contextData,
comments,
changedFilesWithSHA,
reviewData,
imageUrlMap,
} = githubData;
const { eventData } = context;
const formattedContext = formatContext(contextData, true); // Reviews are always for PRs
const formattedComments = formatComments(comments, imageUrlMap);
const formattedReviewComments = formatReviewComments(
reviewData,
imageUrlMap,
);
const formattedChangedFiles =
formatChangedFilesWithSHA(changedFilesWithSHA);
const formattedBody = contextData?.body
? formatBody(contextData.body, imageUrlMap)
: "No description provided";
return `You are Claude, an AI assistant specialized in code reviews for GitHub pull requests. You are operating in REVIEW MODE, which means you should focus on providing thorough code review feedback using GitHub MCP tools for inline comments and suggestions.
<formatted_context>
${formattedContext}
</formatted_context>
<comments>
${formattedComments || "No comments yet"}
</comments>
<review_comments>
${formattedReviewComments || "No review comments"}
</review_comments>
<changed_files>
${formattedChangedFiles}
</changed_files>
<formatted_body>
${formattedBody}
</formatted_body>
${
(eventData.eventName === "issue_comment" ||
eventData.eventName === "pull_request_review_comment" ||
eventData.eventName === "pull_request_review") &&
eventData.commentBody
? `<trigger_comment>
User @${context.triggerUsername}: ${eventData.commentBody}
</trigger_comment>`
: ""
}
${
context.directPrompt
? `<direct_prompt>
${context.directPrompt}
</direct_prompt>`
: ""
}
REVIEW MODE WORKFLOW:
1. First, understand the PR context:
- Use mcp__github__get_pull_request to get PR metadata
- Use mcp__github__get_pull_request_diff to see all changes
- Use mcp__github__get_pull_request_files to list modified files
- Read specific files using the Read tool for deeper analysis
2. Create a pending review:
- Use mcp__github__create_pending_pull_request_review to start your review
- This allows you to batch comments before submitting
3. Add inline comments:
- Use mcp__github__add_comment_to_pending_review for each issue or suggestion
- Parameters:
* path: The file path (e.g., "src/index.js")
* line: Line number for single-line comments
* startLine & line: For multi-line comments
* side: "LEFT" (old code) or "RIGHT" (new code)
* subjectType: "line" for line-level comments
* body: Your comment text
- For code suggestions, use this format in the body:
\`\`\`suggestion
corrected code here
\`\`\`
4. Submit your review:
- Use mcp__github__submit_pending_pull_request_review
- Parameters:
* event: "COMMENT" (general feedback), "REQUEST_CHANGES" (issues found), or "APPROVE" (if appropriate)
* body: Overall review summary
5. Update tracking comment:
- Use mcp__github_comment__update_claude_comment to update your progress
REVIEW GUIDELINES:
- Focus on:
* Security vulnerabilities
* Bugs and logic errors
* Performance issues
* Code quality and maintainability
* Best practices and standards
* Edge cases and error handling
- Provide:
* Specific, actionable feedback
* Code suggestions when possible
* Clear explanations of issues
* Constructive criticism
* Recognition of good practices
- Communication:
* All feedback goes through GitHub's review system
* Update your tracking comment with progress
* Be professional and respectful
Before starting, analyze the PR inside <analysis> tags:
<analysis>
- PR title and description
- Number of files changed and scope
- Type of changes (feature, bug fix, refactor, etc.)
- Key areas to focus on
- Review strategy
</analysis>
Then proceed with the review workflow described above.`;
},
async prepare({
context,
octokit,
githubToken,
}: ModeOptions): Promise<ModeResult> {
if (!isEntityContext(context)) {
throw new Error("Review mode requires entity context");
}
const commentData = await createInitialComment(octokit.rest, context);
const commentId = commentData.id;
const githubData = await fetchGitHubData({
octokits: octokit,
repository: `${context.repository.owner}/${context.repository.repo}`,
prNumber: context.entityNumber.toString(),
isPR: context.isPR,
triggerUsername: context.actor,
});
const branchInfo = await setupBranch(octokit, githubData, context);
if (!context.inputs.useCommitSigning) {
try {
await configureGitAuth(githubToken, context, commentData.user);
} catch (error) {
console.error("Failed to configure git authentication:", error);
throw error;
}
}
const modeContext = this.prepareContext(context, {
commentId,
baseBranch: branchInfo.baseBranch,
claudeBranch: branchInfo.claudeBranch,
});
await createPrompt(reviewMode, modeContext, githubData, context);
const additionalMcpConfig = process.env.MCP_CONFIG || "";
const mcpConfig = await prepareMcpConfig({
githubToken,
owner: context.repository.owner,
repo: context.repository.repo,
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
baseBranch: branchInfo.baseBranch,
additionalMcpConfig,
claudeCommentId: commentId.toString(),
allowedTools: [...this.getAllowedTools(), ...context.inputs.allowedTools],
context,
});
core.setOutput("mcp_config", mcpConfig);
return {
commentId,
branchInfo,
mcpConfig,
};
},
};

View File

@@ -1,6 +1,6 @@
import type { GitHubContext } from "../github/context";
export type ModeName = "tag" | "agent";
export type ModeName = "tag" | "agent" | "review";
export type ModeContext = {
mode: ModeName;
@@ -54,6 +54,16 @@ export type Mode = {
*/
shouldCreateTrackingComment(): boolean;
/**
* Generates a custom prompt for this mode (optional).
* If not provided, the default prompt will be used.
* @returns The prompt string or undefined to use default
*/
generatePrompt?(
context: any, // PreparedContext from create-prompt
githubData: any, // FetchDataResult from github/data/fetcher
): string | undefined;
/**
* Prepares the GitHub environment for this mode.
* Each mode decides how to handle different event types.