Compare commits

..

8 Commits

Author SHA1 Message Date
GitHub Actions
1b4ac7d7e0 chore: bump Claude Code version to 1.0.65 2025-07-31 22:02:40 +00:00
GitHub Actions
1f6e3225b0 chore: bump Claude Code version to 1.0.64 2025-07-30 21:49:34 +00:00
atsushi-ishibashi
6672e9b357 Remove empty XML tags in Issue context to reduce token usage (#369)
* chore: remove empty xml tags

* format
2025-07-30 07:30:32 -07:00
aki77
950bdc01df fix: update GitHub MCP server tool name for PR review comments (#363)
Update add_pull_request_review_comment_to_pending_review to add_comment_to_pending_review
  following upstream change in github/github-mcp-server#697

  - Update .github/workflows/claude-review.yml
  - Update examples/claude-auto-review.yml
2025-07-30 07:20:20 -07:00
atsushi-ishibashi
15dd796e97 use total_cost_usd (#366) 2025-07-30 07:19:29 -07:00
atsushi-ishibashi
fd012347a2 feat: exclude hidden (minimized) comments from GitHub Issues and PRs (#368)
* feat: ignore minimized comments

* fix tests
2025-07-30 07:18:34 -07:00
Ashwin Bhat
5bdc533a52 docs: enhance CLAUDE.md with comprehensive architecture overview (#362)
* docs: enhance CLAUDE.md with comprehensive architecture overview

- Add detailed two-phase execution architecture documentation
- Document mode system (tag/agent) and extensible registry pattern
- Include comprehensive GitHub integration layer breakdown
- Add MCP server architecture and authentication flow details
- Document branch strategy, comment threading, and code conventions
- Provide complete project structure with component descriptions

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* docs: clarify base-action dual purpose and remove branch strategy

- Explain base-action serves as both standalone published action and internal logic
- Remove branch strategy section as requested
- Improve architecture documentation clarity

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-29 18:03:45 -07:00
YutaSaito
d45539c118 fix: move env var before image name in docker run for github-mcp-server (#361)
In the previous commit (e07ea013bd), the GITHUB_HOST variable was placed after the image name in the Docker run command, which caused a runtime error. This commit moves the -e option before the image name so it is correctly passed into the container.
2025-07-29 16:22:39 -07:00
12 changed files with 345 additions and 44 deletions

View File

@@ -30,4 +30,4 @@ jobs:
Be constructive and specific in your feedback. Give inline comments where applicable. Be constructive and specific in your feedback. Give inline comments where applicable.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff" allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"

128
CLAUDE.md
View File

@@ -1,10 +1,11 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Tools ## Development Tools
- Runtime: Bun 1.2.11 - Runtime: Bun 1.2.11
- TypeScript with strict configuration
## Common Development Tasks ## Common Development Tasks
@@ -17,42 +18,119 @@ bun test
# Formatting # Formatting
bun run format # Format code with prettier bun run format # Format code with prettier
bun run format:check # Check code formatting bun run format:check # Check code formatting
# Type checking
bun run typecheck # Run TypeScript type checker
``` ```
## Architecture Overview ## Architecture Overview
This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action: This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action operates in two main phases:
1. **Trigger Detection**: Uses `check-trigger.ts` to determine if Claude should respond based on comment/issue content ### Phase 1: Preparation (`src/entrypoints/prepare.ts`)
2. **Context Gathering**: Fetches GitHub data (PRs, issues, comments) via `github-data-fetcher.ts` and formats it using `github-data-formatter.ts`
3. **AI Integration**: Supports multiple Claude providers (Anthropic API, AWS Bedrock, Google Vertex AI)
4. **Prompt Creation**: Generates context-rich prompts using `create-prompt.ts`
5. **MCP Server Integration**: Installs and configures GitHub MCP server for extended functionality
### Key Components 1. **Authentication Setup**: Establishes GitHub token via OIDC or GitHub App
2. **Permission Validation**: Verifies actor has write permissions
3. **Trigger Detection**: Uses mode-specific logic to determine if Claude should respond
4. **Context Creation**: Prepares GitHub context and initial tracking comment
- **Trigger System**: Responds to `/claude` comments or issue assignments ### Phase 2: Execution (`base-action/`)
- **Authentication**: OIDC-based token exchange for secure GitHub interactions
- **Cloud Integration**: Supports direct Anthropic API, AWS Bedrock, and Google Vertex AI The `base-action/` directory contains the core Claude Code execution logic, which serves a dual purpose:
- **GitHub Operations**: Creates branches, posts comments, and manages PRs/issues
- **Standalone Action**: Published separately as `@anthropic-ai/claude-code-base-action` for direct use
- **Inner Logic**: Used internally by this GitHub Action after preparation phase completes
Execution steps:
1. **MCP Server Setup**: Installs and configures GitHub MCP server for tool access
2. **Prompt Generation**: Creates context-rich prompts from GitHub data
3. **Claude Integration**: Executes via multiple providers (Anthropic API, AWS Bedrock, Google Vertex AI)
4. **Result Processing**: Updates comments and creates branches/PRs as needed
### Key Architectural Components
#### Mode System (`src/modes/`)
- **Tag Mode** (`tag/`): Responds to `@claude` mentions and issue assignments
- **Agent Mode** (`agent/`): Automated execution without trigger checking
- Extensible registry pattern in `modes/registry.ts`
#### GitHub Integration (`src/github/`)
- **Context Parsing** (`context.ts`): Unified GitHub event handling
- **Data Fetching** (`data/fetcher.ts`): Retrieves PR/issue data via GraphQL/REST
- **Data Formatting** (`data/formatter.ts`): Converts GitHub data to Claude-readable format
- **Branch Operations** (`operations/branch.ts`): Handles branch creation and cleanup
- **Comment Management** (`operations/comments/`): Creates and updates tracking comments
#### MCP Server Integration (`src/mcp/`)
- **GitHub Actions Server** (`github-actions-server.ts`): Workflow and CI access
- **GitHub Comment Server** (`github-comment-server.ts`): Comment operations
- **GitHub File Operations** (`github-file-ops-server.ts`): File system access
- Auto-installation and configuration in `install-mcp-server.ts`
#### Authentication & Security (`src/github/`)
- **Token Management** (`token.ts`): OIDC token exchange and GitHub App authentication
- **Permission Validation** (`validation/permissions.ts`): Write access verification
- **Actor Validation** (`validation/actor.ts`): Human vs bot detection
### Project Structure ### Project Structure
``` ```
src/ src/
├── check-trigger.ts # Determines if Claude should respond ├── entrypoints/ # Action entry points
├── create-prompt.ts # Generates contextual prompts ├── prepare.ts # Main preparation logic
├── github-data-fetcher.ts # Retrieves GitHub data │ ├── update-comment-link.ts # Post-execution comment updates
├── github-data-formatter.ts # Formats GitHub data for prompts │ └── format-turns.ts # Claude conversation formatting
├── install-mcp-server.ts # Sets up GitHub MCP server ├── github/ # GitHub integration layer
├── update-comment-with-link.ts # Updates comments with job links │ ├── api/ # REST/GraphQL clients
└── types/ │ ├── data/ # Data fetching and formatting
── github.ts # TypeScript types for GitHub data ── operations/ # Branch, comment, git operations
│ ├── validation/ # Permission and trigger validation
│ └── utils/ # Image downloading, sanitization
├── modes/ # Execution modes
│ ├── tag/ # @claude mention mode
│ ├── agent/ # Automation mode
│ └── registry.ts # Mode selection logic
├── mcp/ # MCP server implementations
├── prepare/ # Preparation orchestration
└── utils/ # Shared utilities
``` ```
## Important Notes ## Important Implementation Notes
- Actions are triggered by `@claude` comments or issue assignment unless a different trigger_phrase is specified ### Authentication Flow
- The action creates branches for issues and pushes to PR branches directly
- All actions create OIDC tokens for secure authentication - Uses GitHub OIDC token exchange for secure authentication
- Progress is tracked through dynamic comment updates with checkboxes - Supports custom GitHub Apps via `APP_ID` and `APP_PRIVATE_KEY`
- Falls back to official Claude GitHub App if no custom app provided
### MCP Server Architecture
- Each MCP server has specific GitHub API access patterns
- Servers are auto-installed in `~/.claude/mcp/github-{type}-server/`
- Configuration merged with user-provided MCP config via `mcp_config` input
### Mode System Design
- Modes implement `Mode` interface with `shouldTrigger()` and `prepare()` methods
- Registry validates mode compatibility with GitHub event types
- Agent mode bypasses all trigger checking for automation scenarios
### Comment Threading
- Single tracking comment updated throughout execution
- Progress indicated via dynamic checkboxes
- Links to job runs and created branches/PRs
- Sticky comment option for consolidated PR comments
## Code Conventions
- Use Bun-specific TypeScript configuration with `moduleResolution: "bundler"`
- Strict TypeScript with `noUnusedLocals` and `noUnusedParameters` enabled
- Prefer explicit error handling with detailed error messages
- Use discriminated unions for GitHub context types
- Implement retry logic for GitHub API operations via `utils/retry.ts`

View File

@@ -172,7 +172,7 @@ runs:
echo "Base-action dependencies installed" echo "Base-action dependencies installed"
cd - cd -
# Install Claude Code globally # Install Claude Code globally
bun install -g @anthropic-ai/claude-code@1.0.63 bun install -g @anthropic-ai/claude-code@1.0.65
- name: Setup Network Restrictions - name: Setup Network Restrictions
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != '' if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''

View File

@@ -115,7 +115,7 @@ runs:
- name: Install Claude Code - name: Install Claude Code
shell: bash shell: bash
run: bun install -g @anthropic-ai/claude-code@1.0.63 run: bun install -g @anthropic-ai/claude-code@1.0.65
- name: Run Claude Code Action - name: Run Claude Code Action
shell: bash shell: bash

View File

@@ -35,4 +35,4 @@ jobs:
Provide constructive feedback with specific suggestions for improvement. Provide constructive feedback with specific suggestions for improvement.
Use inline comments to highlight specific areas of concern. Use inline comments to highlight specific areas of concern.
# allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff" # allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"

View File

@@ -587,23 +587,28 @@ ${formattedBody}
${formattedComments || "No comments"} ${formattedComments || "No comments"}
</comments> </comments>
<review_comments> ${
${eventData.isPR ? formattedReviewComments || "No review comments" : ""} eventData.isPR
</review_comments> ? `<review_comments>
${formattedReviewComments || "No review comments"}
</review_comments>`
: ""
}
<changed_files> ${
${eventData.isPR ? formattedChangedFiles || "No files changed" : ""} eventData.isPR
</changed_files>${imagesInfo} ? `<changed_files>
${formattedChangedFiles || "No files changed"}
</changed_files>`
: ""
}${imagesInfo}
<event_type>${eventType}</event_type> <event_type>${eventType}</event_type>
<is_pr>${eventData.isPR ? "true" : "false"}</is_pr> <is_pr>${eventData.isPR ? "true" : "false"}</is_pr>
<trigger_context>${triggerContext}</trigger_context> <trigger_context>${triggerContext}</trigger_context>
<repository>${context.repository}</repository> <repository>${context.repository}</repository>
${ ${eventData.isPR && eventData.prNumber ? `<pr_number>${eventData.prNumber}</pr_number>` : ""}
eventData.isPR ${!eventData.isPR && eventData.issueNumber ? `<issue_number>${eventData.issueNumber}</issue_number>` : ""}
? `<pr_number>${eventData.prNumber}</pr_number>`
: `<issue_number>${eventData.issueNumber ?? ""}</issue_number>`
}
<claude_comment_id>${context.claudeCommentId}</claude_comment_id> <claude_comment_id>${context.claudeCommentId}</claude_comment_id>
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username> <trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name> <trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>

View File

@@ -393,7 +393,7 @@ export function formatGroupedContent(groupedContent: GroupedContent[]): string {
markdown += "---\n\n"; markdown += "---\n\n";
} else if (itemType === "final_result") { } else if (itemType === "final_result") {
const data = item.data || {}; const data = item.data || {};
const cost = (data as any).cost_usd || 0; const cost = (data as any).total_cost_usd || (data as any).cost_usd || 0;
const duration = (data as any).duration_ms || 0; const duration = (data as any).duration_ms || 0;
const resultText = (data as any).result || ""; const resultText = (data as any).result || "";

View File

@@ -46,6 +46,7 @@ export const PR_QUERY = `
login login
} }
createdAt createdAt
isMinimized
} }
} }
reviews(first: 100) { reviews(first: 100) {
@@ -69,6 +70,7 @@ export const PR_QUERY = `
login login
} }
createdAt createdAt
isMinimized
} }
} }
} }
@@ -98,6 +100,7 @@ export const ISSUE_QUERY = `
login login
} }
createdAt createdAt
isMinimized
} }
} }
} }

View File

@@ -134,7 +134,7 @@ export async function fetchGitHubData({
// Prepare all comments for image processing // Prepare all comments for image processing
const issueComments: CommentWithImages[] = comments const issueComments: CommentWithImages[] = comments
.filter((c) => c.body) .filter((c) => c.body && !c.isMinimized)
.map((c) => ({ .map((c) => ({
type: "issue_comment" as const, type: "issue_comment" as const,
id: c.databaseId, id: c.databaseId,
@@ -154,7 +154,7 @@ export async function fetchGitHubData({
const reviewComments: CommentWithImages[] = const reviewComments: CommentWithImages[] =
reviewData?.nodes reviewData?.nodes
?.flatMap((r) => r.comments?.nodes ?? []) ?.flatMap((r) => r.comments?.nodes ?? [])
.filter((c) => c.body) .filter((c) => c.body && !c.isMinimized)
.map((c) => ({ .map((c) => ({
type: "review_comment" as const, type: "review_comment" as const,
id: c.databaseId, id: c.databaseId,

View File

@@ -50,6 +50,7 @@ export function formatComments(
imageUrlMap?: Map<string, string>, imageUrlMap?: Map<string, string>,
): string { ): string {
return comments return comments
.filter((comment) => !comment.isMinimized)
.map((comment) => { .map((comment) => {
let body = comment.body; let body = comment.body;
@@ -96,6 +97,7 @@ export function formatReviewComments(
review.comments.nodes.length > 0 review.comments.nodes.length > 0
) { ) {
const comments = review.comments.nodes const comments = review.comments.nodes
.filter((comment) => !comment.isMinimized)
.map((comment) => { .map((comment) => {
let body = comment.body; let body = comment.body;
@@ -110,7 +112,9 @@ export function formatReviewComments(
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`; return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
}) })
.join("\n"); .join("\n");
reviewOutput += `\n${comments}`; if (comments) {
reviewOutput += `\n${comments}`;
}
} }
return reviewOutput; return reviewOutput;

