mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
19 Commits
v0.0.48
...
km/add-mcp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
340104d9cf | ||
|
|
9abdfa8a3a | ||
|
|
56179f5fc9 | ||
|
|
0e5fbc0d44 | ||
|
|
6c5d11c8c3 | ||
|
|
106183c4c0 | ||
|
|
7cc3cff20b | ||
|
|
b4cc5cd6c5 | ||
|
|
1b4ac7d7e0 | ||
|
|
1f6e3225b0 | ||
|
|
6672e9b357 | ||
|
|
950bdc01df | ||
|
|
15dd796e97 | ||
|
|
fd012347a2 | ||
|
|
5bdc533a52 | ||
|
|
d45539c118 | ||
|
|
daac7e353f | ||
|
|
bdfdd1f788 | ||
|
|
ec0e9b4f87 |
2
.github/workflows/claude-review.yml
vendored
2
.github/workflows/claude-review.yml
vendored
@@ -30,4 +30,4 @@ jobs:
|
||||
|
||||
Be constructive and specific in your feedback. Give inline comments where applicable.
|
||||
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
128
CLAUDE.md
@@ -1,10 +1,11 @@
|
||||
# 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
|
||||
|
||||
- Runtime: Bun 1.2.11
|
||||
- TypeScript with strict configuration
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
@@ -17,42 +18,119 @@ bun test
|
||||
# Formatting
|
||||
bun run format # Format code with prettier
|
||||
bun run format:check # Check code formatting
|
||||
|
||||
# Type checking
|
||||
bun run typecheck # Run TypeScript type checker
|
||||
```
|
||||
|
||||
## 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
|
||||
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
|
||||
### Phase 1: Preparation (`src/entrypoints/prepare.ts`)
|
||||
|
||||
### 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
|
||||
- **Authentication**: OIDC-based token exchange for secure GitHub interactions
|
||||
- **Cloud Integration**: Supports direct Anthropic API, AWS Bedrock, and Google Vertex AI
|
||||
- **GitHub Operations**: Creates branches, posts comments, and manages PRs/issues
|
||||
### Phase 2: Execution (`base-action/`)
|
||||
|
||||
The `base-action/` directory contains the core Claude Code execution logic, which serves a dual purpose:
|
||||
|
||||
- **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
|
||||
|
||||
```
|
||||
src/
|
||||
├── check-trigger.ts # Determines if Claude should respond
|
||||
├── create-prompt.ts # Generates contextual prompts
|
||||
├── github-data-fetcher.ts # Retrieves GitHub data
|
||||
├── github-data-formatter.ts # Formats GitHub data for prompts
|
||||
├── install-mcp-server.ts # Sets up GitHub MCP server
|
||||
├── update-comment-with-link.ts # Updates comments with job links
|
||||
└── types/
|
||||
└── github.ts # TypeScript types for GitHub data
|
||||
├── entrypoints/ # Action entry points
|
||||
│ ├── prepare.ts # Main preparation logic
|
||||
│ ├── update-comment-link.ts # Post-execution comment updates
|
||||
│ └── format-turns.ts # Claude conversation formatting
|
||||
├── github/ # GitHub integration layer
|
||||
│ ├── api/ # REST/GraphQL clients
|
||||
│ ├── data/ # Data fetching and formatting
|
||||
│ ├── 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
|
||||
- The action creates branches for issues and pushes to PR branches directly
|
||||
- All actions create OIDC tokens for secure authentication
|
||||
- Progress is tracked through dynamic comment updates with checkboxes
|
||||
### Authentication Flow
|
||||
|
||||
- Uses GitHub OIDC token exchange for secure authentication
|
||||
- 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`
|
||||
|
||||
85
README.md
85
README.md
@@ -167,36 +167,36 @@ jobs:
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||
| `mode` | Execution mode: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking) | No | `tag` |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
||||
| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | No | - |
|
||||
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
|
||||
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
|
||||
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
||||
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
|
||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
|
||||
| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - |
|
||||
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
|
||||
| `disallowed_tools` | Tools that Claude should never use | No | "" |
|
||||
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
|
||||
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
|
||||
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
||||
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
|
||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
|
||||
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
|
||||
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
|
||||
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
||||
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
||||
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||
| `mode` | Execution mode: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation), 'experimental-review' (for PR reviews) | No | `tag` |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
||||
| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | No | - |
|
||||
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
|
||||
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
|
||||
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
||||
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
|
||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
|
||||
| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - |
|
||||
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
|
||||
| `disallowed_tools` | Tools that Claude should never use | No | "" |
|
||||
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
|
||||
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
|
||||
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
||||
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
|
||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
|
||||
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
|
||||
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
|
||||
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
||||
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
||||
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||
|
||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||
|
||||
@@ -204,7 +204,7 @@ jobs:
|
||||
|
||||
## Execution Modes
|
||||
|
||||
The action supports two execution modes, each optimized for different use cases:
|
||||
The action supports three execution modes, each optimized for different use cases:
|
||||
|
||||
### Tag Mode (Default)
|
||||
|
||||
@@ -238,7 +238,28 @@ For automation and scheduled tasks without trigger checking.
|
||||
Check for outdated dependencies and create an issue if any are found.
|
||||
```
|
||||
|
||||
See [`examples/claude-modes.yml`](./examples/claude-modes.yml) for complete examples of each mode.
|
||||
### Experimental Review Mode
|
||||
|
||||
> **EXPERIMENTAL**: This mode is under active development and may change significantly. Use with caution in production workflows.
|
||||
|
||||
Specialized mode for automated PR code reviews using GitHub's review API.
|
||||
|
||||
- **Triggers**: Automatically on PR events (opened, synchronize, reopened) when configured in workflow
|
||||
- **Features**: Creates inline review comments with suggestions, batches feedback into a single review
|
||||
- **Use case**: Automated code reviews, security scanning, best practices enforcement
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: experimental-review
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
custom_instructions: |
|
||||
Focus on security vulnerabilities, performance issues, and code quality.
|
||||
```
|
||||
|
||||
Review mode automatically includes GitHub MCP tools for creating pending reviews and inline comments. See [`examples/claude-experimental-review-mode.yml`](./examples/claude-experimental-review-mode.yml) for a complete example.
|
||||
|
||||
See [`examples/claude-modes.yml`](./examples/claude-modes.yml) for complete examples of available modes.
|
||||
|
||||
### Using Custom MCP Configuration
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ inputs:
|
||||
|
||||
# Mode configuration
|
||||
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), 'experimental-review' (experimental mode for code reviews with inline comments and suggestions)"
|
||||
required: false
|
||||
default: "tag"
|
||||
|
||||
@@ -158,7 +158,7 @@ runs:
|
||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||
ACTIONS_TOKEN: ${{ github.token }}
|
||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||
|
||||
@@ -172,7 +172,7 @@ runs:
|
||||
echo "Base-action dependencies installed"
|
||||
cd -
|
||||
# Install Claude Code globally
|
||||
bun install -g @anthropic-ai/claude-code@1.0.62
|
||||
bun install -g @anthropic-ai/claude-code@1.0.66
|
||||
|
||||
- name: Setup Network Restrictions
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||
|
||||
@@ -115,7 +115,7 @@ runs:
|
||||
|
||||
- name: Install Claude Code
|
||||
shell: bash
|
||||
run: bun install -g @anthropic-ai/claude-code@1.0.62
|
||||
run: bun install -g @anthropic-ai/claude-code@1.0.66
|
||||
|
||||
- name: Run Claude Code Action
|
||||
shell: bash
|
||||
|
||||
@@ -35,4 +35,4 @@ jobs:
|
||||
|
||||
Provide constructive feedback with specific suggestions for improvement.
|
||||
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"
|
||||
|
||||
45
examples/claude-experimental-review-mode.yml
Normal file
45
examples/claude-experimental-review-mode.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
name: Claude Experimental Review Mode
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
code-review:
|
||||
# Run on PR events, or when someone comments "@claude review" on a PR
|
||||
if: |
|
||||
github.event_name == 'pull_request' ||
|
||||
(github.event_name == 'issue_comment' &&
|
||||
github.event.issue.pull_request &&
|
||||
contains(github.event.comment.body, '@claude review'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Full history for better diff analysis
|
||||
|
||||
- name: Code Review with Claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: experimental-review
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# github_token not needed - uses default GITHUB_TOKEN for GitHub operations
|
||||
timeout_minutes: "30"
|
||||
custom_instructions: |
|
||||
Focus on:
|
||||
- Code quality and maintainability
|
||||
- Security vulnerabilities
|
||||
- Performance issues
|
||||
- Best practices and design patterns
|
||||
- Test coverage gaps
|
||||
|
||||
Be constructive and provide specific suggestions for improvements.
|
||||
Use GitHub's suggestion format when proposing code changes.
|
||||
40
examples/workflow-dispatch-agent.yml
Normal file
40
examples/workflow-dispatch-agent.yml
Normal file
@@ -0,0 +1,40 @@
|
||||
name: Claude Commit Analysis
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
analysis_type:
|
||||
description: "Type of analysis to perform"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- summarize-commit
|
||||
- security-review
|
||||
default: "summarize-commit"
|
||||
|
||||
jobs:
|
||||
analyze-commit:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
||||
|
||||
- name: Run Claude Analysis
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: agent
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
override_prompt: |
|
||||
Analyze the latest commit in this repository.
|
||||
|
||||
${{ github.event.inputs.analysis_type == 'summarize-commit' && 'Task: Provide a clear, concise summary of what changed in the latest commit. Include the commit message, files changed, and the purpose of the changes.' || '' }}
|
||||
|
||||
${{ github.event.inputs.analysis_type == 'security-review' && 'Task: Review the latest commit for potential security vulnerabilities. Check for exposed secrets, insecure coding patterns, dependency vulnerabilities, or any other security concerns. Provide specific recommendations if issues are found.' || '' }}
|
||||
@@ -530,6 +530,7 @@ export function generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
mode: Mode,
|
||||
): string {
|
||||
if (context.overridePrompt) {
|
||||
return substitutePromptVariables(
|
||||
@@ -539,6 +540,19 @@ export function generatePrompt(
|
||||
);
|
||||
}
|
||||
|
||||
// Use the mode's prompt generator
|
||||
return mode.generatePrompt(context, githubData, useCommitSigning);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the default prompt for tag mode
|
||||
* @internal
|
||||
*/
|
||||
export function generateDefaultPrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean = false,
|
||||
): string {
|
||||
const {
|
||||
contextData,
|
||||
comments,
|
||||
@@ -587,23 +601,28 @@ ${formattedBody}
|
||||
${formattedComments || "No comments"}
|
||||
</comments>
|
||||
|
||||
<review_comments>
|
||||
${eventData.isPR ? formattedReviewComments || "No review comments" : ""}
|
||||
</review_comments>
|
||||
${
|
||||
eventData.isPR
|
||||
? `<review_comments>
|
||||
${formattedReviewComments || "No review comments"}
|
||||
</review_comments>`
|
||||
: ""
|
||||
}
|
||||
|
||||
<changed_files>
|
||||
${eventData.isPR ? formattedChangedFiles || "No files changed" : ""}
|
||||
</changed_files>${imagesInfo}
|
||||
${
|
||||
eventData.isPR
|
||||
? `<changed_files>
|
||||
${formattedChangedFiles || "No files changed"}
|
||||
</changed_files>`
|
||||
: ""
|
||||
}${imagesInfo}
|
||||
|
||||
<event_type>${eventType}</event_type>
|
||||
<is_pr>${eventData.isPR ? "true" : "false"}</is_pr>
|
||||
<trigger_context>${triggerContext}</trigger_context>
|
||||
<repository>${context.repository}</repository>
|
||||
${
|
||||
eventData.isPR
|
||||
? `<pr_number>${eventData.prNumber}</pr_number>`
|
||||
: `<issue_number>${eventData.issueNumber ?? ""}</issue_number>`
|
||||
}
|
||||
${eventData.isPR && eventData.prNumber ? `<pr_number>${eventData.prNumber}</pr_number>` : ""}
|
||||
${!eventData.isPR && eventData.issueNumber ? `<issue_number>${eventData.issueNumber}</issue_number>` : ""}
|
||||
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
||||
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
||||
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
||||
@@ -801,15 +820,20 @@ export async function createPrompt(
|
||||
context: ParsedGitHubContext,
|
||||
) {
|
||||
try {
|
||||
// Tag mode requires a comment ID
|
||||
if (mode.name === "tag" && !modeContext.commentId) {
|
||||
throw new Error("Tag mode requires a comment ID for prompt generation");
|
||||
// Prepare the context for prompt generation
|
||||
let claudeCommentId: string = "";
|
||||
if (mode.name === "tag") {
|
||||
if (!modeContext.commentId) {
|
||||
throw new Error(
|
||||
`${mode.name} mode requires a comment ID for prompt generation`,
|
||||
);
|
||||
}
|
||||
claudeCommentId = modeContext.commentId.toString();
|
||||
}
|
||||
|
||||
// Prepare the context for prompt generation
|
||||
const preparedContext = prepareContext(
|
||||
context,
|
||||
modeContext.commentId?.toString() || "",
|
||||
claudeCommentId,
|
||||
modeContext.baseBranch,
|
||||
modeContext.claudeBranch,
|
||||
);
|
||||
@@ -823,6 +847,7 @@ export async function createPrompt(
|
||||
preparedContext,
|
||||
githubData,
|
||||
context.inputs.useCommitSigning,
|
||||
mode,
|
||||
);
|
||||
|
||||
// Log the final prompt to console
|
||||
|
||||
@@ -372,8 +372,12 @@ export function formatGroupedContent(groupedContent: GroupedContent[]): string {
|
||||
const usage = item.usage || {};
|
||||
if (Object.keys(usage).length > 0) {
|
||||
const inputTokens = usage.input_tokens || 0;
|
||||
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
|
||||
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
||||
const totalInputTokens =
|
||||
inputTokens + cacheCreationTokens + cacheReadTokens;
|
||||
const outputTokens = usage.output_tokens || 0;
|
||||
markdown += `*Token usage: ${inputTokens} input, ${outputTokens} output*\n\n`;
|
||||
markdown += `*Token usage: ${totalInputTokens} input, ${outputTokens} output*\n\n`;
|
||||
}
|
||||
|
||||
// Only add separator if this section had content
|
||||
@@ -393,7 +397,7 @@ export function formatGroupedContent(groupedContent: GroupedContent[]): string {
|
||||
markdown += "---\n\n";
|
||||
} else if (itemType === "final_result") {
|
||||
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 resultText = (data as any).result || "";
|
||||
|
||||
|
||||
@@ -7,40 +7,60 @@
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { setupGitHubToken } from "../github/token";
|
||||
import { checkHumanActor } from "../github/validation/actor";
|
||||
import { checkWritePermissions } from "../github/validation/permissions";
|
||||
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 { createOctokit } from "../github/api/client";
|
||||
import { fetchGitHubData } from "../github/data/fetcher";
|
||||
import { parseGitHubContext } from "../github/context";
|
||||
import { getMode } from "../modes/registry";
|
||||
import { createPrompt } from "../create-prompt";
|
||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||
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 === "experimental-review") {
|
||||
// For experimental-review mode, use the default GitHub Action token
|
||||
githubToken = process.env.DEFAULT_WORKFLOW_TOKEN || "";
|
||||
if (!githubToken) {
|
||||
throw new Error(
|
||||
"DEFAULT_WORKFLOW_TOKEN not found for experimental-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)
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// Step 3: Check write permissions
|
||||
const hasWritePermissions = await checkWritePermissions(
|
||||
octokit.rest,
|
||||
context,
|
||||
);
|
||||
if (!hasWritePermissions) {
|
||||
throw new Error(
|
||||
"Actor does not have write permissions to the repository",
|
||||
// Step 3: Check write permissions (only for entity contexts)
|
||||
if (isEntityContext(context)) {
|
||||
const hasWritePermissions = await checkWritePermissions(
|
||||
octokit.rest,
|
||||
context,
|
||||
);
|
||||
if (!hasWritePermissions) {
|
||||
throw new Error(
|
||||
"Actor does not have write permissions to the repository",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Get mode and check trigger conditions
|
||||
const mode = getMode(context.inputs.mode);
|
||||
const mode = getMode(validatedMode, context);
|
||||
const containsTrigger = mode.shouldTrigger(context);
|
||||
|
||||
// Set output for action.yml to check
|
||||
@@ -51,65 +71,16 @@ async function run() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 5: Check if actor is human
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
// Step 6: Create initial tracking comment (mode-aware)
|
||||
// Some modes (e.g., agent mode) may not need tracking comments
|
||||
let commentId: number | undefined;
|
||||
let commentData:
|
||||
| Awaited<ReturnType<typeof createInitialComment>>
|
||||
| undefined;
|
||||
if (mode.shouldCreateTrackingComment()) {
|
||||
commentData = await createInitialComment(octokit.rest, context);
|
||||
commentId = commentData.id;
|
||||
}
|
||||
|
||||
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
|
||||
const githubData = await fetchGitHubData({
|
||||
octokits: octokit,
|
||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||
prNumber: context.entityNumber.toString(),
|
||||
isPR: context.isPR,
|
||||
triggerUsername: context.actor,
|
||||
});
|
||||
|
||||
// Step 8: Setup branch
|
||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||
|
||||
// Step 9: Configure git authentication if not using commit signing
|
||||
if (!context.inputs.useCommitSigning) {
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, commentData?.user || null);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 10: Create prompt file
|
||||
const modeContext = mode.prepareContext(context, {
|
||||
commentId,
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
claudeBranch: branchInfo.claudeBranch,
|
||||
});
|
||||
|
||||
await createPrompt(mode, modeContext, githubData, context);
|
||||
|
||||
// Step 11: Get MCP configuration
|
||||
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: context.inputs.allowedTools,
|
||||
// Step 5: Use the new modular prepare function
|
||||
const result = await prepare({
|
||||
context,
|
||||
octokit,
|
||||
mode,
|
||||
githubToken,
|
||||
});
|
||||
core.setOutput("mcp_config", mcpConfig);
|
||||
|
||||
// Set the MCP config output
|
||||
core.setOutput("mcp_config", result.mcpConfig);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import {
|
||||
parseGitHubContext,
|
||||
isPullRequestReviewCommentEvent,
|
||||
isEntityContext,
|
||||
} from "../github/context";
|
||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
|
||||
@@ -23,7 +24,14 @@ async function run() {
|
||||
const triggerUsername = process.env.TRIGGER_USERNAME;
|
||||
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// This script is only called for entity-based events
|
||||
if (!isEntityContext(context)) {
|
||||
throw new Error("update-comment-link requires an entity context");
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repository;
|
||||
|
||||
const octokit = createOctokit(githubToken);
|
||||
|
||||
const serverUrl = GITHUB_SERVER_URL;
|
||||
|
||||
@@ -46,6 +46,7 @@ export const PR_QUERY = `
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
isMinimized
|
||||
}
|
||||
}
|
||||
reviews(first: 100) {
|
||||
@@ -69,6 +70,7 @@ export const PR_QUERY = `
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
isMinimized
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -98,6 +100,7 @@ export const ISSUE_QUERY = `
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
isMinimized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,54 @@ import type {
|
||||
PullRequestReviewEvent,
|
||||
PullRequestReviewCommentEvent,
|
||||
} from "@octokit/webhooks-types";
|
||||
// Custom types for GitHub Actions events that aren't webhooks
|
||||
export type WorkflowDispatchEvent = {
|
||||
action?: never;
|
||||
inputs?: Record<string, any>;
|
||||
ref?: string;
|
||||
repository: {
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
sender: {
|
||||
login: string;
|
||||
};
|
||||
workflow: string;
|
||||
};
|
||||
|
||||
export type ScheduleEvent = {
|
||||
action?: never;
|
||||
schedule?: string;
|
||||
repository: {
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
import type { ModeName } from "../modes/types";
|
||||
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
|
||||
|
||||
export type ParsedGitHubContext = {
|
||||
// Event name constants for better maintainability
|
||||
const ENTITY_EVENT_NAMES = [
|
||||
"issues",
|
||||
"issue_comment",
|
||||
"pull_request",
|
||||
"pull_request_review",
|
||||
"pull_request_review_comment",
|
||||
] as const;
|
||||
|
||||
const AUTOMATION_EVENT_NAMES = ["workflow_dispatch", "schedule"] as const;
|
||||
|
||||
// Derive types from constants for better maintainability
|
||||
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
|
||||
type AutomationEventName = (typeof AUTOMATION_EVENT_NAMES)[number];
|
||||
|
||||
// Common fields shared by all context types
|
||||
type BaseContext = {
|
||||
runId: string;
|
||||
eventName: string;
|
||||
eventAction?: string;
|
||||
repository: {
|
||||
owner: string;
|
||||
@@ -20,14 +62,6 @@ export type ParsedGitHubContext = {
|
||||
full_name: string;
|
||||
};
|
||||
actor: string;
|
||||
payload:
|
||||
| IssuesEvent
|
||||
| IssueCommentEvent
|
||||
| PullRequestEvent
|
||||
| PullRequestReviewEvent
|
||||
| PullRequestReviewCommentEvent;
|
||||
entityNumber: number;
|
||||
isPR: boolean;
|
||||
inputs: {
|
||||
mode: ModeName;
|
||||
triggerPhrase: string;
|
||||
@@ -46,7 +80,29 @@ export type ParsedGitHubContext = {
|
||||
};
|
||||
};
|
||||
|
||||
export function parseGitHubContext(): ParsedGitHubContext {
|
||||
// Context for entity-based events (issues, PRs, comments)
|
||||
export type ParsedGitHubContext = BaseContext & {
|
||||
eventName: EntityEventName;
|
||||
payload:
|
||||
| IssuesEvent
|
||||
| IssueCommentEvent
|
||||
| PullRequestEvent
|
||||
| PullRequestReviewEvent
|
||||
| PullRequestReviewCommentEvent;
|
||||
entityNumber: number;
|
||||
isPR: boolean;
|
||||
};
|
||||
|
||||
// Context for automation events (workflow_dispatch, schedule)
|
||||
export type AutomationContext = BaseContext & {
|
||||
eventName: AutomationEventName;
|
||||
payload: WorkflowDispatchEvent | ScheduleEvent;
|
||||
};
|
||||
|
||||
// Union type for all contexts
|
||||
export type GitHubContext = ParsedGitHubContext | AutomationContext;
|
||||
|
||||
export function parseGitHubContext(): GitHubContext {
|
||||
const context = github.context;
|
||||
|
||||
const modeInput = process.env.MODE ?? DEFAULT_MODE;
|
||||
@@ -56,7 +112,6 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
||||
|
||||
const commonFields = {
|
||||
runId: process.env.GITHUB_RUN_ID!,
|
||||
eventName: context.eventName,
|
||||
eventAction: context.payload.action,
|
||||
repository: {
|
||||
owner: context.repo.owner,
|
||||
@@ -86,49 +141,69 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
||||
|
||||
switch (context.eventName) {
|
||||
case "issues": {
|
||||
const payload = context.payload as IssuesEvent;
|
||||
return {
|
||||
...commonFields,
|
||||
payload: context.payload as IssuesEvent,
|
||||
entityNumber: (context.payload as IssuesEvent).issue.number,
|
||||
eventName: "issues",
|
||||
payload,
|
||||
entityNumber: payload.issue.number,
|
||||
isPR: false,
|
||||
};
|
||||
}
|
||||
case "issue_comment": {
|
||||
const payload = context.payload as IssueCommentEvent;
|
||||
return {
|
||||
...commonFields,
|
||||
payload: context.payload as IssueCommentEvent,
|
||||
entityNumber: (context.payload as IssueCommentEvent).issue.number,
|
||||
isPR: Boolean(
|
||||
(context.payload as IssueCommentEvent).issue.pull_request,
|
||||
),
|
||||
eventName: "issue_comment",
|
||||
payload,
|
||||
entityNumber: payload.issue.number,
|
||||
isPR: Boolean(payload.issue.pull_request),
|
||||
};
|
||||
}
|
||||
case "pull_request": {
|
||||
const payload = context.payload as PullRequestEvent;
|
||||
return {
|
||||
...commonFields,
|
||||
payload: context.payload as PullRequestEvent,
|
||||
entityNumber: (context.payload as PullRequestEvent).pull_request.number,
|
||||
eventName: "pull_request",
|
||||
payload,
|
||||
entityNumber: payload.pull_request.number,
|
||||
isPR: true,
|
||||
};
|
||||
}
|
||||
case "pull_request_review": {
|
||||
const payload = context.payload as PullRequestReviewEvent;
|
||||
return {
|
||||
...commonFields,
|
||||
payload: context.payload as PullRequestReviewEvent,
|
||||
entityNumber: (context.payload as PullRequestReviewEvent).pull_request
|
||||
.number,
|
||||
eventName: "pull_request_review",
|
||||
payload,
|
||||
entityNumber: payload.pull_request.number,
|
||||
isPR: true,
|
||||
};
|
||||
}
|
||||
case "pull_request_review_comment": {
|
||||
const payload = context.payload as PullRequestReviewCommentEvent;
|
||||
return {
|
||||
...commonFields,
|
||||
payload: context.payload as PullRequestReviewCommentEvent,
|
||||
entityNumber: (context.payload as PullRequestReviewCommentEvent)
|
||||
.pull_request.number,
|
||||
eventName: "pull_request_review_comment",
|
||||
payload,
|
||||
entityNumber: payload.pull_request.number,
|
||||
isPR: true,
|
||||
};
|
||||
}
|
||||
case "workflow_dispatch": {
|
||||
return {
|
||||
...commonFields,
|
||||
eventName: "workflow_dispatch",
|
||||
payload: context.payload as unknown as WorkflowDispatchEvent,
|
||||
};
|
||||
}
|
||||
case "schedule": {
|
||||
return {
|
||||
...commonFields,
|
||||
eventName: "schedule",
|
||||
payload: context.payload as unknown as ScheduleEvent,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported event type: ${context.eventName}`);
|
||||
}
|
||||
@@ -162,37 +237,53 @@ export function parseAdditionalPermissions(s: string): Map<string, string> {
|
||||
}
|
||||
|
||||
export function isIssuesEvent(
|
||||
context: ParsedGitHubContext,
|
||||
context: GitHubContext,
|
||||
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
||||
return context.eventName === "issues";
|
||||
}
|
||||
|
||||
export function isIssueCommentEvent(
|
||||
context: ParsedGitHubContext,
|
||||
context: GitHubContext,
|
||||
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
|
||||
return context.eventName === "issue_comment";
|
||||
}
|
||||
|
||||
export function isPullRequestEvent(
|
||||
context: ParsedGitHubContext,
|
||||
context: GitHubContext,
|
||||
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
|
||||
return context.eventName === "pull_request";
|
||||
}
|
||||
|
||||
export function isPullRequestReviewEvent(
|
||||
context: ParsedGitHubContext,
|
||||
context: GitHubContext,
|
||||
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
|
||||
return context.eventName === "pull_request_review";
|
||||
}
|
||||
|
||||
export function isPullRequestReviewCommentEvent(
|
||||
context: ParsedGitHubContext,
|
||||
context: GitHubContext,
|
||||
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
||||
return context.eventName === "pull_request_review_comment";
|
||||
}
|
||||
|
||||
export function isIssuesAssignedEvent(
|
||||
context: ParsedGitHubContext,
|
||||
context: GitHubContext,
|
||||
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
||||
return isIssuesEvent(context) && context.eventAction === "assigned";
|
||||
}
|
||||
|
||||
// Type guard to check if context is an entity context (has entityNumber and isPR)
|
||||
export function isEntityContext(
|
||||
context: GitHubContext,
|
||||
): context is ParsedGitHubContext {
|
||||
return ENTITY_EVENT_NAMES.includes(context.eventName as EntityEventName);
|
||||
}
|
||||
|
||||
// Type guard to check if context is an automation context
|
||||
export function isAutomationContext(
|
||||
context: GitHubContext,
|
||||
): context is AutomationContext {
|
||||
return AUTOMATION_EVENT_NAMES.includes(
|
||||
context.eventName as AutomationEventName,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ export async function fetchGitHubData({
|
||||
|
||||
// Prepare all comments for image processing
|
||||
const issueComments: CommentWithImages[] = comments
|
||||
.filter((c) => c.body)
|
||||
.filter((c) => c.body && !c.isMinimized)
|
||||
.map((c) => ({
|
||||
type: "issue_comment" as const,
|
||||
id: c.databaseId,
|
||||
@@ -154,7 +154,7 @@ export async function fetchGitHubData({
|
||||
const reviewComments: CommentWithImages[] =
|
||||
reviewData?.nodes
|
||||
?.flatMap((r) => r.comments?.nodes ?? [])
|
||||
.filter((c) => c.body)
|
||||
.filter((c) => c.body && !c.isMinimized)
|
||||
.map((c) => ({
|
||||
type: "review_comment" as const,
|
||||
id: c.databaseId,
|
||||
|
||||
@@ -50,6 +50,7 @@ export function formatComments(
|
||||
imageUrlMap?: Map<string, string>,
|
||||
): string {
|
||||
return comments
|
||||
.filter((comment) => !comment.isMinimized)
|
||||
.map((comment) => {
|
||||
let body = comment.body;
|
||||
|
||||
@@ -96,6 +97,7 @@ export function formatReviewComments(
|
||||
review.comments.nodes.length > 0
|
||||
) {
|
||||
const comments = review.comments.nodes
|
||||
.filter((comment) => !comment.isMinimized)
|
||||
.map((comment) => {
|
||||
let body = comment.body;
|
||||
|
||||
@@ -110,7 +112,9 @@ export function formatReviewComments(
|
||||
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
|
||||
})
|
||||
.join("\n");
|
||||
reviewOutput += `\n${comments}`;
|
||||
if (comments) {
|
||||
reviewOutput += `\n${comments}`;
|
||||
}
|
||||
}
|
||||
|
||||
return reviewOutput;
|
||||
|
||||
@@ -10,6 +10,7 @@ export type GitHubComment = {
|
||||
body: string;
|
||||
author: GitHubAuthor;
|
||||
createdAt: string;
|
||||
isMinimized?: boolean;
|
||||
};
|
||||
|
||||
export type GitHubReviewComment = GitHubComment & {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import { isEntityContext } from "../github/context";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
type PrepareConfigParams = {
|
||||
@@ -12,7 +13,7 @@ type PrepareConfigParams = {
|
||||
additionalMcpConfig?: string;
|
||||
claudeCommentId?: string;
|
||||
allowedTools: string[];
|
||||
context: ParsedGitHubContext;
|
||||
context: GitHubContext;
|
||||
};
|
||||
|
||||
async function checkActionsReadPermission(
|
||||
@@ -115,7 +116,7 @@ export async function prepareMcpConfig(
|
||||
const hasActionsReadPermission =
|
||||
context.inputs.additionalPermissions.get("actions") === "read";
|
||||
|
||||
if (context.isPR && hasActionsReadPermission) {
|
||||
if (isEntityContext(context) && context.isPR && hasActionsReadPermission) {
|
||||
// Verify the token actually has actions:read permission
|
||||
const actuallyHasPermission = await checkActionsReadPermission(
|
||||
process.env.ACTIONS_TOKEN || "",
|
||||
@@ -141,7 +142,9 @@ export async function prepareMcpConfig(
|
||||
GITHUB_TOKEN: process.env.ACTIONS_TOKEN,
|
||||
REPO_OWNER: owner,
|
||||
REPO_NAME: repo,
|
||||
PR_NUMBER: context.entityNumber.toString(),
|
||||
PR_NUMBER: isEntityContext(context)
|
||||
? context.entityNumber?.toString() || ""
|
||||
: "",
|
||||
RUNNER_TEMP: process.env.RUNNER_TEMP || "/tmp",
|
||||
},
|
||||
};
|
||||
@@ -156,10 +159,13 @@ export async function prepareMcpConfig(
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"-e",
|
||||
"GITHUB_HOST",
|
||||
"ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
|
||||
],
|
||||
env: {
|
||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||
GITHUB_HOST: GITHUB_SERVER_URL,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
import type { Mode } from "../types";
|
||||
import * as core from "@actions/core";
|
||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||
import { isAutomationContext } from "../../github/context";
|
||||
import type { PreparedContext } from "../../create-prompt/types";
|
||||
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||
|
||||
/**
|
||||
* Agent mode implementation.
|
||||
*
|
||||
* This mode is designed for automation and workflow_dispatch scenarios.
|
||||
* It always triggers (no checking), allows highly flexible configurations,
|
||||
* and works well with override_prompt for custom workflows.
|
||||
*
|
||||
* In the future, this mode could restrict certain tools for safety in automation contexts,
|
||||
* e.g., disallowing WebSearch or limiting file system operations.
|
||||
* This mode is specifically designed for automation events (workflow_dispatch and schedule).
|
||||
* It bypasses the standard trigger checking and comment tracking used by tag mode,
|
||||
* making it ideal for scheduled tasks and manual workflow runs.
|
||||
*/
|
||||
export const agentMode: Mode = {
|
||||
name: "agent",
|
||||
description: "Automation mode that always runs without trigger checking",
|
||||
description: "Automation mode for workflow_dispatch and schedule events",
|
||||
|
||||
shouldTrigger() {
|
||||
return true;
|
||||
shouldTrigger(context) {
|
||||
// Only trigger for automation events
|
||||
return isAutomationContext(context);
|
||||
},
|
||||
|
||||
prepareContext(context, data) {
|
||||
prepareContext(context) {
|
||||
// Agent mode doesn't use comment tracking or branch management
|
||||
return {
|
||||
mode: "agent",
|
||||
githubContext: context,
|
||||
commentId: data?.commentId,
|
||||
baseBranch: data?.baseBranch,
|
||||
claudeBranch: data?.claudeBranch,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -39,4 +39,73 @@ export const agentMode: Mode = {
|
||||
shouldCreateTrackingComment() {
|
||||
return false;
|
||||
},
|
||||
|
||||
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
||||
// Agent mode handles automation events (workflow_dispatch, schedule) only
|
||||
|
||||
// Agent mode doesn't need to create prompt files here - handled by createPrompt
|
||||
|
||||
// Export tool environment variables for agent mode
|
||||
const baseTools = [
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
];
|
||||
|
||||
// Add user-specified tools
|
||||
const allowedTools = [...baseTools, ...context.inputs.allowedTools];
|
||||
const disallowedTools = [
|
||||
"WebSearch",
|
||||
"WebFetch",
|
||||
...context.inputs.disallowedTools,
|
||||
];
|
||||
|
||||
// Export as INPUT_ prefixed variables for the base action
|
||||
core.exportVariable("INPUT_ALLOWED_TOOLS", allowedTools.join(","));
|
||||
core.exportVariable("INPUT_DISALLOWED_TOOLS", disallowedTools.join(","));
|
||||
|
||||
// Get MCP configuration using the same setup as other modes
|
||||
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||
const mcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: "", // Agent mode doesn't use branches
|
||||
baseBranch: "",
|
||||
additionalMcpConfig,
|
||||
claudeCommentId: undefined, // Agent mode doesn't track comments
|
||||
allowedTools: [...baseTools, ...context.inputs.allowedTools],
|
||||
context,
|
||||
});
|
||||
|
||||
core.setOutput("mcp_config", mcpConfig);
|
||||
|
||||
return {
|
||||
commentId: undefined,
|
||||
branchInfo: {
|
||||
baseBranch: "",
|
||||
currentBranch: "",
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: mcpConfig,
|
||||
};
|
||||
},
|
||||
|
||||
generatePrompt(context: PreparedContext): string {
|
||||
// Agent mode uses override or direct prompt, no GitHub data needed
|
||||
if (context.overridePrompt) {
|
||||
return context.overridePrompt;
|
||||
}
|
||||
|
||||
if (context.directPrompt) {
|
||||
return context.directPrompt;
|
||||
}
|
||||
|
||||
// Minimal fallback - repository is a string in PreparedContext
|
||||
return `Repository: ${context.repository}`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -13,9 +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", "experimental-review"] as const;
|
||||
|
||||
/**
|
||||
* All available modes.
|
||||
@@ -24,15 +27,17 @@ export const VALID_MODES = ["tag", "agent"] as const;
|
||||
const modes = {
|
||||
tag: tagMode,
|
||||
agent: agentMode,
|
||||
"experimental-review": reviewMode,
|
||||
} as const satisfies Record<ModeName, Mode>;
|
||||
|
||||
/**
|
||||
* Retrieves a mode by name.
|
||||
* Retrieves a mode by name and validates it can handle the event type.
|
||||
* @param name The mode name to retrieve
|
||||
* @param context The GitHub context to validate against
|
||||
* @returns The requested mode
|
||||
* @throws Error if the mode is not found
|
||||
* @throws Error if the mode is not found or cannot handle the event
|
||||
*/
|
||||
export function getMode(name: ModeName): Mode {
|
||||
export function getMode(name: ModeName, context: GitHubContext): Mode {
|
||||
const mode = modes[name];
|
||||
if (!mode) {
|
||||
const validModes = VALID_MODES.join("', '");
|
||||
@@ -40,6 +45,14 @@ export function getMode(name: ModeName): Mode {
|
||||
`Invalid mode '${name}'. Valid modes are: '${validModes}'. Please check your workflow configuration.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Validate mode can handle the event type
|
||||
if (name === "tag" && isAutomationContext(context)) {
|
||||
throw new Error(
|
||||
`Tag mode cannot handle ${context.eventName} events. Use 'agent' mode for automation events.`,
|
||||
);
|
||||
}
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
|
||||
352
src/modes/review/index.ts
Normal file
352
src/modes/review/index.ts
Normal file
@@ -0,0 +1,352 @@
|
||||
import * as core from "@actions/core";
|
||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||
import { checkContainsTrigger } from "../../github/validation/trigger";
|
||||
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, isPullRequestEvent } 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: "experimental-review",
|
||||
description:
|
||||
"Experimental code review mode for inline comments and suggestions",
|
||||
|
||||
shouldTrigger(context) {
|
||||
if (!isEntityContext(context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Review mode only works on PRs
|
||||
if (!context.isPR) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For pull_request events, only trigger on specific actions
|
||||
if (isPullRequestEvent(context)) {
|
||||
const allowedActions = ["opened", "synchronize", "reopened"];
|
||||
const action = context.payload.action;
|
||||
return allowedActions.includes(action);
|
||||
}
|
||||
|
||||
// For other events (comments), check for trigger phrase
|
||||
return checkContainsTrigger(context);
|
||||
},
|
||||
|
||||
prepareContext(context, data) {
|
||||
return {
|
||||
mode: "experimental-review",
|
||||
githubContext: context,
|
||||
commentId: data?.commentId,
|
||||
baseBranch: data?.baseBranch,
|
||||
claudeBranch: data?.claudeBranch,
|
||||
};
|
||||
},
|
||||
|
||||
getAllowedTools() {
|
||||
return [
|
||||
// Context tools - to know who the current user is
|
||||
"mcp__github__get_me",
|
||||
// Core review tools
|
||||
"mcp__github__create_pending_pull_request_review",
|
||||
"mcp__github__add_comment_to_pending_review",
|
||||
"mcp__github__submit_pending_pull_request_review",
|
||||
"mcp__github__delete_pending_pull_request_review",
|
||||
"mcp__github__create_and_submit_pull_request_review",
|
||||
// Comment tools
|
||||
"mcp__github__add_issue_comment",
|
||||
// PR information tools
|
||||
"mcp__github__get_pull_request",
|
||||
"mcp__github__get_pull_request_reviews",
|
||||
"mcp__github__get_pull_request_status",
|
||||
];
|
||||
},
|
||||
|
||||
getDisallowedTools() {
|
||||
return [];
|
||||
},
|
||||
|
||||
shouldCreateTrackingComment() {
|
||||
return false; // Review mode uses the review body instead of a tracking comment
|
||||
},
|
||||
|
||||
generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
): string {
|
||||
// Support overridePrompt
|
||||
if (context.overridePrompt) {
|
||||
return context.overridePrompt;
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<repository>${context.repository}</repository>
|
||||
${eventData.isPR && eventData.prNumber ? `<pr_number>${eventData.prNumber}</pr_number>` : ""}
|
||||
|
||||
<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:
|
||||
- You are reviewing PR #${eventData.isPR && eventData.prNumber ? eventData.prNumber : "[PR number]"} in ${context.repository}
|
||||
- Use mcp__github__get_pull_request to get PR metadata
|
||||
- Use the Read, Grep, and Glob tools to examine the modified files directly from disk
|
||||
- This provides the full context and latest state of the code
|
||||
- Look at the changed_files section above to see which files were modified
|
||||
|
||||
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 (startLine is the first line, line is the last)
|
||||
* side: "LEFT" (old code) or "RIGHT" (new code)
|
||||
* subjectType: "line" for line-level comments
|
||||
* body: Your comment text
|
||||
|
||||
- When to use multi-line comments:
|
||||
* When replacing multiple consecutive lines
|
||||
* When the fix requires changes across several lines
|
||||
* Example: To replace lines 19-20, use startLine: 19, line: 20
|
||||
|
||||
- For code suggestions, use this EXACT format in the body:
|
||||
\`\`\`suggestion
|
||||
corrected code here
|
||||
\`\`\`
|
||||
|
||||
CRITICAL: GitHub suggestion blocks must ONLY contain the replacement for the specific line(s) being commented on:
|
||||
- For single-line comments: Replace ONLY that line
|
||||
- For multi-line comments: Replace ONLY the lines in the range
|
||||
- Do NOT include surrounding context or function signatures
|
||||
- Do NOT suggest changes that span beyond the commented lines
|
||||
|
||||
Example for line 19 \`var name = user.name;\`:
|
||||
WRONG:
|
||||
\\\`\\\`\\\`suggestion
|
||||
function processUser(user) {
|
||||
if (!user) throw new Error('Invalid user');
|
||||
const name = user.name;
|
||||
\\\`\\\`\\\`
|
||||
|
||||
CORRECT:
|
||||
\\\`\\\`\\\`suggestion
|
||||
const name = user.name;
|
||||
\\\`\\\`\\\`
|
||||
|
||||
For validation suggestions, comment on the function declaration line or create separate comments for each concern.
|
||||
|
||||
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: Write a comprehensive review summary that includes:
|
||||
- Overview of what was reviewed (files, scope, focus areas)
|
||||
- Summary of all issues found (with counts by severity if applicable)
|
||||
- Key recommendations and action items
|
||||
- Highlights of good practices observed
|
||||
- Overall assessment and recommendation
|
||||
- The body should be detailed and informative since it's the main review content
|
||||
- Structure the body with clear sections using markdown headers
|
||||
|
||||
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 (following GitHub's format exactly)
|
||||
* Clear explanations of issues
|
||||
* Constructive criticism
|
||||
* Recognition of good practices
|
||||
* For complex changes that require multiple modifications:
|
||||
- Create separate comments for each logical change
|
||||
- Or explain the full solution in text without a suggestion block
|
||||
|
||||
- Communication:
|
||||
* All feedback goes through GitHub's review system
|
||||
* Be professional and respectful
|
||||
* Your review body is the main communication channel
|
||||
|
||||
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.
|
||||
|
||||
IMPORTANT: Your review body is the primary way users will understand your feedback. Make it comprehensive and well-structured with:
|
||||
- Executive summary at the top
|
||||
- Detailed findings organized by severity or category
|
||||
- Clear action items and recommendations
|
||||
- Recognition of good practices
|
||||
This ensures users get value from the review even before checking individual inline comments.`;
|
||||
},
|
||||
|
||||
async prepare({
|
||||
context,
|
||||
octokit,
|
||||
githubToken,
|
||||
}: ModeOptions): Promise<ModeResult> {
|
||||
if (!isEntityContext(context)) {
|
||||
throw new Error("Review mode requires entity context");
|
||||
}
|
||||
|
||||
// Review mode doesn't create a tracking comment
|
||||
const githubData = await fetchGitHubData({
|
||||
octokits: octokit,
|
||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||
prNumber: context.entityNumber.toString(),
|
||||
isPR: context.isPR,
|
||||
triggerUsername: context.actor,
|
||||
});
|
||||
|
||||
// Review mode doesn't need branch setup or git auth since it only creates comments
|
||||
// Using minimal branch info since review mode doesn't create or modify branches
|
||||
const branchInfo = {
|
||||
baseBranch: "main",
|
||||
currentBranch: "",
|
||||
claudeBranch: undefined, // Review mode doesn't create branches
|
||||
};
|
||||
|
||||
const modeContext = this.prepareContext(context, {
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
claudeBranch: branchInfo.claudeBranch,
|
||||
});
|
||||
|
||||
await createPrompt(reviewMode, modeContext, githubData, context);
|
||||
|
||||
// Export tool environment variables for review mode
|
||||
const baseTools = [
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
];
|
||||
|
||||
// Add mode-specific and user-specified tools
|
||||
const allowedTools = [
|
||||
...baseTools,
|
||||
...this.getAllowedTools(),
|
||||
...context.inputs.allowedTools,
|
||||
];
|
||||
const disallowedTools = [
|
||||
"WebSearch",
|
||||
"WebFetch",
|
||||
...context.inputs.disallowedTools,
|
||||
];
|
||||
|
||||
// Export as INPUT_ prefixed variables for the base action
|
||||
core.exportVariable("INPUT_ALLOWED_TOOLS", allowedTools.join(","));
|
||||
core.exportVariable("INPUT_DISALLOWED_TOOLS", disallowedTools.join(","));
|
||||
|
||||
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,
|
||||
allowedTools: [...this.getAllowedTools(), ...context.inputs.allowedTools],
|
||||
context,
|
||||
});
|
||||
|
||||
core.setOutput("mcp_config", mcpConfig);
|
||||
|
||||
return {
|
||||
branchInfo,
|
||||
mcpConfig,
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,16 @@
|
||||
import type { Mode } from "../types";
|
||||
import * as core from "@actions/core";
|
||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||
import { checkContainsTrigger } from "../../github/validation/trigger";
|
||||
import { checkHumanActor } from "../../github/validation/actor";
|
||||
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 { createPrompt, generateDefaultPrompt } from "../../create-prompt";
|
||||
import { isEntityContext } from "../../github/context";
|
||||
import type { PreparedContext } from "../../create-prompt/types";
|
||||
import type { FetchDataResult } from "../../github/data/fetcher";
|
||||
|
||||
/**
|
||||
* Tag mode implementation.
|
||||
@@ -13,6 +24,10 @@ export const tagMode: Mode = {
|
||||
description: "Traditional implementation mode triggered by @claude mentions",
|
||||
|
||||
shouldTrigger(context) {
|
||||
// Tag mode only handles entity events
|
||||
if (!isEntityContext(context)) {
|
||||
return false;
|
||||
}
|
||||
return checkContainsTrigger(context);
|
||||
},
|
||||
|
||||
@@ -37,4 +52,82 @@ export const tagMode: Mode = {
|
||||
shouldCreateTrackingComment() {
|
||||
return true;
|
||||
},
|
||||
|
||||
async prepare({
|
||||
context,
|
||||
octokit,
|
||||
githubToken,
|
||||
}: ModeOptions): Promise<ModeResult> {
|
||||
// Tag mode only handles entity-based events
|
||||
if (!isEntityContext(context)) {
|
||||
throw new Error("Tag mode requires entity context");
|
||||
}
|
||||
|
||||
// Check if actor is human
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
// Create initial tracking comment
|
||||
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,
|
||||
});
|
||||
|
||||
// Setup branch
|
||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||
|
||||
// Configure git authentication if not using commit signing
|
||||
if (!context.inputs.useCommitSigning) {
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, commentData.user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Create prompt file
|
||||
const modeContext = this.prepareContext(context, {
|
||||
commentId,
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
claudeBranch: branchInfo.claudeBranch,
|
||||
});
|
||||
|
||||
await createPrompt(tagMode, modeContext, githubData, context);
|
||||
|
||||
// Get MCP configuration
|
||||
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: context.inputs.allowedTools,
|
||||
context,
|
||||
});
|
||||
|
||||
core.setOutput("mcp_config", mcpConfig);
|
||||
|
||||
return {
|
||||
commentId,
|
||||
branchInfo,
|
||||
mcpConfig,
|
||||
};
|
||||
},
|
||||
|
||||
generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
): string {
|
||||
return generateDefaultPrompt(context, githubData, useCommitSigning);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import type { PreparedContext } from "../create-prompt/types";
|
||||
import type { FetchDataResult } from "../github/data/fetcher";
|
||||
import type { Octokits } from "../github/api/client";
|
||||
|
||||
export type ModeName = "tag" | "agent";
|
||||
export type ModeName = "tag" | "agent" | "experimental-review";
|
||||
|
||||
export type ModeContext = {
|
||||
mode: ModeName;
|
||||
githubContext: ParsedGitHubContext;
|
||||
githubContext: GitHubContext;
|
||||
commentId?: number;
|
||||
baseBranch?: string;
|
||||
claudeBranch?: string;
|
||||
@@ -32,12 +35,12 @@ export type Mode = {
|
||||
/**
|
||||
* Determines if this mode should trigger based on the GitHub context
|
||||
*/
|
||||
shouldTrigger(context: ParsedGitHubContext): boolean;
|
||||
shouldTrigger(context: GitHubContext): boolean;
|
||||
|
||||
/**
|
||||
* Prepares the mode context with any additional data needed for prompt generation
|
||||
*/
|
||||
prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext;
|
||||
prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
|
||||
|
||||
/**
|
||||
* Returns the list of tools that should be allowed for this mode
|
||||
@@ -53,4 +56,38 @@ export type Mode = {
|
||||
* Determines if this mode should create a tracking comment
|
||||
*/
|
||||
shouldCreateTrackingComment(): boolean;
|
||||
|
||||
/**
|
||||
* Generates the prompt for this mode.
|
||||
* @returns The complete prompt string
|
||||
*/
|
||||
generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
): string;
|
||||
|
||||
/**
|
||||
* Prepares the GitHub environment for this mode.
|
||||
* Each mode decides how to handle different event types.
|
||||
* @returns PrepareResult with commentId, branchInfo, and mcpConfig
|
||||
*/
|
||||
prepare(options: ModeOptions): Promise<ModeResult>;
|
||||
};
|
||||
|
||||
// Define types for mode prepare method
|
||||
export type ModeOptions = {
|
||||
context: GitHubContext;
|
||||
octokit: Octokits;
|
||||
githubToken: string;
|
||||
};
|
||||
|
||||
export type ModeResult = {
|
||||
commentId?: number;
|
||||
branchInfo: {
|
||||
baseBranch: string;
|
||||
claudeBranch?: string;
|
||||
currentBranch: string;
|
||||
};
|
||||
mcpConfig: string;
|
||||
};
|
||||
|
||||
20
src/prepare/index.ts
Normal file
20
src/prepare/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Main prepare module that delegates to the mode's prepare method
|
||||
*/
|
||||
|
||||
import type { PrepareOptions, PrepareResult } from "./types";
|
||||
|
||||
export async function prepare(options: PrepareOptions): Promise<PrepareResult> {
|
||||
const { mode, context, octokit, githubToken } = options;
|
||||
|
||||
console.log(
|
||||
`Preparing with mode: ${mode.name} for event: ${context.eventName}`,
|
||||
);
|
||||
|
||||
// Delegate to the mode's prepare method
|
||||
return mode.prepare({
|
||||
context,
|
||||
octokit,
|
||||
githubToken,
|
||||
});
|
||||
}
|
||||
20
src/prepare/types.ts
Normal file
20
src/prepare/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import type { Octokits } from "../github/api/client";
|
||||
import type { Mode } from "../modes/types";
|
||||
|
||||
export type PrepareResult = {
|
||||
commentId?: number;
|
||||
branchInfo: {
|
||||
baseBranch: string;
|
||||
claudeBranch?: string;
|
||||
currentBranch: string;
|
||||
};
|
||||
mcpConfig: string;
|
||||
};
|
||||
|
||||
export type PrepareOptions = {
|
||||
context: GitHubContext;
|
||||
octokit: Octokits;
|
||||
mode: Mode;
|
||||
githubToken: string;
|
||||
};
|
||||
@@ -3,13 +3,37 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import {
|
||||
generatePrompt,
|
||||
generateDefaultPrompt,
|
||||
getEventTypeAndContext,
|
||||
buildAllowedToolsString,
|
||||
buildDisallowedToolsString,
|
||||
} from "../src/create-prompt";
|
||||
import type { PreparedContext } from "../src/create-prompt";
|
||||
import type { Mode } from "../src/modes/types";
|
||||
|
||||
describe("generatePrompt", () => {
|
||||
// Create a mock tag mode that uses the default prompt
|
||||
const mockTagMode: Mode = {
|
||||
name: "tag",
|
||||
description: "Tag mode",
|
||||
shouldTrigger: () => true,
|
||||
prepareContext: (context) => ({ mode: "tag", githubContext: context }),
|
||||
getAllowedTools: () => [],
|
||||
getDisallowedTools: () => [],
|
||||
shouldCreateTrackingComment: () => true,
|
||||
generatePrompt: (context, githubData, useCommitSigning) =>
|
||||
generateDefaultPrompt(context, githubData, useCommitSigning),
|
||||
prepare: async () => ({
|
||||
commentId: 123,
|
||||
branchInfo: {
|
||||
baseBranch: "main",
|
||||
currentBranch: "main",
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: "{}",
|
||||
}),
|
||||
};
|
||||
|
||||
const mockGitHubData = {
|
||||
contextData: {
|
||||
title: "Test PR",
|
||||
@@ -133,7 +157,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
|
||||
@@ -161,7 +185,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -187,7 +211,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -215,7 +239,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -242,7 +266,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -269,7 +293,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("<direct_prompt>");
|
||||
expect(prompt).toContain("Fix the bug in the login form");
|
||||
@@ -292,7 +316,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -317,7 +341,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("CUSTOM INSTRUCTIONS:\nAlways use TypeScript");
|
||||
});
|
||||
@@ -336,7 +360,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toBe("Simple prompt for owner/repo PR #123");
|
||||
expect(prompt).not.toContain("You are Claude, an AI assistant");
|
||||
@@ -371,7 +395,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("Repository: test/repo");
|
||||
expect(prompt).toContain("PR: 456");
|
||||
@@ -418,7 +442,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, issueGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, issueGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toBe("Issue #789: Bug: Login form broken in owner/repo");
|
||||
});
|
||||
@@ -438,7 +462,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toBe("PR: 123, Issue: , Comment: ");
|
||||
});
|
||||
@@ -458,7 +482,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||
@@ -481,7 +505,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
||||
// With commit signing disabled, co-author info appears in git commit instructions
|
||||
@@ -503,7 +527,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain PR-specific instructions (git commands when not using signing)
|
||||
expect(prompt).toContain("git push");
|
||||
@@ -534,7 +558,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain Issue-specific instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -573,7 +597,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain the actual branch name with timestamp
|
||||
expect(prompt).toContain(
|
||||
@@ -603,7 +627,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain branch-specific instructions like issues
|
||||
expect(prompt).toContain(
|
||||
@@ -641,7 +665,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain open PR instructions (git commands when not using signing)
|
||||
expect(prompt).toContain("git push");
|
||||
@@ -672,7 +696,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -700,7 +724,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -728,7 +752,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -752,7 +776,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
|
||||
|
||||
// Should have git command instructions
|
||||
expect(prompt).toContain("Use git commands via the Bash tool");
|
||||
@@ -781,7 +805,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, true);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
|
||||
|
||||
// Should have commit signing tool instructions
|
||||
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
||||
|
||||
@@ -252,6 +252,63 @@ describe("formatComments", () => {
|
||||
`[user1 at 2023-01-01T00:00:00Z]: Image: `,
|
||||
);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
@@ -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: `,
|
||||
);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
|
||||
10
test/fixtures/sample-turns-expected-output.md
vendored
10
test/fixtures/sample-turns-expected-output.md
vendored
@@ -28,7 +28,7 @@ if __name__ == "__main__":
|
||||
print(result)
|
||||
```
|
||||
|
||||
*Token usage: 100 input, 75 output*
|
||||
*Token usage: 150 input, 75 output*
|
||||
|
||||
---
|
||||
|
||||
@@ -47,7 +47,7 @@ I can see the debug print statement that needs to be removed. Let me fix this by
|
||||
|
||||
**→** File successfully edited. The debug print statement has been removed.
|
||||
|
||||
*Token usage: 200 input, 50 output*
|
||||
*Token usage: 300 input, 50 output*
|
||||
|
||||
---
|
||||
|
||||
@@ -70,7 +70,7 @@ Perfect! I've successfully removed the debug print statement from the function.
|
||||
|
||||
**→** Successfully posted review comment to PR #123
|
||||
|
||||
*Token usage: 150 input, 80 output*
|
||||
*Token usage: 225 input, 80 output*
|
||||
|
||||
---
|
||||
|
||||
@@ -82,7 +82,7 @@ Great! I've successfully completed the requested task:
|
||||
|
||||
The debug print statement has been removed as requested by the reviewers.
|
||||
|
||||
*Token usage: 180 input, 60 output*
|
||||
*Token usage: 270 input, 60 output*
|
||||
|
||||
---
|
||||
|
||||
@@ -91,5 +91,3 @@ The debug print statement has been removed as requested by the reviewers.
|
||||
Successfully removed debug print statement from file and added review comment to document the change.
|
||||
|
||||
**Cost:** $0.0347 | **Duration:** 18.8s
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ParsedGitHubContext } from "../src/github/context";
|
||||
import type {
|
||||
ParsedGitHubContext,
|
||||
AutomationContext,
|
||||
} from "../src/github/context";
|
||||
import type {
|
||||
IssuesEvent,
|
||||
IssueCommentEvent,
|
||||
@@ -38,7 +41,7 @@ export const createMockContext = (
|
||||
): ParsedGitHubContext => {
|
||||
const baseContext: ParsedGitHubContext = {
|
||||
runId: "1234567890",
|
||||
eventName: "",
|
||||
eventName: "issue_comment", // Default to a valid entity event
|
||||
eventAction: "",
|
||||
repository: defaultRepository,
|
||||
actor: "test-actor",
|
||||
@@ -55,6 +58,22 @@ export const createMockContext = (
|
||||
return { ...baseContext, ...overrides };
|
||||
};
|
||||
|
||||
export const createMockAutomationContext = (
|
||||
overrides: Partial<AutomationContext> = {},
|
||||
): AutomationContext => {
|
||||
const baseContext: AutomationContext = {
|
||||
runId: "1234567890",
|
||||
eventName: "workflow_dispatch",
|
||||
eventAction: undefined,
|
||||
repository: defaultRepository,
|
||||
actor: "test-actor",
|
||||
payload: {} as any,
|
||||
inputs: defaultInputs,
|
||||
};
|
||||
|
||||
return { ...baseContext, ...overrides };
|
||||
};
|
||||
|
||||
export const mockIssueOpenedContext: ParsedGitHubContext = {
|
||||
runId: "1234567890",
|
||||
eventName: "issues",
|
||||
|
||||
@@ -1,82 +1,59 @@
|
||||
import { describe, test, expect, beforeEach } from "bun:test";
|
||||
import { agentMode } from "../../src/modes/agent";
|
||||
import type { ParsedGitHubContext } from "../../src/github/context";
|
||||
import { createMockContext } from "../mockContext";
|
||||
import type { GitHubContext } from "../../src/github/context";
|
||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||
|
||||
describe("Agent Mode", () => {
|
||||
let mockContext: ParsedGitHubContext;
|
||||
let mockContext: GitHubContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = createMockContext({
|
||||
mockContext = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
isPR: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("agent mode has correct properties and behavior", () => {
|
||||
// Basic properties
|
||||
test("agent mode has correct properties", () => {
|
||||
expect(agentMode.name).toBe("agent");
|
||||
expect(agentMode.description).toBe(
|
||||
"Automation mode that always runs without trigger checking",
|
||||
"Automation mode for workflow_dispatch and schedule events",
|
||||
);
|
||||
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
|
||||
|
||||
// Tool methods return empty arrays
|
||||
expect(agentMode.getAllowedTools()).toEqual([]);
|
||||
expect(agentMode.getDisallowedTools()).toEqual([]);
|
||||
|
||||
// Always triggers regardless of context
|
||||
const contextWithoutTrigger = createMockContext({
|
||||
eventName: "workflow_dispatch",
|
||||
isPR: false,
|
||||
inputs: {
|
||||
...createMockContext().inputs,
|
||||
triggerPhrase: "@claude",
|
||||
},
|
||||
payload: {} as any,
|
||||
});
|
||||
expect(agentMode.shouldTrigger(contextWithoutTrigger)).toBe(true);
|
||||
});
|
||||
|
||||
test("prepareContext includes all required data", () => {
|
||||
const data = {
|
||||
commentId: 789,
|
||||
baseBranch: "develop",
|
||||
claudeBranch: "claude/automated-task",
|
||||
};
|
||||
|
||||
const context = agentMode.prepareContext(mockContext, data);
|
||||
|
||||
expect(context.mode).toBe("agent");
|
||||
expect(context.githubContext).toBe(mockContext);
|
||||
expect(context.commentId).toBe(789);
|
||||
expect(context.baseBranch).toBe("develop");
|
||||
expect(context.claudeBranch).toBe("claude/automated-task");
|
||||
});
|
||||
|
||||
test("prepareContext works without data", () => {
|
||||
test("prepareContext returns minimal data", () => {
|
||||
const context = agentMode.prepareContext(mockContext);
|
||||
|
||||
expect(context.mode).toBe("agent");
|
||||
expect(context.githubContext).toBe(mockContext);
|
||||
expect(context.commentId).toBeUndefined();
|
||||
expect(context.baseBranch).toBeUndefined();
|
||||
expect(context.claudeBranch).toBeUndefined();
|
||||
// Agent mode doesn't use comment tracking or branch management
|
||||
expect(Object.keys(context)).toEqual(["mode", "githubContext"]);
|
||||
});
|
||||
|
||||
test("agent mode triggers for all event types", () => {
|
||||
const events = [
|
||||
"push",
|
||||
"schedule",
|
||||
"workflow_dispatch",
|
||||
"repository_dispatch",
|
||||
test("agent mode only triggers for workflow_dispatch and schedule events", () => {
|
||||
// Should trigger for automation events
|
||||
const workflowDispatchContext = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true);
|
||||
|
||||
const scheduleContext = createMockAutomationContext({
|
||||
eventName: "schedule",
|
||||
});
|
||||
expect(agentMode.shouldTrigger(scheduleContext)).toBe(true);
|
||||
|
||||
// Should NOT trigger for entity events
|
||||
const entityEvents = [
|
||||
"issue_comment",
|
||||
"pull_request",
|
||||
];
|
||||
"pull_request_review",
|
||||
"issues",
|
||||
] as const;
|
||||
|
||||
events.forEach((eventName) => {
|
||||
const context = createMockContext({ eventName, isPR: false });
|
||||
expect(agentMode.shouldTrigger(context)).toBe(true);
|
||||
entityEvents.forEach((eventName) => {
|
||||
const context = createMockContext({ eventName });
|
||||
expect(agentMode.shouldTrigger(context)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,34 +3,78 @@ import { getMode, isValidMode } from "../../src/modes/registry";
|
||||
import type { ModeName } from "../../src/modes/types";
|
||||
import { tagMode } from "../../src/modes/tag";
|
||||
import { agentMode } from "../../src/modes/agent";
|
||||
import { reviewMode } from "../../src/modes/review";
|
||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||
|
||||
describe("Mode Registry", () => {
|
||||
test("getMode returns tag mode by default", () => {
|
||||
const mode = getMode("tag");
|
||||
const mockContext = createMockContext({
|
||||
eventName: "issue_comment",
|
||||
});
|
||||
|
||||
const mockWorkflowDispatchContext = createMockAutomationContext({
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
|
||||
const mockScheduleContext = createMockAutomationContext({
|
||||
eventName: "schedule",
|
||||
});
|
||||
|
||||
test("getMode returns tag mode for standard events", () => {
|
||||
const mode = getMode("tag", mockContext);
|
||||
expect(mode).toBe(tagMode);
|
||||
expect(mode.name).toBe("tag");
|
||||
});
|
||||
|
||||
test("getMode returns agent mode", () => {
|
||||
const mode = getMode("agent");
|
||||
const mode = getMode("agent", mockContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode returns experimental-review mode", () => {
|
||||
const mode = getMode("experimental-review", mockContext);
|
||||
expect(mode).toBe(reviewMode);
|
||||
expect(mode.name).toBe("experimental-review");
|
||||
});
|
||||
|
||||
test("getMode throws error for tag mode with workflow_dispatch event", () => {
|
||||
expect(() => getMode("tag", mockWorkflowDispatchContext)).toThrow(
|
||||
"Tag mode cannot handle workflow_dispatch events. Use 'agent' mode for automation events.",
|
||||
);
|
||||
});
|
||||
|
||||
test("getMode throws error for tag mode with schedule event", () => {
|
||||
expect(() => getMode("tag", mockScheduleContext)).toThrow(
|
||||
"Tag mode cannot handle schedule events. Use 'agent' mode for automation events.",
|
||||
);
|
||||
});
|
||||
|
||||
test("getMode allows agent mode for workflow_dispatch event", () => {
|
||||
const mode = getMode("agent", mockWorkflowDispatchContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode allows agent mode for schedule event", () => {
|
||||
const mode = getMode("agent", mockScheduleContext);
|
||||
expect(mode).toBe(agentMode);
|
||||
expect(mode.name).toBe("agent");
|
||||
});
|
||||
|
||||
test("getMode throws error for invalid mode", () => {
|
||||
const invalidMode = "invalid" as unknown as ModeName;
|
||||
expect(() => getMode(invalidMode)).toThrow(
|
||||
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.",
|
||||
expect(() => getMode(invalidMode, mockContext)).toThrow(
|
||||
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent', 'experimental-review'. Please check your workflow configuration.",
|
||||
);
|
||||
});
|
||||
|
||||
test("isValidMode returns true for all valid modes", () => {
|
||||
expect(isValidMode("tag")).toBe(true);
|
||||
expect(isValidMode("agent")).toBe(true);
|
||||
expect(isValidMode("experimental-review")).toBe(true);
|
||||
});
|
||||
|
||||
test("isValidMode returns false for invalid mode", () => {
|
||||
expect(isValidMode("invalid")).toBe(false);
|
||||
expect(isValidMode("review")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,15 +299,4 @@ describe("parseEnvVarsWithContext", () => {
|
||||
expect(result.allowedTools).toBe("Tool1,Tool2");
|
||||
});
|
||||
});
|
||||
|
||||
test("should throw error for unsupported event type", () => {
|
||||
process.env = BASE_ENV;
|
||||
const unsupportedContext = createMockContext({
|
||||
eventName: "unsupported_event",
|
||||
eventAction: "whatever",
|
||||
});
|
||||
expect(() => prepareContext(unsupportedContext, "12345")).toThrow(
|
||||
"Unsupported event type: unsupported_event",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -474,17 +474,6 @@ describe("checkContainsTrigger", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-matching events", () => {
|
||||
it("should return false for non-matching event type", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "push",
|
||||
eventAction: "created",
|
||||
payload: {} as any,
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("escapeRegExp", () => {
|
||||
|
||||
Reference in New Issue
Block a user