mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
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:
@@ -26,7 +26,7 @@ inputs:
|
|||||||
|
|
||||||
# Mode configuration
|
# Mode configuration
|
||||||
mode:
|
mode:
|
||||||
description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking)"
|
description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking), 'review' (for code reviews with inline comments and suggestions)"
|
||||||
required: false
|
required: false
|
||||||
default: "tag"
|
default: "tag"
|
||||||
|
|
||||||
|
|||||||
@@ -530,6 +530,7 @@ export function generatePrompt(
|
|||||||
context: PreparedContext,
|
context: PreparedContext,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
useCommitSigning: boolean,
|
useCommitSigning: boolean,
|
||||||
|
mode?: Mode,
|
||||||
): string {
|
): string {
|
||||||
if (context.overridePrompt) {
|
if (context.overridePrompt) {
|
||||||
return substitutePromptVariables(
|
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 {
|
const {
|
||||||
contextData,
|
contextData,
|
||||||
comments,
|
comments,
|
||||||
@@ -808,9 +817,11 @@ export async function createPrompt(
|
|||||||
try {
|
try {
|
||||||
// Prepare the context for prompt generation
|
// Prepare the context for prompt generation
|
||||||
let claudeCommentId: string = "";
|
let claudeCommentId: string = "";
|
||||||
if (mode.name === "tag") {
|
if (mode.name === "tag" || mode.name === "review") {
|
||||||
if (!modeContext.commentId) {
|
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();
|
claudeCommentId = modeContext.commentId.toString();
|
||||||
}
|
}
|
||||||
@@ -831,6 +842,7 @@ export async function createPrompt(
|
|||||||
preparedContext,
|
preparedContext,
|
||||||
githubData,
|
githubData,
|
||||||
context.inputs.useCommitSigning,
|
context.inputs.useCommitSigning,
|
||||||
|
mode,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log the final prompt to console
|
// Log the final prompt to console
|
||||||
|
|||||||
@@ -10,13 +10,35 @@ import { setupGitHubToken } from "../github/token";
|
|||||||
import { checkWritePermissions } from "../github/validation/permissions";
|
import { checkWritePermissions } from "../github/validation/permissions";
|
||||||
import { createOctokit } from "../github/api/client";
|
import { createOctokit } from "../github/api/client";
|
||||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
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";
|
import { prepare } from "../prepare";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
// Step 1: Setup GitHub token
|
// Step 1: Get mode first to determine authentication method
|
||||||
const githubToken = await setupGitHubToken();
|
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);
|
const octokit = createOctokit(githubToken);
|
||||||
|
|
||||||
// Step 2: Parse GitHub context (once for all operations)
|
// Step 2: Parse GitHub context (once for all operations)
|
||||||
@@ -36,7 +58,7 @@ async function run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 4: Get mode and check trigger conditions
|
// 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);
|
const containsTrigger = mode.shouldTrigger(context);
|
||||||
|
|
||||||
// Set output for action.yml to check
|
// Set output for action.yml to check
|
||||||
|
|||||||
@@ -13,11 +13,12 @@
|
|||||||
import type { Mode, ModeName } from "./types";
|
import type { Mode, ModeName } from "./types";
|
||||||
import { tagMode } from "./tag";
|
import { tagMode } from "./tag";
|
||||||
import { agentMode } from "./agent";
|
import { agentMode } from "./agent";
|
||||||
|
import { reviewMode } from "./review";
|
||||||
import type { GitHubContext } from "../github/context";
|
import type { GitHubContext } from "../github/context";
|
||||||
import { isAutomationContext } from "../github/context";
|
import { isAutomationContext } from "../github/context";
|
||||||
|
|
||||||
export const DEFAULT_MODE = "tag" as const;
|
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.
|
* All available modes.
|
||||||
@@ -26,6 +27,7 @@ export const VALID_MODES = ["tag", "agent"] as const;
|
|||||||
const modes = {
|
const modes = {
|
||||||
tag: tagMode,
|
tag: tagMode,
|
||||||
agent: agentMode,
|
agent: agentMode,
|
||||||
|
review: reviewMode,
|
||||||
} as const satisfies Record<ModeName, Mode>;
|
} as const satisfies Record<ModeName, Mode>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
257
src/modes/review/index.ts
Normal file
257
src/modes/review/index.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { GitHubContext } from "../github/context";
|
import type { GitHubContext } from "../github/context";
|
||||||
|
|
||||||
export type ModeName = "tag" | "agent";
|
export type ModeName = "tag" | "agent" | "review";
|
||||||
|
|
||||||
export type ModeContext = {
|
export type ModeContext = {
|
||||||
mode: ModeName;
|
mode: ModeName;
|
||||||
@@ -54,6 +54,16 @@ export type Mode = {
|
|||||||
*/
|
*/
|
||||||
shouldCreateTrackingComment(): boolean;
|
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.
|
* Prepares the GitHub environment for this mode.
|
||||||
* Each mode decides how to handle different event types.
|
* Each mode decides how to handle different event types.
|
||||||
|
|||||||
Reference in New Issue
Block a user