mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-24 07:24:12 +08:00
Compare commits
1 Commits
v0.0.25
...
ashwin/tri
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e35c991da9 |
2
.github/workflows/claude-review.yml
vendored
2
.github/workflows/claude-review.yml
vendored
@@ -29,4 +29,4 @@ jobs:
|
|||||||
|
|
||||||
Be constructive and specific in your feedback. Give inline comments where applicable.
|
Be constructive and specific in your feedback. Give inline comments where applicable.
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
allowed_tools: "mcp__github__add_pull_request_review_comment"
|
||||||
|
|||||||
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
|||||||
"--rm",
|
"--rm",
|
||||||
"-e",
|
"-e",
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||||
"ghcr.io/github/github-mcp-server:sha-6d69797"
|
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|||||||
138
.github/workflows/release.yml
vendored
138
.github/workflows/release.yml
vendored
@@ -1,138 +0,0 @@
|
|||||||
name: Create Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
dry_run:
|
|
||||||
description: "Dry run (only show what would be created)"
|
|
||||||
required: false
|
|
||||||
type: boolean
|
|
||||||
default: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
outputs:
|
|
||||||
next_version: ${{ steps.next_version.outputs.next_version }}
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get latest tag
|
|
||||||
id: get_latest_tag
|
|
||||||
run: |
|
|
||||||
# Get only version tags (v + number pattern)
|
|
||||||
latest_tag=$(git tag -l 'v[0-9]*' | sort -V | tail -1 || echo "v0.0.0")
|
|
||||||
if [ -z "$latest_tag" ]; then
|
|
||||||
latest_tag="v0.0.0"
|
|
||||||
fi
|
|
||||||
echo "latest_tag=$latest_tag" >> $GITHUB_OUTPUT
|
|
||||||
echo "Latest tag: $latest_tag"
|
|
||||||
|
|
||||||
- name: Calculate next version
|
|
||||||
id: next_version
|
|
||||||
run: |
|
|
||||||
latest_tag="${{ steps.get_latest_tag.outputs.latest_tag }}"
|
|
||||||
# Remove 'v' prefix and split by dots
|
|
||||||
version=${latest_tag#v}
|
|
||||||
IFS='.' read -ra VERSION_PARTS <<< "$version"
|
|
||||||
|
|
||||||
# Increment patch version
|
|
||||||
major=${VERSION_PARTS[0]:-0}
|
|
||||||
minor=${VERSION_PARTS[1]:-0}
|
|
||||||
patch=${VERSION_PARTS[2]:-0}
|
|
||||||
patch=$((patch + 1))
|
|
||||||
|
|
||||||
next_version="v${major}.${minor}.${patch}"
|
|
||||||
echo "next_version=$next_version" >> $GITHUB_OUTPUT
|
|
||||||
echo "Next version: $next_version"
|
|
||||||
|
|
||||||
- name: Display dry run info
|
|
||||||
if: ${{ inputs.dry_run }}
|
|
||||||
run: |
|
|
||||||
echo "🔍 DRY RUN MODE"
|
|
||||||
echo "Would create tag: ${{ steps.next_version.outputs.next_version }}"
|
|
||||||
echo "From commit: ${{ github.sha }}"
|
|
||||||
echo "Previous tag: ${{ steps.get_latest_tag.outputs.latest_tag }}"
|
|
||||||
|
|
||||||
- name: Create and push tag
|
|
||||||
if: ${{ !inputs.dry_run }}
|
|
||||||
run: |
|
|
||||||
next_version="${{ steps.next_version.outputs.next_version }}"
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
|
|
||||||
git tag -a "$next_version" -m "Release $next_version"
|
|
||||||
git push origin "$next_version"
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
if: ${{ !inputs.dry_run }}
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
next_version="${{ steps.next_version.outputs.next_version }}"
|
|
||||||
|
|
||||||
gh release create "$next_version" \
|
|
||||||
--title "$next_version" \
|
|
||||||
--generate-notes \
|
|
||||||
--latest=false # We want to keep beta as the latest
|
|
||||||
|
|
||||||
update-beta-tag:
|
|
||||||
needs: create-release
|
|
||||||
if: ${{ !inputs.dry_run }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Update beta tag
|
|
||||||
run: |
|
|
||||||
# Get the latest version tag
|
|
||||||
VERSION=$(git tag -l 'v[0-9]*' | sort -V | tail -1)
|
|
||||||
|
|
||||||
# Update the beta tag to point to this release
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git tag -fa beta -m "Update beta tag to ${VERSION}"
|
|
||||||
git push origin beta --force
|
|
||||||
|
|
||||||
- name: Update beta release to be latest
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ github.token }}
|
|
||||||
run: |
|
|
||||||
# Update beta release to be marked as latest
|
|
||||||
gh release edit beta --latest
|
|
||||||
|
|
||||||
update-major-tag:
|
|
||||||
needs: create-release
|
|
||||||
if: ${{ !inputs.dry_run }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Update major version tag
|
|
||||||
run: |
|
|
||||||
next_version="${{ needs.create-release.outputs.next_version }}"
|
|
||||||
# Extract major version (e.g., v0 from v0.0.20)
|
|
||||||
major_version=$(echo "$next_version" | cut -d. -f1)
|
|
||||||
|
|
||||||
# Update the major version tag to point to this release
|
|
||||||
git config user.name "github-actions[bot]"
|
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
||||||
git tag -fa "$major_version" -m "Update $major_version tag to $next_version"
|
|
||||||
git push origin "$major_version" --force
|
|
||||||
|
|
||||||
echo "Updated $major_version tag to point to $next_version"
|
|
||||||
@@ -50,6 +50,20 @@ Thank you for your interest in contributing to Claude Code Action! This document
|
|||||||
bun test
|
bun test
|
||||||
```
|
```
|
||||||
|
|
||||||
|
2. **Integration Tests** (using GitHub Actions locally):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This script:
|
||||||
|
|
||||||
|
- Installs `act` if not present (requires Homebrew on macOS)
|
||||||
|
- Runs the GitHub Action workflow locally using Docker
|
||||||
|
- Requires your `ANTHROPIC_API_KEY` to be set
|
||||||
|
|
||||||
|
On Apple Silicon Macs, the script automatically adds the `--container-architecture linux/amd64` flag to avoid compatibility issues.
|
||||||
|
|
||||||
## Pull Request Process
|
## Pull Request Process
|
||||||
|
|
||||||
1. Create a new branch from `main`:
|
1. Create a new branch from `main`:
|
||||||
@@ -89,7 +103,13 @@ Thank you for your interest in contributing to Claude Code Action! This document
|
|||||||
|
|
||||||
When modifying the action:
|
When modifying the action:
|
||||||
|
|
||||||
1. Test in a real GitHub Actions workflow by:
|
1. Test locally with the test script:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./test-local.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Test in a real GitHub Actions workflow by:
|
||||||
- Creating a test repository
|
- Creating a test repository
|
||||||
- Using your branch as the action source:
|
- Using your branch as the action source:
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
73
README.md
73
README.md
@@ -49,7 +49,7 @@ on:
|
|||||||
pull_request_review_comment:
|
pull_request_review_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
issues:
|
issues:
|
||||||
types: [opened, assigned, labeled]
|
types: [opened, assigned]
|
||||||
pull_request_review:
|
pull_request_review:
|
||||||
types: [submitted]
|
types: [submitted]
|
||||||
|
|
||||||
@@ -65,15 +65,11 @@ 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
|
||||||
# DEBUG: true
|
# DEBUG: true
|
||||||
# API_URL: https://api.example.com
|
# API_URL: https://api.example.com
|
||||||
# Optional: limit the number of conversation turns
|
|
||||||
# max_turns: "5"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
@@ -82,8 +78,6 @@ 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 | - |
|
|
||||||
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
||||||
| `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 | - |
|
||||||
@@ -95,9 +89,7 @@ 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)
|
||||||
@@ -154,40 +146,6 @@ For MCP servers that require sensitive information like API keys or tokens, use
|
|||||||
# ... other inputs
|
# ... other inputs
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using Python MCP Servers with uv
|
|
||||||
|
|
||||||
For Python-based MCP servers managed with `uv`, you need to specify the directory containing your server:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: anthropics/claude-code-action@beta
|
|
||||||
with:
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
mcp_config: |
|
|
||||||
{
|
|
||||||
"mcpServers": {
|
|
||||||
"my-python-server": {
|
|
||||||
"type": "stdio",
|
|
||||||
"command": "uv",
|
|
||||||
"args": [
|
|
||||||
"--directory",
|
|
||||||
"${{ github.workspace }}/path/to/server/",
|
|
||||||
"run",
|
|
||||||
"server_file.py"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
allowed_tools: "my-python-server__<tool_name>" # Replace <tool_name> with your server's tool names
|
|
||||||
# ... other inputs
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, if your Python MCP server is at `mcp_servers/weather.py`, you would use:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
"args":
|
|
||||||
["--directory", "${{ github.workspace }}/mcp_servers/", "run", "weather.py"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**Important**:
|
**Important**:
|
||||||
|
|
||||||
- Always use GitHub Secrets (`${{ secrets.SECRET_NAME }}`) for sensitive values like API keys, tokens, or passwords. Never hardcode secrets directly in the workflow file.
|
- Always use GitHub Secrets (`${{ secrets.SECRET_NAME }}`) for sensitive values like API keys, tokens, or passwords. Never hardcode secrets directly in the workflow file.
|
||||||
@@ -353,24 +311,6 @@ You can pass custom environment variables to Claude Code execution using the `cl
|
|||||||
|
|
||||||
The `claude_env` input accepts YAML format where each line defines a key-value pair. These environment variables will be available to Claude Code during execution, allowing it to run tests, build processes, or other commands that depend on specific environment configurations.
|
The `claude_env` input accepts YAML format where each line defines a key-value pair. These environment variables will be available to Claude Code during execution, allowing it to run tests, build processes, or other commands that depend on specific environment configurations.
|
||||||
|
|
||||||
### Limiting Conversation Turns
|
|
||||||
|
|
||||||
You can use the `max_turns` parameter to limit the number of back-and-forth exchanges Claude can have during task execution. This is useful for:
|
|
||||||
|
|
||||||
- Controlling costs by preventing runaway conversations
|
|
||||||
- Setting time boundaries for automated workflows
|
|
||||||
- Ensuring predictable behavior in CI/CD pipelines
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: anthropics/claude-code-action@beta
|
|
||||||
with:
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
max_turns: "5" # Limit to 5 conversation turns
|
|
||||||
# ... other inputs
|
|
||||||
```
|
|
||||||
|
|
||||||
When the turn limit is reached, Claude will stop execution gracefully. Choose a value that gives Claude enough turns to complete typical tasks while preventing excessive usage.
|
|
||||||
|
|
||||||
### Custom Tools
|
### Custom Tools
|
||||||
|
|
||||||
By default, Claude only has access to:
|
By default, Claude only has access to:
|
||||||
@@ -386,15 +326,8 @@ Claude does **not** have access to execute arbitrary Bash commands by default. I
|
|||||||
```yaml
|
```yaml
|
||||||
- uses: anthropics/claude-code-action@beta
|
- uses: anthropics/claude-code-action@beta
|
||||||
with:
|
with:
|
||||||
allowed_tools: |
|
allowed_tools: "Bash(npm install),Bash(npm run test),Edit,Replace,NotebookEditCell"
|
||||||
Bash(npm install)
|
disallowed_tools: "TaskOutput,KillTask"
|
||||||
Bash(npm run test)
|
|
||||||
Edit
|
|
||||||
Replace
|
|
||||||
NotebookEditCell
|
|
||||||
disallowed_tools: |
|
|
||||||
TaskOutput
|
|
||||||
KillTask
|
|
||||||
# ... other inputs
|
# ... other inputs
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
20
ROADMAP.md
20
ROADMAP.md
@@ -1,20 +0,0 @@
|
|||||||
# Claude Code GitHub Action Roadmap
|
|
||||||
|
|
||||||
Thank you for trying out the beta of our GitHub Action! This document outlines our path to `v1.0`. Items are not necessarily in priority order.
|
|
||||||
|
|
||||||
## Path to 1.0
|
|
||||||
|
|
||||||
- **Ability to see GitHub Action CI results** - This will enable Claude to look at CI failures and make updates to PRs to fix test failures, lint errors, and the like.
|
|
||||||
- **Cross-repo support** - Enable Claude to work across multiple repositories in a single session
|
|
||||||
- **Ability to modify workflow files** - Let Claude update GitHub Actions workflows and other CI configuration files
|
|
||||||
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
|
|
||||||
- **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
|
|
||||||
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
|
|
||||||
- **Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude
|
|
||||||
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Note:** This roadmap represents our current vision for reaching `v1.0` and is subject to change based on user feedback and development priorities.
|
|
||||||
|
|
||||||
We welcome feedback on these planned features! If you're interested in contributing to any of these features, please open an issue to discuss implementation details with us. We're also open to suggestions for new features not listed here.
|
|
||||||
25
action.yml
25
action.yml
@@ -12,17 +12,9 @@ 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,10 +62,6 @@ inputs:
|
|||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
max_turns:
|
|
||||||
description: "Maximum number of conversation turns"
|
|
||||||
required: false
|
|
||||||
default: ""
|
|
||||||
timeout_minutes:
|
timeout_minutes:
|
||||||
description: "Timeout in minutes for execution"
|
description: "Timeout in minutes for execution"
|
||||||
required: false
|
required: false
|
||||||
@@ -95,21 +83,19 @@ runs:
|
|||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
cd ${GITHUB_ACTION_PATH}
|
cd ${{ github.action_path }}
|
||||||
bun install
|
bun install
|
||||||
|
|
||||||
- name: Prepare action
|
- name: Prepare action
|
||||||
id: prepare
|
id: prepare
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
|
bun run ${{ github.action_path }}/src/entrypoints/prepare.ts
|
||||||
env:
|
env:
|
||||||
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||||
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
ASSIGNEE_TRIGGER: ${{ inputs.assignee_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 }}
|
|
||||||
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
||||||
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
||||||
MCP_CONFIG: ${{ inputs.mcp_config }}
|
MCP_CONFIG: ${{ inputs.mcp_config }}
|
||||||
@@ -119,13 +105,12 @@ runs:
|
|||||||
- 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@ba0557c14198bf2fbafbfe80932dde39e574a14c # v0.0.26
|
uses: anthropics/claude-code-base-action@1370ac97fbba9bddec20ea2924b5726bf10d8b94 # v0.0.9
|
||||||
with:
|
with:
|
||||||
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
||||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||||
disallowed_tools: ${{ env.DISALLOWED_TOOLS }}
|
disallowed_tools: ${{ env.DISALLOWED_TOOLS }}
|
||||||
timeout_minutes: ${{ inputs.timeout_minutes }}
|
timeout_minutes: ${{ inputs.timeout_minutes }}
|
||||||
max_turns: ${{ inputs.max_turns }}
|
|
||||||
model: ${{ inputs.model || inputs.anthropic_model }}
|
model: ${{ inputs.model || inputs.anthropic_model }}
|
||||||
mcp_config: ${{ steps.prepare.outputs.mcp_config }}
|
mcp_config: ${{ steps.prepare.outputs.mcp_config }}
|
||||||
use_bedrock: ${{ inputs.use_bedrock }}
|
use_bedrock: ${{ inputs.use_bedrock }}
|
||||||
@@ -162,7 +147,7 @@ runs:
|
|||||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
|
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
|
bun run ${{ github.action_path }}/src/entrypoints/update-comment-link.ts
|
||||||
env:
|
env:
|
||||||
REPOSITORY: ${{ github.repository }}
|
REPOSITORY: ${{ github.repository }}
|
||||||
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
||||||
|
|||||||
2
bun.lock
2
bun.lock
@@ -2,7 +2,7 @@
|
|||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@anthropic-ai/claude-code-action",
|
"name": "claude-pr-action",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ jobs:
|
|||||||
|
|
||||||
Provide constructive feedback with specific suggestions for improvement.
|
Provide constructive feedback with specific suggestions for improvement.
|
||||||
Use inline comments to highlight specific areas of concern.
|
Use inline comments to highlight specific areas of concern.
|
||||||
# allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
# allowed_tools: "mcp__github__add_pull_request_review_comment"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@anthropic-ai/claude-code-action",
|
"name": "claude-pr-action",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export type { CommonFields, PreparedContext } from "./types";
|
|||||||
|
|
||||||
const BASE_ALLOWED_TOOLS = [
|
const BASE_ALLOWED_TOOLS = [
|
||||||
"Edit",
|
"Edit",
|
||||||
"MultiEdit",
|
|
||||||
"Glob",
|
"Glob",
|
||||||
"Grep",
|
"Grep",
|
||||||
"LS",
|
"LS",
|
||||||
@@ -36,35 +35,38 @@ const BASE_ALLOWED_TOOLS = [
|
|||||||
];
|
];
|
||||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||||
|
|
||||||
export function buildAllowedToolsString(customAllowedTools?: string[]): string {
|
export function buildAllowedToolsString(customAllowedTools?: string): string {
|
||||||
let baseTools = [...BASE_ALLOWED_TOOLS];
|
let baseTools = [...BASE_ALLOWED_TOOLS];
|
||||||
|
|
||||||
let allAllowedTools = baseTools.join(",");
|
let allAllowedTools = baseTools.join(",");
|
||||||
if (customAllowedTools && customAllowedTools.length > 0) {
|
if (customAllowedTools) {
|
||||||
allAllowedTools = `${allAllowedTools},${customAllowedTools.join(",")}`;
|
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
|
||||||
}
|
}
|
||||||
return allAllowedTools;
|
return allAllowedTools;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildDisallowedToolsString(
|
export function buildDisallowedToolsString(
|
||||||
customDisallowedTools?: string[],
|
customDisallowedTools?: string,
|
||||||
allowedTools?: string[],
|
allowedTools?: string,
|
||||||
): string {
|
): string {
|
||||||
let disallowedTools = [...DISALLOWED_TOOLS];
|
let disallowedTools = [...DISALLOWED_TOOLS];
|
||||||
|
|
||||||
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
|
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
|
||||||
if (allowedTools && allowedTools.length > 0) {
|
if (allowedTools) {
|
||||||
|
const allowedToolsArray = allowedTools
|
||||||
|
.split(",")
|
||||||
|
.map((tool) => tool.trim());
|
||||||
disallowedTools = disallowedTools.filter(
|
disallowedTools = disallowedTools.filter(
|
||||||
(tool) => !allowedTools.includes(tool),
|
(tool) => !allowedToolsArray.includes(tool),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let allDisallowedTools = disallowedTools.join(",");
|
let allDisallowedTools = disallowedTools.join(",");
|
||||||
if (customDisallowedTools && customDisallowedTools.length > 0) {
|
if (customDisallowedTools) {
|
||||||
if (allDisallowedTools) {
|
if (allDisallowedTools) {
|
||||||
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools.join(",")}`;
|
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
|
||||||
} else {
|
} else {
|
||||||
allDisallowedTools = customDisallowedTools.join(",");
|
allDisallowedTools = customDisallowedTools;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return allDisallowedTools;
|
return allDisallowedTools;
|
||||||
@@ -81,7 +83,6 @@ 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;
|
||||||
@@ -119,10 +120,8 @@ export function prepareContext(
|
|||||||
triggerPhrase,
|
triggerPhrase,
|
||||||
...(triggerUsername && { triggerUsername }),
|
...(triggerUsername && { triggerUsername }),
|
||||||
...(customInstructions && { customInstructions }),
|
...(customInstructions && { customInstructions }),
|
||||||
...(allowedTools.length > 0 && { allowedTools: allowedTools.join(",") }),
|
...(allowedTools && { allowedTools }),
|
||||||
...(disallowedTools.length > 0 && {
|
...(disallowedTools && { disallowedTools }),
|
||||||
disallowedTools: disallowedTools.join(","),
|
|
||||||
}),
|
|
||||||
...(directPrompt && { directPrompt }),
|
...(directPrompt && { directPrompt }),
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
};
|
};
|
||||||
@@ -243,7 +242,7 @@ export function prepareContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventAction === "assigned") {
|
if (eventAction === "assigned") {
|
||||||
if (!assigneeTrigger && !directPrompt) {
|
if (!assigneeTrigger) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"ASSIGNEE_TRIGGER is required for issue assigned event",
|
"ASSIGNEE_TRIGGER is required for issue assigned event",
|
||||||
);
|
);
|
||||||
@@ -255,20 +254,7 @@ 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 = {
|
||||||
@@ -342,17 +328,10 @@ 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: eventData.assigneeTrigger
|
triggerContext: `issue assigned to '${eventData.assigneeTrigger}'`,
|
||||||
? `issue assigned to '${eventData.assigneeTrigger}'`
|
|
||||||
: `issue assigned event`,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
case "pull_request":
|
case "pull_request":
|
||||||
@@ -439,7 +418,6 @@ ${
|
|||||||
}
|
}
|
||||||
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
||||||
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
||||||
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
|
||||||
<trigger_phrase>${context.triggerPhrase}</trigger_phrase>
|
<trigger_phrase>${context.triggerPhrase}</trigger_phrase>
|
||||||
${
|
${
|
||||||
(eventData.eventName === "issue_comment" ||
|
(eventData.eventName === "issue_comment" ||
|
||||||
@@ -486,7 +464,6 @@ 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.
|
||||||
@@ -526,14 +503,12 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
? `
|
? `
|
||||||
- Push directly using mcp__github_file_ops__commit_files to the existing branch (works for both new and existing files).
|
- Push directly using mcp__github_file_ops__commit_files to the existing branch (works for both new and existing files).
|
||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes with this tool and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
- When pushing changes with this tool and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.`
|
||||||
- Use: "Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>"`
|
|
||||||
: `
|
: `
|
||||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
||||||
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
||||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||||
- When pushing changes and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
- When pushing changes and TRIGGER_USERNAME is not "Unknown", include a "Co-authored-by: ${context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>" line in the commit message.
|
||||||
- Use: "Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>"
|
|
||||||
${
|
${
|
||||||
eventData.claudeBranch
|
eventData.claudeBranch
|
||||||
? `- Provide a URL to create a PR manually in this format:
|
? `- Provide a URL to create a PR manually in this format:
|
||||||
@@ -646,9 +621,7 @@ export async function createPrompt(
|
|||||||
claudeBranch,
|
claudeBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
await mkdir("/tmp/claude-prompts", { recursive: true });
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Generate the prompt
|
// Generate the prompt
|
||||||
const promptContent = generatePrompt(preparedContext, githubData);
|
const promptContent = generatePrompt(preparedContext, githubData);
|
||||||
@@ -659,18 +632,15 @@ export async function createPrompt(
|
|||||||
console.log("=======================");
|
console.log("=======================");
|
||||||
|
|
||||||
// Write the prompt file
|
// Write the prompt file
|
||||||
await writeFile(
|
await writeFile("/tmp/claude-prompts/claude-prompt.txt", promptContent);
|
||||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
|
||||||
promptContent,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Set allowed tools
|
// Set allowed tools
|
||||||
const allAllowedTools = buildAllowedToolsString(
|
const allAllowedTools = buildAllowedToolsString(
|
||||||
context.inputs.allowedTools,
|
preparedContext.allowedTools,
|
||||||
);
|
);
|
||||||
const allDisallowedTools = buildDisallowedToolsString(
|
const allDisallowedTools = buildDisallowedToolsString(
|
||||||
context.inputs.disallowedTools,
|
preparedContext.disallowedTools,
|
||||||
context.inputs.allowedTools,
|
preparedContext.allowedTools,
|
||||||
);
|
);
|
||||||
|
|
||||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
||||||
|
|||||||
@@ -65,17 +65,7 @@ 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 = {
|
||||||
@@ -95,7 +85,6 @@ export type EventData =
|
|||||||
| IssueCommentEvent
|
| IssueCommentEvent
|
||||||
| IssueOpenedEvent
|
| IssueOpenedEvent
|
||||||
| IssueAssignedEvent
|
| IssueAssignedEvent
|
||||||
| IssueLabeledEvent
|
|
||||||
| PullRequestEvent;
|
| PullRequestEvent;
|
||||||
|
|
||||||
// Combined type with separate eventData field
|
// Combined type with separate eventData field
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ async function run() {
|
|||||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||||
prNumber: context.entityNumber.toString(),
|
prNumber: context.entityNumber.toString(),
|
||||||
isPR: context.isPR,
|
isPR: context.isPR,
|
||||||
triggerUsername: context.actor,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Step 8: Setup branch
|
// Step 8: Setup branch
|
||||||
@@ -93,7 +92,6 @@ async function run() {
|
|||||||
branch: branchInfo.currentBranch,
|
branch: branchInfo.currentBranch,
|
||||||
additionalMcpConfig,
|
additionalMcpConfig,
|
||||||
claudeCommentId: commentId.toString(),
|
claudeCommentId: commentId.toString(),
|
||||||
allowedTools: context.inputs.allowedTools,
|
|
||||||
});
|
});
|
||||||
core.setOutput("mcp_config", mcpConfig);
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -104,11 +104,3 @@ export const ISSUE_QUERY = `
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const USER_QUERY = `
|
|
||||||
query($login: String!) {
|
|
||||||
user(login: $login) {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as github from "@actions/github";
|
import * as github from "@actions/github";
|
||||||
import type {
|
import type {
|
||||||
IssuesEvent,
|
IssuesEvent,
|
||||||
IssuesAssignedEvent,
|
|
||||||
IssueCommentEvent,
|
IssueCommentEvent,
|
||||||
PullRequestEvent,
|
PullRequestEvent,
|
||||||
PullRequestReviewEvent,
|
PullRequestReviewEvent,
|
||||||
@@ -29,13 +28,11 @@ 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;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,13 +52,11 @@ 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: process.env.ALLOWED_TOOLS ?? "",
|
||||||
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
|
disallowedTools: 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/",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -115,14 +110,6 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseMultilineInput(s: string): string[] {
|
|
||||||
return s
|
|
||||||
.split(/,|[\n\r]+/)
|
|
||||||
.map((tool) => tool.replace(/#.+$/, ""))
|
|
||||||
.map((tool) => tool.trim())
|
|
||||||
.filter((tool) => tool.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isIssuesEvent(
|
export function isIssuesEvent(
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
): context is ParsedGitHubContext & { payload: IssuesEvent } {
|
||||||
@@ -152,9 +139,3 @@ export function isPullRequestReviewCommentEvent(
|
|||||||
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
|
||||||
return context.eventName === "pull_request_review_comment";
|
return context.eventName === "pull_request_review_comment";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isIssuesAssignedEvent(
|
|
||||||
context: ParsedGitHubContext,
|
|
||||||
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
|
|
||||||
return isIssuesEvent(context) && context.eventAction === "assigned";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { execSync } from "child_process";
|
import { execSync } from "child_process";
|
||||||
import type { Octokits } from "../api/client";
|
|
||||||
import { ISSUE_QUERY, PR_QUERY, USER_QUERY } from "../api/queries/github";
|
|
||||||
import type {
|
import type {
|
||||||
|
GitHubPullRequest,
|
||||||
|
GitHubIssue,
|
||||||
GitHubComment,
|
GitHubComment,
|
||||||
GitHubFile,
|
GitHubFile,
|
||||||
GitHubIssue,
|
|
||||||
GitHubPullRequest,
|
|
||||||
GitHubReview,
|
GitHubReview,
|
||||||
IssueQueryResponse,
|
|
||||||
PullRequestQueryResponse,
|
PullRequestQueryResponse,
|
||||||
|
IssueQueryResponse,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { CommentWithImages } from "../utils/image-downloader";
|
import { PR_QUERY, ISSUE_QUERY } from "../api/queries/github";
|
||||||
|
import type { Octokits } from "../api/client";
|
||||||
import { downloadCommentImages } from "../utils/image-downloader";
|
import { downloadCommentImages } from "../utils/image-downloader";
|
||||||
|
import type { CommentWithImages } from "../utils/image-downloader";
|
||||||
|
|
||||||
type FetchDataParams = {
|
type FetchDataParams = {
|
||||||
octokits: Octokits;
|
octokits: Octokits;
|
||||||
repository: string;
|
repository: string;
|
||||||
prNumber: string;
|
prNumber: string;
|
||||||
isPR: boolean;
|
isPR: boolean;
|
||||||
triggerUsername?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GitHubFileWithSHA = GitHubFile & {
|
export type GitHubFileWithSHA = GitHubFile & {
|
||||||
@@ -32,7 +31,6 @@ export type FetchDataResult = {
|
|||||||
changedFilesWithSHA: GitHubFileWithSHA[];
|
changedFilesWithSHA: GitHubFileWithSHA[];
|
||||||
reviewData: { nodes: GitHubReview[] } | null;
|
reviewData: { nodes: GitHubReview[] } | null;
|
||||||
imageUrlMap: Map<string, string>;
|
imageUrlMap: Map<string, string>;
|
||||||
triggerDisplayName?: string | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function fetchGitHubData({
|
export async function fetchGitHubData({
|
||||||
@@ -40,7 +38,6 @@ export async function fetchGitHubData({
|
|||||||
repository,
|
repository,
|
||||||
prNumber,
|
prNumber,
|
||||||
isPR,
|
isPR,
|
||||||
triggerUsername,
|
|
||||||
}: FetchDataParams): Promise<FetchDataResult> {
|
}: FetchDataParams): Promise<FetchDataResult> {
|
||||||
const [owner, repo] = repository.split("/");
|
const [owner, repo] = repository.split("/");
|
||||||
if (!owner || !repo) {
|
if (!owner || !repo) {
|
||||||
@@ -104,14 +101,6 @@ export async function fetchGitHubData({
|
|||||||
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
let changedFilesWithSHA: GitHubFileWithSHA[] = [];
|
||||||
if (isPR && changedFiles.length > 0) {
|
if (isPR && changedFiles.length > 0) {
|
||||||
changedFilesWithSHA = changedFiles.map((file) => {
|
changedFilesWithSHA = changedFiles.map((file) => {
|
||||||
// Don't compute SHA for deleted files
|
|
||||||
if (file.changeType === "DELETED") {
|
|
||||||
return {
|
|
||||||
...file,
|
|
||||||
sha: "deleted",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use git hash-object to compute the SHA for the current file content
|
// Use git hash-object to compute the SHA for the current file content
|
||||||
const sha = execSync(`git hash-object "${file.path}"`, {
|
const sha = execSync(`git hash-object "${file.path}"`, {
|
||||||
@@ -194,12 +183,6 @@ export async function fetchGitHubData({
|
|||||||
allComments,
|
allComments,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch trigger user display name if username is provided
|
|
||||||
let triggerDisplayName: string | null | undefined;
|
|
||||||
if (triggerUsername) {
|
|
||||||
triggerDisplayName = await fetchUserDisplayName(octokits, triggerUsername);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contextData,
|
contextData,
|
||||||
comments,
|
comments,
|
||||||
@@ -207,27 +190,5 @@ export async function fetchGitHubData({
|
|||||||
changedFilesWithSHA,
|
changedFilesWithSHA,
|
||||||
reviewData,
|
reviewData,
|
||||||
imageUrlMap,
|
imageUrlMap,
|
||||||
triggerDisplayName,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserQueryResponse = {
|
|
||||||
user: {
|
|
||||||
name: string | null;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function fetchUserDisplayName(
|
|
||||||
octokits: Octokits,
|
|
||||||
login: string,
|
|
||||||
): Promise<string | null> {
|
|
||||||
try {
|
|
||||||
const result = await octokits.graphql<UserQueryResponse>(USER_QUERY, {
|
|
||||||
login,
|
|
||||||
});
|
|
||||||
return result.user.name;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Failed to fetch user display name for ${login}:`, error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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, branchPrefix } = context.inputs;
|
const { baseBranch } = context.inputs;
|
||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
@@ -45,16 +45,9 @@ export async function setupBranch(
|
|||||||
|
|
||||||
const branchName = prData.headRefName;
|
const branchName = prData.headRefName;
|
||||||
|
|
||||||
// Determine optimal fetch depth based on PR commit count, with a minimum of 20
|
// Execute git commands to checkout PR branch (shallow fetch for performance)
|
||||||
const commitCount = prData.commits.totalCount;
|
// Fetch the branch with a depth of 20 to avoid fetching too much history, while still allowing for some context
|
||||||
const fetchDepth = Math.max(commitCount, 20);
|
await $`git fetch origin --depth=20 ${branchName}`;
|
||||||
|
|
||||||
console.log(
|
|
||||||
`PR #${entityNumber}: ${commitCount} commits, using fetch depth ${fetchDepth}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute git commands to checkout PR branch (dynamic depth based on PR size)
|
|
||||||
await $`git fetch origin --depth=${fetchDepth} ${branchName}`;
|
|
||||||
await $`git checkout ${branchName}`;
|
await $`git checkout ${branchName}`;
|
||||||
|
|
||||||
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
||||||
@@ -97,7 +90,7 @@ export async function setupBranch(
|
|||||||
.split("T")
|
.split("T")
|
||||||
.join("_");
|
.join("_");
|
||||||
|
|
||||||
const newBranch = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
|
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the SHA of the source branch
|
// Get the SHA of the source branch
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// Types for GitHub GraphQL query responses
|
// Types for GitHub GraphQL query responses
|
||||||
export type GitHubAuthor = {
|
export type GitHubAuthor = {
|
||||||
login: string;
|
login: string;
|
||||||
name?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GitHubComment = {
|
export type GitHubComment = {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import {
|
import {
|
||||||
isIssuesEvent,
|
isIssuesEvent,
|
||||||
isIssuesAssignedEvent,
|
|
||||||
isIssueCommentEvent,
|
isIssueCommentEvent,
|
||||||
isPullRequestEvent,
|
isPullRequestEvent,
|
||||||
isPullRequestReviewEvent,
|
isPullRequestReviewEvent,
|
||||||
@@ -13,7 +12,7 @@ import type { ParsedGitHubContext } from "../context";
|
|||||||
|
|
||||||
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
||||||
const {
|
const {
|
||||||
inputs: { assigneeTrigger, labelTrigger, triggerPhrase, directPrompt },
|
inputs: { assigneeTrigger, triggerPhrase, directPrompt },
|
||||||
} = context;
|
} = context;
|
||||||
|
|
||||||
// If direct prompt is provided, always trigger
|
// If direct prompt is provided, always trigger
|
||||||
@@ -23,10 +22,10 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for assignee trigger
|
// Check for assignee trigger
|
||||||
if (isIssuesAssignedEvent(context)) {
|
if (isIssuesEvent(context) && context.eventAction === "assigned") {
|
||||||
// Remove @ symbol from assignee_trigger if present
|
// Remove @ symbol from assignee_trigger if present
|
||||||
let triggerUser = assigneeTrigger.replace(/^@/, "");
|
let triggerUser = assigneeTrigger.replace(/^@/, "");
|
||||||
const assigneeUsername = context.payload.assignee?.login || "";
|
const assigneeUsername = context.payload.issue.assignee?.login || "";
|
||||||
|
|
||||||
if (triggerUser && assigneeUsername === triggerUser) {
|
if (triggerUser && assigneeUsername === triggerUser) {
|
||||||
console.log(`Issue assigned to trigger user '${triggerUser}'`);
|
console.log(`Issue assigned to trigger user '${triggerUser}'`);
|
||||||
@@ -34,16 +33,6 @@ 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 || "";
|
||||||
|
|||||||
@@ -466,7 +466,6 @@ server.tool(
|
|||||||
|
|
||||||
const octokit = new Octokit({
|
const octokit = new Octokit({
|
||||||
auth: githubToken,
|
auth: githubToken,
|
||||||
baseUrl: GITHUB_API_URL,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const isPullRequestReviewComment =
|
const isPullRequestReviewComment =
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { GITHUB_API_URL } from "../github/api/config";
|
|
||||||
|
|
||||||
type PrepareConfigParams = {
|
type PrepareConfigParams = {
|
||||||
githubToken: string;
|
githubToken: string;
|
||||||
@@ -8,7 +7,6 @@ type PrepareConfigParams = {
|
|||||||
branch: string;
|
branch: string;
|
||||||
additionalMcpConfig?: string;
|
additionalMcpConfig?: string;
|
||||||
claudeCommentId?: string;
|
claudeCommentId?: string;
|
||||||
allowedTools: string[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function prepareMcpConfig(
|
export async function prepareMcpConfig(
|
||||||
@@ -21,17 +19,24 @@ export async function prepareMcpConfig(
|
|||||||
branch,
|
branch,
|
||||||
additionalMcpConfig,
|
additionalMcpConfig,
|
||||||
claudeCommentId,
|
claudeCommentId,
|
||||||
allowedTools,
|
|
||||||
} = params;
|
} = params;
|
||||||
try {
|
try {
|
||||||
const allowedToolsList = allowedTools || [];
|
const baseMcpConfig = {
|
||||||
|
|
||||||
const hasGitHubMcpTools = allowedToolsList.some((tool) =>
|
|
||||||
tool.startsWith("mcp__github__"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
|
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
|
github: {
|
||||||
|
command: "docker",
|
||||||
|
args: [
|
||||||
|
"run",
|
||||||
|
"-i",
|
||||||
|
"--rm",
|
||||||
|
"-e",
|
||||||
|
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||||
|
"ghcr.io/anthropics/github-mcp-server:sha-7382253",
|
||||||
|
],
|
||||||
|
env: {
|
||||||
|
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
github_file_ops: {
|
github_file_ops: {
|
||||||
command: "bun",
|
command: "bun",
|
||||||
args: [
|
args: [
|
||||||
@@ -47,29 +52,11 @@ export async function prepareMcpConfig(
|
|||||||
...(claudeCommentId && { CLAUDE_COMMENT_ID: claudeCommentId }),
|
...(claudeCommentId && { CLAUDE_COMMENT_ID: claudeCommentId }),
|
||||||
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
|
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
|
||||||
IS_PR: process.env.IS_PR || "false",
|
IS_PR: process.env.IS_PR || "false",
|
||||||
GITHUB_API_URL: GITHUB_API_URL,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hasGitHubMcpTools) {
|
|
||||||
baseMcpConfig.mcpServers.github = {
|
|
||||||
command: "docker",
|
|
||||||
args: [
|
|
||||||
"run",
|
|
||||||
"-i",
|
|
||||||
"--rm",
|
|
||||||
"-e",
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
||||||
"ghcr.io/github/github-mcp-server:sha-6d69797", // https://github.com/github/github-mcp-server/releases/tag/v0.5.0
|
|
||||||
],
|
|
||||||
env: {
|
|
||||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge with additional MCP config if provided
|
// Merge with additional MCP config if provided
|
||||||
if (additionalMcpConfig && additionalMcpConfig.trim()) {
|
if (additionalMcpConfig && additionalMcpConfig.trim()) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -226,33 +226,6 @@ 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",
|
||||||
@@ -343,7 +316,7 @@ describe("generatePrompt", () => {
|
|||||||
|
|
||||||
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
||||||
expect(prompt).toContain(
|
expect(prompt).toContain(
|
||||||
'Use: "Co-authored-by: johndoe <johndoe@users.noreply.github.com>"',
|
"Co-authored-by: johndoe <johndoe@users.noreply.github.com>",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -641,51 +614,6 @@ 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", () => {
|
||||||
@@ -724,7 +652,7 @@ describe("buildAllowedToolsString", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should append custom tools when provided", () => {
|
test("should append custom tools when provided", () => {
|
||||||
const customTools = ["Tool1", "Tool2", "Tool3"];
|
const customTools = "Tool1,Tool2,Tool3";
|
||||||
const result = buildAllowedToolsString(customTools);
|
const result = buildAllowedToolsString(customTools);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
@@ -755,7 +683,7 @@ describe("buildDisallowedToolsString", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should append custom disallowed tools when provided", () => {
|
test("should append custom disallowed tools when provided", () => {
|
||||||
const customDisallowedTools = ["BadTool1", "BadTool2"];
|
const customDisallowedTools = "BadTool1,BadTool2";
|
||||||
const result = buildDisallowedToolsString(customDisallowedTools);
|
const result = buildDisallowedToolsString(customDisallowedTools);
|
||||||
|
|
||||||
// Base disallowed tools should be present
|
// Base disallowed tools should be present
|
||||||
@@ -773,8 +701,8 @@ describe("buildDisallowedToolsString", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
|
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
|
||||||
const customDisallowedTools = ["BadTool1", "BadTool2"];
|
const customDisallowedTools = "BadTool1,BadTool2";
|
||||||
const allowedTools = ["WebSearch", "SomeOtherTool"];
|
const allowedTools = "WebSearch,SomeOtherTool";
|
||||||
const result = buildDisallowedToolsString(
|
const result = buildDisallowedToolsString(
|
||||||
customDisallowedTools,
|
customDisallowedTools,
|
||||||
allowedTools,
|
allowedTools,
|
||||||
@@ -792,7 +720,7 @@ describe("buildDisallowedToolsString", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
|
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
|
||||||
const allowedTools = ["WebSearch", "WebFetch", "SomeOtherTool"];
|
const allowedTools = "WebSearch,WebFetch,SomeOtherTool";
|
||||||
const result = buildDisallowedToolsString(undefined, allowedTools);
|
const result = buildDisallowedToolsString(undefined, allowedTools);
|
||||||
|
|
||||||
// Both hardcoded disallowed tools should be removed
|
// Both hardcoded disallowed tools should be removed
|
||||||
@@ -804,8 +732,8 @@ describe("buildDisallowedToolsString", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
|
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
|
||||||
const customDisallowedTools = ["BadTool1", "BadTool2"];
|
const customDisallowedTools = "BadTool1,BadTool2";
|
||||||
const allowedTools = ["WebSearch", "WebFetch"];
|
const allowedTools = "WebSearch,WebFetch";
|
||||||
const result = buildDisallowedToolsString(
|
const result = buildDisallowedToolsString(
|
||||||
customDisallowedTools,
|
customDisallowedTools,
|
||||||
allowedTools,
|
allowedTools,
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
import { describe, it, expect } from "bun:test";
|
|
||||||
import { parseMultilineInput } from "../../src/github/context";
|
|
||||||
|
|
||||||
describe("parseMultilineInput", () => {
|
|
||||||
it("should parse a comma-separated string", () => {
|
|
||||||
const input = `Bash(bun install),Bash(bun test:*),Bash(bun typecheck)`;
|
|
||||||
const result = parseMultilineInput(input);
|
|
||||||
expect(result).toEqual([
|
|
||||||
"Bash(bun install)",
|
|
||||||
"Bash(bun test:*)",
|
|
||||||
"Bash(bun typecheck)",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse multiline string", () => {
|
|
||||||
const input = `Bash(bun install)
|
|
||||||
Bash(bun test:*)
|
|
||||||
Bash(bun typecheck)`;
|
|
||||||
const result = parseMultilineInput(input);
|
|
||||||
expect(result).toEqual([
|
|
||||||
"Bash(bun install)",
|
|
||||||
"Bash(bun test:*)",
|
|
||||||
"Bash(bun typecheck)",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse comma-separated multiline line", () => {
|
|
||||||
const input = `Bash(bun install),Bash(bun test:*)
|
|
||||||
Bash(bun typecheck)`;
|
|
||||||
const result = parseMultilineInput(input);
|
|
||||||
expect(result).toEqual([
|
|
||||||
"Bash(bun install)",
|
|
||||||
"Bash(bun test:*)",
|
|
||||||
"Bash(bun typecheck)",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should ignore comments", () => {
|
|
||||||
const input = `Bash(bun install),
|
|
||||||
Bash(bun test:*) # For testing
|
|
||||||
# For type checking
|
|
||||||
Bash(bun typecheck)
|
|
||||||
`;
|
|
||||||
const result = parseMultilineInput(input);
|
|
||||||
expect(result).toEqual([
|
|
||||||
"Bash(bun install)",
|
|
||||||
"Bash(bun test:*)",
|
|
||||||
"Bash(bun typecheck)",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse an empty string", () => {
|
|
||||||
const input = "";
|
|
||||||
const result = parseMultilineInput(input);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -24,19 +24,21 @@ describe("prepareMcpConfig", () => {
|
|||||||
processExitSpy.mockRestore();
|
processExitSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return base config when no additional config is provided and no allowed_tools", async () => {
|
test("should return base config when no additional config is provided", async () => {
|
||||||
const result = await prepareMcpConfig({
|
const result = await prepareMcpConfig({
|
||||||
githubToken: "test-token",
|
githubToken: "test-token",
|
||||||
owner: "test-owner",
|
owner: "test-owner",
|
||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
expect(parsed.mcpServers).toBeDefined();
|
expect(parsed.mcpServers).toBeDefined();
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
||||||
|
"test-token",
|
||||||
|
);
|
||||||
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
||||||
"test-token",
|
"test-token",
|
||||||
);
|
);
|
||||||
@@ -47,60 +49,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should include github MCP server when mcp__github__ tools are allowed", async () => {
|
|
||||||
const result = await prepareMcpConfig({
|
|
||||||
githubToken: "test-token",
|
|
||||||
owner: "test-owner",
|
|
||||||
repo: "test-repo",
|
|
||||||
branch: "test-branch",
|
|
||||||
allowedTools: [
|
|
||||||
"mcp__github__create_issue",
|
|
||||||
"mcp__github_file_ops__commit_files",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
|
||||||
expect(parsed.mcpServers).toBeDefined();
|
|
||||||
expect(parsed.mcpServers.github).toBeDefined();
|
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
||||||
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
|
||||||
"test-token",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should not include github MCP server when only file_ops tools are allowed", async () => {
|
|
||||||
const result = await prepareMcpConfig({
|
|
||||||
githubToken: "test-token",
|
|
||||||
owner: "test-owner",
|
|
||||||
repo: "test-repo",
|
|
||||||
branch: "test-branch",
|
|
||||||
allowedTools: [
|
|
||||||
"mcp__github_file_ops__commit_files",
|
|
||||||
"mcp__github_file_ops__update_claude_comment",
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
|
||||||
expect(parsed.mcpServers).toBeDefined();
|
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should include file_ops server even when no GitHub tools are allowed", async () => {
|
|
||||||
const result = await prepareMcpConfig({
|
|
||||||
githubToken: "test-token",
|
|
||||||
owner: "test-owner",
|
|
||||||
repo: "test-repo",
|
|
||||||
branch: "test-branch",
|
|
||||||
allowedTools: ["Edit", "Read", "Write"],
|
|
||||||
});
|
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
|
||||||
expect(parsed.mcpServers).toBeDefined();
|
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should return base config when additional config is empty string", async () => {
|
test("should return base config when additional config is empty string", async () => {
|
||||||
const result = await prepareMcpConfig({
|
const result = await prepareMcpConfig({
|
||||||
githubToken: "test-token",
|
githubToken: "test-token",
|
||||||
@@ -108,12 +56,11 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: "",
|
additionalMcpConfig: "",
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
expect(parsed.mcpServers).toBeDefined();
|
expect(parsed.mcpServers).toBeDefined();
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -125,12 +72,11 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: " \n\t ",
|
additionalMcpConfig: " \n\t ",
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
expect(parsed.mcpServers).toBeDefined();
|
expect(parsed.mcpServers).toBeDefined();
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -154,10 +100,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: additionalConfig,
|
additionalMcpConfig: additionalConfig,
|
||||||
allowedTools: [
|
|
||||||
"mcp__github__create_issue",
|
|
||||||
"mcp__github_file_ops__commit_files",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
@@ -191,10 +133,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: additionalConfig,
|
additionalMcpConfig: additionalConfig,
|
||||||
allowedTools: [
|
|
||||||
"mcp__github__create_issue",
|
|
||||||
"mcp__github_file_ops__commit_files",
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
@@ -231,13 +169,12 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: additionalConfig,
|
additionalMcpConfig: additionalConfig,
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
expect(parsed.customProperty).toBe("custom-value");
|
expect(parsed.customProperty).toBe("custom-value");
|
||||||
expect(parsed.anotherProperty).toEqual({ nested: "value" });
|
expect(parsed.anotherProperty).toEqual({ nested: "value" });
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.custom_server).toBeDefined();
|
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -250,14 +187,13 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: invalidJson,
|
additionalMcpConfig: invalidJson,
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||||
);
|
);
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -270,7 +206,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: nonObjectJson,
|
additionalMcpConfig: nonObjectJson,
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
@@ -280,7 +215,7 @@ describe("prepareMcpConfig", () => {
|
|||||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||||
);
|
);
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,7 +228,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: nullJson,
|
additionalMcpConfig: nullJson,
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
@@ -303,7 +237,7 @@ describe("prepareMcpConfig", () => {
|
|||||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||||
);
|
);
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,7 +250,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: arrayJson,
|
additionalMcpConfig: arrayJson,
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
@@ -325,7 +258,7 @@ describe("prepareMcpConfig", () => {
|
|||||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||||
"Merging additional MCP server configuration with built-in servers",
|
"Merging additional MCP server configuration with built-in servers",
|
||||||
);
|
);
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
|
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
|
||||||
expect(parsed[0]).toBe(1);
|
expect(parsed[0]).toBe(1);
|
||||||
@@ -362,13 +295,12 @@ describe("prepareMcpConfig", () => {
|
|||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
additionalMcpConfig: additionalConfig,
|
additionalMcpConfig: additionalConfig,
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
expect(parsed.mcpServers.server1).toBeDefined();
|
expect(parsed.mcpServers.server1).toBeDefined();
|
||||||
expect(parsed.mcpServers.server2).toBeDefined();
|
expect(parsed.mcpServers.server2).toBeDefined();
|
||||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
|
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
|
||||||
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
|
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
|
||||||
expect(parsed.otherConfig.nested.deeply).toBe("value");
|
expect(parsed.otherConfig.nested.deeply).toBe("value");
|
||||||
@@ -383,7 +315,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
owner: "test-owner",
|
owner: "test-owner",
|
||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
@@ -403,7 +334,6 @@ describe("prepareMcpConfig", () => {
|
|||||||
owner: "test-owner",
|
owner: "test-owner",
|
||||||
repo: "test-repo",
|
repo: "test-repo",
|
||||||
branch: "test-branch",
|
branch: "test-branch",
|
||||||
allowedTools: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsed = JSON.parse(result);
|
const parsed = JSON.parse(result);
|
||||||
|
|||||||
@@ -10,16 +10,14 @@ 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: "",
|
||||||
disallowedTools: [] as string[],
|
disallowedTools: "",
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
useBedrock: false,
|
useBedrock: false,
|
||||||
useVertex: false,
|
useVertex: false,
|
||||||
timeoutMinutes: 30,
|
timeoutMinutes: 30,
|
||||||
branchPrefix: "claude/",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRepository = {
|
const defaultRepository = {
|
||||||
@@ -93,12 +91,6 @@ export const mockIssueAssignedContext: ParsedGitHubContext = {
|
|||||||
actor: "admin-user",
|
actor: "admin-user",
|
||||||
payload: {
|
payload: {
|
||||||
action: "assigned",
|
action: "assigned",
|
||||||
assignee: {
|
|
||||||
login: "claude-bot",
|
|
||||||
id: 11111,
|
|
||||||
avatar_url: "https://avatars.githubusercontent.com/u/11111",
|
|
||||||
html_url: "https://github.com/claude-bot",
|
|
||||||
},
|
|
||||||
issue: {
|
issue: {
|
||||||
number: 123,
|
number: 123,
|
||||||
title: "Feature: Add dark mode support",
|
title: "Feature: Add dark mode support",
|
||||||
@@ -130,46 +122,6 @@ 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,12 +62,10 @@ describe("checkWritePermissions", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
labelTrigger: "",
|
allowedTools: "",
|
||||||
allowedTools: [],
|
disallowedTools: "",
|
||||||
disallowedTools: [],
|
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
branchPrefix: "claude/",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -219,55 +219,6 @@ 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", () => {
|
||||||
@@ -291,7 +242,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
...mockPullRequestCommentContext,
|
...mockPullRequestCommentContext,
|
||||||
inputs: {
|
inputs: {
|
||||||
...mockPullRequestCommentContext.inputs,
|
...mockPullRequestCommentContext.inputs,
|
||||||
allowedTools: ["Tool1", "Tool2"],
|
allowedTools: "Tool1,Tool2",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = prepareContext(contextWithAllowedTools, "12345");
|
const result = prepareContext(contextWithAllowedTools, "12345");
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { describe, it, expect } from "bun:test";
|
|||||||
import {
|
import {
|
||||||
createMockContext,
|
createMockContext,
|
||||||
mockIssueAssignedContext,
|
mockIssueAssignedContext,
|
||||||
mockIssueLabeledContext,
|
|
||||||
mockIssueCommentContext,
|
mockIssueCommentContext,
|
||||||
mockIssueOpenedContext,
|
mockIssueOpenedContext,
|
||||||
mockPullRequestReviewContext,
|
mockPullRequestReviewContext,
|
||||||
@@ -30,12 +29,10 @@ 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/",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -58,12 +55,10 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "/claude",
|
triggerPhrase: "/claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
labelTrigger: "",
|
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: "",
|
||||||
disallowedTools: [],
|
disallowedTools: "",
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
branchPrefix: "claude/",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
@@ -92,11 +87,6 @@ describe("checkContainsTrigger", () => {
|
|||||||
...mockIssueAssignedContext,
|
...mockIssueAssignedContext,
|
||||||
payload: {
|
payload: {
|
||||||
...mockIssueAssignedContext.payload,
|
...mockIssueAssignedContext.payload,
|
||||||
assignee: {
|
|
||||||
...(mockIssueAssignedContext.payload as IssuesAssignedEvent)
|
|
||||||
.assignee,
|
|
||||||
login: "otherUser",
|
|
||||||
},
|
|
||||||
issue: {
|
issue: {
|
||||||
...(mockIssueAssignedContext.payload as IssuesAssignedEvent).issue,
|
...(mockIssueAssignedContext.payload as IssuesAssignedEvent).issue,
|
||||||
assignee: {
|
assignee: {
|
||||||
@@ -112,39 +102,6 @@ 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;
|
||||||
@@ -270,12 +227,10 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
labelTrigger: "",
|
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: "",
|
||||||
disallowedTools: [],
|
disallowedTools: "",
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
branchPrefix: "claude/",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -299,12 +254,10 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
labelTrigger: "",
|
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: "",
|
||||||
disallowedTools: [],
|
disallowedTools: "",
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
branchPrefix: "claude/",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(true);
|
expect(checkContainsTrigger(context)).toBe(true);
|
||||||
@@ -328,12 +281,10 @@ describe("checkContainsTrigger", () => {
|
|||||||
inputs: {
|
inputs: {
|
||||||
triggerPhrase: "@claude",
|
triggerPhrase: "@claude",
|
||||||
assigneeTrigger: "",
|
assigneeTrigger: "",
|
||||||
labelTrigger: "",
|
|
||||||
directPrompt: "",
|
directPrompt: "",
|
||||||
allowedTools: [],
|
allowedTools: "",
|
||||||
disallowedTools: [],
|
disallowedTools: "",
|
||||||
customInstructions: "",
|
customInstructions: "",
|
||||||
branchPrefix: "claude/",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(checkContainsTrigger(context)).toBe(false);
|
expect(checkContainsTrigger(context)).toBe(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user