Compare commits

..

1 Commits

Author SHA1 Message Date
Yuta Saito
6820ee5d50 fix: move env var before image name in docker run for github-mcp-server
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-30 07:48:09 +09:00
12 changed files with 44 additions and 345 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_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_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"

128
CLAUDE.md
View File

@@ -1,11 +1,10 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude 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
@@ -18,119 +17,42 @@ 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 operates in two main phases: This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action:
### Phase 1: Preparation (`src/entrypoints/prepare.ts`) 1. **Trigger Detection**: Uses `check-trigger.ts` to determine if Claude should respond based on comment/issue content
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
1. **Authentication Setup**: Establishes GitHub token via OIDC or GitHub App ### Key Components
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
### Phase 2: Execution (`base-action/`) - **Trigger System**: Responds to `/claude` comments or issue assignments
- **Authentication**: OIDC-based token exchange for secure GitHub interactions
The `base-action/` directory contains the core Claude Code execution logic, which serves a dual purpose: - **Cloud Integration**: Supports direct Anthropic API, AWS Bedrock, and Google Vertex AI
- **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/
├── entrypoints/ # Action entry points ├── check-trigger.ts # Determines if Claude should respond
├── prepare.ts # Main preparation logic ├── create-prompt.ts # Generates contextual prompts
│ ├── update-comment-link.ts # Post-execution comment updates ├── github-data-fetcher.ts # Retrieves GitHub data
│ └── format-turns.ts # Claude conversation formatting ├── github-data-formatter.ts # Formats GitHub data for prompts
├── github/ # GitHub integration layer ├── install-mcp-server.ts # Sets up GitHub MCP server
│ ├── api/ # REST/GraphQL clients ├── update-comment-with-link.ts # Updates comments with job links
│ ├── data/ # Data fetching and formatting └── types/
── operations/ # Branch, comment, git operations ── github.ts # TypeScript types for GitHub data
│ ├── 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 Implementation Notes ## Important Notes
### Authentication Flow - Actions are triggered by `@claude` comments or issue assignment unless a different trigger_phrase is specified
- The action creates branches for issues and pushes to PR branches directly
- Uses GitHub OIDC token exchange for secure authentication - All actions create OIDC tokens for secure authentication
- Supports custom GitHub Apps via `APP_ID` and `APP_PRIVATE_KEY` - Progress is tracked through dynamic comment updates with checkboxes
- 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.64 bun install -g @anthropic-ai/claude-code@1.0.63
- 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.64 run: bun install -g @anthropic-ai/claude-code@1.0.63
- 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_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_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"

View File

@@ -587,28 +587,23 @@ ${formattedBody}
${formattedComments || "No comments"} ${formattedComments || "No comments"}
</comments> </comments>
${ <review_comments>
eventData.isPR ${eventData.isPR ? formattedReviewComments || "No review comments" : ""}
? `<review_comments> </review_comments>
${formattedReviewComments || "No review comments"}
</review_comments>`
: ""
}
${ <changed_files>
eventData.isPR ${eventData.isPR ? formattedChangedFiles || "No files changed" : ""}
? `<changed_files> </changed_files>${imagesInfo}
${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.issueNumber ? `<issue_number>${eventData.issueNumber}</issue_number>` : ""} eventData.isPR
? `<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).total_cost_usd || (data as any).cost_usd || 0; const cost = (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,7 +46,6 @@ export const PR_QUERY = `
login login
} }
createdAt createdAt
isMinimized
} }
} }
reviews(first: 100) { reviews(first: 100) {
@@ -70,7 +69,6 @@ export const PR_QUERY = `
login login
} }
createdAt createdAt
isMinimized
} }
} }
} }
@@ -100,7 +98,6 @@ 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 && !c.isMinimized) .filter((c) => c.body)
.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 && !c.isMinimized) .filter((c) => c.body)
.map((c) => ({ .map((c) => ({
type: "review_comment" as const, type: "review_comment" as const,
id: c.databaseId, id: c.databaseId,

View File

@@ -50,7 +50,6 @@ 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;
@@ -97,7 +96,6 @@ 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;
@@ -112,10 +110,8 @@ export function formatReviewComments(
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`; return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
}) })
.join("\n"); .join("\n");
if (comments) {
reviewOutput += `\n${comments}`; reviewOutput += `\n${comments}`;
} }
}
return reviewOutput; return reviewOutput;
}); });

View File

@@ -10,7 +10,6 @@ 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,63 +252,6 @@ 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", () => {
@@ -574,159 +517,6 @@ 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", () => {