mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 07:24:12 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00f9595fb4 | ||
|
|
79f2086fce | ||
|
|
bcb072b63f | ||
|
|
e3b3e531a7 | ||
|
|
a7665d3698 | ||
|
|
91c510a769 | ||
|
|
1e006bf2d0 | ||
|
|
ece712ea81 | ||
|
|
032008d3b6 | ||
|
|
b0d9b8c4cd | ||
|
|
c831be8f54 | ||
|
|
38254908ae | ||
|
|
882586e496 | ||
|
|
28aaa5404d |
4
FAQ.md
4
FAQ.md
@@ -12,6 +12,10 @@ The `github-actions` user cannot trigger subsequent GitHub Actions workflows. Th
|
|||||||
|
|
||||||
Only users with **write permissions** to the repository can trigger Claude. This is a security feature to prevent unauthorized use. Make sure the user commenting has at least write access to the repository.
|
Only users with **write permissions** to the repository can trigger Claude. This is a security feature to prevent unauthorized use. Make sure the user commenting has at least write access to the repository.
|
||||||
|
|
||||||
|
### Why can't I assign @claude to an issue on my repository?
|
||||||
|
|
||||||
|
If you're in a public repository, you should be able to assign to Claude without issue. If it's a private organization repository, you can only assign to users in your own organization, which Claude isn't. In this case, you'll need to make a custom user in that case.
|
||||||
|
|
||||||
### Why am I getting OIDC authentication errors?
|
### Why am I getting OIDC authentication errors?
|
||||||
|
|
||||||
If you're using the default GitHub App authentication, you must add the `id-token: write` permission to your workflow:
|
If you're using the default GitHub App authentication, you must add the `id-token: write` permission to your workflow:
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ on:
|
|||||||
pull_request_review_comment:
|
pull_request_review_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
issues:
|
issues:
|
||||||
types: [opened, assigned]
|
types: [opened, assigned, labeled]
|
||||||
pull_request_review:
|
pull_request_review:
|
||||||
types: [submitted]
|
types: [submitted]
|
||||||
|
|
||||||
@@ -65,6 +65,8 @@ jobs:
|
|||||||
# trigger_phrase: "/claude"
|
# trigger_phrase: "/claude"
|
||||||
# Optional: add assignee trigger for issues
|
# Optional: add assignee trigger for issues
|
||||||
# assignee_trigger: "claude"
|
# assignee_trigger: "claude"
|
||||||
|
# Optional: add label trigger for issues
|
||||||
|
# label_trigger: "claude"
|
||||||
# Optional: add custom environment variables (YAML format)
|
# Optional: add custom environment variables (YAML format)
|
||||||
# claude_env: |
|
# claude_env: |
|
||||||
# NODE_ENV: test
|
# NODE_ENV: test
|
||||||
@@ -80,8 +82,10 @@ jobs:
|
|||||||
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
| --------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | 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 | - |
|
| `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` |
|
| `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 | - |
|
| `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 | - |
|
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
|
||||||
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
|
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
|
||||||
@@ -92,7 +96,9 @@ jobs:
|
|||||||
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | 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 | "" |
|
| `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 | - |
|
| `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` |
|
| `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 | "" |
|
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
|
||||||
|
|
||||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||||
|
|||||||
18
action.yml
18
action.yml
@@ -12,9 +12,17 @@ inputs:
|
|||||||
assignee_trigger:
|
assignee_trigger:
|
||||||
description: "The assignee username that triggers the action (e.g. @claude)"
|
description: "The assignee username that triggers the action (e.g. @claude)"
|
||||||
required: false
|
required: false
|
||||||
|
label_trigger:
|
||||||
|
description: "The label that triggers the action (e.g. claude)"
|
||||||
|
required: false
|
||||||
|
default: "claude"
|
||||||
base_branch:
|
base_branch:
|
||||||
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
|
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
|
||||||
required: false
|
required: false
|
||||||
|
branch_prefix:
|
||||||
|
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
||||||
|
required: false
|
||||||
|
default: "claude/"
|
||||||
|
|
||||||
# Claude Code configuration
|
# Claude Code configuration
|
||||||
model:
|
model:
|
||||||
@@ -70,6 +78,10 @@ inputs:
|
|||||||
description: "Timeout in minutes for execution"
|
description: "Timeout in minutes for execution"
|
||||||
required: false
|
required: false
|
||||||
default: "30"
|
default: "30"
|
||||||
|
use_sticky_comment:
|
||||||
|
description: "Use just one comment to deliver issue/PR comments"
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
execution_file:
|
execution_file:
|
||||||
@@ -98,7 +110,9 @@ runs:
|
|||||||
env:
|
env:
|
||||||
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||||
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
||||||
|
LABEL_TRIGGER: ${{ inputs.label_trigger }}
|
||||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||||
|
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
||||||
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
||||||
DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
|
DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
|
||||||
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
||||||
@@ -106,11 +120,12 @@ runs:
|
|||||||
MCP_CONFIG: ${{ inputs.mcp_config }}
|
MCP_CONFIG: ${{ inputs.mcp_config }}
|
||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
id: claude-code
|
id: claude-code
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||||
uses: anthropics/claude-code-base-action@f382bd1ea00f26043eb461ebabebe0d850572a71 # v0.0.24
|
uses: anthropics/claude-code-base-action@bdaad5f64e7ad7a8c0be290a3c49d0fa7e1bb442 # v0.0.29
|
||||||
with:
|
with:
|
||||||
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
||||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||||
@@ -170,6 +185,7 @@ runs:
|
|||||||
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
||||||
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||||
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
||||||
|
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||||
|
|
||||||
- name: Display Claude Code Report
|
- name: Display Claude Code Report
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
name: Claude Task Executor
|
|
||||||
|
|
||||||
on:
|
|
||||||
repository_dispatch:
|
|
||||||
types: [claude-task]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
id-token: write # Required for OIDC authentication
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
execute-claude-task:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Execute Claude Task
|
|
||||||
uses: anthropics/claude-code-action@main
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
# Base branch for creating task branches
|
|
||||||
base_branch: main
|
|
||||||
# Optional: Custom instructions for Claude
|
|
||||||
custom_instructions: |
|
|
||||||
Follow the CLAUDE.md guidelines strictly.
|
|
||||||
Commit changes with descriptive messages.
|
|
||||||
# Optional: Tool restrictions
|
|
||||||
allowed_tools: |
|
|
||||||
file_editor
|
|
||||||
bash_command
|
|
||||||
github_comment
|
|
||||||
mcp__github__create_or_update_file
|
|
||||||
# Optional: Anthropic API configuration
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
# Or use AWS Bedrock
|
|
||||||
# aws_access_key: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
||||||
# aws_secret_key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
||||||
# aws_region: us-east-1
|
|
||||||
# Or use Google Vertex AI
|
|
||||||
# google_credentials: ${{ secrets.GOOGLE_CREDENTIALS }}
|
|
||||||
# vertex_project: my-project
|
|
||||||
# vertex_location: us-central1
|
|
||||||
# Example: Triggering this workflow from another service
|
|
||||||
#
|
|
||||||
# curl -X POST \
|
|
||||||
# https://api.github.com/repos/owner/repo/dispatches \
|
|
||||||
# -H "Authorization: token $GITHUB_TOKEN" \
|
|
||||||
# -H "Accept: application/vnd.github.v3+json" \
|
|
||||||
# -d '{
|
|
||||||
# "event_type": "claude-task",
|
|
||||||
# "client_payload": {
|
|
||||||
# "description": "Analyze the codebase and create a comprehensive test suite for the authentication module",
|
|
||||||
# "progress_endpoint": "https://api.example.com/claude/progress",
|
|
||||||
# "correlation_id": "task-auth-tests-2024-01-17"
|
|
||||||
# }
|
|
||||||
# }'
|
|
||||||
#
|
|
||||||
# The progress_endpoint will receive POST requests with:
|
|
||||||
# {
|
|
||||||
# "repository": "owner/repo",
|
|
||||||
# "run_id": "123456789",
|
|
||||||
# "correlation_id": "task-auth-tests-2024-01-17",
|
|
||||||
# "status": "in_progress" | "completed" | "failed",
|
|
||||||
# "message": "Current progress description",
|
|
||||||
# "completed_tasks": ["task1", "task2"],
|
|
||||||
# "current_task": "Working on task3",
|
|
||||||
# "timestamp": "2024-01-17T12:00:00Z"
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# Authentication: Progress updates include a GitHub OIDC token in the Authorization header
|
|
||||||
118
pr-summary.md
118
pr-summary.md
@@ -1,118 +0,0 @@
|
|||||||
## Summary
|
|
||||||
|
|
||||||
Adds support for `repository_dispatch` events, enabling backend services to programmatically trigger Claude to perform tasks and receive progress updates via API.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant Backend as Backend Service
|
|
||||||
participant GH as GitHub
|
|
||||||
participant Action as Claude Action
|
|
||||||
participant Claude as Claude
|
|
||||||
participant MCP as Progress MCP Server
|
|
||||||
participant API as Progress API
|
|
||||||
|
|
||||||
Backend->>GH: POST /repos/{owner}/{repo}/dispatches
|
|
||||||
Note over Backend,GH: Payload includes:<br/>- description (task)<br/>- progress_endpoint<br/>- correlation_id
|
|
||||||
|
|
||||||
GH->>Action: Trigger workflow<br/>(repository_dispatch)
|
|
||||||
|
|
||||||
Action->>Action: Parse dispatch payload
|
|
||||||
Note over Action: Extract task description,<br/>endpoint, correlation_id
|
|
||||||
|
|
||||||
Action->>MCP: Install Progress Server
|
|
||||||
Note over MCP: Configure with:<br/>- PROGRESS_ENDPOINT<br/>- CORRELATION_ID<br/>- GITHUB_RUN_ID
|
|
||||||
|
|
||||||
Action->>Claude: Execute task with<br/>MCP tools available
|
|
||||||
|
|
||||||
loop Task Execution
|
|
||||||
Claude->>MCP: update_claude_progress()
|
|
||||||
MCP->>MCP: Get OIDC token
|
|
||||||
MCP->>API: POST progress update
|
|
||||||
Note over API: Payload includes:<br/>- correlation_id<br/>- status<br/>- message<br/>- completed_tasks
|
|
||||||
API->>Backend: Forward update
|
|
||||||
end
|
|
||||||
|
|
||||||
Claude->>Action: Task complete
|
|
||||||
Action->>GH: Commit changes
|
|
||||||
```
|
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
### 1. Repository Dispatch Support
|
|
||||||
|
|
||||||
- New event handler for `repository_dispatch` events
|
|
||||||
- Extracts task description, progress endpoint, and correlation ID from `client_payload`
|
|
||||||
- Bypasses GitHub UI interaction for fully programmatic operation
|
|
||||||
|
|
||||||
### 2. Progress Reporting MCP Server
|
|
||||||
|
|
||||||
- New MCP server (`progress-server.ts`) for sending progress updates
|
|
||||||
- OIDC authentication for secure API communication
|
|
||||||
- Includes correlation ID in all updates for request tracking
|
|
||||||
|
|
||||||
### 3. Simplified Dispatch Prompts
|
|
||||||
|
|
||||||
- Focused instructions for dispatch events (no PR/issue context)
|
|
||||||
- Clear directives: answer questions or implement changes
|
|
||||||
- Automatic progress updates at start and completion
|
|
||||||
|
|
||||||
## Implementation Details
|
|
||||||
|
|
||||||
### Triggering a Dispatch
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -X POST \
|
|
||||||
https://api.github.com/repos/{owner}/{repo}/dispatches \
|
|
||||||
-H "Authorization: token $GITHUB_TOKEN" \
|
|
||||||
-H "Accept: application/vnd.github.v3+json" \
|
|
||||||
-d '{
|
|
||||||
"event_type": "claude-task",
|
|
||||||
"client_payload": {
|
|
||||||
"description": "Implement a new feature that...",
|
|
||||||
"progress_endpoint": "https://api.example.com/progress",
|
|
||||||
"correlation_id": "req-123-abc"
|
|
||||||
}
|
|
||||||
}'
|
|
||||||
```
|
|
||||||
|
|
||||||
### Progress Update Payload
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"repository": "owner/repo",
|
|
||||||
"run_id": "123456789",
|
|
||||||
"correlation_id": "req-123-abc",
|
|
||||||
"status": "in_progress",
|
|
||||||
"message": "Implementing feature...",
|
|
||||||
"completed_tasks": ["Setup environment", "Created base structure"],
|
|
||||||
"current_task": "Writing tests",
|
|
||||||
"timestamp": "2024-01-17T12:00:00Z"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security
|
|
||||||
|
|
||||||
- **OIDC Authentication**: All progress updates use GitHub OIDC tokens
|
|
||||||
- **Correlation IDs**: Included in request body (not URL) for security
|
|
||||||
- **Endpoint Validation**: Progress endpoint must be explicitly provided
|
|
||||||
- **No Credential Storage**: Tokens are generated per-request
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
To test the repository_dispatch flow:
|
|
||||||
|
|
||||||
1. Configure workflow with `repository_dispatch` trigger
|
|
||||||
2. Send dispatch event with required payload
|
|
||||||
3. Monitor GitHub Actions logs for execution
|
|
||||||
4. Verify progress updates at configured endpoint
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
- Added `repository_dispatch` event handling in `context.ts`
|
|
||||||
- Created new `progress-server.ts` MCP server
|
|
||||||
- Updated `isDispatch` flag across all event types
|
|
||||||
- Modified prompt generation for dispatch events
|
|
||||||
- Made `githubData` optional for dispatch workflows
|
|
||||||
- Added correlation ID support throughout the pipeline
|
|
||||||
@@ -81,6 +81,7 @@ export function prepareContext(
|
|||||||
const eventAction = context.eventAction;
|
const eventAction = context.eventAction;
|
||||||
const triggerPhrase = context.inputs.triggerPhrase || "@claude";
|
const triggerPhrase = context.inputs.triggerPhrase || "@claude";
|
||||||
const assigneeTrigger = context.inputs.assigneeTrigger;
|
const assigneeTrigger = context.inputs.assigneeTrigger;
|
||||||
|
const labelTrigger = context.inputs.labelTrigger;
|
||||||
const customInstructions = context.inputs.customInstructions;
|
const customInstructions = context.inputs.customInstructions;
|
||||||
const allowedTools = context.inputs.allowedTools;
|
const allowedTools = context.inputs.allowedTools;
|
||||||
const disallowedTools = context.inputs.disallowedTools;
|
const disallowedTools = context.inputs.disallowedTools;
|
||||||
@@ -242,7 +243,7 @@ export function prepareContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventAction === "assigned") {
|
if (eventAction === "assigned") {
|
||||||
if (!assigneeTrigger) {
|
if (!assigneeTrigger && !directPrompt) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"ASSIGNEE_TRIGGER is required for issue assigned event",
|
"ASSIGNEE_TRIGGER is required for issue assigned event",
|
||||||
);
|
);
|
||||||
@@ -254,7 +255,20 @@ export function prepareContext(
|
|||||||
issueNumber,
|
issueNumber,
|
||||||
baseBranch,
|
baseBranch,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
assigneeTrigger,
|
...(assigneeTrigger && { assigneeTrigger }),
|
||||||
|
};
|
||||||
|
} else if (eventAction === "labeled") {
|
||||||
|
if (!labelTrigger) {
|
||||||
|
throw new Error("LABEL_TRIGGER is required for issue labeled event");
|
||||||
|
}
|
||||||
|
eventData = {
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "labeled",
|
||||||
|
isPR: false,
|
||||||
|
issueNumber,
|
||||||
|
baseBranch,
|
||||||
|
claudeBranch,
|
||||||
|
labelTrigger,
|
||||||
};
|
};
|
||||||
} else if (eventAction === "opened") {
|
} else if (eventAction === "opened") {
|
||||||
eventData = {
|
eventData = {
|
||||||
@@ -328,10 +342,17 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
|||||||
eventType: "ISSUE_CREATED",
|
eventType: "ISSUE_CREATED",
|
||||||
triggerContext: `new issue with '${envVars.triggerPhrase}' in body`,
|
triggerContext: `new issue with '${envVars.triggerPhrase}' in body`,
|
||||||
};
|
};
|
||||||
|
} else if (eventData.eventAction === "labeled") {
|
||||||
|
return {
|
||||||
|
eventType: "ISSUE_LABELED",
|
||||||
|
triggerContext: `issue labeled with '${eventData.labelTrigger}'`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
eventType: "ISSUE_ASSIGNED",
|
eventType: "ISSUE_ASSIGNED",
|
||||||
triggerContext: `issue assigned to '${eventData.assigneeTrigger}'`,
|
triggerContext: eventData.assigneeTrigger
|
||||||
|
? `issue assigned to '${eventData.assigneeTrigger}'`
|
||||||
|
: `issue assigned event`,
|
||||||
};
|
};
|
||||||
|
|
||||||
case "pull_request":
|
case "pull_request":
|
||||||
@@ -465,6 +486,7 @@ Follow these steps:
|
|||||||
- Analyze the pre-fetched data provided above.
|
- Analyze the pre-fetched data provided above.
|
||||||
- For ISSUE_CREATED: Read the issue body to find the request after the trigger phrase.
|
- For ISSUE_CREATED: Read the issue body to find the request after the trigger phrase.
|
||||||
- For ISSUE_ASSIGNED: Read the entire issue body to understand the task.
|
- For ISSUE_ASSIGNED: Read the entire issue body to understand the task.
|
||||||
|
- For ISSUE_LABELED: Read the entire issue body to understand the task.
|
||||||
${eventData.eventName === "issue_comment" || eventData.eventName === "pull_request_review_comment" || eventData.eventName === "pull_request_review" ? ` - For comment/review events: Your instructions are in the <trigger_comment> tag above.` : ""}
|
${eventData.eventName === "issue_comment" || eventData.eventName === "pull_request_review_comment" || eventData.eventName === "pull_request_review" ? ` - For comment/review events: Your instructions are in the <trigger_comment> tag above.` : ""}
|
||||||
${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was provided and is shown in the <direct_prompt> tag above. This is not from any GitHub comment but a direct instruction to execute.` : ""}
|
${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was provided and is shown in the <direct_prompt> tag above. This is not from any GitHub comment but a direct instruction to execute.` : ""}
|
||||||
- IMPORTANT: Only the comment/issue containing '${context.triggerPhrase}' has your instructions.
|
- IMPORTANT: Only the comment/issue containing '${context.triggerPhrase}' has your instructions.
|
||||||
|
|||||||
@@ -65,7 +65,17 @@ type IssueAssignedEvent = {
|
|||||||
issueNumber: string;
|
issueNumber: string;
|
||||||
baseBranch: string;
|
baseBranch: string;
|
||||||
claudeBranch: string;
|
claudeBranch: string;
|
||||||
assigneeTrigger: string;
|
assigneeTrigger?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IssueLabeledEvent = {
|
||||||
|
eventName: "issues";
|
||||||
|
eventAction: "labeled";
|
||||||
|
isPR: false;
|
||||||
|
issueNumber: string;
|
||||||
|
baseBranch: string;
|
||||||
|
claudeBranch: string;
|
||||||
|
labelTrigger: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PullRequestEvent = {
|
type PullRequestEvent = {
|
||||||
@@ -85,6 +95,7 @@ export type EventData =
|
|||||||
| IssueCommentEvent
|
| IssueCommentEvent
|
||||||
| IssueOpenedEvent
|
| IssueOpenedEvent
|
||||||
| IssueAssignedEvent
|
| IssueAssignedEvent
|
||||||
|
| IssueLabeledEvent
|
||||||
| PullRequestEvent;
|
| PullRequestEvent;
|
||||||
|
|
||||||
// Combined type with separate eventData field
|
// Combined type with separate eventData field
|
||||||
|
|||||||
@@ -29,11 +29,14 @@ export type ParsedGitHubContext = {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: string;
|
triggerPhrase: string;
|
||||||
assigneeTrigger: string;
|
assigneeTrigger: string;
|
||||||
|
labelTrigger: string;
|
||||||
allowedTools: string[];
|
allowedTools: string[];
|
||||||
disallowedTools: string[];
|
disallowedTools: string[];
|
||||||
customInstructions: string;
|
customInstructions: string;
|
||||||
directPrompt: string;
|
directPrompt: string;
|
||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
|
branchPrefix: string;
|
||||||
|
useStickyComment: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,11 +56,14 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
|
||||||
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
|
||||||
|
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
||||||
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
|
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
|
||||||
disallowedTools: parseMultilineInput(process.env.DISALLOWED_TOOLS ?? ""),
|
disallowedTools: parseMultilineInput(process.env.DISALLOWED_TOOLS ?? ""),
|
||||||
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
||||||
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
||||||
baseBranch: process.env.BASE_BRANCH,
|
baseBranch: process.env.BASE_BRANCH,
|
||||||
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||||
|
useStickyComment: process.env.STICKY_COMMENT === "true",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export async function setupBranch(
|
|||||||
): Promise<BranchInfo> {
|
): Promise<BranchInfo> {
|
||||||
const { owner, repo } = context.repository;
|
const { owner, repo } = context.repository;
|
||||||
const entityNumber = context.entityNumber;
|
const entityNumber = context.entityNumber;
|
||||||
const { baseBranch } = context.inputs;
|
const { baseBranch, branchPrefix } = context.inputs;
|
||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
@@ -97,7 +97,7 @@ export async function setupBranch(
|
|||||||
.split("T")
|
.split("T")
|
||||||
.join("_");
|
.join("_");
|
||||||
|
|
||||||
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
const newBranch = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the SHA of the source branch
|
// Get the SHA of the source branch
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { appendFileSync } from "fs";
|
|||||||
import { createJobRunLink, createCommentBody } from "./common";
|
import { createJobRunLink, createCommentBody } from "./common";
|
||||||
import {
|
import {
|
||||||
isPullRequestReviewCommentEvent,
|
isPullRequestReviewCommentEvent,
|
||||||
|
isPullRequestEvent,
|
||||||
type ParsedGitHubContext,
|
type ParsedGitHubContext,
|
||||||
} from "../../context";
|
} from "../../context";
|
||||||
import type { Octokit } from "@octokit/rest";
|
import type { Octokit } from "@octokit/rest";
|
||||||
@@ -25,8 +26,39 @@ export async function createInitialComment(
|
|||||||
try {
|
try {
|
||||||
let response;
|
let response;
|
||||||
|
|
||||||
// Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
|
if (
|
||||||
if (isPullRequestReviewCommentEvent(context)) {
|
context.inputs.useStickyComment &&
|
||||||
|
context.isPR &&
|
||||||
|
!isPullRequestEvent(context)
|
||||||
|
) {
|
||||||
|
const comments = await octokit.rest.issues.listComments({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: context.entityNumber,
|
||||||
|
});
|
||||||
|
const existingComment = comments.data.find(
|
||||||
|
(comment) =>
|
||||||
|
comment.user?.login.indexOf("claude[bot]") !== -1 ||
|
||||||
|
comment.body === initialBody,
|
||||||
|
);
|
||||||
|
if (existingComment) {
|
||||||
|
response = await octokit.rest.issues.updateComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: existingComment.id,
|
||||||
|
body: initialBody,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Create new comment if no existing one found
|
||||||
|
response = await octokit.rest.issues.createComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
issue_number: context.entityNumber,
|
||||||
|
body: initialBody,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (isPullRequestReviewCommentEvent(context)) {
|
||||||
|
// Only use createReplyForReviewComment if it's a PR review comment AND we have a comment_id
|
||||||
response = await octokit.rest.pulls.createReplyForReviewComment({
|
response = await octokit.rest.pulls.createReplyForReviewComment({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { ParsedGitHubContext } from "../context";
|
|||||||
|
|
||||||
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
||||||
const {
|
const {
|
||||||
inputs: { assigneeTrigger, triggerPhrase, directPrompt },
|
inputs: { assigneeTrigger, labelTrigger, triggerPhrase, directPrompt },
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
// If direct prompt is provided, always trigger
|
// If direct prompt is provided, always trigger
|
||||||
@@ -34,6 +34,16 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for label trigger
|
||||||
|
if (isIssuesEvent(context) && context.eventAction === "labeled") {
|
||||||
|
const labelName = (context.payload as any).label?.name || "";
|
||||||
|
|
||||||
|
if (labelTrigger && labelName === labelTrigger) {
|
||||||
|
console.log(`Issue labeled with trigger label '${labelTrigger}'`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check for issue body and title trigger on issue creation
|
// Check for issue body and title trigger on issue creation
|
||||||
if (isIssuesEvent(context) && context.eventAction === "opened") {
|
if (isIssuesEvent(context) && context.eventAction === "opened") {
|
||||||
const issueBody = context.payload.issue.body || "";
|
const issueBody = context.payload.issue.body || "";
|
||||||
|
|||||||
@@ -125,13 +125,58 @@ server.tool(
|
|||||||
? filePath
|
? filePath
|
||||||
: join(REPO_DIR, filePath);
|
: join(REPO_DIR, filePath);
|
||||||
|
|
||||||
const content = await readFile(fullPath, "utf-8");
|
// Check if file is binary (images, etc.)
|
||||||
return {
|
const isBinaryFile =
|
||||||
path: filePath,
|
/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test(
|
||||||
mode: "100644",
|
filePath,
|
||||||
type: "blob",
|
);
|
||||||
content: content,
|
|
||||||
};
|
if (isBinaryFile) {
|
||||||
|
// For binary files, create a blob first using the Blobs API
|
||||||
|
const binaryContent = await readFile(fullPath);
|
||||||
|
|
||||||
|
// Create blob using Blobs API (supports encoding parameter)
|
||||||
|
const blobUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/blobs`;
|
||||||
|
const blobResponse = await fetch(blobUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${githubToken}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: binaryContent.toString("base64"),
|
||||||
|
encoding: "base64",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!blobResponse.ok) {
|
||||||
|
const errorText = await blobResponse.text();
|
||||||
|
throw new Error(
|
||||||
|
`Failed to create blob for ${filePath}: ${blobResponse.status} - ${errorText}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blobData = (await blobResponse.json()) as { sha: string };
|
||||||
|
|
||||||
|
// Return tree entry with blob SHA
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
mode: "100644",
|
||||||
|
type: "blob",
|
||||||
|
sha: blobData.sha,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// For text files, include content directly in tree
|
||||||
|
const content = await readFile(fullPath, "utf-8");
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
mode: "100644",
|
||||||
|
type: "blob",
|
||||||
|
content: content,
|
||||||
|
};
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -226,6 +226,33 @@ describe("generatePrompt", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should generate prompt for issue labeled event", () => {
|
||||||
|
const envVars: PreparedContext = {
|
||||||
|
repository: "owner/repo",
|
||||||
|
claudeCommentId: "12345",
|
||||||
|
triggerPhrase: "@claude",
|
||||||
|
eventData: {
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "labeled",
|
||||||
|
isPR: false,
|
||||||
|
issueNumber: "888",
|
||||||
|
baseBranch: "main",
|
||||||
|
claudeBranch: "claude/issue-888-20240101_120000",
|
||||||
|
labelTrigger: "claude-task",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||||
|
|
||||||
|
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
|
||||||
|
expect(prompt).toContain(
|
||||||
|
"<trigger_context>issue labeled with 'claude-task'</trigger_context>",
|
||||||
|
);
|
||||||
|
expect(prompt).toContain(
|
||||||
|
"[Create a PR](https://github.com/owner/repo/compare/main",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test("should include direct prompt when provided", () => {
|
test("should include direct prompt when provided", () => {
|
||||||
const envVars: PreparedContext = {
|
const envVars: PreparedContext = {
|
||||||
repository: "owner/repo",
|
repository: "owner/repo",
|
||||||
@@ -614,6 +641,51 @@ describe("getEventTypeAndContext", () => {
|
|||||||
expect(result.eventType).toBe("ISSUE_ASSIGNED");
|
expect(result.eventType).toBe("ISSUE_ASSIGNED");
|
||||||
expect(result.triggerContext).toBe("issue assigned to 'claude-bot'");
|
expect(result.triggerContext).toBe("issue assigned to 'claude-bot'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should return correct type and context for issue labeled", () => {
|
||||||
|
const envVars: PreparedContext = {
|
||||||
|
repository: "owner/repo",
|
||||||
|
claudeCommentId: "12345",
|
||||||
|
triggerPhrase: "@claude",
|
||||||
|
eventData: {
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "labeled",
|
||||||
|
isPR: false,
|
||||||
|
issueNumber: "888",
|
||||||
|
baseBranch: "main",
|
||||||
|
claudeBranch: "claude/issue-888-20240101_120000",
|
||||||
|
labelTrigger: "claude-task",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getEventTypeAndContext(envVars);
|
||||||
|
|
||||||
|
expect(result.eventType).toBe("ISSUE_LABELED");
|
||||||
|
expect(result.triggerContext).toBe("issue labeled with 'claude-task'");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return correct type and context for issue assigned without assigneeTrigger", () => {
|
||||||
|
const envVars: PreparedContext = {
|
||||||
|
repository: "owner/repo",
|
||||||
|
claudeCommentId: "12345",
|
||||||
|
triggerPhrase: "@claude",
|
||||||
|
directPrompt: "Please assess this issue",
|
||||||
|
eventData: {
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "assigned",
|
||||||
|
isPR: false,
|
||||||
|
issueNumber: "999",
|
||||||
|
baseBranch: "main",
|
||||||
|
claudeBranch: "claude/issue-999-20240101_120000",
|
||||||
|
// No assigneeTrigger when using directPrompt
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = getEventTypeAndContext(envVars);
|
||||||
|
|
||||||
|
expect(result.eventType).toBe("ISSUE_ASSIGNED");
|
||||||
|
expect(result.triggerContext).toBe("issue assigned event");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildAllowedToolsString", () => {
|
describe("buildAllowedToolsString", () => {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type {
|
|||||||
const defaultInputs = {
|
const defaultInputs = {
|
||||||
triggerPhrase: "/claude",
|
triggerPhrase: "/claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
|
labelTrigger: "",
|
||||||
anthropicModel: "claude-3-7-sonnet-20250219",
|
anthropicModel: "claude-3-7-sonnet-20250219",
|
||||||
allowedTools: [] as string[],
|
allowedTools: [] as string[],
|
||||||
disallowedTools: [] as string[],
|
disallowedTools: [] as string[],
|
||||||
@@ -18,6 +19,8 @@ const defaultInputs = {
|
|||||||
useBedrock: false,
|
useBedrock: false,
|
||||||
useVertex: false,
|
useVertex: false,
|
||||||
timeoutMinutes: 30,
|
timeoutMinutes: 30,
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useStickyComment: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRepository = {
|
const defaultRepository = {
|
||||||
@@ -128,6 +131,46 @@ export const mockIssueAssignedContext: ParsedGitHubContext = {
|
|||||||
inputs: { ...defaultInputs, assigneeTrigger: "@claude-bot" },
|
inputs: { ...defaultInputs, assigneeTrigger: "@claude-bot" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mockIssueLabeledContext: ParsedGitHubContext = {
|
||||||
|
runId: "1234567890",
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "labeled",
|
||||||
|
repository: defaultRepository,
|
||||||
|
actor: "admin-user",
|
||||||
|
payload: {
|
||||||
|
action: "labeled",
|
||||||
|
issue: {
|
||||||
|
number: 1234,
|
||||||
|
title: "Enhancement: Improve search functionality",
|
||||||
|
body: "The current search is too slow and needs optimization",
|
||||||
|
user: {
|
||||||
|
login: "alice-wonder",
|
||||||
|
id: 54321,
|
||||||
|
avatar_url: "https://avatars.githubusercontent.com/u/54321",
|
||||||
|
html_url: "https://github.com/alice-wonder",
|
||||||
|
},
|
||||||
|
assignee: null,
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
id: 987654321,
|
||||||
|
name: "claude-task",
|
||||||
|
color: "f29513",
|
||||||
|
description: "Label for Claude AI interactions",
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
name: "test-repo",
|
||||||
|
full_name: "test-owner/test-repo",
|
||||||
|
private: false,
|
||||||
|
owner: {
|
||||||
|
login: "test-owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as IssuesEvent,
|
||||||
|
entityNumber: 1234,
|
||||||
|
isPR: false,
|
||||||
|
inputs: { ...defaultInputs, labelTrigger: "claude-task" },
|
||||||
|
};
|
||||||
|
|
||||||
// Issue comment on issue event
|
// Issue comment on issue event
|
||||||
export const mockIssueCommentContext: ParsedGitHubContext = {
|
export const mockIssueCommentContext: ParsedGitHubContext = {
|
||||||
runId: "1234567890",
|
runId: "1234567890",
|
||||||
|
|||||||
@@ -62,10 +62,13 @@ describe("checkWritePermissions", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
|
labelTrigger: "",
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
disallowedTools: [],
|
disallowedTools: [],
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useStickyComment: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -219,6 +219,55 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
),
|
),
|
||||||
).toThrow("BASE_BRANCH is required for issues event");
|
).toThrow("BASE_BRANCH is required for issues event");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should allow issue assigned event with direct_prompt and no assigneeTrigger", () => {
|
||||||
|
const contextWithDirectPrompt = createMockContext({
|
||||||
|
...mockIssueAssignedContext,
|
||||||
|
inputs: {
|
||||||
|
...mockIssueAssignedContext.inputs,
|
||||||
|
assigneeTrigger: "", // No assignee trigger
|
||||||
|
directPrompt: "Please assess this issue", // But direct prompt is provided
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = prepareContext(
|
||||||
|
contextWithDirectPrompt,
|
||||||
|
"12345",
|
||||||
|
"main",
|
||||||
|
"claude/issue-123-20240101_120000",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.eventData.eventName).toBe("issues");
|
||||||
|
expect(result.eventData.isPR).toBe(false);
|
||||||
|
expect(result.directPrompt).toBe("Please assess this issue");
|
||||||
|
if (
|
||||||
|
result.eventData.eventName === "issues" &&
|
||||||
|
result.eventData.eventAction === "assigned"
|
||||||
|
) {
|
||||||
|
expect(result.eventData.issueNumber).toBe("123");
|
||||||
|
expect(result.eventData.assigneeTrigger).toBeUndefined();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should throw error when neither assigneeTrigger nor directPrompt provided for issue assigned event", () => {
|
||||||
|
const contextWithoutTriggers = createMockContext({
|
||||||
|
...mockIssueAssignedContext,
|
||||||
|
inputs: {
|
||||||
|
...mockIssueAssignedContext.inputs,
|
||||||
|
assigneeTrigger: "", // No assignee trigger
|
||||||
|
directPrompt: "", // No direct prompt
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
prepareContext(
|
||||||
|
contextWithoutTriggers,
|
||||||
|
"12345",
|
||||||
|
"main",
|
||||||
|
"claude/issue-123-20240101_120000",
|
||||||
|
),
|
||||||
|
).toThrow("ASSIGNEE_TRIGGER is required for issue assigned event");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("optional fields", () => {
|
describe("optional fields", () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { describe, it, expect } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
createMockContext,
|
createMockContext,
|
||||||
mockIssueAssignedContext,
|
mockIssueAssignedContext,
|
||||||
|
mockIssueLabeledContext,
|
||||||
mockIssueCommentContext,
|
mockIssueCommentContext,
|
||||||
mockIssueOpenedContext,
|
mockIssueOpenedContext,
|
||||||
mockPullRequestReviewContext,
|
mockPullRequestReviewContext,
|
||||||
@@ -29,10 +30,13 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "/claude",
|
triggerPhrase: "/claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
|
labelTrigger: "",
|
||||||
directPrompt: "Fix the bug in the login form",
|
directPrompt: "Fix the bug in the login form",
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
disallowedTools: [],
|
disallowedTools: [],
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useStickyComment: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -55,10 +59,13 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "/claude",
|
triggerPhrase: "/claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
|
labelTrigger: "",
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
disallowedTools: [],
|
disallowedTools: [],
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useStickyComment: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
@@ -107,6 +114,39 @@ describe("checkContainsTrigger", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("label trigger", () => {
|
||||||
|
it("should return true when issue is labeled with the trigger label", () => {
|
||||||
|
const context = mockIssueLabeledContext;
|
||||||
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false when issue is labeled with a different label", () => {
|
||||||
|
const context = {
|
||||||
|
...mockIssueLabeledContext,
|
||||||
|
payload: {
|
||||||
|
...mockIssueLabeledContext.payload,
|
||||||
|
label: {
|
||||||
|
...(mockIssueLabeledContext.payload as any).label,
|
||||||
|
name: "bug",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as ParsedGitHubContext;
|
||||||
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for non-labeled events", () => {
|
||||||
|
const context = {
|
||||||
|
...mockIssueLabeledContext,
|
||||||
|
eventAction: "opened",
|
||||||
|
payload: {
|
||||||
|
...mockIssueLabeledContext.payload,
|
||||||
|
action: "opened",
|
||||||
|
},
|
||||||
|
} as ParsedGitHubContext;
|
||||||
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("issue body and title trigger", () => {
|
describe("issue body and title trigger", () => {
|
||||||
it("should return true when issue body contains trigger phrase", () => {
|
it("should return true when issue body contains trigger phrase", () => {
|
||||||
const context = mockIssueOpenedContext;
|
const context = mockIssueOpenedContext;
|
||||||
@@ -232,10 +272,13 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
|
labelTrigger: "",
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
disallowedTools: [],
|
disallowedTools: [],
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useStickyComment: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -259,10 +302,13 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
|
labelTrigger: "",
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
disallowedTools: [],
|
disallowedTools: [],
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useStickyComment: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -286,10 +332,13 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
|
labelTrigger: "",
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
disallowedTools: [],
|
disallowedTools: [],
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
|
branchPrefix: "claude/",
|
||||||
|
useStickyComment: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user