View File

@@ -10,6 +10,7 @@ export type GitHubComment = {
body: string; body: string;
author: GitHubAuthor; author: GitHubAuthor;
createdAt: string; createdAt: string;
isMinimized?: boolean;
}; };
export type GitHubReviewComment = GitHubComment & { export type GitHubReviewComment = GitHubComment & {

View File

@@ -252,6 +252,63 @@ describe("formatComments", () => {
`[user1 at 2023-01-01T00:00:00Z]: Image: ![](https://github.com/user-attachments/assets/test.png)`, `[user1 at 2023-01-01T00:00:00Z]: Image: ![](https://github.com/user-attachments/assets/test.png)`,
); );
}); });
test("filters out minimized comments", () => {
const comments: GitHubComment[] = [
{
id: "1",
databaseId: "100001",
body: "Normal comment",
author: { login: "user1" },
createdAt: "2023-01-01T00:00:00Z",
isMinimized: false,
},
{
id: "2",
databaseId: "100002",
body: "Minimized comment",
author: { login: "user2" },
createdAt: "2023-01-02T00:00:00Z",
isMinimized: true,
},
{
id: "3",
databaseId: "100003",
body: "Another normal comment",
author: { login: "user3" },
createdAt: "2023-01-03T00:00:00Z",
},
];
const result = formatComments(comments);
expect(result).toBe(
`[user1 at 2023-01-01T00:00:00Z]: Normal comment\n\n[user3 at 2023-01-03T00:00:00Z]: Another normal comment`,
);
});
test("returns empty string when all comments are minimized", () => {
const comments: GitHubComment[] = [
{
id: "1",
databaseId: "100001",
body: "Minimized comment 1",
author: { login: "user1" },
createdAt: "2023-01-01T00:00:00Z",
isMinimized: true,
},
{
id: "2",
databaseId: "100002",
body: "Minimized comment 2",
author: { login: "user2" },
createdAt: "2023-01-02T00:00:00Z",
isMinimized: true,
},
];
const result = formatComments(comments);
expect(result).toBe("");
});
}); });
describe("formatReviewComments", () => { describe("formatReviewComments", () => {
@@ -517,6 +574,159 @@ describe("formatReviewComments", () => {
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: ![](https://github.com/user-attachments/assets/test.png)`, `[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: ![](https://github.com/user-attachments/assets/test.png)`,
); );
}); });
test("filters out minimized review comments", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "Review with mixed comments",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Normal review comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: false,
},
{
id: "comment2",
databaseId: "200002",
body: "Minimized review comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
{
id: "comment3",
databaseId: "200003",
body: "Another normal comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/main.ts",
line: 10,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview with mixed comments\n [Comment on src/index.ts:42]: Normal review comment\n [Comment on src/main.ts:10]: Another normal comment`,
);
});
test("returns review with only body when all review comments are minimized", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "Review body only",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Minimized comment 1",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: true,
},
{
id: "comment2",
databaseId: "200002",
body: "Minimized comment 2",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body only`,
);
});
test("handles multiple reviews with mixed minimized comments", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "First review",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Good comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: false,
},
],
},
},
{
id: "review2",
databaseId: "300002",
author: { login: "reviewer2" },
body: "Second review",
state: "COMMENTED",
submittedAt: "2023-01-02T00:00:00Z",
comments: {
nodes: [
{
id: "comment2",
databaseId: "200002",
body: "Spam comment",
author: { login: "reviewer2" },
createdAt: "2023-01-02T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nFirst review\n [Comment on src/index.ts:42]: Good comment\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: COMMENTED\nSecond review`,
);
});
}); });
describe("formatChangedFiles", () => { describe("formatChangedFiles", () => {