mirror of
https://github.com/markwylde/claude-code-gitea-action.git
synced 2026-02-19 18:12:50 +08:00
Compare commits
148 Commits
merge/upst
...
beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92631f4d12 | ||
|
|
225a4e6f3a | ||
|
|
ebd4882b3e | ||
|
|
5bcd15c520 | ||
|
|
3305a16297 | ||
|
|
582a02ee24 | ||
|
|
64b322f2b0 | ||
|
|
04892eb63d | ||
|
|
eb1aa3696e | ||
|
|
426380f01b | ||
|
|
77f51d2905 | ||
|
|
7e5b42b197 | ||
|
|
1b7c7a77d3 | ||
|
|
bd70a3ef2b | ||
|
|
f4954b5256 | ||
|
|
93f8ab56c2 | ||
|
|
93028b410e | ||
|
|
838d4d9d25 | ||
|
|
7ed3b616d5 | ||
|
|
09ea2f00e1 | ||
|
|
455b943dd7 | ||
|
|
063d17ebb2 | ||
|
|
2e92922dd6 | ||
|
|
a5528eec74 | ||
|
|
1d4650c102 | ||
|
|
86d6f44e34 | ||
|
|
c1adac956c | ||
|
|
f197e7bfd5 | ||
|
|
89f9131f6c | ||
|
|
b78e1c0244 | ||
|
|
abf075daf2 | ||
|
|
a3ff61d47a | ||
|
|
1b7eb924f1 | ||
|
|
0f7dfed927 | ||
|
|
11a01b7183 | ||
|
|
69dec299f8 | ||
|
|
1a8e7d330a | ||
|
|
9975f36410 | ||
|
|
c1ffc8a0e8 | ||
|
|
13e47489f4 | ||
|
|
765fadc6a6 | ||
|
|
fd2c17f101 | ||
|
|
a4a723b927 | ||
|
|
d22fa6061b | ||
|
|
63f1c772bd | ||
|
|
fb823f6dd6 | ||
|
|
9e9123239f | ||
|
|
791fcb9fd1 | ||
|
|
9365bbe4af | ||
|
|
2e6fc44bd4 | ||
|
|
a6ca65328b | ||
|
|
ce697c0d4c | ||
|
|
b60e3f0e60 | ||
|
|
3ed14485f8 | ||
|
|
45408b4058 | ||
|
|
1f8cfe7658 | ||
|
|
a6888c03f2 | ||
|
|
c041f89493 | ||
|
|
0c127307fa | ||
|
|
8a20581ed5 | ||
|
|
a2ad6b7b4e | ||
|
|
f0925925f1 | ||
|
|
ef8c0a650e | ||
|
|
dd49718216 | ||
|
|
be4b56e1ea | ||
|
|
dfef61fdee | ||
|
|
5218d84d4f | ||
|
|
c05ccc5ce4 | ||
|
|
41e5ba9012 | ||
|
|
e6f32c8321 | ||
|
|
ada5bc42eb | ||
|
|
d6d3ddd4a7 | ||
|
|
0630ef383a | ||
|
|
9c7e1bac94 | ||
|
|
dc65f4ac98 | ||
|
|
88be3fe6f5 | ||
|
|
a47fdbe49f | ||
|
|
28f8362010 | ||
|
|
9f02f6f6d4 | ||
|
|
79cee96324 | ||
|
|
194fca8b05 | ||
|
|
0f913a6e0e | ||
|
|
68b7ca379c | ||
|
|
900322ca88 | ||
|
|
8f0a7fe9d3 | ||
|
|
db36412854 | ||
|
|
f05d669d5f | ||
|
|
e89411bb6f | ||
|
|
02e9ed3181 | ||
|
|
78b07473f5 | ||
|
|
f562ed53e2 | ||
|
|
a1507aefdc | ||
|
|
ae66eb6a64 | ||
|
|
432c7cc889 | ||
|
|
0b138d9d49 | ||
|
|
c34e066a3b | ||
|
|
449c6791bd | ||
|
|
2b67ac084b | ||
|
|
76de8a48fc | ||
|
|
a80505bbfb | ||
|
|
af23644a50 | ||
|
|
98e6a902bf | ||
|
|
8b2bd6d04f | ||
|
|
4f4f43f044 | ||
|
|
8a5d751740 | ||
|
|
bc423b47f5 | ||
|
|
6d5c92076b | ||
|
|
fec554fc7c | ||
|
|
59ca6e42d9 | ||
|
|
7afc848186 | ||
|
|
6debac392b | ||
|
|
55fb6a96d0 | ||
|
|
15db2b3c79 | ||
|
|
188d526721 | ||
|
|
a519840051 | ||
|
|
85287e957d | ||
|
|
c6a07895d7 | ||
|
|
0c5d54472f | ||
|
|
2845685880 | ||
|
|
b39377f9bc | ||
|
|
618565bc0e | ||
|
|
0d9513b3b3 | ||
|
|
458e4b9e7f | ||
|
|
d66adfb7fa | ||
|
|
d829b4d14b | ||
|
|
0a78530f89 | ||
|
|
20e09ef881 | ||
|
|
56179f5fc9 | ||
|
|
0e5fbc0d44 | ||
|
|
b4cc5cd6c5 | ||
|
|
1b4ac7d7e0 | ||
|
|
1f6e3225b0 | ||
|
|
6672e9b357 | ||
|
|
950bdc01df | ||
|
|
15dd796e97 | ||
|
|
fd012347a2 | ||
|
|
5bdc533a52 | ||
|
|
d45539c118 | ||
|
|
daac7e353f | ||
|
|
bdfdd1f788 | ||
|
|
ec0e9b4f87 | ||
|
|
af32fd318a | ||
|
|
e07ea013bd | ||
|
|
6037d754ac | ||
|
|
04b2df22d4 | ||
|
|
1fd3bbc91b | ||
|
|
a8399fe052 | ||
|
|
f640f38102 |
3
.github/workflows/claude-review.yml
vendored
3
.github/workflows/claude-review.yml
vendored
@@ -29,5 +29,6 @@ jobs:
|
||||
- Documentation consistency: Verify that README.md and other documentation files are updated to reflect any code changes (especially new inputs, features, or configuration options)
|
||||
|
||||
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 && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
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"
|
||||
|
||||
3
.github/workflows/claude.yml
vendored
3
.github/workflows/claude.yml
vendored
@@ -33,7 +33,8 @@ jobs:
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
|
||||
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."
|
||||
model: "claude-opus-4-20250514"
|
||||
|
||||
3
.github/workflows/issue-triage.yml
vendored
3
.github/workflows/issue-triage.yml
vendored
@@ -103,4 +103,5 @@ jobs:
|
||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||
timeout_minutes: "5"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
|
||||
6
.github/workflows/test-base-action.yml
vendored
6
.github/workflows/test-base-action.yml
vendored
@@ -23,7 +23,8 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: ${{ github.event.inputs.test_prompt || 'List the files in the current directory starting with "package"' }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
allowed_tools: "LS,Read"
|
||||
timeout_minutes: "3"
|
||||
|
||||
@@ -81,7 +82,8 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt_file: "test-prompt.txt"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
allowed_tools: "LS,Read"
|
||||
timeout_minutes: "3"
|
||||
|
||||
|
||||
3
.github/workflows/test-claude-env.yml
vendored
3
.github/workflows/test-claude-env.yml
vendored
@@ -19,7 +19,8 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Use the Bash tool to run: echo "VAR1: $VAR1" && echo "VAR2: $VAR2"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_env: |
|
||||
# This is a comment
|
||||
VAR1: value1
|
||||
|
||||
90
.github/workflows/test-custom-executables.yml
vendored
Normal file
90
.github/workflows/test-custom-executables.yml
vendored
Normal file
@@ -0,0 +1,90 @@
|
||||
name: Test Custom Executables
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-custom-executables:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||
|
||||
- name: Install Bun manually
|
||||
run: |
|
||||
echo "Installing Bun..."
|
||||
curl -fsSL https://bun.sh/install | bash
|
||||
echo "Bun installed at: $HOME/.bun/bin/bun"
|
||||
|
||||
# Verify Bun installation
|
||||
if [ -f "$HOME/.bun/bin/bun" ]; then
|
||||
echo "✅ Bun executable found"
|
||||
$HOME/.bun/bin/bun --version
|
||||
else
|
||||
echo "❌ Bun executable not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Install Claude Code manually
|
||||
run: |
|
||||
echo "Installing Claude Code..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash -s latest
|
||||
echo "Claude Code installed at: $HOME/.local/bin/claude"
|
||||
|
||||
# Verify Claude installation
|
||||
if [ -f "$HOME/.local/bin/claude" ]; then
|
||||
echo "✅ Claude executable found"
|
||||
ls -la "$HOME/.local/bin/claude"
|
||||
else
|
||||
echo "❌ Claude executable not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Test with both custom executables
|
||||
id: custom-test
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: |
|
||||
List the files in the current directory starting with "package"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
path_to_claude_code_executable: /home/runner/.local/bin/claude
|
||||
path_to_bun_executable: /home/runner/.bun/bin/bun
|
||||
allowed_tools: "LS,Read"
|
||||
|
||||
- name: Verify custom executables worked
|
||||
run: |
|
||||
OUTPUT_FILE="${{ steps.custom-test.outputs.execution_file }}"
|
||||
CONCLUSION="${{ steps.custom-test.outputs.conclusion }}"
|
||||
|
||||
echo "Conclusion: $CONCLUSION"
|
||||
echo "Output file: $OUTPUT_FILE"
|
||||
|
||||
if [ "$CONCLUSION" = "success" ]; then
|
||||
echo "✅ Action completed successfully with both custom executables"
|
||||
else
|
||||
echo "❌ Action failed with custom executables"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -f "$OUTPUT_FILE" ] && [ -s "$OUTPUT_FILE" ]; then
|
||||
echo "✅ Execution log file created successfully"
|
||||
if jq . "$OUTPUT_FILE" > /dev/null 2>&1; then
|
||||
echo "✅ Output is valid JSON"
|
||||
# Verify the task was completed
|
||||
if grep -q "package" "$OUTPUT_FILE"; then
|
||||
echo "✅ Claude successfully listed package files"
|
||||
else
|
||||
echo "⚠️ Could not verify if package files were listed"
|
||||
fi
|
||||
else
|
||||
echo "❌ Output is not valid JSON"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "❌ Execution log file not found or empty"
|
||||
exit 1
|
||||
fi
|
||||
6
.github/workflows/test-mcp-servers.yml
vendored
6
.github/workflows/test-mcp-servers.yml
vendored
@@ -28,7 +28,8 @@ jobs:
|
||||
id: claude-test
|
||||
with:
|
||||
prompt: "List all available tools"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
env:
|
||||
# Change to test directory so it finds .mcp.json
|
||||
CLAUDE_WORKING_DIR: ${{ github.workspace }}/base-action/test/mcp-test
|
||||
@@ -109,7 +110,8 @@ jobs:
|
||||
id: claude-config-test
|
||||
with:
|
||||
prompt: "List all available tools"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
mcp_config: '{"mcpServers":{"test-server":{"type":"stdio","command":"bun","args":["simple-mcp-server.ts"],"env":{}}}}'
|
||||
env:
|
||||
# Change to test directory so bun can find the MCP server script
|
||||
|
||||
12
.github/workflows/test-settings.yml
vendored
12
.github/workflows/test-settings.yml
vendored
@@ -19,7 +19,8 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "Hello from settings test"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
settings: |
|
||||
{
|
||||
"permissions": {
|
||||
@@ -69,7 +70,8 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "This should not work"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
settings: |
|
||||
{
|
||||
"permissions": {
|
||||
@@ -112,7 +114,8 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "Hello from settings file test"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
settings: "test-settings.json"
|
||||
timeout_minutes: "2"
|
||||
|
||||
@@ -167,7 +170,8 @@ jobs:
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "This should not work from file"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY && secrets.ANTHROPIC_API_KEY || secrets.CLAUDE_CREDENTIALS }}
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
settings: "test-settings.json"
|
||||
timeout_minutes: "2"
|
||||
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
dist
|
||||
|
||||
**/.claude/settings.local.json
|
||||
|
||||
102
README.md
102
README.md
@@ -19,7 +19,7 @@ A Gitea action that provides a general-purpose [Claude Code](https://claude.ai/c
|
||||
|
||||
**Requirements**: You must be a repository admin to complete these steps.
|
||||
|
||||
1. Add `ANTHROPIC_API_KEY` or `CLAUDE_CREDENTIALS` to your repository secrets
|
||||
1. Add `ANTHROPIC_API_KEY` to your repository secrets
|
||||
2. Add `GITEA_TOKEN` to your repository secrets (a personal access token with repository read/write permissions)
|
||||
3. Copy the workflow file from [`examples/gitea-claude.yml`](./examples/gitea-claude.yml) into your repository's `.gitea/workflows/`
|
||||
|
||||
@@ -47,7 +47,6 @@ jobs:
|
||||
- uses: markwylde/claude-code-gitea-action@v1.0.5
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # if you want to use direct API
|
||||
claude_credentials: ${{ secrets.CLAUDE_CREDENTIALS }} # if you have a Claude Max subscription
|
||||
gitea_token: ${{ secrets.GITEA_TOKEN }} # could be another users token (specific Claude user?)
|
||||
claude_git_name: Claude # optional
|
||||
claude_git_email: claude@anthropic.com # optional
|
||||
@@ -57,8 +56,8 @@ jobs:
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex). Set to 'use-oauth' when using claude_credentials | No\* | - |
|
||||
| `claude_credentials` | Claude OAuth credentials JSON for Claude AI Max subscription authentication | No | - |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | - |
|
||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
||||
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
|
||||
| `gitea_token` | Gitea token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||
@@ -69,7 +68,7 @@ jobs:
|
||||
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
|
||||
| `disallowed_tools` | Tools that Claude should never use | No | "" |
|
||||
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
|
||||
| `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 and PR assignment | No | - |
|
||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||
| `claude_git_name` | Git user.name for commits made by Claude | No | `Claude` |
|
||||
| `claude_git_email` | Git user.email for commits made by Claude | No | `claude@anthropic.com` |
|
||||
@@ -78,45 +77,6 @@ jobs:
|
||||
|
||||
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
|
||||
|
||||
## Claude Max Authentication
|
||||
|
||||
This action supports authentication using Claude Max OAuth credentials. This allows users with Claude Max subscriptions to use their existing authentication.
|
||||
|
||||
### Setup
|
||||
|
||||
1. **Get OAuth Credentials**: Use Claude Code to generate OAuth credentials:
|
||||
|
||||
```
|
||||
/auth-setup
|
||||
```
|
||||
|
||||
2. **Add Credentials to Repository**: Add the generated JSON credentials as a repository secret named `CLAUDE_CREDENTIALS`.
|
||||
|
||||
It should look like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"claudeAiOauth": {
|
||||
"accessToken": "sk-ant-xxx",
|
||||
"refreshToken": "sk-ant-xxx",
|
||||
"expiresAt": 1748707000000,
|
||||
"scopes": ["user:inference", "user:profile"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
3. **Configure Workflow**: Set up your workflow to use OAuth authentication:
|
||||
|
||||
```yaml
|
||||
- uses: markwylde/claude-code-gitea-action@v1.0.5
|
||||
with:
|
||||
anthropic_api_key: "use-oauth"
|
||||
claude_credentials: ${{ secrets.CLAUDE_CREDENTIALS }}
|
||||
gitea_token: ${{ secrets.GITEA_TOKEN }}
|
||||
```
|
||||
|
||||
When `anthropic_api_key` is set to `'use-oauth'`, the action will use the OAuth credentials provided in `claude_credentials` instead of a direct API key.
|
||||
|
||||
## Gitea Configuration
|
||||
|
||||
This action has been enhanced to work with Gitea installations. The main differences from GitHub are:
|
||||
@@ -125,6 +85,31 @@ This action has been enhanced to work with Gitea installations. The main differe
|
||||
|
||||
2. **API URL Configuration**: You must specify your Gitea server URL using the `gitea_api_url` input.
|
||||
|
||||
3. **Custom Server URL**: For Gitea instances running in containers, you can override link generation using the `GITEA_SERVER_URL` environment variable.
|
||||
|
||||
### Custom Server URL Configuration
|
||||
|
||||
When running Gitea in containers, the action may generate links using internal container URLs (e.g., `http://gitea:3000`) instead of your public URL. To fix this, set the `GITEA_SERVER_URL` environment variable:
|
||||
|
||||
```yaml
|
||||
- uses: markwylde/claude-code-gitea-action@v1.0.5
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
gitea_token: ${{ secrets.GITEA_TOKEN }}
|
||||
env:
|
||||
# Override the internal container URL with your public URL
|
||||
GITEA_SERVER_URL: https://gitea.example.com
|
||||
```
|
||||
|
||||
**How it works:**
|
||||
- The action first checks for `GITEA_SERVER_URL` (user-configurable)
|
||||
- Falls back to `GITHUB_SERVER_URL` (automatically set by Gitea Actions)
|
||||
- Uses `https://github.com` as final fallback
|
||||
|
||||
This ensures that all links in Claude's comments (job runs, branches, etc.) point to your public Gitea instance instead of internal container addresses.
|
||||
|
||||
See [`examples/gitea-custom-url.yml`](./examples/gitea-custom-url.yml) for a complete example.
|
||||
|
||||
### Gitea Setup Notes
|
||||
|
||||
- Use a Gitea personal access token "GITEA_TOKEN"
|
||||
@@ -575,10 +560,33 @@ For a complete list of available settings and their descriptions, see the [Claud
|
||||
|
||||
## Cloud Providers
|
||||
|
||||
You can authenticate with Claude using any of these three methods:
|
||||
You can authenticate with Claude using any of these methods:
|
||||
|
||||
1. Direct Anthropic API (default)
|
||||
2. Anthropic OAuth credentials (Claude Max subscription)
|
||||
1. **Direct Anthropic API** (default) - Use your Anthropic API key
|
||||
2. **Claude Code OAuth Token** - Use OAuth token from Claude Code application
|
||||
|
||||
### Using Claude Code OAuth Token
|
||||
|
||||
If you have access to [Claude Code](https://claude.ai/code), you can use OAuth authentication instead of an API key:
|
||||
|
||||
1. **Generate OAuth Token**: run the following command and follow instructions:
|
||||
```
|
||||
claude setup-token
|
||||
```
|
||||
This will generate an OAuth token that you can use for authentication.
|
||||
|
||||
2. **Add Token to Repository**: Add the generated token as a repository secret named `CLAUDE_CODE_OAUTH_TOKEN`.
|
||||
|
||||
3. **Configure Workflow**: Use the OAuth token in your workflow:
|
||||
|
||||
```yaml
|
||||
- uses: markwylde/claude-code-gitea-action@v1.0.5
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
gitea_token: ${{ secrets.GITEA_TOKEN }}
|
||||
```
|
||||
|
||||
When `claude_code_oauth_token` is provided, it will be used instead of `anthropic_api_key` for authentication.
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
118
action.yml
118
action.yml
@@ -16,19 +16,17 @@ inputs:
|
||||
description: "The label that triggers the action (e.g. claude)"
|
||||
required: false
|
||||
default: "claude"
|
||||
base_branch:
|
||||
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
|
||||
required: false
|
||||
branch_prefix:
|
||||
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
||||
required: false
|
||||
default: "claude/"
|
||||
|
||||
# Mode configuration
|
||||
mode:
|
||||
description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking)"
|
||||
description: "Execution mode for the action. Valid modes: 'tag' (default) or 'agent'"
|
||||
required: false
|
||||
default: "tag"
|
||||
base_branch:
|
||||
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
|
||||
required: false
|
||||
|
||||
# Claude Code configuration
|
||||
model:
|
||||
@@ -37,9 +35,6 @@ inputs:
|
||||
anthropic_model:
|
||||
description: "DEPRECATED: Use 'model' instead. Model to use (provider-specific format required for Bedrock/Vertex)"
|
||||
required: false
|
||||
fallback_model:
|
||||
description: "Enable automatic fallback to specified model when primary model is unavailable"
|
||||
required: false
|
||||
allowed_tools:
|
||||
description: "Additional tools for Claude to use (the base GitHub tools will always be included)"
|
||||
required: false
|
||||
@@ -60,31 +55,48 @@ inputs:
|
||||
description: "Complete replacement of Claude's prompt with custom template (supports variable substitution)"
|
||||
required: false
|
||||
default: ""
|
||||
mcp_config:
|
||||
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
|
||||
|
||||
# New Claude Code settings
|
||||
settings:
|
||||
description: "Path to Claude Code settings JSON file, or settings JSON string"
|
||||
required: false
|
||||
default: ""
|
||||
system_prompt:
|
||||
description: "Override system prompt"
|
||||
required: false
|
||||
default: ""
|
||||
append_system_prompt:
|
||||
description: "Append to system prompt"
|
||||
required: false
|
||||
default: ""
|
||||
claude_env:
|
||||
description: "Custom environment variables to pass to Claude Code execution (YAML multiline format)"
|
||||
required: false
|
||||
default: ""
|
||||
additional_permissions:
|
||||
description: "Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results"
|
||||
required: false
|
||||
default: ""
|
||||
claude_env:
|
||||
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
|
||||
required: false
|
||||
default: ""
|
||||
settings:
|
||||
description: "Claude Code settings as JSON string or path to settings JSON file"
|
||||
fallback_model:
|
||||
description: "Enable automatic fallback to specified model when default model is overloaded"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
# Auth configuration
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex). Set to 'use-oauth' when using claude_credentials"
|
||||
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)"
|
||||
required: false
|
||||
claude_credentials:
|
||||
description: "Claude OAuth credentials JSON for Claude AI Max subscription authentication"
|
||||
claude_code_oauth_token:
|
||||
description: "Claude Code OAuth token (alternative to anthropic_api_key)"
|
||||
required: false
|
||||
default: ""
|
||||
gitea_token:
|
||||
description: "Gitea token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
|
||||
required: false
|
||||
path_to_claude_code_executable:
|
||||
description: "Path to a custom Claude Code executable to use instead of installing"
|
||||
required: false
|
||||
default: ""
|
||||
use_bedrock:
|
||||
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
@@ -93,6 +105,10 @@ inputs:
|
||||
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
default: "false"
|
||||
use_node_cache:
|
||||
description: "Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files)"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
max_turns:
|
||||
description: "Maximum number of conversation turns"
|
||||
@@ -130,14 +146,14 @@ runs:
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${GITHUB_ACTION_PATH}
|
||||
cd ${{ github.action_path }}
|
||||
bun install
|
||||
|
||||
- name: Prepare action
|
||||
id: prepare
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
|
||||
bun run ${{ github.action_path }}/src/entrypoints/prepare.ts
|
||||
env:
|
||||
MODE: ${{ inputs.mode }}
|
||||
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||
@@ -149,21 +165,53 @@ runs:
|
||||
DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
|
||||
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
||||
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
||||
OVERRIDE_PROMPT: ${{ inputs.override_prompt }}
|
||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.gitea_token }}
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
GITEA_API_URL: ${{ env.GITHUB_SERVER_URL }}
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }}
|
||||
|
||||
- name: Install Claude
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
# Install Claude Code if no custom executable is provided
|
||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||
echo "Installing Claude Code..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.117
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}")
|
||||
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
|
||||
fi
|
||||
# TODO pass claude_code_executable as input and use it here
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude-code
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
# Run the base-action
|
||||
bun run ${GITHUB_ACTION_PATH}/base-action/src/index.ts
|
||||
uses: anthropics/claude-code-base-action@v0.0.63
|
||||
with:
|
||||
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||
disallowed_tools: ${{ env.DISALLOWED_TOOLS }}
|
||||
max_turns: ${{ inputs.max_turns }}
|
||||
timeout_minutes: ${{ inputs.timeout_minutes }}
|
||||
model: ${{ inputs.model || inputs.anthropic_model }}
|
||||
mcp_config: ${{ steps.prepare.outputs.mcp_config }}
|
||||
use_bedrock: ${{ inputs.use_bedrock }}
|
||||
use_vertex: ${{ inputs.use_vertex }}
|
||||
anthropic_api_key: ${{ inputs.anthropic_api_key }}
|
||||
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
|
||||
settings: ${{ inputs.settings }}
|
||||
system_prompt: ${{ inputs.system_prompt }}
|
||||
append_system_prompt: ${{ inputs.append_system_prompt }}
|
||||
claude_env: ${{ inputs.claude_env }}
|
||||
fallback_model: ${{ inputs.fallback_model }}
|
||||
use_node_cache: ${{ inputs.use_node_cache }}
|
||||
env:
|
||||
# Core configuration
|
||||
PROMPT_FILE: /tmp/claude-prompts/claude-prompt.txt
|
||||
@@ -176,7 +224,15 @@ runs:
|
||||
USE_BEDROCK: ${{ inputs.use_bedrock }}
|
||||
USE_VERTEX: ${{ inputs.use_vertex }}
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
|
||||
# New settings support
|
||||
SETTINGS: ${{ inputs.settings }}
|
||||
SYSTEM_PROMPT: ${{ inputs.system_prompt }}
|
||||
APPEND_SYSTEM_PROMPT: ${{ inputs.append_system_prompt }}
|
||||
CLAUDE_ENV: ${{ inputs.claude_env }}
|
||||
FALLBACK_MODEL: ${{ inputs.fallback_model }}
|
||||
USE_NODE_CACHE: ${{ inputs.use_node_cache }}
|
||||
|
||||
# GitHub token for repository access
|
||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||
@@ -193,8 +249,6 @@ runs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
|
||||
ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL }}
|
||||
|
||||
# GCP configuration
|
||||
ANTHROPIC_VERTEX_PROJECT_ID: ${{ env.ANTHROPIC_VERTEX_PROJECT_ID }}
|
||||
CLOUD_ML_REGION: ${{ env.CLOUD_ML_REGION }}
|
||||
GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||
@@ -207,7 +261,7 @@ runs:
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
|
||||
bun run ${{ github.action_path }}/src/entrypoints/update-comment-link.ts
|
||||
env:
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
||||
|
||||
33
docs/capabilities-and-limitations.md
Normal file
33
docs/capabilities-and-limitations.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Capabilities and Limitations
|
||||
|
||||
## What Claude Can Do
|
||||
|
||||
- **Respond in a Single Comment**: Claude operates by updating a single initial comment with progress and results
|
||||
- **Answer Questions**: Analyze code and provide explanations
|
||||
- **Implement Code Changes**: Make simple to moderate code changes based on requests
|
||||
- **Prepare Pull Requests**: Creates commits on a branch and links back to a prefilled PR creation page
|
||||
- **Perform Code Reviews**: Analyze PR changes and provide detailed feedback
|
||||
- **Smart Branch Handling**:
|
||||
- When triggered on an **issue**: Always creates a new branch for the work
|
||||
- When triggered on an **open PR**: Always pushes directly to the existing PR branch
|
||||
- When triggered on a **closed PR**: Creates a new branch since the original is no longer active
|
||||
- **View GitHub Actions Results**: Can access workflow runs, job logs, and test results on the PR where it's tagged when `actions: read` permission is configured (see [Additional Permissions for CI/CD Integration](./configuration.md#additional-permissions-for-cicd-integration))
|
||||
|
||||
## What Claude Cannot Do
|
||||
|
||||
- **Submit PR Reviews**: Claude cannot submit formal GitHub PR reviews
|
||||
- **Approve PRs**: For security reasons, Claude cannot approve pull requests
|
||||
- **Post Multiple Comments**: Claude only acts by updating its initial comment
|
||||
- **Execute Commands Outside Its Context**: Claude only has access to the repository and PR/issue context it's triggered in
|
||||
- **Run Arbitrary Bash Commands**: By default, Claude cannot execute Bash commands unless explicitly allowed using the `allowed_tools` configuration
|
||||
- **Perform Branch Operations**: Cannot merge branches, rebase, or perform other git operations beyond pushing commits
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **Trigger Detection**: Listens for comments containing the trigger phrase (default: `@claude`) or issue assignment to a specific user
|
||||
2. **Context Gathering**: Analyzes the PR/issue, comments, code changes
|
||||
3. **Smart Responses**: Either answers questions or implements changes
|
||||
4. **Branch Management**: Creates new PRs for human authors, pushes directly for Claude's own PRs
|
||||
5. **Communication**: Posts updates at every step to keep you informed
|
||||
|
||||
This action is built on top of [`anthropics/claude-code-base-action`](https://github.com/anthropics/claude-code-base-action).
|
||||
99
docs/cloud-providers.md
Normal file
99
docs/cloud-providers.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Cloud Providers
|
||||
|
||||
You can authenticate with Claude using any of these three methods:
|
||||
|
||||
1. Direct Anthropic API (default)
|
||||
2. Amazon Bedrock with OIDC authentication
|
||||
3. Google Vertex AI with OIDC authentication
|
||||
|
||||
For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions#using-with-aws-bedrock-%26-google-vertex-ai).
|
||||
|
||||
**Note**:
|
||||
|
||||
- Bedrock and Vertex use OIDC authentication exclusively
|
||||
- AWS Bedrock automatically uses cross-region inference profiles for certain models
|
||||
- For cross-region inference profile models, you need to request and be granted access to the Claude models in all regions that the inference profile uses
|
||||
|
||||
## Model Configuration
|
||||
|
||||
Use provider-specific model names based on your chosen provider:
|
||||
|
||||
```yaml
|
||||
# For direct Anthropic API (default)
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# ... other inputs
|
||||
|
||||
# For Amazon Bedrock with OIDC
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
claude_args: |
|
||||
--model anthropic.claude-4-0-sonnet-20250805-v1:0
|
||||
# ... other inputs
|
||||
|
||||
# For Google Vertex AI with OIDC
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_vertex: "true"
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet@20250805
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
## OIDC Authentication for Bedrock and Vertex
|
||||
|
||||
Both AWS Bedrock and GCP Vertex AI require OIDC authentication.
|
||||
|
||||
```yaml
|
||||
# For AWS Bedrock with OIDC
|
||||
- name: Configure AWS Credentials (OIDC)
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
|
||||
aws-region: us-west-2
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
claude_args: |
|
||||
--model anthropic.claude-4-0-sonnet-20250805-v1:0
|
||||
# ... other inputs
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
```
|
||||
|
||||
```yaml
|
||||
# For GCP Vertex AI with OIDC
|
||||
- name: Authenticate to Google Cloud
|
||||
uses: google-github-actions/auth@v2
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
|
||||
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_vertex: "true"
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet@20250805
|
||||
# ... other inputs
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
```
|
||||
373
docs/configuration.md
Normal file
373
docs/configuration.md
Normal file
@@ -0,0 +1,373 @@
|
||||
# Advanced Configuration
|
||||
|
||||
## Using Custom MCP Configuration
|
||||
|
||||
You can add custom MCP (Model Context Protocol) servers to extend Claude's capabilities using the `--mcp-config` flag in `claude_args`. These servers merge with the built-in GitHub MCP servers.
|
||||
|
||||
### Basic Example: Adding a Sequential Thinking Server
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--mcp-config '{"mcpServers": {"sequential-thinking": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]}}}'
|
||||
--allowedTools mcp__sequential-thinking__sequentialthinking
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
### Passing Secrets to MCP Servers
|
||||
|
||||
For MCP servers that require sensitive information like API keys or tokens, you can create a configuration file with GitHub Secrets:
|
||||
|
||||
```yaml
|
||||
- name: Create MCP Config
|
||||
run: |
|
||||
cat > /tmp/mcp-config.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"custom-api-server": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@example/api-server"],
|
||||
"env": {
|
||||
"API_KEY": "${{ secrets.CUSTOM_API_KEY }}",
|
||||
"BASE_URL": "https://api.example.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--mcp-config /tmp/mcp-config.json
|
||||
# ... 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
|
||||
- name: Create MCP Config for Python Server
|
||||
run: |
|
||||
cat > /tmp/mcp-config.json << 'EOF'
|
||||
{
|
||||
"mcpServers": {
|
||||
"my-python-server": {
|
||||
"type": "stdio",
|
||||
"command": "uv",
|
||||
"args": [
|
||||
"--directory",
|
||||
"${{ github.workspace }}/path/to/server/",
|
||||
"run",
|
||||
"server_file.py"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--mcp-config /tmp/mcp-config.json
|
||||
--allowedTools 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"]
|
||||
```
|
||||
|
||||
### Multiple MCP Servers
|
||||
|
||||
You can add multiple MCP servers by using multiple `--mcp-config` flags:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--mcp-config /tmp/config1.json
|
||||
--mcp-config /tmp/config2.json
|
||||
--mcp-config '{"mcpServers": {"inline-server": {"command": "npx", "args": ["@example/server"]}}}'
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
**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.
|
||||
- Your custom servers will override any built-in servers with the same name.
|
||||
- The `claude_args` supports multiple `--mcp-config` flags that will be merged together.
|
||||
|
||||
## Additional Permissions for CI/CD Integration
|
||||
|
||||
The `additional_permissions` input allows Claude to access GitHub Actions workflow information when you grant the necessary permissions. This is particularly useful for analyzing CI/CD failures and debugging workflow issues.
|
||||
|
||||
### Enabling GitHub Actions Access
|
||||
|
||||
To allow Claude to view workflow run results, job logs, and CI status:
|
||||
|
||||
1. **Grant the necessary permission to your GitHub token**:
|
||||
|
||||
- When using the default `GITHUB_TOKEN`, add the `actions: read` permission to your workflow:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
actions: read # Add this line
|
||||
```
|
||||
|
||||
2. **Configure the action with additional permissions**:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
3. **Claude will automatically get access to CI/CD tools**:
|
||||
When you enable `actions: read`, Claude can use the following MCP tools:
|
||||
- `mcp__github_ci__get_ci_status` - View workflow run statuses
|
||||
- `mcp__github_ci__get_workflow_run_details` - Get detailed workflow information
|
||||
- `mcp__github_ci__download_job_log` - Download and analyze job logs
|
||||
|
||||
### Example: Debugging Failed CI Runs
|
||||
|
||||
```yaml
|
||||
name: Claude CI Helper
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
actions: read # Required for CI access
|
||||
|
||||
jobs:
|
||||
claude-ci-helper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
# Now Claude can respond to "@claude why did the CI fail?"
|
||||
```
|
||||
|
||||
**Important Notes**:
|
||||
|
||||
- The GitHub token must have the `actions: read` permission in your workflow
|
||||
- If the permission is missing, Claude will warn you and suggest adding it
|
||||
- Currently, only `actions: read` is supported, but the format allows for future extensions
|
||||
|
||||
## Custom Environment Variables
|
||||
|
||||
You can pass custom environment variables to Claude Code execution using the `settings` input. This is useful for CI/test setups that require specific environment variables:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
settings: |
|
||||
{
|
||||
"env": {
|
||||
"NODE_ENV": "test",
|
||||
"CI": "true",
|
||||
"DATABASE_URL": "postgres://test:test@localhost:5432/test_db"
|
||||
}
|
||||
}
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
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 limit the number of back-and-forth exchanges Claude can have during task execution using the `claude_args` input. 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@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--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
|
||||
|
||||
By default, Claude only has access to:
|
||||
|
||||
- File operations (reading, committing, editing files, read-only git commands)
|
||||
- Comment management (creating/updating comments)
|
||||
- Basic GitHub operations
|
||||
|
||||
Claude does **not** have access to execute arbitrary Bash commands by default. If you want Claude to run specific commands (e.g., npm install, npm test), you must explicitly allow them using the `claude_args` configuration:
|
||||
|
||||
**Note**: If your repository has a `.mcp.json` file in the root directory, Claude will automatically detect and use the MCP server tools defined there. However, these tools still need to be explicitly allowed.
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_args: |
|
||||
--allowedTools "Bash(npm install),Bash(npm run test),Edit,Replace,NotebookEditCell"
|
||||
--disallowedTools "TaskOutput,KillTask"
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
**Note**: The base GitHub tools are always included. Use `--allowedTools` to add additional tools (including specific Bash commands), and `--disallowedTools` to prevent specific tools from being used.
|
||||
|
||||
## Custom Model
|
||||
|
||||
Specify a Claude model using `claude_args`:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet-20250805
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
For provider-specific models:
|
||||
|
||||
```yaml
|
||||
# AWS Bedrock
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
claude_args: |
|
||||
--model anthropic.claude-4-0-sonnet-20250805-v1:0
|
||||
# ... other inputs
|
||||
|
||||
# Google Vertex AI
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_vertex: "true"
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet@20250805
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
## Claude Code Settings
|
||||
|
||||
You can provide Claude Code settings to customize behavior such as model selection, environment variables, permissions, and hooks. Settings can be provided either as a JSON string or a path to a settings file.
|
||||
|
||||
### Option 1: Settings File
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
settings: "path/to/settings.json"
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
### Option 2: Inline Settings
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
settings: |
|
||||
{
|
||||
"model": "claude-opus-4-1-20250805",
|
||||
"env": {
|
||||
"DEBUG": "true",
|
||||
"API_URL": "https://api.example.com"
|
||||
},
|
||||
"permissions": {
|
||||
"allow": ["Bash", "Read"],
|
||||
"deny": ["WebFetch"]
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "Bash",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "echo Running bash command..."
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
The settings support all Claude Code settings options including:
|
||||
|
||||
- `model`: Override the default model
|
||||
- `env`: Environment variables for the session
|
||||
- `permissions`: Tool usage permissions
|
||||
- `hooks`: Pre/post tool execution hooks
|
||||
- And more...
|
||||
|
||||
For a complete list of available settings and their descriptions, see the [Claude Code settings documentation](https://docs.anthropic.com/en/docs/claude-code/settings).
|
||||
|
||||
**Notes**:
|
||||
|
||||
- The `enableAllProjectMcpServers` setting is always set to `true` by this action to ensure MCP servers work correctly.
|
||||
- The `claude_args` input provides direct access to Claude Code CLI arguments and takes precedence over settings.
|
||||
- We recommend using `claude_args` for simple configurations and `settings` for complex configurations with hooks and environment variables.
|
||||
|
||||
## Migration from Deprecated Inputs
|
||||
|
||||
Many individual input parameters have been consolidated into `claude_args` or `settings`. Here's how to migrate:
|
||||
|
||||
| Old Input | New Approach |
|
||||
| --------------------- | -------------------------------------------------------- |
|
||||
| `allowed_tools` | Use `claude_args: "--allowedTools Tool1,Tool2"` |
|
||||
| `disallowed_tools` | Use `claude_args: "--disallowedTools Tool1,Tool2"` |
|
||||
| `max_turns` | Use `claude_args: "--max-turns 10"` |
|
||||
| `model` | Use `claude_args: "--model claude-4-0-sonnet-20250805"` |
|
||||
| `claude_env` | Use `settings` with `"env"` object |
|
||||
| `custom_instructions` | Use `claude_args: "--system-prompt 'Your instructions'"` |
|
||||
| `mcp_config` | Use `claude_args: "--mcp-config '{...}'"` |
|
||||
| `direct_prompt` | Use `prompt` input instead |
|
||||
| `override_prompt` | Use `prompt` with GitHub context variables |
|
||||
|
||||
## Custom Executables for Specialized Environments
|
||||
|
||||
For specialized environments like Nix, custom container setups, or other package management systems where the default installation doesn't work, you can provide your own executables:
|
||||
|
||||
### Custom Claude Code Executable
|
||||
|
||||
Use `path_to_claude_code_executable` to provide your own Claude Code binary instead of using the automatically installed version:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
path_to_claude_code_executable: "/path/to/custom/claude"
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
### Custom Bun Executable
|
||||
|
||||
Use `path_to_bun_executable` to provide your own Bun runtime instead of the default installation:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
path_to_bun_executable: "/path/to/custom/bun"
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
**Important**: Using incompatible versions may cause the action to fail. Ensure your custom executables are compatible with the action's requirements.
|
||||
122
docs/custom-automations.md
Normal file
122
docs/custom-automations.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Custom Automations
|
||||
|
||||
These examples show how to configure Claude to act automatically based on GitHub events. When you provide a `prompt` input, the action automatically runs in agent mode without requiring manual @mentions. Without a `prompt`, it runs in interactive mode, responding to @claude mentions.
|
||||
|
||||
## Mode Detection & Tracking Comments
|
||||
|
||||
The action automatically detects which mode to use based on your configuration:
|
||||
|
||||
- **Interactive Mode** (no `prompt` input): Responds to @claude mentions, creates tracking comments with progress indicators
|
||||
- **Automation Mode** (with `prompt` input): Executes immediately, **does not create tracking comments**
|
||||
|
||||
> **Note**: In v1, automation mode intentionally does not create tracking comments by default to reduce noise in automated workflows. If you need progress tracking, use the `track_progress: true` input parameter.
|
||||
|
||||
## Supported GitHub Events
|
||||
|
||||
This action supports the following GitHub events ([learn more GitHub event triggers](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)):
|
||||
|
||||
- `pull_request` or `pull_request_target` - When PRs are opened or synchronized
|
||||
- `issue_comment` - When comments are created on issues or PRs
|
||||
- `pull_request_comment` - When comments are made on PR diffs
|
||||
- `issues` - When issues are opened or assigned
|
||||
- `pull_request_review` - When PR reviews are submitted
|
||||
- `pull_request_review_comment` - When comments are made on PR reviews
|
||||
- `repository_dispatch` - Custom events triggered via API
|
||||
- `workflow_dispatch` - Manual workflow triggers (coming soon)
|
||||
|
||||
## Automated Documentation Updates
|
||||
|
||||
Automatically update documentation when specific files change (see [`examples/claude-pr-path-specific.yml`](../examples/claude-pr-path-specific.yml)):
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "src/api/**/*.ts"
|
||||
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Update the API documentation in README.md to reflect
|
||||
the changes made to the API endpoints in this PR.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
When API files are modified, the action automatically detects that a `prompt` is provided and runs in agent mode. Claude updates your README with the latest endpoint documentation and pushes the changes back to the PR, keeping your docs in sync with your code.
|
||||
|
||||
## Author-Specific Code Reviews
|
||||
|
||||
Automatically review PRs from specific authors or external contributors (see [`examples/claude-review-from-author.yml`](../examples/claude-review-from-author.yml)):
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review-by-author:
|
||||
if: |
|
||||
github.event.pull_request.user.login == 'developer1' ||
|
||||
github.event.pull_request.user.login == 'external-contributor'
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Please provide a thorough review of this pull request.
|
||||
Pay extra attention to coding standards, security practices,
|
||||
and test coverage since this is from an external contributor.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
Perfect for automatically reviewing PRs from new team members, external contributors, or specific developers who need extra guidance. The action automatically runs in agent mode when a `prompt` is provided.
|
||||
|
||||
## Custom Prompt Templates
|
||||
|
||||
Use the `prompt` input with GitHub context variables for dynamic automation:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Analyze PR #${{ github.event.pull_request.number }} in ${{ github.repository }} for security vulnerabilities.
|
||||
|
||||
Focus on:
|
||||
- SQL injection risks
|
||||
- XSS vulnerabilities
|
||||
- Authentication bypasses
|
||||
- Exposed secrets or credentials
|
||||
|
||||
Provide severity ratings (Critical/High/Medium/Low) for any issues found.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
You can access any GitHub context variable using the standard GitHub Actions syntax:
|
||||
|
||||
- `${{ github.repository }}` - The repository name
|
||||
- `${{ github.event.pull_request.number }}` - PR number
|
||||
- `${{ github.event.issue.number }}` - Issue number
|
||||
- `${{ github.event.pull_request.title }}` - PR title
|
||||
- `${{ github.event.pull_request.body }}` - PR description
|
||||
- `${{ github.event.comment.body }}` - Comment text
|
||||
- `${{ github.actor }}` - User who triggered the workflow
|
||||
- `${{ github.base_ref }}` - Base branch for PRs
|
||||
- `${{ github.head_ref }}` - Head branch for PRs
|
||||
|
||||
## Advanced Configuration with claude_args
|
||||
|
||||
For more control over Claude's behavior, use the `claude_args` input to pass CLI arguments directly:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: "Review this PR for performance issues"
|
||||
claude_args: |
|
||||
--max-turns 15
|
||||
--model claude-4-0-sonnet-20250805
|
||||
--allowedTools Edit,Read,Write,Bash
|
||||
--system-prompt "You are a performance optimization expert. Focus on identifying bottlenecks and suggesting improvements."
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
This provides full access to Claude Code CLI capabilities while maintaining the simplified action interface.
|
||||
128
docs/experimental.md
Normal file
128
docs/experimental.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Experimental Features
|
||||
|
||||
**Note:** Experimental features are considered unstable and not supported for production use. They may change or be removed at any time.
|
||||
|
||||
## Automatic Mode Detection
|
||||
|
||||
The action intelligently detects the appropriate execution mode based on your workflow context, eliminating the need for manual mode configuration.
|
||||
|
||||
### Interactive Mode (Tag Mode)
|
||||
|
||||
Activated when Claude detects @mentions, issue assignments, or labels—without an explicit `prompt`.
|
||||
|
||||
- **Triggers**: `@claude` mentions in comments, issue assignment to claude user, label application
|
||||
- **Features**: Creates tracking comments with progress checkboxes, full implementation capabilities
|
||||
- **Use case**: Interactive code assistance, Q&A, and implementation requests
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# No prompt needed - responds to @claude mentions
|
||||
```
|
||||
|
||||
### Automation Mode (Agent Mode)
|
||||
|
||||
Automatically activated when you provide a `prompt` input.
|
||||
|
||||
- **Triggers**: Any GitHub event when `prompt` input is provided
|
||||
- **Features**: Direct execution without requiring @claude mentions, streamlined for automation
|
||||
- **Use case**: Automated PR reviews, scheduled tasks, workflow automation
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
Check for outdated dependencies and create an issue if any are found.
|
||||
# Automatically runs in agent mode when prompt is provided
|
||||
```
|
||||
|
||||
### How It Works
|
||||
|
||||
The action uses this logic to determine the mode:
|
||||
|
||||
1. **If `prompt` is provided** → Runs in **agent mode** for automation
|
||||
2. **If no `prompt` but @claude is mentioned** → Runs in **tag mode** for interaction
|
||||
3. **If neither** → No action is taken
|
||||
|
||||
This automatic detection ensures your workflows are simpler and more intuitive, without needing to understand or configure different modes.
|
||||
|
||||
### Advanced Mode Control
|
||||
|
||||
For specialized use cases, you can fine-tune behavior using `claude_args`:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: "Review this PR"
|
||||
claude_args: |
|
||||
--max-turns 20
|
||||
--system-prompt "You are a code review specialist"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
## Network Restrictions
|
||||
|
||||
For enhanced security, you can restrict Claude's network access to specific domains only. This feature is particularly useful for:
|
||||
|
||||
- Enterprise environments with strict security policies
|
||||
- Preventing access to external services
|
||||
- Limiting Claude to only your internal APIs and services
|
||||
|
||||
When `experimental_allowed_domains` is set, Claude can only access the domains you explicitly list. You'll need to include the appropriate provider domains based on your authentication method.
|
||||
|
||||
### Provider-Specific Examples
|
||||
|
||||
#### If using Anthropic API or subscription
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Or: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
experimental_allowed_domains: |
|
||||
.anthropic.com
|
||||
```
|
||||
|
||||
#### If using AWS Bedrock
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
experimental_allowed_domains: |
|
||||
bedrock.*.amazonaws.com
|
||||
bedrock-runtime.*.amazonaws.com
|
||||
```
|
||||
|
||||
#### If using Google Vertex AI
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_vertex: "true"
|
||||
experimental_allowed_domains: |
|
||||
*.googleapis.com
|
||||
vertexai.googleapis.com
|
||||
```
|
||||
|
||||
### Common GitHub Domains
|
||||
|
||||
In addition to your provider domains, you may need to include GitHub-related domains. For GitHub.com users, common domains include:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
experimental_allowed_domains: |
|
||||
.anthropic.com # For Anthropic API
|
||||
.github.com
|
||||
.githubusercontent.com
|
||||
ghcr.io
|
||||
.blob.core.windows.net
|
||||
```
|
||||
|
||||
For GitHub Enterprise users, replace the GitHub.com domains above with your enterprise domains (e.g., `.github.company.com`, `packages.company.com`, etc.).
|
||||
|
||||
To determine which domains your workflow needs, you can temporarily run without restrictions and monitor the network requests, or check your GitHub Enterprise configuration for the specific services you use.
|
||||
356
docs/migration-guide.md
Normal file
356
docs/migration-guide.md
Normal file
@@ -0,0 +1,356 @@
|
||||
# Migration Guide: v0.x to v1.0
|
||||
|
||||
This guide helps you migrate from Claude Code Action v0.x to v1.0. The new version introduces intelligent mode detection and simplified configuration while maintaining backward compatibility for most use cases.
|
||||
|
||||
## Overview of Changes
|
||||
|
||||
### 🎯 Key Improvements in v1.0
|
||||
|
||||
1. **Automatic Mode Detection** - No more manual `mode` configuration
|
||||
2. **Simplified Configuration** - Unified `prompt` and `claude_args` inputs
|
||||
3. **Better SDK Alignment** - Closer integration with Claude Code CLI
|
||||
|
||||
### ⚠️ Breaking Changes
|
||||
|
||||
The following inputs have been deprecated and replaced:
|
||||
|
||||
| Deprecated Input | Replacement | Notes |
|
||||
| --------------------- | ------------------------------------ | --------------------------------------------- |
|
||||
| `mode` | Auto-detected | Action automatically chooses based on context |
|
||||
| `direct_prompt` | `prompt` | Direct drop-in replacement |
|
||||
| `override_prompt` | `prompt` | Use GitHub context variables instead |
|
||||
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
|
||||
| `max_turns` | `claude_args: --max-turns` | Use CLI format |
|
||||
| `model` | `claude_args: --model` | Specify via CLI |
|
||||
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
|
||||
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
|
||||
| `claude_env` | `settings` with env object | Use settings JSON |
|
||||
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
|
||||
| `timeout_minutes` | Use GitHub Actions `timeout-minutes` | Configure at job level instead of input level |
|
||||
|
||||
## Migration Examples
|
||||
|
||||
### Basic Interactive Workflow (@claude mentions)
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: "tag"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
custom_instructions: "Follow our coding standards"
|
||||
max_turns: "10"
|
||||
allowed_tools: "Edit,Read,Write"
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--max-turns 10
|
||||
--system-prompt "Follow our coding standards"
|
||||
--allowedTools Edit,Read,Write
|
||||
```
|
||||
|
||||
### Automation Workflow
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: "agent"
|
||||
direct_prompt: "Review this PR for security issues"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
model: "claude-3-5-sonnet-20241022"
|
||||
allowed_tools: "Edit,Read,Write"
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this PR for security issues
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet-20250805
|
||||
--allowedTools Edit,Read,Write
|
||||
```
|
||||
|
||||
> **⚠️ Important**: For PR reviews, always include the repository and PR context in your prompt. This ensures Claude knows which PR to review.
|
||||
|
||||
### Automation with Progress Tracking (New in v1.0)
|
||||
|
||||
**Missing the tracking comments from v0.x agent mode?** The new `track_progress` input brings them back!
|
||||
|
||||
In v1.0, automation mode (with `prompt` input) doesn't create tracking comments by default to reduce noise. However, if you need progress visibility, you can use the `track_progress` feature:
|
||||
|
||||
**Before (v0.x with tracking):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: "agent"
|
||||
direct_prompt: "Review this PR for security issues"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
**After (v1.0 with tracking):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
track_progress: true # Forces tag mode with tracking comments
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this PR for security issues
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
#### Benefits of `track_progress`
|
||||
|
||||
1. **Preserves GitHub Context**: Automatically includes all PR/issue details, comments, and attachments
|
||||
2. **Brings Back Tracking Comments**: Creates progress indicators just like v0.x agent mode
|
||||
3. **Works with Custom Prompts**: Your `prompt` is injected as custom instructions while maintaining context
|
||||
|
||||
#### Supported Events for `track_progress`
|
||||
|
||||
The `track_progress` input only works with these GitHub events:
|
||||
|
||||
**Pull Request Events:**
|
||||
|
||||
- `opened` - New PR created
|
||||
- `synchronize` - PR updated with new commits
|
||||
- `ready_for_review` - Draft PR marked as ready
|
||||
- `reopened` - Previously closed PR reopened
|
||||
|
||||
**Issue Events:**
|
||||
|
||||
- `opened` - New issue created
|
||||
- `edited` - Issue title or body modified
|
||||
- `labeled` - Label added to issue
|
||||
- `assigned` - Issue assigned to user
|
||||
|
||||
> **Note**: Using `track_progress: true` with unsupported events will cause an error.
|
||||
|
||||
### Custom Template with Variables
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
override_prompt: |
|
||||
Analyze PR #$PR_NUMBER in $REPOSITORY
|
||||
Changed files: $CHANGED_FILES
|
||||
Focus on security vulnerabilities
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Analyze this pull request focusing on security vulnerabilities in the changed files.
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
```
|
||||
|
||||
> **💡 Tip**: While you can access GitHub context variables in your prompt, it's recommended to use the standard `REPO:` and `PR NUMBER:` format for consistency.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
claude_env: |
|
||||
NODE_ENV: test
|
||||
CI: true
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
settings: |
|
||||
{
|
||||
"env": {
|
||||
"NODE_ENV": "test",
|
||||
"CI": "true"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Timeout Configuration
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
timeout_minutes: 30
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
claude-task:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30 # Moved to job level
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
## How Mode Detection Works
|
||||
|
||||
The action now automatically detects the appropriate mode:
|
||||
|
||||
1. **If `prompt` is provided** → Runs in **automation mode**
|
||||
|
||||
- Executes immediately without waiting for @claude mentions
|
||||
- Perfect for scheduled tasks, PR automation, etc.
|
||||
|
||||
2. **If no `prompt` but @claude is mentioned** → Runs in **interactive mode**
|
||||
|
||||
- Waits for and responds to @claude mentions
|
||||
- Creates tracking comments with progress
|
||||
|
||||
3. **If neither** → No action is taken
|
||||
|
||||
## Advanced Configuration with claude_args
|
||||
|
||||
The `claude_args` input provides direct access to Claude Code CLI arguments:
|
||||
|
||||
```yaml
|
||||
claude_args: |
|
||||
--max-turns 15
|
||||
--model claude-4-0-sonnet-20250805
|
||||
--allowedTools Edit,Read,Write,Bash
|
||||
--disallowedTools WebSearch
|
||||
--system-prompt "You are a senior engineer focused on code quality"
|
||||
--mcp-config '{"mcpServers": {"custom": {"command": "npx", "args": ["-y", "@example/server"]}}}'
|
||||
```
|
||||
|
||||
### Common claude_args Options
|
||||
|
||||
| Option | Description | Example |
|
||||
| ------------------- | ------------------------ | -------------------------------------- |
|
||||
| `--max-turns` | Limit conversation turns | `--max-turns 10` |
|
||||
| `--model` | Specify Claude model | `--model claude-4-0-sonnet-20250805` |
|
||||
| `--allowedTools` | Enable specific tools | `--allowedTools Edit,Read,Write` |
|
||||
| `--disallowedTools` | Disable specific tools | `--disallowedTools WebSearch` |
|
||||
| `--system-prompt` | Add system instructions | `--system-prompt "Focus on security"` |
|
||||
| `--mcp-config` | Add MCP server config | `--mcp-config '{"mcpServers": {...}}'` |
|
||||
|
||||
## Provider-Specific Updates
|
||||
|
||||
### AWS Bedrock
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
claude_args: |
|
||||
--model anthropic.claude-4-0-sonnet-20250805-v1:0
|
||||
```
|
||||
|
||||
### Google Vertex AI
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_vertex: "true"
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet@20250805
|
||||
```
|
||||
|
||||
## MCP Configuration Migration
|
||||
|
||||
### Adding Custom MCP Servers
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mcp_config: |
|
||||
{
|
||||
"mcpServers": {
|
||||
"custom-server": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@example/server"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_args: |
|
||||
--mcp-config '{"mcpServers": {"custom-server": {"command": "npx", "args": ["-y", "@example/server"]}}}'
|
||||
```
|
||||
|
||||
You can also pass MCP configuration from a file:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_args: |
|
||||
--mcp-config /path/to/mcp-config.json
|
||||
```
|
||||
|
||||
## Step-by-Step Migration Checklist
|
||||
|
||||
- [ ] Update action version from `@beta` to `@v1`
|
||||
- [ ] Remove `mode` input (auto-detected now)
|
||||
- [ ] Replace `direct_prompt` with `prompt`
|
||||
- [ ] Replace `override_prompt` with `prompt` using GitHub context
|
||||
- [ ] Move `custom_instructions` to `claude_args` with `--system-prompt`
|
||||
- [ ] Convert `max_turns` to `claude_args` with `--max-turns`
|
||||
- [ ] Convert `model` to `claude_args` with `--model`
|
||||
- [ ] Convert `allowed_tools` to `claude_args` with `--allowedTools`
|
||||
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
|
||||
- [ ] Move `claude_env` to `settings` JSON format
|
||||
- [ ] Move `mcp_config` to `claude_args` with `--mcp-config`
|
||||
- [ ] Replace `timeout_minutes` with GitHub Actions `timeout-minutes` at job level
|
||||
- [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode
|
||||
- [ ] Test workflow in a non-production environment
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues during migration:
|
||||
|
||||
1. Check the [FAQ](./faq.md) for common questions
|
||||
2. Review [example workflows](../examples/) for reference
|
||||
3. Open an [issue](https://github.com/anthropics/claude-code-action/issues) for support
|
||||
|
||||
## Version Compatibility
|
||||
|
||||
- **v0.x workflows** will continue to work but with deprecation warnings
|
||||
- **v1.0** is the recommended version for all new workflows
|
||||
- Future versions may remove deprecated inputs entirely
|
||||
43
docs/security.md
Normal file
43
docs/security.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Security
|
||||
|
||||
## Access Control
|
||||
|
||||
- **Repository Access**: The action can only be triggered by users with write access to the repository
|
||||
- **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots
|
||||
- **⚠️ Non-Write User Access (RISKY)**: The `allowed_non_write_users` parameter allows bypassing the write permission requirement. **This is a significant security risk and should only be used for workflows with extremely limited permissions** (e.g., issue labeling workflows that only have `issues: write` permission). This feature:
|
||||
- Only works when `github_token` is provided as input (not with GitHub App authentication)
|
||||
- Accepts either a comma-separated list of specific usernames or `*` to allow all users
|
||||
- **Should be used with extreme caution** as it bypasses the primary security mechanism of this action
|
||||
- Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope
|
||||
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
|
||||
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
|
||||
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
||||
|
||||
## GitHub App Permissions
|
||||
|
||||
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
|
||||
|
||||
- **Pull Requests**: Read and write to create PRs and push changes
|
||||
- **Issues**: Read and write to respond to issues
|
||||
- **Contents**: Read and write to modify repository files
|
||||
|
||||
## Commit Signing
|
||||
|
||||
All commits made by Claude through this action are automatically signed with commit signatures. This ensures the authenticity and integrity of commits, providing a verifiable trail of changes made by the action.
|
||||
|
||||
## ⚠️ Authentication Protection
|
||||
|
||||
**CRITICAL: Never hardcode your Anthropic API key or OAuth token in workflow files!**
|
||||
|
||||
Your authentication credentials must always be stored in GitHub secrets to prevent unauthorized access:
|
||||
|
||||
```yaml
|
||||
# CORRECT ✅
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# OR
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# NEVER DO THIS ❌
|
||||
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
|
||||
claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable!
|
||||
```
|
||||
146
docs/setup.md
Normal file
146
docs/setup.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Setup Guide
|
||||
|
||||
## Manual Setup (Direct API)
|
||||
|
||||
**Requirements**: You must be a repository admin to complete these steps.
|
||||
|
||||
1. Install the Claude GitHub app to your repository: https://github.com/apps/claude
|
||||
2. Add authentication to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions)):
|
||||
- Either `ANTHROPIC_API_KEY` for API key authentication
|
||||
- Or `CLAUDE_CODE_OAUTH_TOKEN` for OAuth token authentication (Pro and Max users can generate this by running `claude setup-token` locally)
|
||||
3. Copy the workflow file from [`examples/claude.yml`](../examples/claude.yml) into your repository's `.github/workflows/`
|
||||
|
||||
## Using a Custom GitHub App
|
||||
|
||||
If you prefer not to install the official Claude app, you can create your own GitHub App to use with this action. This gives you complete control over permissions and access.
|
||||
|
||||
**When you may want to use a custom GitHub App:**
|
||||
|
||||
- You need more restrictive permissions than the official app
|
||||
- Organization policies prevent installing third-party apps
|
||||
- You're using AWS Bedrock or Google Vertex AI
|
||||
|
||||
**Steps to create and use a custom GitHub App:**
|
||||
|
||||
1. **Create a new GitHub App:**
|
||||
|
||||
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
|
||||
- Click "New GitHub App"
|
||||
- Configure the app with these minimum permissions:
|
||||
- **Repository permissions:**
|
||||
- Contents: Read & Write
|
||||
- Issues: Read & Write
|
||||
- Pull requests: Read & Write
|
||||
- **Account permissions:** None required
|
||||
- Set "Where can this GitHub App be installed?" to your preference
|
||||
- Create the app
|
||||
|
||||
2. **Generate and download a private key:**
|
||||
|
||||
- After creating the app, scroll down to "Private keys"
|
||||
- Click "Generate a private key"
|
||||
- Download the `.pem` file (keep this secure!)
|
||||
|
||||
3. **Install the app on your repository:**
|
||||
|
||||
- Go to the app's settings page
|
||||
- Click "Install App"
|
||||
- Select the repositories where you want to use Claude
|
||||
|
||||
4. **Add the app credentials to your repository secrets:**
|
||||
|
||||
- Go to your repository's Settings → Secrets and variables → Actions
|
||||
- Add these secrets:
|
||||
- `APP_ID`: Your GitHub App's ID (found in the app settings)
|
||||
- `APP_PRIVATE_KEY`: The contents of the downloaded `.pem` file
|
||||
|
||||
5. **Update your workflow to use the custom app:**
|
||||
|
||||
```yaml
|
||||
name: Claude with Custom App
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# ... other triggers
|
||||
|
||||
jobs:
|
||||
claude-response:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Generate a token from your custom app
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Use Claude with your custom app's token
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
# ... other configuration
|
||||
```
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- The custom app must have read/write permissions for Issues, Pull Requests, and Contents
|
||||
- Your app's token will have the exact permissions you configured, nothing more
|
||||
|
||||
For more information on creating GitHub Apps, see the [GitHub documentation](https://docs.github.com/en/apps/creating-github-apps).
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
**⚠️ IMPORTANT: Never commit API keys directly to your repository! Always use GitHub Actions secrets.**
|
||||
|
||||
To securely use your Anthropic API key:
|
||||
|
||||
1. Add your API key as a repository secret:
|
||||
|
||||
- Go to your repository's Settings
|
||||
- Navigate to "Secrets and variables" → "Actions"
|
||||
- Click "New repository secret"
|
||||
- Name it `ANTHROPIC_API_KEY`
|
||||
- Paste your API key as the value
|
||||
|
||||
2. Reference the secret in your workflow:
|
||||
```yaml
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
**Never do this:**
|
||||
|
||||
```yaml
|
||||
# ❌ WRONG - Exposes your API key
|
||||
anthropic_api_key: "sk-ant-..."
|
||||
```
|
||||
|
||||
**Always do this:**
|
||||
|
||||
```yaml
|
||||
# ✅ CORRECT - Uses GitHub secrets
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
This applies to all sensitive values including API keys, access tokens, and credentials.
|
||||
We also recommend that you always use short-lived tokens when possible
|
||||
|
||||
## Setting Up GitHub Secrets
|
||||
|
||||
1. Go to your repository's Settings
|
||||
2. Click on "Secrets and variables" → "Actions"
|
||||
3. Click "New repository secret"
|
||||
4. For authentication, choose one:
|
||||
- API Key: Name: `ANTHROPIC_API_KEY`, Value: Your Anthropic API key (starting with `sk-ant-`)
|
||||
- OAuth Token: Name: `CLAUDE_CODE_OAUTH_TOKEN`, Value: Your Claude Code OAuth token (Pro and Max users can generate this by running `claude setup-token` locally)
|
||||
5. Click "Add secret"
|
||||
|
||||
### Best Practices for Authentication
|
||||
|
||||
1. ✅ Always use `${{ secrets.ANTHROPIC_API_KEY }}` or `${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}` in workflows
|
||||
2. ✅ Never commit API keys or tokens to version control
|
||||
3. ✅ Regularly rotate your API keys and tokens
|
||||
4. ✅ Use environment secrets for organization-wide access
|
||||
5. ❌ Never share API keys or tokens in pull requests or issues
|
||||
6. ❌ Avoid logging workflow variables that might contain keys
|
||||
591
docs/solutions.md
Normal file
591
docs/solutions.md
Normal file
@@ -0,0 +1,591 @@
|
||||
# Solutions & Use Cases
|
||||
|
||||
This guide provides complete, ready-to-use solutions for common automation scenarios with Claude Code Action. Each solution includes working examples, configuration details, and expected outcomes.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Automatic PR Code Review](#automatic-pr-code-review)
|
||||
- [Review Only Specific File Paths](#review-only-specific-file-paths)
|
||||
- [Review PRs from External Contributors](#review-prs-from-external-contributors)
|
||||
- [Custom PR Review Checklist](#custom-pr-review-checklist)
|
||||
- [Scheduled Repository Maintenance](#scheduled-repository-maintenance)
|
||||
- [Issue Auto-Triage and Labeling](#issue-auto-triage-and-labeling)
|
||||
- [Documentation Sync on API Changes](#documentation-sync-on-api-changes)
|
||||
- [Security-Focused PR Reviews](#security-focused-pr-reviews)
|
||||
|
||||
---
|
||||
|
||||
## Automatic PR Code Review
|
||||
|
||||
**When to use:** Automatically review every PR opened or updated in your repository.
|
||||
|
||||
### Basic Example (No Tracking)
|
||||
|
||||
```yaml
|
||||
name: Claude Auto Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please review this pull request with a focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Use `gh pr comment` for top-level feedback.
|
||||
Use `mcp__github_inline_comment__create_inline_comment` to highlight specific code issues.
|
||||
Only post GitHub comments - don't submit review text as messages.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Triggers on `opened` and `synchronize` (new commits)
|
||||
- Always include `REPO` and `PR NUMBER` for context
|
||||
- Specify tools for commenting and reviewing
|
||||
- PR branch is pre-checked out
|
||||
|
||||
**Expected Output:** Claude posts review comments directly to the PR with inline annotations where appropriate.
|
||||
|
||||
### Enhanced Example (With Progress Tracking)
|
||||
|
||||
Want visual progress tracking for PR reviews? Use `track_progress: true` to get tracking comments like in v0.x:
|
||||
|
||||
```yaml
|
||||
name: Claude Auto Review with Tracking
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
track_progress: true # ✨ Enables tracking comments
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please review this pull request with a focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Provide detailed feedback using inline comments for specific issues.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
```
|
||||
|
||||
**Benefits of Progress Tracking:**
|
||||
|
||||
- **Visual Progress Indicators**: Shows "In progress" status with checkboxes
|
||||
- **Preserves Full Context**: Automatically includes all PR details, comments, and attachments
|
||||
- **Migration-Friendly**: Perfect for teams moving from v0.x who miss tracking comments
|
||||
- **Works with Custom Prompts**: Your prompt becomes custom instructions while maintaining GitHub context
|
||||
|
||||
**Expected Output:**
|
||||
|
||||
1. Claude creates a tracking comment: "Claude Code is reviewing this pull request..."
|
||||
2. Updates the comment with progress checkboxes as it works
|
||||
3. Posts detailed review feedback with inline annotations
|
||||
4. Updates tracking comment to "Completed" when done
|
||||
|
||||
---
|
||||
|
||||
## Review Only Specific File Paths
|
||||
|
||||
**When to use:** Review PRs only when specific critical files change.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Review Critical Files
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "src/auth/**"
|
||||
- "src/api/**"
|
||||
- "config/security.yml"
|
||||
|
||||
jobs:
|
||||
security-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
This PR modifies critical authentication or API files.
|
||||
|
||||
Please provide a security-focused review with emphasis on:
|
||||
- Authentication and authorization flows
|
||||
- Input validation and sanitization
|
||||
- SQL injection or XSS vulnerabilities
|
||||
- API security best practices
|
||||
|
||||
Note: The PR branch is already checked out.
|
||||
|
||||
Post detailed security findings as PR comments.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- `paths:` filter triggers only for specific file changes
|
||||
- Custom prompt emphasizes security for sensitive areas
|
||||
- Useful for compliance or security reviews
|
||||
|
||||
**Expected Output:** Security-focused review when critical files are modified.
|
||||
|
||||
---
|
||||
|
||||
## Review PRs from External Contributors
|
||||
|
||||
**When to use:** Apply stricter review criteria for external or new contributors.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: External Contributor Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
external-review:
|
||||
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
CONTRIBUTOR: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
This is a first-time contribution from @${{ github.event.pull_request.user.login }}.
|
||||
|
||||
Please provide a comprehensive review focusing on:
|
||||
- Compliance with project coding standards
|
||||
- Proper test coverage (unit and integration)
|
||||
- Documentation for new features
|
||||
- Potential breaking changes
|
||||
- License header requirements
|
||||
|
||||
Be welcoming but thorough in your review. Use inline comments for code-specific feedback.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr view:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- `if:` condition targets specific contributor types
|
||||
- Includes contributor username in context
|
||||
- Emphasis on onboarding and standards
|
||||
|
||||
**Expected Output:** Detailed review helping new contributors understand project standards.
|
||||
|
||||
---
|
||||
|
||||
## Custom PR Review Checklist
|
||||
|
||||
**When to use:** Enforce specific review criteria for your team's workflow.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: PR Review Checklist
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
checklist-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this PR against our team checklist:
|
||||
|
||||
## Code Quality
|
||||
- [ ] Code follows our style guide
|
||||
- [ ] No commented-out code
|
||||
- [ ] Meaningful variable names
|
||||
- [ ] DRY principle followed
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests for new functions
|
||||
- [ ] Integration tests for new endpoints
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Test coverage > 80%
|
||||
|
||||
## Documentation
|
||||
- [ ] README updated if needed
|
||||
- [ ] API docs updated
|
||||
- [ ] Inline comments for complex logic
|
||||
- [ ] CHANGELOG.md updated
|
||||
|
||||
## Security
|
||||
- [ ] No hardcoded credentials
|
||||
- [ ] Input validation implemented
|
||||
- [ ] Proper error handling
|
||||
- [ ] No sensitive data in logs
|
||||
|
||||
For each item, check if it's satisfied and comment on any that need attention.
|
||||
Post a summary comment with checklist results.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Structured checklist in prompt
|
||||
- Systematic review approach
|
||||
- Team-specific criteria
|
||||
|
||||
**Expected Output:** Systematic review with checklist results and specific feedback.
|
||||
|
||||
---
|
||||
|
||||
## Scheduled Repository Maintenance
|
||||
|
||||
**When to use:** Regular automated maintenance tasks.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Weekly Maintenance
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Every Sunday at midnight
|
||||
workflow_dispatch: # Manual trigger option
|
||||
|
||||
jobs:
|
||||
maintenance:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
|
||||
Perform weekly repository maintenance:
|
||||
|
||||
1. Check for outdated dependencies in package.json
|
||||
2. Scan for security vulnerabilities using `npm audit`
|
||||
3. Review open issues older than 90 days
|
||||
4. Check for TODO comments in recent commits
|
||||
5. Verify README.md examples still work
|
||||
|
||||
Create a single issue summarizing any findings.
|
||||
If critical security issues are found, also comment on open PRs.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Read,Bash(npm:*),Bash(gh issue:*),Bash(git:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- `schedule:` for automated runs
|
||||
- `workflow_dispatch:` for manual triggering
|
||||
- Comprehensive tool permissions for analysis
|
||||
|
||||
**Expected Output:** Weekly maintenance report as GitHub issue.
|
||||
|
||||
---
|
||||
|
||||
## Issue Auto-Triage and Labeling
|
||||
|
||||
**When to use:** Automatically categorize and prioritize new issues.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Issue Triage
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE NUMBER: ${{ github.event.issue.number }}
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
BODY: ${{ github.event.issue.body }}
|
||||
AUTHOR: ${{ github.event.issue.user.login }}
|
||||
|
||||
Analyze this new issue and:
|
||||
1. Determine if it's a bug report, feature request, or question
|
||||
2. Assess priority (critical, high, medium, low)
|
||||
3. Suggest appropriate labels
|
||||
4. Check if it duplicates existing issues
|
||||
|
||||
Based on your analysis, add the appropriate labels using:
|
||||
`gh issue edit [number] --add-label "label1,label2"`
|
||||
|
||||
If it appears to be a duplicate, post a comment mentioning the original issue.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Bash(gh issue:*),Bash(gh search:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Triggered on new issues
|
||||
- Issue context in prompt
|
||||
- Label management capabilities
|
||||
|
||||
**Expected Output:** Automatically labeled and categorized issues.
|
||||
|
||||
---
|
||||
|
||||
## Documentation Sync on API Changes
|
||||
|
||||
**When to use:** Keep docs up-to-date when API code changes.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Sync API Documentation
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "src/api/**/*.ts"
|
||||
- "src/routes/**/*.ts"
|
||||
|
||||
jobs:
|
||||
doc-sync:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
This PR modifies API endpoints. Please:
|
||||
|
||||
1. Review the API changes in src/api and src/routes
|
||||
2. Update API.md to document any new or changed endpoints
|
||||
3. Ensure OpenAPI spec is updated if needed
|
||||
4. Update example requests/responses
|
||||
|
||||
Use standard REST API documentation format.
|
||||
Commit any documentation updates to this PR branch.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Read,Write,Edit,Bash(git:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Path-specific trigger
|
||||
- Write permissions for doc updates
|
||||
- Git tools for committing
|
||||
|
||||
**Expected Output:** API documentation automatically updated with code changes.
|
||||
|
||||
---
|
||||
|
||||
## Security-Focused PR Reviews
|
||||
|
||||
**When to use:** Deep security analysis for sensitive repositories.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Security Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Optional: Add track_progress: true for visual progress tracking during security reviews
|
||||
# track_progress: true
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Perform a comprehensive security review:
|
||||
|
||||
## OWASP Top 10 Analysis
|
||||
- SQL Injection vulnerabilities
|
||||
- Cross-Site Scripting (XSS)
|
||||
- Broken Authentication
|
||||
- Sensitive Data Exposure
|
||||
- XML External Entities (XXE)
|
||||
- Broken Access Control
|
||||
- Security Misconfiguration
|
||||
- Cross-Site Request Forgery (CSRF)
|
||||
- Using Components with Known Vulnerabilities
|
||||
- Insufficient Logging & Monitoring
|
||||
|
||||
## Additional Security Checks
|
||||
- Hardcoded secrets or credentials
|
||||
- Insecure cryptographic practices
|
||||
- Unsafe deserialization
|
||||
- Server-Side Request Forgery (SSRF)
|
||||
- Race conditions or TOCTOU issues
|
||||
|
||||
Rate severity as: CRITICAL, HIGH, MEDIUM, LOW, or NONE.
|
||||
Post detailed findings with recommendations.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Security-focused prompt structure
|
||||
- OWASP alignment
|
||||
- Severity rating system
|
||||
|
||||
**Expected Output:** Detailed security analysis with prioritized findings.
|
||||
|
||||
---
|
||||
|
||||
## Tips for All Solutions
|
||||
|
||||
### Always Include GitHub Context
|
||||
|
||||
```yaml
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
[Your specific instructions]
|
||||
```
|
||||
|
||||
### Common Tool Permissions
|
||||
|
||||
- **PR Comments**: `Bash(gh pr comment:*)`
|
||||
- **Inline Comments**: `mcp__github_inline_comment__create_inline_comment`
|
||||
- **File Operations**: `Read,Write,Edit`
|
||||
- **Git Operations**: `Bash(git:*)`
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Be specific in your prompts
|
||||
- Include expected output format
|
||||
- Set clear success criteria
|
||||
- Provide context about the repository
|
||||
- Use inline comments for code-specific feedback
|
||||
223
docs/usage.md
Normal file
223
docs/usage.md
Normal file
@@ -0,0 +1,223 @@
|
||||
# Usage
|
||||
|
||||
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
|
||||
|
||||
```yaml
|
||||
name: Claude Assistant
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
pull_request_review_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, assigned, labeled]
|
||||
pull_request_review:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude-response:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Or use OAuth token instead:
|
||||
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
|
||||
# Optional: provide a prompt for automation workflows
|
||||
# prompt: "Review this PR for security issues"
|
||||
|
||||
# Optional: pass advanced arguments to Claude CLI
|
||||
# claude_args: |
|
||||
# --max-turns 10
|
||||
# --model claude-4-0-sonnet-20250805
|
||||
|
||||
# Optional: add custom trigger phrase (default: @claude)
|
||||
# trigger_phrase: "/claude"
|
||||
# Optional: add assignee trigger for issues
|
||||
# assignee_trigger: "claude"
|
||||
# Optional: add label trigger for issues
|
||||
# label_trigger: "claude"
|
||||
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
|
||||
# additional_permissions: |
|
||||
# actions: read
|
||||
# Optional: allow bot users to trigger the action
|
||||
# allowed_bots: "dependabot[bot],renovate[bot]"
|
||||
```
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------- |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
||||
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
|
||||
| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` |
|
||||
| `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" |
|
||||
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
|
||||
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
|
||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
||||
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
|
||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
|
||||
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
|
||||
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
||||
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
||||
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` |
|
||||
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` |
|
||||
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
|
||||
| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" |
|
||||
| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" |
|
||||
| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" |
|
||||
|
||||
### Deprecated Inputs
|
||||
|
||||
These inputs are deprecated and will be removed in a future version:
|
||||
|
||||
| Input | Description | Migration Path |
|
||||
| --------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| `mode` | **DEPRECATED**: Mode is now automatically detected based on workflow context | Remove this input; the action auto-detects the correct mode |
|
||||
| `direct_prompt` | **DEPRECATED**: Use `prompt` instead | Replace with `prompt` |
|
||||
| `override_prompt` | **DEPRECATED**: Use `prompt` with template variables or `claude_args` with `--system-prompt` | Use `prompt` for templates or `claude_args` for system prompts |
|
||||
| `custom_instructions` | **DEPRECATED**: Use `claude_args` with `--system-prompt` or include in `prompt` | Move instructions to `prompt` or use `claude_args` |
|
||||
| `max_turns` | **DEPRECATED**: Use `claude_args` with `--max-turns` instead | Use `claude_args: "--max-turns 5"` |
|
||||
| `model` | **DEPRECATED**: Use `claude_args` with `--model` instead | Use `claude_args: "--model claude-4-0-sonnet-20250805"` |
|
||||
| `fallback_model` | **DEPRECATED**: Use `claude_args` with fallback configuration | Configure fallback in `claude_args` or `settings` |
|
||||
| `allowed_tools` | **DEPRECATED**: Use `claude_args` with `--allowedTools` instead | Use `claude_args: "--allowedTools Edit,Read,Write"` |
|
||||
| `disallowed_tools` | **DEPRECATED**: Use `claude_args` with `--disallowedTools` instead | Use `claude_args: "--disallowedTools WebSearch"` |
|
||||
| `mcp_config` | **DEPRECATED**: Use `claude_args` with `--mcp-config` instead | Use `claude_args: "--mcp-config '{...}'"` |
|
||||
| `claude_env` | **DEPRECATED**: Use `settings` with env configuration | Configure environment in `settings` JSON |
|
||||
|
||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||
|
||||
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
|
||||
|
||||
## Upgrading from v0.x?
|
||||
|
||||
For a comprehensive guide on migrating from v0.x to v1.0, including step-by-step instructions and examples, see our **[Migration Guide](./migration-guide.md)**.
|
||||
|
||||
### Quick Migration Examples
|
||||
|
||||
#### Interactive Workflows (with @claude mentions)
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: "tag"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
custom_instructions: "Focus on security"
|
||||
max_turns: "10"
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--max-turns 10
|
||||
--system-prompt "Focus on security"
|
||||
```
|
||||
|
||||
#### Automation Workflows
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: "agent"
|
||||
direct_prompt: "Update the API documentation"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
model: "claude-4-0-sonnet-20250805"
|
||||
allowed_tools: "Edit,Read,Write"
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Update the API documentation to reflect changes in this PR
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet-20250805
|
||||
--allowedTools Edit,Read,Write
|
||||
```
|
||||
|
||||
#### Custom Templates
|
||||
|
||||
**Before (v0.x):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
override_prompt: |
|
||||
Analyze PR #$PR_NUMBER for security issues.
|
||||
Focus on: $CHANGED_FILES
|
||||
```
|
||||
|
||||
**After (v1.0):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Analyze PR #${{ github.event.pull_request.number }} for security issues.
|
||||
Focus on the changed files in this PR.
|
||||
```
|
||||
|
||||
## Ways to Tag @claude
|
||||
|
||||
These examples show how to interact with Claude using comments in PRs and issues. By default, Claude will be triggered anytime you mention `@claude`, but you can customize the exact trigger phrase using the `trigger_phrase` input in the workflow.
|
||||
|
||||
Claude will see the full PR context, including any comments.
|
||||
|
||||
### Ask Questions
|
||||
|
||||
Add a comment to a PR or issue:
|
||||
|
||||
```
|
||||
@claude What does this function do and how could we improve it?
|
||||
```
|
||||
|
||||
Claude will analyze the code and provide a detailed explanation with suggestions.
|
||||
|
||||
### Request Fixes
|
||||
|
||||
Ask Claude to implement specific changes:
|
||||
|
||||
```
|
||||
@claude Can you add error handling to this function?
|
||||
```
|
||||
|
||||
### Code Review
|
||||
|
||||
Get a thorough review:
|
||||
|
||||
```
|
||||
@claude Please review this PR and suggest improvements
|
||||
```
|
||||
|
||||
Claude will analyze the changes and provide feedback.
|
||||
|
||||
### Fix Bugs from Screenshots
|
||||
|
||||
Upload a screenshot of a bug and ask Claude to fix it:
|
||||
|
||||
```
|
||||
@claude Here's a screenshot of a bug I'm seeing [upload screenshot]. Can you fix it?
|
||||
```
|
||||
|
||||
Claude can see and analyze images, making it easy to fix visual bugs or UI issues.
|
||||
97
examples/ci-failure-auto-fix.yml
Normal file
97
examples/ci-failure-auto-fix.yml
Normal file
@@ -0,0 +1,97 @@
|
||||
name: Auto Fix CI Failures
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
issues: write
|
||||
id-token: write # Required for OIDC token exchange
|
||||
|
||||
jobs:
|
||||
auto-fix:
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'failure' &&
|
||||
github.event.workflow_run.pull_requests[0] &&
|
||||
!startsWith(github.event.workflow_run.head_branch, 'claude-auto-fix-ci-')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup git identity
|
||||
run: |
|
||||
git config --global user.email "claude[bot]@users.noreply.github.com"
|
||||
git config --global user.name "claude[bot]"
|
||||
|
||||
- name: Create fix branch
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get CI failure details
|
||||
id: failure_details
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = await github.rest.actions.getWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure');
|
||||
|
||||
let errorLogs = [];
|
||||
for (const job of failedJobs) {
|
||||
const logs = await github.rest.actions.downloadJobLogsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
job_id: job.id
|
||||
});
|
||||
errorLogs.push({
|
||||
jobName: job.name,
|
||||
logs: logs.data
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
runUrl: run.data.html_url,
|
||||
failedJobs: failedJobs.map(j => j.name),
|
||||
errorLogs: errorLogs
|
||||
};
|
||||
|
||||
- name: Fix CI failures with Claude
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
/fix-ci
|
||||
Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }}
|
||||
Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }}
|
||||
PR Number: ${{ github.event.workflow_run.pull_requests[0].number }}
|
||||
Branch Name: ${{ steps.branch.outputs.branch_name }}
|
||||
Base Branch: ${{ github.event.workflow_run.head_branch }}
|
||||
Repository: ${{ github.repository }}
|
||||
|
||||
Error logs:
|
||||
${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*)'"
|
||||
23
examples/gitea-custom-url.yml
Normal file
23
examples/gitea-custom-url.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
name: Claude PR Review with Custom Gitea URL
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
claude-review:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Claude Code Analysis
|
||||
uses: markwylde/claude-code-gitea-action@gitea
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
claude-api-key: ${{ secrets.CLAUDE_API_KEY }}
|
||||
env:
|
||||
# Set this to your public Gitea URL to override the internal container URL
|
||||
# This ensures that links in comments point to the correct public URL
|
||||
GITEA_SERVER_URL: https://gitea.example.com
|
||||
|
||||
# Note: GITHUB_SERVER_URL is automatically set by Gitea Actions to the internal URL
|
||||
# but it will be overridden by GITEA_SERVER_URL if set above
|
||||
63
examples/issue-deduplication.yml
Normal file
63
examples/issue-deduplication.yml
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Issue Deduplication
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
deduplicate:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check for duplicate issues
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Analyze this new issue and check if it's a duplicate of existing issues in the repository.
|
||||
|
||||
Issue: #${{ github.event.issue.number }}
|
||||
Repository: ${{ github.repository }}
|
||||
|
||||
Your task:
|
||||
1. Use mcp__github__get_issue to get details of the current issue (#${{ github.event.issue.number }})
|
||||
2. Search for similar existing issues using mcp__github__search_issues with relevant keywords from the issue title and body
|
||||
3. Compare the new issue with existing ones to identify potential duplicates
|
||||
|
||||
Criteria for duplicates:
|
||||
- Same bug or error being reported
|
||||
- Same feature request (even if worded differently)
|
||||
- Same question being asked
|
||||
- Issues describing the same root problem
|
||||
|
||||
If you find duplicates:
|
||||
- Add a comment on the new issue linking to the original issue(s)
|
||||
- Apply a "duplicate" label to the new issue
|
||||
- Be polite and explain why it's a duplicate
|
||||
- Suggest the user follow the original issue for updates
|
||||
|
||||
If it's NOT a duplicate:
|
||||
- Don't add any comments
|
||||
- You may apply appropriate topic labels based on the issue content
|
||||
|
||||
Use these tools:
|
||||
- mcp__github__get_issue: Get issue details
|
||||
- mcp__github__search_issues: Search for similar issues
|
||||
- mcp__github__list_issues: List recent issues if needed
|
||||
- mcp__github__create_issue_comment: Add a comment if duplicate found
|
||||
- mcp__github__update_issue: Add labels
|
||||
|
||||
Be thorough but efficient. Focus on finding true duplicates, not just similar issues.
|
||||
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github__get_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_issue_comment,mcp__github__update_issue,mcp__github__get_issue_comments"
|
||||
29
examples/issue-triage.yml
Normal file
29
examples/issue-triage.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
name: Claude Issue Triage
|
||||
description: Run Claude Code for issue triage in GitHub Actions
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage-issue:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Claude Code for Issue Triage
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
# NOTE: /label-issue here requires a .claude/commands/label-issue.md file in your repo (see this repo's .claude directory for an example)
|
||||
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER${{ github.event.issue.number }}"
|
||||
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
42
examples/manual-code-analysis.yml
Normal file
42
examples/manual-code-analysis.yml
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Claude Commit Analysis
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
analysis_type:
|
||||
description: "Type of analysis to perform"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- summarize-commit
|
||||
- security-review
|
||||
default: "summarize-commit"
|
||||
|
||||
jobs:
|
||||
analyze-commit:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
||||
|
||||
- name: Run Claude Analysis
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
|
||||
Analyze the latest commit in this repository.
|
||||
|
||||
${{ github.event.inputs.analysis_type == 'summarize-commit' && 'Task: Provide a clear, concise summary of what changed in the latest commit. Include the commit message, files changed, and the purpose of the changes.' || '' }}
|
||||
|
||||
${{ github.event.inputs.analysis_type == 'security-review' && 'Task: Review the latest commit for potential security vulnerabilities. Check for exposed secrets, insecure coding patterns, dependency vulnerabilities, or any other security concerns. Provide specific recommendations if issues are found.' || '' }}
|
||||
74
examples/pr-review-comprehensive.yml
Normal file
74
examples/pr-review-comprehensive.yml
Normal file
@@ -0,0 +1,74 @@
|
||||
name: PR Review with Progress Tracking
|
||||
|
||||
# This example demonstrates how to use the track_progress feature to get
|
||||
# visual progress tracking for PR reviews, similar to v0.x agent mode.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
|
||||
jobs:
|
||||
review-with-tracking:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: PR Review with Progress Tracking
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
# Enable progress tracking
|
||||
track_progress: true
|
||||
|
||||
# Your custom review instructions
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Perform a comprehensive code review with the following focus areas:
|
||||
|
||||
1. **Code Quality**
|
||||
- Clean code principles and best practices
|
||||
- Proper error handling and edge cases
|
||||
- Code readability and maintainability
|
||||
|
||||
2. **Security**
|
||||
- Check for potential security vulnerabilities
|
||||
- Validate input sanitization
|
||||
- Review authentication/authorization logic
|
||||
|
||||
3. **Performance**
|
||||
- Identify potential performance bottlenecks
|
||||
- Review database queries for efficiency
|
||||
- Check for memory leaks or resource issues
|
||||
|
||||
4. **Testing**
|
||||
- Verify adequate test coverage
|
||||
- Review test quality and edge cases
|
||||
- Check for missing test scenarios
|
||||
|
||||
5. **Documentation**
|
||||
- Ensure code is properly documented
|
||||
- Verify README updates for new features
|
||||
- Check API documentation accuracy
|
||||
|
||||
Provide detailed feedback using inline comments for specific issues.
|
||||
Use top-level comments for general observations or praise.
|
||||
|
||||
# Tools for comprehensive PR review
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
|
||||
# When track_progress is enabled:
|
||||
# - Creates a tracking comment with progress checkboxes
|
||||
# - Includes all PR context (comments, attachments, images)
|
||||
# - Updates progress as the review proceeds
|
||||
# - Marks as completed when done
|
||||
48
examples/pr-review-filtered-authors.yml
Normal file
48
examples/pr-review-filtered-authors.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Claude Review - Specific Authors
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review-by-author:
|
||||
# Only run for PRs from specific authors
|
||||
if: |
|
||||
github.event.pull_request.user.login == 'developer1' ||
|
||||
github.event.pull_request.user.login == 'developer2' ||
|
||||
github.event.pull_request.user.login == 'external-contributor'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Review PR from Specific Author
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please provide a thorough review of this pull request.
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Since this is from a specific author that requires careful review,
|
||||
please pay extra attention to:
|
||||
- Adherence to project coding standards
|
||||
- Proper error handling
|
||||
- Security best practices
|
||||
- Test coverage
|
||||
- Documentation
|
||||
|
||||
Provide detailed feedback and suggestions for improvement.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*)"
|
||||
49
examples/pr-review-filtered-paths.yml
Normal file
49
examples/pr-review-filtered-paths.yml
Normal file
@@ -0,0 +1,49 @@
|
||||
name: Claude Review - Path Specific
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
# Only run when specific paths are modified
|
||||
- "src/**/*.js"
|
||||
- "src/**/*.ts"
|
||||
- "api/**/*.py"
|
||||
# You can add more specific patterns as needed
|
||||
|
||||
jobs:
|
||||
claude-review-paths:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Claude Code Review
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please review this pull request focusing on the changed files.
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Provide feedback on:
|
||||
- Code quality and adherence to best practices
|
||||
- Potential bugs or edge cases
|
||||
- Performance considerations
|
||||
- Security implications
|
||||
- Suggestions for improvement
|
||||
|
||||
Since this PR touches critical source code paths, please be thorough
|
||||
in your review and provide inline comments where appropriate.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*)"
|
||||
232
package-lock.json
generated
232
package-lock.json
generated
@@ -1,19 +1,18 @@
|
||||
{
|
||||
"name": "claude-pr-action",
|
||||
"name": "@anthropic-ai/claude-code-action",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "claude-pr-action",
|
||||
"name": "@anthropic-ai/claude-code-action",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/sdk": "^0.30.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"zod": "^3.24.4"
|
||||
@@ -212,73 +211,73 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql": {
|
||||
"version": "8.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz",
|
||||
"integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==",
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.2.tgz",
|
||||
"integrity": "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/request": "^9.2.3",
|
||||
"@octokit/types": "^14.0.0",
|
||||
"@octokit/request": "^10.0.4",
|
||||
"@octokit/types": "^15.0.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
|
||||
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz",
|
||||
"integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0",
|
||||
"@octokit/types": "^15.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz",
|
||||
"integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/@octokit/request": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
|
||||
"integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz",
|
||||
"integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^10.1.4",
|
||||
"@octokit/request-error": "^6.1.8",
|
||||
"@octokit/types": "^14.0.0",
|
||||
"fast-content-type-parse": "^2.0.0",
|
||||
"@octokit/endpoint": "^11.0.1",
|
||||
"@octokit/request-error": "^7.0.1",
|
||||
"@octokit/types": "^15.0.0",
|
||||
"fast-content-type-parse": "^3.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/@octokit/request-error": {
|
||||
"version": "6.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
|
||||
"integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz",
|
||||
"integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0"
|
||||
"@octokit/types": "^15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz",
|
||||
"integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
"@octokit/openapi-types": "^26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/graphql/node_modules/universal-user-agent": {
|
||||
@@ -383,176 +382,149 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz",
|
||||
"integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==",
|
||||
"version": "22.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz",
|
||||
"integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^6.1.4",
|
||||
"@octokit/plugin-paginate-rest": "^11.4.2",
|
||||
"@octokit/plugin-request-log": "^5.3.1",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^13.3.0"
|
||||
"@octokit/core": "^7.0.2",
|
||||
"@octokit/plugin-paginate-rest": "^13.0.1",
|
||||
"@octokit/plugin-request-log": "^6.0.0",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^16.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/auth-token": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
|
||||
"integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz",
|
||||
"integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/core": {
|
||||
"version": "6.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
|
||||
"integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.5.tgz",
|
||||
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/auth-token": "^5.0.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/request": "^9.2.3",
|
||||
"@octokit/request-error": "^6.1.8",
|
||||
"@octokit/types": "^14.0.0",
|
||||
"before-after-hook": "^3.0.2",
|
||||
"@octokit/auth-token": "^6.0.0",
|
||||
"@octokit/graphql": "^9.0.2",
|
||||
"@octokit/request": "^10.0.4",
|
||||
"@octokit/request-error": "^7.0.1",
|
||||
"@octokit/types": "^15.0.0",
|
||||
"before-after-hook": "^4.0.0",
|
||||
"universal-user-agent": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/core/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/endpoint": {
|
||||
"version": "10.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
|
||||
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz",
|
||||
"integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0",
|
||||
"@octokit/types": "^15.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/endpoint/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/openapi-types": {
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
|
||||
"integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
|
||||
"version": "26.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz",
|
||||
"integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/plugin-paginate-rest": {
|
||||
"version": "11.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz",
|
||||
"integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==",
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.2.0.tgz",
|
||||
"integrity": "sha512-YuAlyjR8o5QoRSOvMHxSJzPtogkNMgeMv2mpccrvdUGeC3MKyfi/hS+KiFwyH/iRKIKyx+eIMsDjbt3p9r2GYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.10.0"
|
||||
"@octokit/types": "^15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/plugin-request-log": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz",
|
||||
"integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz",
|
||||
"integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/plugin-rest-endpoint-methods": {
|
||||
"version": "13.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz",
|
||||
"integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==",
|
||||
"version": "16.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.1.0.tgz",
|
||||
"integrity": "sha512-nCsyiKoGRnhH5LkH8hJEZb9swpqOcsW+VXv1QoyUNQXJeVODG4+xM6UICEqyqe9XFr6LkL8BIiFCPev8zMDXPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^13.10.0"
|
||||
"@octokit/types": "^15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@octokit/core": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/request": {
|
||||
"version": "9.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
|
||||
"integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
|
||||
"version": "10.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz",
|
||||
"integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/endpoint": "^10.1.4",
|
||||
"@octokit/request-error": "^6.1.8",
|
||||
"@octokit/types": "^14.0.0",
|
||||
"fast-content-type-parse": "^2.0.0",
|
||||
"@octokit/endpoint": "^11.0.1",
|
||||
"@octokit/request-error": "^7.0.1",
|
||||
"@octokit/types": "^15.0.0",
|
||||
"fast-content-type-parse": "^3.0.0",
|
||||
"universal-user-agent": "^7.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/request-error": {
|
||||
"version": "6.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
|
||||
"integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz",
|
||||
"integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/types": "^14.0.0"
|
||||
"@octokit/types": "^15.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
"node": ">= 20"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/request-error/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/types": {
|
||||
"version": "15.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.0.tgz",
|
||||
"integrity": "sha512-8o6yDfmoGJUIeR9OfYU0/TUJTnMPG2r68+1yEdUeG2Fdqpj8Qetg0ziKIgcBm0RW/j29H41WP37CYCEhp6GoHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/@octokit/request/node_modules/@octokit/types": {
|
||||
"version": "14.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
|
||||
"integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@octokit/openapi-types": "^25.1.0"
|
||||
"@octokit/openapi-types": "^26.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/before-after-hook": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
|
||||
"integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz",
|
||||
"integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@octokit/rest/node_modules/universal-user-agent": {
|
||||
@@ -1043,9 +1015,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fast-content-type-parse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
|
||||
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz",
|
||||
"integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/sdk": "^0.30.0",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@octokit/webhooks-types": "^7.6.1",
|
||||
"node-fetch": "^3.3.2",
|
||||
"zod": "^3.24.4"
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { homedir } from "os";
|
||||
|
||||
interface OAuthCredentials {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
interface ClaudeCredentialsInput {
|
||||
claudeAiOauth: {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
expiresAt: number;
|
||||
scopes: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export async function setupOAuthCredentials(credentialsJson: string) {
|
||||
try {
|
||||
// Parse the credentials JSON
|
||||
const parsedCredentials: ClaudeCredentialsInput =
|
||||
JSON.parse(credentialsJson);
|
||||
|
||||
if (!parsedCredentials.claudeAiOauth) {
|
||||
throw new Error("Invalid credentials format: missing claudeAiOauth");
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken, expiresAt } =
|
||||
parsedCredentials.claudeAiOauth;
|
||||
|
||||
if (!accessToken || !refreshToken || !expiresAt) {
|
||||
throw new Error(
|
||||
"Invalid credentials format: missing required OAuth fields",
|
||||
);
|
||||
}
|
||||
|
||||
const claudeDir = join(homedir(), ".claude");
|
||||
const credentialsPath = join(claudeDir, ".credentials.json");
|
||||
|
||||
// Create the .claude directory if it doesn't exist
|
||||
await mkdir(claudeDir, { recursive: true });
|
||||
|
||||
// Create the credentials JSON structure
|
||||
const credentialsData = {
|
||||
claudeAiOauth: {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
expiresAt,
|
||||
scopes: ["user:inference", "user:profile"],
|
||||
},
|
||||
};
|
||||
|
||||
// Write the credentials file
|
||||
await writeFile(credentialsPath, JSON.stringify(credentialsData, null, 2));
|
||||
|
||||
console.log(`OAuth credentials written to ${credentialsPath}`);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to setup OAuth credentials: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from "../github/context";
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
import type { CommonFields, PreparedContext, EventData } from "./types";
|
||||
import { GITEA_SERVER_URL } from "../github/api/config";
|
||||
import type { Mode, ModeContext } from "../modes/types";
|
||||
export type { CommonFields, PreparedContext } from "./types";
|
||||
|
||||
const BASE_ALLOWED_TOOLS = [
|
||||
@@ -62,37 +62,74 @@ const BASE_ALLOWED_TOOLS = [
|
||||
];
|
||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||
|
||||
export function buildAllowedToolsString(
|
||||
customAllowedTools?: string[],
|
||||
): string {
|
||||
let baseTools = [...BASE_ALLOWED_TOOLS];
|
||||
const ACTIONS_ALLOWED_TOOLS = [
|
||||
"mcp__github_actions__get_ci_status",
|
||||
"mcp__github_actions__get_workflow_run_details",
|
||||
"mcp__github_actions__download_job_log",
|
||||
];
|
||||
|
||||
let allAllowedTools = baseTools.join(",");
|
||||
if (customAllowedTools && customAllowedTools.length > 0) {
|
||||
allAllowedTools = `${allAllowedTools},${customAllowedTools.join(",")}`;
|
||||
const COMMIT_SIGNING_TOOLS = [
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__delete_files",
|
||||
"mcp__github_file_ops__update_claude_comment",
|
||||
];
|
||||
|
||||
function normalizeToolList(input?: string | string[]): string[] {
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
return allAllowedTools;
|
||||
|
||||
const tools = Array.isArray(input) ? input : input.split(",");
|
||||
return tools
|
||||
.map((tool) => tool.trim())
|
||||
.filter((tool): tool is string => tool.length > 0);
|
||||
}
|
||||
|
||||
export function buildAllowedToolsString(
|
||||
customAllowedTools?: string | string[],
|
||||
includeActionsReadTools = false,
|
||||
useCommitSigning = false,
|
||||
): string {
|
||||
const allowedTools = new Set<string>(BASE_ALLOWED_TOOLS);
|
||||
|
||||
if (includeActionsReadTools) {
|
||||
for (const tool of ACTIONS_ALLOWED_TOOLS) {
|
||||
allowedTools.add(tool);
|
||||
}
|
||||
}
|
||||
|
||||
if (useCommitSigning) {
|
||||
for (const tool of COMMIT_SIGNING_TOOLS) {
|
||||
allowedTools.add(tool);
|
||||
}
|
||||
}
|
||||
|
||||
for (const tool of normalizeToolList(customAllowedTools)) {
|
||||
allowedTools.add(tool);
|
||||
}
|
||||
|
||||
return Array.from(allowedTools).join(",");
|
||||
}
|
||||
|
||||
export function buildDisallowedToolsString(
|
||||
customDisallowedTools?: string[],
|
||||
allowedTools?: string[],
|
||||
customDisallowedTools?: string | string[],
|
||||
allowedTools?: string | string[],
|
||||
): string {
|
||||
let disallowedTools = [...DISALLOWED_TOOLS];
|
||||
|
||||
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
|
||||
if (allowedTools && allowedTools.length > 0) {
|
||||
disallowedTools = disallowedTools.filter(
|
||||
(tool) => !allowedTools.includes(tool),
|
||||
);
|
||||
const allowedList = normalizeToolList(allowedTools);
|
||||
if (allowedList.length > 0) {
|
||||
disallowedTools = disallowedTools.filter((tool) => !allowedList.includes(tool));
|
||||
}
|
||||
|
||||
let allDisallowedTools = disallowedTools.join(",");
|
||||
if (customDisallowedTools && customDisallowedTools.length > 0) {
|
||||
const customList = normalizeToolList(customDisallowedTools);
|
||||
if (customList.length > 0) {
|
||||
if (allDisallowedTools) {
|
||||
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools.join(",")}`;
|
||||
allDisallowedTools = `${allDisallowedTools},${customList.join(",")}`;
|
||||
} else {
|
||||
allDisallowedTools = customDisallowedTools.join(",");
|
||||
allDisallowedTools = customList.join(",");
|
||||
}
|
||||
}
|
||||
return allDisallowedTools;
|
||||
@@ -266,7 +303,6 @@ export function prepareContext(
|
||||
if (!baseBranch) {
|
||||
throw new Error("BASE_BRANCH is required for issues event");
|
||||
}
|
||||
|
||||
if (eventAction === "assigned") {
|
||||
if (!assigneeTrigger && !directPrompt) {
|
||||
throw new Error(
|
||||
@@ -279,7 +315,7 @@ export function prepareContext(
|
||||
isPR: false,
|
||||
issueNumber,
|
||||
baseBranch,
|
||||
assigneeTrigger,
|
||||
...(assigneeTrigger && { assigneeTrigger }),
|
||||
...(claudeBranch && { claudeBranch }),
|
||||
};
|
||||
} else if (eventAction === "labeled") {
|
||||
@@ -292,7 +328,7 @@ export function prepareContext(
|
||||
isPR: false,
|
||||
issueNumber,
|
||||
baseBranch,
|
||||
claudeBranch,
|
||||
...(claudeBranch && { claudeBranch }),
|
||||
labelTrigger,
|
||||
};
|
||||
} else if (eventAction === "opened") {
|
||||
@@ -302,7 +338,7 @@ export function prepareContext(
|
||||
isPR: false,
|
||||
issueNumber,
|
||||
baseBranch,
|
||||
...(claudeBranch && { claudeBranch }),
|
||||
claudeBranch,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Unsupported issue action: ${eventAction}`);
|
||||
@@ -393,64 +429,6 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
||||
}
|
||||
}
|
||||
|
||||
function getCommitInstructions(
|
||||
eventData: EventData,
|
||||
githubData: FetchDataResult,
|
||||
context: PreparedContext,
|
||||
useCommitSigning: boolean,
|
||||
): string {
|
||||
const coAuthorLine =
|
||||
(githubData.triggerDisplayName ?? context.triggerUsername !== "Unknown")
|
||||
? `Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>`
|
||||
: "";
|
||||
|
||||
if (useCommitSigning) {
|
||||
if (eventData.isPR && !eventData.claudeBranch) {
|
||||
return `
|
||||
- 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).
|
||||
- When pushing changes with this tool and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
||||
- Use: "${coAuthorLine}"`;
|
||||
} else {
|
||||
return `
|
||||
- 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)
|
||||
- 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.
|
||||
- Use: "${coAuthorLine}"`;
|
||||
}
|
||||
} else {
|
||||
// Non-signing instructions
|
||||
if (eventData.isPR && !eventData.claudeBranch) {
|
||||
return `
|
||||
- Use git commands via the Bash tool to commit and push your changes:
|
||||
- Stage files: Bash(git add <files>)
|
||||
- Commit with a descriptive message: Bash(git commit -m "<message>")
|
||||
${
|
||||
coAuthorLine
|
||||
? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer:
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin HEAD)`;
|
||||
} else {
|
||||
const branchName = eventData.claudeBranch || eventData.baseBranch;
|
||||
return `
|
||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
||||
- Use git commands via the Bash tool to commit and push your changes:
|
||||
- Stage files: Bash(git add <files>)
|
||||
- Commit with a descriptive message: Bash(git commit -m "<message>")
|
||||
${
|
||||
coAuthorLine
|
||||
? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer:
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin ${branchName})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function substitutePromptVariables(
|
||||
template: string,
|
||||
context: PreparedContext,
|
||||
@@ -517,7 +495,7 @@ function substitutePromptVariables(
|
||||
export function generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
useCommitSigning = false,
|
||||
): string {
|
||||
if (context.overridePrompt) {
|
||||
return substitutePromptVariables(
|
||||
@@ -527,6 +505,8 @@ export function generatePrompt(
|
||||
);
|
||||
}
|
||||
|
||||
const triggerDisplayName = context.triggerUsername ?? "Unknown";
|
||||
|
||||
const {
|
||||
contextData,
|
||||
comments,
|
||||
@@ -594,7 +574,7 @@ ${
|
||||
}
|
||||
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
|
||||
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
|
||||
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
|
||||
<trigger_display_name>${triggerDisplayName}</trigger_display_name>
|
||||
<trigger_phrase>${context.triggerPhrase}</trigger_phrase>
|
||||
${
|
||||
(eventData.eventName === "issue_comment" ||
|
||||
|
||||
@@ -66,7 +66,7 @@ type IssueAssignedEvent = {
|
||||
issueNumber: string;
|
||||
baseBranch: string;
|
||||
claudeBranch?: string;
|
||||
assigneeTrigger: string;
|
||||
assigneeTrigger?: string;
|
||||
};
|
||||
|
||||
type IssueLabeledEvent = {
|
||||
|
||||
@@ -18,29 +18,18 @@ import { createPrompt } from "../create-prompt";
|
||||
import { createClient } from "../github/api/client";
|
||||
import { fetchGitHubData } from "../github/data/fetcher";
|
||||
import { parseGitHubContext } from "../github/context";
|
||||
import { setupOAuthCredentials } from "../claude/oauth-setup";
|
||||
import { getMode } from "../modes/registry";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
// Step 1: Setup OAuth credentials if provided
|
||||
const claudeCredentials = process.env.CLAUDE_CREDENTIALS;
|
||||
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
if (claudeCredentials && anthropicApiKey === "use-oauth") {
|
||||
await setupOAuthCredentials(claudeCredentials);
|
||||
console.log(
|
||||
"OAuth credentials configured for Claude AI Max subscription",
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Setup GitHub token
|
||||
// Step 1: Setup GitHub token
|
||||
const githubToken = await setupGitHubToken();
|
||||
const client = createClient(githubToken);
|
||||
|
||||
// Step 3: Parse GitHub context (once for all operations)
|
||||
// Step 2: Parse GitHub context (once for all operations)
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// Step 4: Check write permissions
|
||||
// Step 3: Check write permissions
|
||||
const hasWritePermissions = await checkWritePermissions(
|
||||
client.api,
|
||||
context,
|
||||
@@ -51,7 +40,7 @@ async function run() {
|
||||
);
|
||||
}
|
||||
|
||||
// Step 5: Check trigger conditions
|
||||
// Step 4: Check trigger conditions
|
||||
const containsTrigger = await checkTriggerAction(context);
|
||||
|
||||
// Set outputs that are always needed
|
||||
@@ -63,14 +52,19 @@ async function run() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 6: Check if actor is human
|
||||
// Step 5: Check if actor is human
|
||||
await checkHumanActor(client.api, context);
|
||||
|
||||
// Step 7: Create initial tracking comment
|
||||
const commentId = await createInitialComment(client.api, context);
|
||||
core.setOutput("claude_comment_id", commentId.toString());
|
||||
const mode = getMode(context.inputs.mode);
|
||||
|
||||
// Step 8: Fetch GitHub data (once for both branch setup and prompt creation)
|
||||
// Step 6: Create initial tracking comment (if required by mode)
|
||||
let commentId: number | undefined;
|
||||
if (mode.shouldCreateTrackingComment()) {
|
||||
commentId = await createInitialComment(client.api, context);
|
||||
core.setOutput("claude_comment_id", commentId!.toString());
|
||||
}
|
||||
|
||||
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
|
||||
const githubData = await fetchGitHubData({
|
||||
client: client,
|
||||
repository: `${context.repository.owner}/${context.repository.repo}`,
|
||||
@@ -78,15 +72,15 @@ async function run() {
|
||||
isPR: context.isPR,
|
||||
});
|
||||
|
||||
// Step 9: Setup branch
|
||||
// Step 8: Setup branch
|
||||
const branchInfo = await setupBranch(client, githubData, context);
|
||||
core.setOutput("BASE_BRANCH", branchInfo.baseBranch);
|
||||
if (branchInfo.claudeBranch) {
|
||||
core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch);
|
||||
}
|
||||
|
||||
// Step 10: Update initial comment with branch link (only if a claude branch was created)
|
||||
if (branchInfo.claudeBranch) {
|
||||
// Step 9: Update initial comment with branch link (only if a claude branch was created)
|
||||
if (commentId && branchInfo.claudeBranch) {
|
||||
await updateTrackingComment(
|
||||
client,
|
||||
context,
|
||||
@@ -95,22 +89,25 @@ async function run() {
|
||||
);
|
||||
}
|
||||
|
||||
// Step 11: Create prompt file
|
||||
await createPrompt(
|
||||
// Step 10: Create prompt file
|
||||
const modeContext = mode.prepareContext(context, {
|
||||
commentId,
|
||||
branchInfo.baseBranch,
|
||||
branchInfo.claudeBranch,
|
||||
githubData,
|
||||
context,
|
||||
);
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
claudeBranch: branchInfo.claudeBranch,
|
||||
});
|
||||
|
||||
// Step 12: Get MCP configuration
|
||||
const mcpConfig = await prepareMcpConfig(
|
||||
await createPrompt(mode, modeContext, githubData, context);
|
||||
|
||||
// Step 11: Get MCP configuration
|
||||
const mcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
context.repository.owner,
|
||||
context.repository.repo,
|
||||
branchInfo.currentBranch,
|
||||
);
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: branchInfo.currentBranch,
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
allowedTools: context.inputs.allowedTools,
|
||||
context,
|
||||
});
|
||||
core.setOutput("mcp_config", mcpConfig);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
@@ -7,8 +7,29 @@ function deriveApiUrl(serverUrl: string): string {
|
||||
return `${serverUrl}/api/v1`;
|
||||
}
|
||||
|
||||
export const GITEA_SERVER_URL =
|
||||
process.env.GITHUB_SERVER_URL || "https://github.com";
|
||||
// Get the appropriate server URL, prioritizing GITEA_SERVER_URL for custom Gitea instances
|
||||
function getServerUrl(): string {
|
||||
// First check for GITEA_SERVER_URL (can be set by user)
|
||||
const giteaServerUrl = process.env.GITEA_SERVER_URL;
|
||||
if (giteaServerUrl && giteaServerUrl !== "") {
|
||||
return giteaServerUrl;
|
||||
}
|
||||
|
||||
// Fall back to GITHUB_SERVER_URL (set by Gitea/GitHub Actions environment)
|
||||
const githubServerUrl = process.env.GITHUB_SERVER_URL;
|
||||
if (githubServerUrl && githubServerUrl !== "") {
|
||||
return githubServerUrl;
|
||||
}
|
||||
|
||||
// Default fallback
|
||||
return "https://github.com";
|
||||
}
|
||||
|
||||
export const GITEA_SERVER_URL = getServerUrl();
|
||||
|
||||
export const GITEA_API_URL =
|
||||
process.env.GITEA_API_URL || deriveApiUrl(GITEA_SERVER_URL);
|
||||
|
||||
// Backwards-compatible aliases for legacy GitHub-specific naming
|
||||
export const GITHUB_SERVER_URL = GITEA_SERVER_URL;
|
||||
export const GITHUB_API_URL = GITEA_API_URL;
|
||||
|
||||
@@ -52,7 +52,7 @@ export async function setupBranch(
|
||||
);
|
||||
|
||||
// Check out the base branch and let Claude create branches as needed
|
||||
await $`git fetch origin ${sourceBranch}`;
|
||||
await $`git fetch origin --depth=1 ${sourceBranch}`;
|
||||
await $`git checkout ${sourceBranch}`;
|
||||
await $`git pull origin ${sourceBranch}`;
|
||||
|
||||
@@ -99,7 +99,7 @@ export async function setupBranch(
|
||||
|
||||
// Ensure we have the latest version of the source branch
|
||||
console.log(`Fetching latest ${sourceBranch}...`);
|
||||
await $`git fetch origin ${sourceBranch}`;
|
||||
await $`git fetch origin --depth=1 ${sourceBranch}`;
|
||||
|
||||
// Checkout the source branch
|
||||
console.log(`Checking out ${sourceBranch}...`);
|
||||
|
||||
@@ -141,14 +141,16 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
|
||||
if (branchLink) {
|
||||
// Extract the branch URL from the link
|
||||
const urlMatch = branchLink.match(/\((https:\/\/.*)\)/);
|
||||
const urlMatch = branchLink.match(/\((https?:\/\/[^\)]+)\)/);
|
||||
if (urlMatch && urlMatch[1]) {
|
||||
branchUrl = urlMatch[1];
|
||||
}
|
||||
|
||||
// Extract branch name from link if not provided
|
||||
if (!finalBranchName) {
|
||||
const branchNameMatch = branchLink.match(/tree\/([^"'\)]+)/);
|
||||
const branchNameMatch = branchLink.match(
|
||||
/(?:tree|src\/branch)\/([^"'\)\s]+)/,
|
||||
);
|
||||
if (branchNameMatch) {
|
||||
finalBranchName = branchNameMatch[1];
|
||||
}
|
||||
@@ -157,10 +159,17 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
||||
|
||||
// If we don't have a URL yet but have a branch name, construct it
|
||||
if (!branchUrl && finalBranchName) {
|
||||
// Extract owner/repo from jobUrl
|
||||
const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//);
|
||||
if (repoMatch) {
|
||||
branchUrl = `${GITEA_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/src/branch/${finalBranchName}`;
|
||||
try {
|
||||
const parsedJobUrl = new URL(jobUrl);
|
||||
const segments = parsedJobUrl.pathname
|
||||
.split("/")
|
||||
.filter((segment) => segment);
|
||||
const [owner, repo] = segments;
|
||||
if (owner && repo) {
|
||||
branchUrl = `${GITEA_SERVER_URL}/${owner}/${repo}/src/branch/${finalBranchName}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Failed to derive branch URL from job URL: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { GITEA_SERVER_URL } from "../../api/config";
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
function getSpinnerHtml(): string {
|
||||
return `<img src="https://raw.githubusercontent.com/markwylde/claude-code-gitea-action/refs/heads/gitea/assets/spinner.gif" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />`;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { $ } from "bun";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import { GITHUB_SERVER_URL } from "../api/config";
|
||||
import { GITEA_SERVER_URL } from "../api/config";
|
||||
|
||||
type GitUser = {
|
||||
login: string;
|
||||
@@ -22,7 +22,7 @@ export async function configureGitAuth(
|
||||
console.log("Configuring git authentication for non-signing mode");
|
||||
|
||||
// Determine the noreply email domain based on GITHUB_SERVER_URL
|
||||
const serverUrl = new URL(GITHUB_SERVER_URL);
|
||||
const serverUrl = new URL(GITEA_SERVER_URL);
|
||||
const noreplyDomain =
|
||||
serverUrl.hostname === "github.com"
|
||||
? "users.noreply.github.com"
|
||||
@@ -46,7 +46,7 @@ export async function configureGitAuth(
|
||||
// Remove the authorization header that actions/checkout sets
|
||||
console.log("Removing existing git authentication headers...");
|
||||
try {
|
||||
await $`git config --unset-all http.${GITHUB_SERVER_URL}/.extraheader`;
|
||||
await $`git config --unset-all http.${GITEA_SERVER_URL}/.extraheader`;
|
||||
console.log("✓ Removed existing authentication headers");
|
||||
} catch (e) {
|
||||
console.log("No existing authentication headers to remove");
|
||||
|
||||
@@ -85,7 +85,7 @@ export async function branchHasChanges(
|
||||
*/
|
||||
export async function fetchBranch(branchName: string): Promise<boolean> {
|
||||
try {
|
||||
await $`git fetch origin ${branchName}`;
|
||||
await $`git fetch origin --depth=1 ${branchName}`;
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
isPullRequestReviewEvent,
|
||||
isPullRequestReviewCommentEvent,
|
||||
} from "../context";
|
||||
import type { IssuesLabeledEvent } from "@octokit/webhooks-types";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
|
||||
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
||||
@@ -41,6 +42,26 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for issue label trigger
|
||||
if (isIssuesEvent(context) && context.eventAction === "labeled") {
|
||||
const triggerLabel = context.inputs.labelTrigger?.trim();
|
||||
const appliedLabel = (context.payload as IssuesLabeledEvent).label?.name
|
||||
?.trim();
|
||||
|
||||
console.log(
|
||||
`Checking label trigger: expected='${triggerLabel}', applied='${appliedLabel}'`,
|
||||
);
|
||||
|
||||
if (
|
||||
triggerLabel &&
|
||||
appliedLabel &&
|
||||
triggerLabel.localeCompare(appliedLabel, undefined, { sensitivity: "accent" }) === 0
|
||||
) {
|
||||
console.log(`Issue labeled with trigger label '${triggerLabel}'`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for issue body and title trigger on issue creation
|
||||
if (isIssuesEvent(context) && context.eventAction === "opened") {
|
||||
const issueBody = context.payload.issue.body || "";
|
||||
@@ -91,6 +112,20 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if trigger user is in requested reviewers (treat same as mention in text)
|
||||
const triggerUser = triggerPhrase.replace(/^@/, "");
|
||||
const requestedReviewers = context.payload.pull_request.requested_reviewers || [];
|
||||
const isReviewerRequested = requestedReviewers.some(reviewer =>
|
||||
'login' in reviewer && reviewer.login === triggerUser
|
||||
);
|
||||
|
||||
if (isReviewerRequested) {
|
||||
console.log(
|
||||
`Pull request has '${triggerUser}' as requested reviewer (treating as trigger)`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for pull request review body trigger
|
||||
|
||||
@@ -942,7 +942,7 @@ server.tool(
|
||||
endpoint += `?style=${style}`;
|
||||
}
|
||||
|
||||
const result = await giteaRequest(endpoint, "POST");
|
||||
await giteaRequest(endpoint, "POST");
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import * as core from "@actions/core";
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
|
||||
export async function prepareMcpConfig(
|
||||
githubToken: string,
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
): Promise<string> {
|
||||
export type PrepareMcpConfigOptions = {
|
||||
githubToken: string;
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch: string;
|
||||
baseBranch?: string;
|
||||
allowedTools?: string[];
|
||||
context?: ParsedGitHubContext;
|
||||
overrideConfig?: string;
|
||||
additionalMcpConfig?: string;
|
||||
};
|
||||
|
||||
export async function prepareMcpConfig({
|
||||
githubToken,
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
}: PrepareMcpConfigOptions): Promise<string> {
|
||||
console.log("[MCP-INSTALL] Preparing MCP configuration...");
|
||||
console.log(`[MCP-INSTALL] Owner: ${owner}`);
|
||||
console.log(`[MCP-INSTALL] Repo: ${repo}`);
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { readFile, writeFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { execSync } from "child_process";
|
||||
|
||||
// Get repository information from environment variables
|
||||
|
||||
@@ -1,50 +1,51 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import { checkAndDeleteEmptyBranch } from "../src/github/operations/branch-cleanup";
|
||||
import type { Octokits } from "../src/github/api/client";
|
||||
import type { GitHubClient } from "../src/github/api/client";
|
||||
import { GITEA_SERVER_URL } from "../src/github/api/config";
|
||||
|
||||
describe("checkAndDeleteEmptyBranch", () => {
|
||||
let consoleLogSpy: any;
|
||||
let consoleErrorSpy: any;
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// Spy on console methods
|
||||
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
||||
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
||||
delete process.env.GITEA_API_URL; // ensure GitHub mode for predictable behaviour
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
const createMockOctokit = (
|
||||
compareResponse?: any,
|
||||
deleteRefError?: Error,
|
||||
): Octokits => {
|
||||
const createMockClient = (
|
||||
options: { branchSha?: string; baseSha?: string; error?: Error } = {},
|
||||
): GitHubClient => {
|
||||
const { branchSha = "branch-sha", baseSha = "base-sha", error } = options;
|
||||
return {
|
||||
rest: {
|
||||
repos: {
|
||||
compareCommitsWithBasehead: async () => ({
|
||||
data: compareResponse || { total_commits: 0 },
|
||||
}),
|
||||
},
|
||||
git: {
|
||||
deleteRef: async () => {
|
||||
if (deleteRefError) {
|
||||
throw deleteRefError;
|
||||
}
|
||||
return { data: {} };
|
||||
},
|
||||
api: {
|
||||
getBranch: async (_owner: string, _repo: string, branch: string) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
data: {
|
||||
commit: {
|
||||
sha: branch.includes("claude/") ? branchSha : baseSha,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
} as any as Octokits;
|
||||
} as unknown as GitHubClient;
|
||||
};
|
||||
|
||||
test("should return no branch link and not delete when branch is undefined", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
test("returns defaults when no claude branch provided", async () => {
|
||||
const client = createMockClient();
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
mockOctokit,
|
||||
client,
|
||||
"owner",
|
||||
"repo",
|
||||
undefined,
|
||||
@@ -56,94 +57,65 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should delete branch and return no link when branch has no commits", async () => {
|
||||
const mockOctokit = createMockOctokit({ total_commits: 0 });
|
||||
test("marks branch for deletion when SHAs match", async () => {
|
||||
const client = createMockClient({ branchSha: "same", baseSha: "same" });
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
mockOctokit,
|
||||
client,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"claude/issue-123",
|
||||
"main",
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(true);
|
||||
expect(result.branchLink).toBe("");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Branch claude/issue-123-20240101_123456 has no commits from Claude, will delete it",
|
||||
"Branch claude/issue-123 has same SHA as base, marking for deletion",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"✅ Deleted empty branch: claude/issue-123-20240101_123456",
|
||||
"Skipping branch deletion - not reliably supported across all Git platforms: claude/issue-123",
|
||||
);
|
||||
});
|
||||
|
||||
test("should not delete branch and return link when branch has commits", async () => {
|
||||
const mockOctokit = createMockOctokit({ total_commits: 3 });
|
||||
test("returns branch link when branch has commits", async () => {
|
||||
const client = createMockClient({ branchSha: "feature", baseSha: "main" });
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
mockOctokit,
|
||||
client,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"claude/issue-123",
|
||||
"main",
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(false);
|
||||
expect(result.branchLink).toBe(
|
||||
`\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123-20240101_123456)`,
|
||||
`\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123)`,
|
||||
);
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("has no commits"),
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Branch claude/issue-123 appears to have commits (different SHA from base)",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle branch comparison errors gracefully", async () => {
|
||||
const mockOctokit = {
|
||||
rest: {
|
||||
repos: {
|
||||
compareCommitsWithBasehead: async () => {
|
||||
throw new Error("API error");
|
||||
},
|
||||
},
|
||||
git: {
|
||||
deleteRef: async () => ({ data: {} }),
|
||||
},
|
||||
},
|
||||
} as any as Octokits;
|
||||
|
||||
test("falls back to branch link when API call fails", async () => {
|
||||
const client = createMockClient({ error: Object.assign(new Error("boom"), { status: 500 }) });
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
mockOctokit,
|
||||
client,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"claude/issue-123",
|
||||
"main",
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(false);
|
||||
expect(result.branchLink).toBe(
|
||||
`\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123-20240101_123456)`,
|
||||
`\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123)`,
|
||||
);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Error checking for commits on Claude branch:",
|
||||
"Error checking branch:",
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle branch deletion errors gracefully", async () => {
|
||||
const deleteError = new Error("Delete failed");
|
||||
const mockOctokit = createMockOctokit({ total_commits: 0 }, deleteError);
|
||||
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"main",
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(true);
|
||||
expect(result.branchLink).toBe("");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to delete branch claude/issue-123-20240101_123456:",
|
||||
deleteError,
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Assuming branch exists due to non-404 error",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
import { updateCommentBody } from "../src/github/operations/comment-logic";
|
||||
|
||||
describe("updateCommentBody", () => {
|
||||
const GITEA_SERVER_URL = "https://gitea.example.com";
|
||||
const JOB_URL = `${GITEA_SERVER_URL}/owner/repo/actions/runs/123`;
|
||||
const BRANCH_BASE_URL = `${GITEA_SERVER_URL}/owner/repo/src/branch`;
|
||||
|
||||
let originalEnv: NodeJS.ProcessEnv;
|
||||
|
||||
beforeEach(() => {
|
||||
originalEnv = { ...process.env };
|
||||
process.env.GITEA_SERVER_URL = GITEA_SERVER_URL;
|
||||
process.env.GITEA_API_URL = `${GITEA_SERVER_URL}/api/v1`;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
const baseInput = {
|
||||
currentBody: "Initial comment body",
|
||||
actionFailed: false,
|
||||
executionDetails: null,
|
||||
jobUrl: "https://github.com/owner/repo/actions/runs/123",
|
||||
jobUrl: JOB_URL,
|
||||
branchName: undefined,
|
||||
triggerUsername: undefined,
|
||||
};
|
||||
@@ -105,20 +121,19 @@ describe("updateCommentBody", () => {
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain(
|
||||
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/src/branch/claude/issue-123-20240101_120000)",
|
||||
`• [\`claude/issue-123-20240101_120000\`](${BRANCH_BASE_URL}/claude/issue-123-20240101_120000)`,
|
||||
);
|
||||
});
|
||||
|
||||
it("extracts branch name from branchLink if branchName not provided", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
branchLink:
|
||||
"\n[View branch](https://github.com/owner/repo/src/branch/branch-name)",
|
||||
branchLink: `\n[View branch](${BRANCH_BASE_URL}/branch-name)`,
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain(
|
||||
"• [`branch-name`](https://github.com/owner/repo/src/branch/branch-name)",
|
||||
`• [\`branch-name\`](${BRANCH_BASE_URL}/branch-name)`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -126,13 +141,13 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody:
|
||||
"Some comment with [View branch](https://github.com/owner/repo/src/branch/branch-name)",
|
||||
`Some comment with [View branch](${BRANCH_BASE_URL}/branch-name)` ,
|
||||
branchName: "new-branch-name",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain(
|
||||
"• [`new-branch-name`](https://github.com/owner/repo/src/branch/new-branch-name)",
|
||||
`• [\`new-branch-name\`](${BRANCH_BASE_URL}/new-branch-name)`,
|
||||
);
|
||||
expect(result).not.toContain("View branch");
|
||||
});
|
||||
@@ -142,12 +157,12 @@ describe("updateCommentBody", () => {
|
||||
it("adds PR link to header when provided", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url)",
|
||||
prLink: "\n[Create a PR](https://gitea.example.com/owner/repo/pr-url)",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/pr-url)",
|
||||
"• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -155,12 +170,12 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody:
|
||||
"Some comment with [Create a PR](https://github.com/owner/repo/pr-url)",
|
||||
"Some comment with [Create a PR](https://gitea.example.com/owner/repo/pr-url)",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/pr-url)",
|
||||
"• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url)",
|
||||
);
|
||||
// Original Create a PR link is removed from body
|
||||
expect(result).not.toContain("[Create a PR]");
|
||||
@@ -170,21 +185,21 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody:
|
||||
"Some comment with [Create a PR](https://github.com/owner/repo/pr-url-from-body)",
|
||||
"Some comment with [Create a PR](https://gitea.example.com/owner/repo/pr-url-from-body)",
|
||||
prLink:
|
||||
"\n[Create a PR](https://github.com/owner/repo/pr-url-provided)",
|
||||
"\n[Create a PR](https://gitea.example.com/owner/repo/pr-url-provided)",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
// Prefers the link found in content over the provided one
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/pr-url-from-body)",
|
||||
"• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url-from-body)",
|
||||
);
|
||||
});
|
||||
|
||||
it("handles complex PR URLs with encoded characters", () => {
|
||||
const complexUrl =
|
||||
"https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20important%20bug%20fix&body=Fixes%20%23123%0A%0A%23%23%20Description%0AThis%20PR%20fixes%20an%20important%20bug%20that%20was%20causing%20issues%20with%20the%20application.%0A%0AGenerated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
|
||||
"https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20important%20bug%20fix&body=Fixes%20%23123%0A%0A%23%23%20Description%0AThis%20PR%20fixes%20an%20important%20bug%20that%20was%20causing%20issues%20with%20the%20application.%0A%0AGenerated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: `Some comment with [Create a PR](${complexUrl})`,
|
||||
@@ -198,7 +213,7 @@ describe("updateCommentBody", () => {
|
||||
|
||||
it("handles PR links with encoded URLs containing parentheses", () => {
|
||||
const complexUrl =
|
||||
"https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20bug%20fix&body=Generated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
|
||||
"https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20bug%20fix&body=Generated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: `This PR was created.\n\n[Create a PR](${complexUrl})`,
|
||||
@@ -217,9 +232,9 @@ describe("updateCommentBody", () => {
|
||||
|
||||
it("handles PR links with unencoded spaces and special characters", () => {
|
||||
const unEncodedUrl =
|
||||
"https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix: update welcome message&body=Generated with [Claude Code](https://claude.ai/code)";
|
||||
"https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix: update welcome message&body=Generated with [Claude Code](https://claude.ai/code)";
|
||||
const expectedEncodedUrl =
|
||||
"https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A+update+welcome+message&body=Generated+with+%5BClaude+Code%5D%28https%3A%2F%2Fclaude.ai%2Fcode%29";
|
||||
"https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A+update+welcome+message&body=Generated+with+%5BClaude+Code%5D%28https%3A%2F%2Fclaude.ai%2Fcode%29";
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: `This PR was created.\n\n[Create a PR](${unEncodedUrl})`,
|
||||
@@ -235,7 +250,7 @@ describe("updateCommentBody", () => {
|
||||
|
||||
it("falls back to prLink parameter when PR link in content cannot be encoded", () => {
|
||||
const invalidUrl = "not-a-valid-url-at-all";
|
||||
const fallbackPrUrl = "https://github.com/owner/repo/pull/123";
|
||||
const fallbackPrUrl = "https://gitea.example.com/owner/repo/pull/123";
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: `This PR was created.\n\n[Create a PR](${invalidUrl})`,
|
||||
@@ -317,7 +332,7 @@ describe("updateCommentBody", () => {
|
||||
"Claude Code is working…\n\n### Todo List:\n- [x] Read README.md\n- [x] Add disclaimer",
|
||||
actionFailed: false,
|
||||
branchName: "claude-branch-123",
|
||||
prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url)",
|
||||
prLink: "\n[Create a PR](https://gitea.example.com/owner/repo/pr-url)",
|
||||
executionDetails: {
|
||||
cost_usd: 0.01,
|
||||
duration_ms: 65000, // 1 minute 5 seconds
|
||||
@@ -333,7 +348,7 @@ describe("updateCommentBody", () => {
|
||||
);
|
||||
expect(result).toContain("—— [View job]");
|
||||
expect(result).toContain(
|
||||
"• [`claude-branch-123`](https://github.com/owner/repo/src/branch/claude-branch-123)",
|
||||
`• [\`claude-branch-123\`](${BRANCH_BASE_URL}/claude-branch-123)`,
|
||||
);
|
||||
expect(result).toContain("• [Create PR ➔]");
|
||||
|
||||
@@ -358,7 +373,7 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody:
|
||||
"Claude Code is working…\n\nI've made changes.\n[Create a PR](https://github.com/owner/repo/pr-url-in-content)\n\n@john-doe",
|
||||
"Claude Code is working…\n\nI've made changes.\n[Create a PR](https://gitea.example.com/owner/repo/pr-url-in-content)\n\n@john-doe",
|
||||
branchName: "feature-branch",
|
||||
triggerUsername: "john-doe",
|
||||
};
|
||||
@@ -367,7 +382,7 @@ describe("updateCommentBody", () => {
|
||||
|
||||
// PR link should be moved to header
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/pr-url-in-content)",
|
||||
"• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url-in-content)",
|
||||
);
|
||||
// Original link should be removed from body
|
||||
expect(result).not.toContain("[Create a PR]");
|
||||
@@ -383,7 +398,7 @@ describe("updateCommentBody", () => {
|
||||
currentBody: "Claude Code is working… <img src='spinner.gif' />",
|
||||
branchName: "claude/pr-456-20240101_120000",
|
||||
prLink:
|
||||
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
|
||||
"\n[Create a PR](https://gitea.example.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
|
||||
triggerUsername: "jane-doe",
|
||||
};
|
||||
|
||||
@@ -391,7 +406,7 @@ describe("updateCommentBody", () => {
|
||||
|
||||
// Should include the PR link in the formatted style
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
|
||||
"• [Create PR ➔](https://gitea.example.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
|
||||
);
|
||||
expect(result).toContain("**Claude finished @jane-doe's task**");
|
||||
});
|
||||
@@ -401,20 +416,19 @@ describe("updateCommentBody", () => {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
branchName: "claude/issue-123-20240101_120000",
|
||||
branchLink:
|
||||
"\n[View branch](https://github.com/owner/repo/src/branch/claude/issue-123-20240101_120000)",
|
||||
branchLink: `\n[View branch](${BRANCH_BASE_URL}/claude/issue-123-20240101_120000)`,
|
||||
prLink:
|
||||
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
|
||||
"\n[Create a PR](https://gitea.example.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
|
||||
// Should include both links in formatted style
|
||||
expect(result).toContain(
|
||||
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/src/branch/claude/issue-123-20240101_120000)",
|
||||
`• [\`claude/issue-123-20240101_120000\`](${BRANCH_BASE_URL}/claude/issue-123-20240101_120000)`,
|
||||
);
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
|
||||
"• [Create PR ➔](https://gitea.example.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
buildDisallowedToolsString,
|
||||
} from "../src/create-prompt";
|
||||
import type { PreparedContext } from "../src/create-prompt";
|
||||
import type { EventData } from "../src/create-prompt/types";
|
||||
|
||||
describe("generatePrompt", () => {
|
||||
const mockGitHubData = {
|
||||
@@ -134,7 +133,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
|
||||
@@ -162,7 +161,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -188,16 +187,14 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
"<trigger_context>new issue with '@claude' in body</trigger_context>",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"[Create a PR](https://github.com/owner/repo/compare/main",
|
||||
);
|
||||
expect(prompt).toContain("The target-branch should be 'main'");
|
||||
expect(prompt).toContain("mcp__gitea__update_issue_comment");
|
||||
expect(prompt).toContain("mcp__gitea__list_branches");
|
||||
});
|
||||
|
||||
test("should generate prompt for issue assigned event", () => {
|
||||
@@ -216,15 +213,14 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
"<trigger_context>issue assigned to 'claude-bot'</trigger_context>",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"[Create a PR](https://github.com/owner/repo/compare/develop",
|
||||
);
|
||||
expect(prompt).toContain("mcp__gitea__list_branches");
|
||||
expect(prompt).toContain("mcp__local_git_ops__checkout_branch");
|
||||
});
|
||||
|
||||
test("should include direct prompt when provided", () => {
|
||||
@@ -243,7 +239,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<direct_prompt>");
|
||||
expect(prompt).toContain("Fix the bug in the login form");
|
||||
@@ -266,7 +262,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -291,7 +287,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("CUSTOM INSTRUCTIONS:\nAlways use TypeScript");
|
||||
});
|
||||
@@ -313,11 +309,11 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
||||
expect(prompt).toContain(
|
||||
"Co-authored-by: johndoe <johndoe@users.noreply.local>",
|
||||
"<trigger_display_name>johndoe</trigger_display_name>",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -334,7 +330,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain PR-specific instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -367,19 +363,12 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain Issue-specific instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/issue-789-20240101_120000)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"IMPORTANT: You are already on the correct branch (claude/issue-789-20240101_120000)",
|
||||
);
|
||||
expect(prompt).toContain("Create a PR](https://github.com/");
|
||||
expect(prompt).toContain(
|
||||
"If you created a branch and made changes, your comment must include the PR URL",
|
||||
);
|
||||
expect(prompt).toContain("mcp__gitea__update_issue_comment");
|
||||
expect(prompt).toContain("mcp__gitea__list_branches");
|
||||
expect(prompt).toContain("mcp__local_git_ops__checkout_branch");
|
||||
|
||||
// Should NOT contain PR-specific instructions
|
||||
expect(prompt).not.toContain(
|
||||
@@ -406,54 +395,11 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain the actual branch name with timestamp
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/issue-123-20240101_120000)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"IMPORTANT: You are already on the correct branch (claude/issue-123-20240101_120000)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"The branch-name is the current branch: claude/issue-123-20240101_120000",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle closed PR with new branch", () => {
|
||||
const envVars: PreparedContext = {
|
||||
repository: "owner/repo",
|
||||
claudeCommentId: "12345",
|
||||
triggerPhrase: "@claude",
|
||||
eventData: {
|
||||
eventName: "issue_comment",
|
||||
commentId: "67890",
|
||||
isPR: true,
|
||||
prNumber: "456",
|
||||
commentBody: "@claude please fix this",
|
||||
claudeBranch: "claude/pr-456-20240101_120000",
|
||||
baseBranch: "main",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
|
||||
// Should contain branch-specific instructions like issues
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-456-20240101_120000)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Create a PR](https://github.com/owner/repo/compare/main",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"The branch-name is the current branch: claude/pr-456-20240101_120000",
|
||||
);
|
||||
expect(prompt).toContain("Reference to the original PR");
|
||||
|
||||
// Should NOT contain open PR instructions
|
||||
expect(prompt).not.toContain(
|
||||
"Commit changes using mcp__local_git_ops__commit_files to the existing branch",
|
||||
);
|
||||
// Should surface the issue number and comment metadata
|
||||
expect(prompt).toContain("<issue_number>123</issue_number>");
|
||||
expect(prompt).toContain("<claude_comment_id>12345</claude_comment_id>");
|
||||
});
|
||||
|
||||
test("should handle open PR without new branch", () => {
|
||||
@@ -471,7 +417,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain open PR instructions
|
||||
expect(prompt).toContain(
|
||||
@@ -488,84 +434,6 @@ describe("generatePrompt", () => {
|
||||
"If you created anything in your branch, your comment must include the PR URL",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle PR review on closed PR with new branch", () => {
|
||||
const envVars: PreparedContext = {
|
||||
repository: "owner/repo",
|
||||
claudeCommentId: "12345",
|
||||
triggerPhrase: "@claude",
|
||||
eventData: {
|
||||
eventName: "pull_request_review",
|
||||
isPR: true,
|
||||
prNumber: "789",
|
||||
commentBody: "@claude please update this",
|
||||
claudeBranch: "claude/pr-789-20240101_123000",
|
||||
baseBranch: "develop",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-789-20240101_123000)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Create a PR](https://github.com/owner/repo/compare/develop",
|
||||
);
|
||||
expect(prompt).toContain("Reference to the original PR");
|
||||
});
|
||||
|
||||
test("should handle PR review comment on closed PR with new branch", () => {
|
||||
const envVars: PreparedContext = {
|
||||
repository: "owner/repo",
|
||||
claudeCommentId: "12345",
|
||||
triggerPhrase: "@claude",
|
||||
eventData: {
|
||||
eventName: "pull_request_review_comment",
|
||||
isPR: true,
|
||||
prNumber: "999",
|
||||
commentId: "review-comment-123",
|
||||
commentBody: "@claude fix this issue",
|
||||
claudeBranch: "claude/pr-999-20240101_140000",
|
||||
baseBranch: "main",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-999-20240101_140000)",
|
||||
);
|
||||
expect(prompt).toContain("Create a PR](https://github.com/");
|
||||
expect(prompt).toContain("Reference to the original PR");
|
||||
});
|
||||
|
||||
test("should handle pull_request event on closed PR with new branch", () => {
|
||||
const envVars: PreparedContext = {
|
||||
repository: "owner/repo",
|
||||
claudeCommentId: "12345",
|
||||
triggerPhrase: "@claude",
|
||||
eventData: {
|
||||
eventName: "pull_request",
|
||||
eventAction: "closed",
|
||||
isPR: true,
|
||||
prNumber: "555",
|
||||
claudeBranch: "claude/pr-555-20240101_150000",
|
||||
baseBranch: "main",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-555-20240101_150000)",
|
||||
);
|
||||
expect(prompt).toContain("Create a PR](https://github.com/");
|
||||
expect(prompt).toContain("Reference to the original PR");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEventTypeAndContext", () => {
|
||||
@@ -612,81 +480,36 @@ describe("getEventTypeAndContext", () => {
|
||||
});
|
||||
|
||||
describe("buildAllowedToolsString", () => {
|
||||
test("should return issue comment tool for regular events", () => {
|
||||
const mockEventData: EventData = {
|
||||
eventName: "issue_comment",
|
||||
commentId: "123",
|
||||
isPR: true,
|
||||
prNumber: "456",
|
||||
commentBody: "Test comment",
|
||||
};
|
||||
test("should include base tools", () => {
|
||||
const result = buildAllowedToolsString();
|
||||
|
||||
const result = buildAllowedToolsString(mockEventData);
|
||||
|
||||
// The base tools should be in the result
|
||||
expect(result).toContain("Edit");
|
||||
expect(result).toContain("Glob");
|
||||
expect(result).toContain("Grep");
|
||||
expect(result).toContain("LS");
|
||||
expect(result).toContain("Read");
|
||||
expect(result).toContain("Write");
|
||||
expect(result).toContain("mcp__github__update_issue_comment");
|
||||
expect(result).not.toContain("mcp__github__update_pull_request_comment");
|
||||
expect(result).toContain("mcp__local_git_ops__commit_files");
|
||||
expect(result).toContain("mcp__local_git_ops__delete_files");
|
||||
expect(result).toContain("mcp__gitea__update_issue_comment");
|
||||
expect(result).toContain("mcp__gitea__update_pull_request_comment");
|
||||
});
|
||||
|
||||
test("should return PR comment tool for inline review comments", () => {
|
||||
const mockEventData: EventData = {
|
||||
eventName: "pull_request_review_comment",
|
||||
isPR: true,
|
||||
prNumber: "456",
|
||||
commentBody: "Test review comment",
|
||||
commentId: "789",
|
||||
};
|
||||
test("should include commit signing tools when enabled", () => {
|
||||
const result = buildAllowedToolsString(undefined, false, true);
|
||||
|
||||
const result = buildAllowedToolsString(mockEventData);
|
||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||
});
|
||||
|
||||
// The base tools should be in the result
|
||||
expect(result).toContain("Edit");
|
||||
expect(result).toContain("Glob");
|
||||
expect(result).toContain("Grep");
|
||||
expect(result).toContain("LS");
|
||||
expect(result).toContain("Read");
|
||||
expect(result).toContain("Write");
|
||||
expect(result).not.toContain("mcp__github__update_issue_comment");
|
||||
expect(result).toContain("mcp__github__update_pull_request_comment");
|
||||
expect(result).toContain("mcp__local_git_ops__commit_files");
|
||||
expect(result).toContain("mcp__local_git_ops__delete_files");
|
||||
test("should include actions tools when actions read permission granted", () => {
|
||||
const result = buildAllowedToolsString([], true, false);
|
||||
|
||||
expect(result).toContain("mcp__github_actions__get_ci_status");
|
||||
expect(result).toContain("mcp__github_actions__download_job_log");
|
||||
});
|
||||
|
||||
test("should append custom tools when provided", () => {
|
||||
const mockEventData: EventData = {
|
||||
eventName: "issue_comment",
|
||||
commentId: "123",
|
||||
isPR: true,
|
||||
prNumber: "456",
|
||||
commentBody: "Test comment",
|
||||
};
|
||||
|
||||
const customTools = "Tool1,Tool2,Tool3";
|
||||
const result = buildAllowedToolsString(mockEventData, customTools);
|
||||
const result = buildAllowedToolsString(customTools);
|
||||
|
||||
// Base tools should be present
|
||||
expect(result).toContain("Edit");
|
||||
expect(result).toContain("Glob");
|
||||
|
||||
// Custom tools should be appended
|
||||
expect(result).toContain("Tool1");
|
||||
expect(result).toContain("Tool2");
|
||||
expect(result).toContain("Tool3");
|
||||
|
||||
// Verify format with comma separation
|
||||
const basePlusCustom = result.split(",");
|
||||
expect(basePlusCustom.length).toBeGreaterThan(10); // At least the base tools plus custom
|
||||
expect(basePlusCustom).toContain("Tool1");
|
||||
expect(basePlusCustom).toContain("Tool2");
|
||||
expect(basePlusCustom).toContain("Tool3");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
83
test/gitea-server-url.test.ts
Normal file
83
test/gitea-server-url.test.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
||||
|
||||
describe("GITEA_SERVER_URL configuration", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment variables
|
||||
process.env = { ...originalEnv };
|
||||
delete process.env.GITEA_SERVER_URL;
|
||||
delete process.env.GITHUB_SERVER_URL;
|
||||
|
||||
// Clear module cache to force re-evaluation
|
||||
delete require.cache[require.resolve("../src/github/api/config")];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it("should prioritize GITEA_SERVER_URL over GITHUB_SERVER_URL", async () => {
|
||||
process.env.GITEA_SERVER_URL = "https://gitea.example.com";
|
||||
process.env.GITHUB_SERVER_URL = "http://gitea:3000";
|
||||
|
||||
const { GITEA_SERVER_URL } = await import("../src/github/api/config");
|
||||
expect(GITEA_SERVER_URL).toBe("https://gitea.example.com");
|
||||
});
|
||||
|
||||
it("should fall back to GITHUB_SERVER_URL when GITEA_SERVER_URL is not set", async () => {
|
||||
process.env.GITHUB_SERVER_URL = "http://gitea:3000";
|
||||
|
||||
const { GITEA_SERVER_URL } = await import("../src/github/api/config");
|
||||
expect(GITEA_SERVER_URL).toBe("http://gitea:3000");
|
||||
});
|
||||
|
||||
it("should use default when neither GITEA_SERVER_URL nor GITHUB_SERVER_URL is set", async () => {
|
||||
const { GITEA_SERVER_URL } = await import("../src/github/api/config");
|
||||
expect(GITEA_SERVER_URL).toBe("https://github.com");
|
||||
});
|
||||
|
||||
it("should ignore empty GITEA_SERVER_URL and use GITHUB_SERVER_URL", async () => {
|
||||
process.env.GITEA_SERVER_URL = "";
|
||||
process.env.GITHUB_SERVER_URL = "http://gitea:3000";
|
||||
|
||||
const { GITEA_SERVER_URL } = await import("../src/github/api/config");
|
||||
expect(GITEA_SERVER_URL).toBe("http://gitea:3000");
|
||||
});
|
||||
|
||||
it("should derive correct API URL from custom GITEA_SERVER_URL", async () => {
|
||||
process.env.GITEA_SERVER_URL = "https://gitea.example.com";
|
||||
|
||||
const { GITEA_API_URL } = await import("../src/github/api/config");
|
||||
expect(GITEA_API_URL).toBe("https://gitea.example.com/api/v1");
|
||||
});
|
||||
|
||||
it("should handle GitHub.com URLs correctly", async () => {
|
||||
process.env.GITEA_SERVER_URL = "https://github.com";
|
||||
|
||||
const { GITEA_API_URL } = await import("../src/github/api/config");
|
||||
expect(GITEA_API_URL).toBe("https://api.github.com");
|
||||
});
|
||||
|
||||
it("should create correct job run links with custom GITEA_SERVER_URL", async () => {
|
||||
process.env.GITEA_SERVER_URL = "https://gitea.example.com";
|
||||
|
||||
// Clear module cache and re-import
|
||||
delete require.cache[require.resolve("../src/github/operations/comments/common")];
|
||||
const { createJobRunLink } = await import("../src/github/operations/comments/common");
|
||||
|
||||
const link = createJobRunLink("owner", "repo", "123");
|
||||
expect(link).toBe("[View job run](https://gitea.example.com/owner/repo/actions/runs/123)");
|
||||
});
|
||||
|
||||
it("should create correct branch links with custom GITEA_SERVER_URL", async () => {
|
||||
process.env.GITEA_SERVER_URL = "https://gitea.example.com";
|
||||
|
||||
// Clear module cache and re-import
|
||||
delete require.cache[require.resolve("../src/github/operations/comments/common")];
|
||||
const { createBranchLink } = await import("../src/github/operations/comments/common");
|
||||
|
||||
const link = createBranchLink("owner", "repo", "feature-branch");
|
||||
expect(link).toBe("\n[View branch](https://gitea.example.com/owner/repo/src/branch/feature-branch/)");
|
||||
});
|
||||
});
|
||||
@@ -1,665 +1,48 @@
|
||||
import { describe, test, expect, spyOn, beforeEach, afterEach } from "bun:test";
|
||||
import {
|
||||
describe,
|
||||
test,
|
||||
expect,
|
||||
spyOn,
|
||||
beforeEach,
|
||||
afterEach,
|
||||
jest,
|
||||
setSystemTime,
|
||||
} from "bun:test";
|
||||
import fs from "fs/promises";
|
||||
import { downloadCommentImages } from "../src/github/utils/image-downloader";
|
||||
import type { CommentWithImages } from "../src/github/utils/image-downloader";
|
||||
import type { Octokits } from "../src/github/api/client";
|
||||
downloadCommentImages,
|
||||
type CommentWithImages,
|
||||
} from "../src/github/utils/image-downloader";
|
||||
|
||||
const noopClient = { api: {} } as any;
|
||||
|
||||
describe("downloadCommentImages", () => {
|
||||
let consoleLogSpy: any;
|
||||
let consoleWarnSpy: any;
|
||||
let consoleErrorSpy: any;
|
||||
let fsMkdirSpy: any;
|
||||
let fsWriteFileSpy: any;
|
||||
let fetchSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Spy on console methods
|
||||
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
||||
consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
|
||||
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
// Spy on fs methods
|
||||
fsMkdirSpy = spyOn(fs, "mkdir").mockResolvedValue(undefined);
|
||||
fsWriteFileSpy = spyOn(fs, "writeFile").mockResolvedValue(undefined);
|
||||
|
||||
// Set fake system time for consistent filenames
|
||||
setSystemTime(new Date("2024-01-01T00:00:00.000Z")); // 1704067200000
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleLogSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
fsMkdirSpy.mockRestore();
|
||||
fsWriteFileSpy.mockRestore();
|
||||
if (fetchSpy) fetchSpy.mockRestore();
|
||||
setSystemTime(); // Reset to real time
|
||||
});
|
||||
|
||||
const createMockOctokit = (): Octokits => {
|
||||
return {
|
||||
rest: {
|
||||
issues: {
|
||||
getComment: jest.fn(),
|
||||
get: jest.fn(),
|
||||
},
|
||||
pulls: {
|
||||
getReviewComment: jest.fn(),
|
||||
getReview: jest.fn(),
|
||||
get: jest.fn(),
|
||||
},
|
||||
},
|
||||
} as any as Octokits;
|
||||
};
|
||||
test("returns empty map and logs disabled message", async () => {
|
||||
const result = await downloadCommentImages(
|
||||
noopClient,
|
||||
"owner",
|
||||
"repo",
|
||||
[] as CommentWithImages[],
|
||||
);
|
||||
|
||||
test("should create download directory", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const comments: CommentWithImages[] = [];
|
||||
|
||||
await downloadCommentImages(mockOctokit, "owner", "repo", comments);
|
||||
|
||||
expect(fsMkdirSpy).toHaveBeenCalledWith("/tmp/github-images", {
|
||||
recursive: true,
|
||||
});
|
||||
expect(result.size).toBe(0);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Image downloading temporarily disabled during Octokit migration",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle comments without images", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
test("ignores provided comments while feature disabled", async () => {
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "123",
|
||||
body: "This is a comment without images",
|
||||
body: "",
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
const result = await downloadCommentImages(noopClient, "owner", "repo", comments);
|
||||
|
||||
expect(result.size).toBe(0);
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("Found"),
|
||||
);
|
||||
});
|
||||
|
||||
test("should detect and download images from issue comments", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl =
|
||||
"https://github.com/user-attachments/assets/test-image.png";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/test.png?jwt=token";
|
||||
|
||||
// Mock octokit response
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
// Mock fetch for image download
|
||||
const mockArrayBuffer = new ArrayBuffer(8);
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => mockArrayBuffer,
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "123",
|
||||
body: `Here's an image: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(mockOctokit.rest.issues.getComment).toHaveBeenCalledWith({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
comment_id: 123,
|
||||
mediaType: { format: "full+json" },
|
||||
});
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledWith(signedUrl);
|
||||
expect(fsWriteFileSpy).toHaveBeenCalledWith(
|
||||
"/tmp/github-images/image-1704067200000-0.png",
|
||||
Buffer.from(mockArrayBuffer),
|
||||
);
|
||||
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get(imageUrl)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.png",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Found 1 image(s) in issue_comment 123",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(`Downloading ${imageUrl}...`);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"✓ Saved: /tmp/github-images/image-1704067200000-0.png",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle review comments", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl =
|
||||
"https://github.com/user-attachments/assets/review-image.jpg";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/review.jpg?jwt=token";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.pulls.getReviewComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "review_comment",
|
||||
id: "456",
|
||||
body: `Review comment with image: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(mockOctokit.rest.pulls.getReviewComment).toHaveBeenCalledWith({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
comment_id: 456,
|
||||
mediaType: { format: "full+json" },
|
||||
});
|
||||
|
||||
expect(result.get(imageUrl)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.jpg",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle review bodies", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl =
|
||||
"https://github.com/user-attachments/assets/review-body.png";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/body.png?jwt=token";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.pulls.getReview = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "review_body",
|
||||
id: "789",
|
||||
pullNumber: "100",
|
||||
body: `Review body: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(mockOctokit.rest.pulls.getReview).toHaveBeenCalledWith({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
pull_number: 100,
|
||||
review_id: 789,
|
||||
mediaType: { format: "full+json" },
|
||||
});
|
||||
|
||||
expect(result.get(imageUrl)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.png",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle issue bodies", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl =
|
||||
"https://github.com/user-attachments/assets/issue-body.gif";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/issue.gif?jwt=token";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.get = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_body",
|
||||
issueNumber: "200",
|
||||
body: `Issue description: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(mockOctokit.rest.issues.get).toHaveBeenCalledWith({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
issue_number: 200,
|
||||
mediaType: { format: "full+json" },
|
||||
});
|
||||
|
||||
expect(result.get(imageUrl)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.gif",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Found 1 image(s) in issue_body 200",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle PR bodies", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl = "https://github.com/user-attachments/assets/pr-body.webp";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/pr.webp?jwt=token";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.pulls.get = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "pr_body",
|
||||
pullNumber: "300",
|
||||
body: `PR description: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith({
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
pull_number: 300,
|
||||
mediaType: { format: "full+json" },
|
||||
});
|
||||
|
||||
expect(result.get(imageUrl)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.webp",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Found 1 image(s) in pr_body 300",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle multiple images in a single comment", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl1 = "https://github.com/user-attachments/assets/image1.png";
|
||||
const imageUrl2 = "https://github.com/user-attachments/assets/image2.jpg";
|
||||
const signedUrl1 =
|
||||
"https://private-user-images.githubusercontent.com/1.png?jwt=token1";
|
||||
const signedUrl2 =
|
||||
"https://private-user-images.githubusercontent.com/2.jpg?jwt=token2";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "999",
|
||||
body: `Two images:  and `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(2);
|
||||
expect(result.size).toBe(2);
|
||||
expect(result.get(imageUrl1)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.png",
|
||||
);
|
||||
expect(result.get(imageUrl2)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-1.jpg",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Found 2 image(s) in issue_comment 999",
|
||||
);
|
||||
});
|
||||
|
||||
test("should skip already downloaded images", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl = "https://github.com/user-attachments/assets/duplicate.png";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/dup.png?jwt=token";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "111",
|
||||
body: `First: `,
|
||||
},
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "222",
|
||||
body: `Second: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1); // Only downloaded once
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get(imageUrl)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.png",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle missing HTML body", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl = "https://github.com/user-attachments/assets/missing.png";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: null,
|
||||
},
|
||||
});
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "333",
|
||||
body: `Missing HTML: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(result.size).toBe(0);
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
"No HTML body found for issue_comment 333",
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle fetch errors", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl = "https://github.com/user-attachments/assets/error.png";
|
||||
const signedUrl =
|
||||
"https://private-user-images.githubusercontent.com/error.png?jwt=token";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: false,
|
||||
status: 404,
|
||||
statusText: "Not Found",
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "444",
|
||||
body: `Error image: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(result.size).toBe(0);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
`✗ Failed to download ${imageUrl}:`,
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
test("should handle API errors gracefully", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl = "https://github.com/user-attachments/assets/api-error.png";
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("API rate limit exceeded"));
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "555",
|
||||
body: `API error: `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(result.size).toBe(0);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to process images for issue_comment 555:",
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
|
||||
test("should extract correct file extensions", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const extensions = [
|
||||
{
|
||||
url: "https://github.com/user-attachments/assets/test.png",
|
||||
ext: ".png",
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user-attachments/assets/test.jpg",
|
||||
ext: ".jpg",
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user-attachments/assets/test.jpeg",
|
||||
ext: ".jpeg",
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user-attachments/assets/test.gif",
|
||||
ext: ".gif",
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user-attachments/assets/test.webp",
|
||||
ext: ".webp",
|
||||
},
|
||||
{
|
||||
url: "https://github.com/user-attachments/assets/test.svg",
|
||||
ext: ".svg",
|
||||
},
|
||||
{
|
||||
// default
|
||||
url: "https://github.com/user-attachments/assets/no-extension",
|
||||
ext: ".png",
|
||||
},
|
||||
];
|
||||
|
||||
let callIndex = 0;
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="https://private-user-images.githubusercontent.com/test?jwt=token">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
for (const { url, ext } of extensions) {
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: `${1000 + callIndex}`,
|
||||
body: `Test: `,
|
||||
},
|
||||
];
|
||||
|
||||
setSystemTime(new Date(1704067200000 + callIndex));
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
expect(result.get(url)).toBe(
|
||||
`/tmp/github-images/image-${1704067200000 + callIndex}-0${ext}`,
|
||||
);
|
||||
|
||||
// Reset for next iteration
|
||||
fsWriteFileSpy.mockClear();
|
||||
callIndex++;
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle mismatched signed URL count", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const imageUrl1 = "https://github.com/user-attachments/assets/img1.png";
|
||||
const imageUrl2 = "https://github.com/user-attachments/assets/img2.png";
|
||||
const signedUrl1 =
|
||||
"https://private-user-images.githubusercontent.com/1.png?jwt=token";
|
||||
|
||||
// Only one signed URL for two images
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
body_html: `<img src="${signedUrl1}">`,
|
||||
},
|
||||
});
|
||||
|
||||
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
|
||||
ok: true,
|
||||
arrayBuffer: async () => new ArrayBuffer(8),
|
||||
} as Response);
|
||||
|
||||
const comments: CommentWithImages[] = [
|
||||
{
|
||||
type: "issue_comment",
|
||||
id: "666",
|
||||
body: `Two images:  `,
|
||||
},
|
||||
];
|
||||
|
||||
const result = await downloadCommentImages(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
comments,
|
||||
);
|
||||
|
||||
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
||||
expect(result.size).toBe(1);
|
||||
expect(result.get(imageUrl1)).toBe(
|
||||
"/tmp/github-images/image-1704067200000-0.png",
|
||||
);
|
||||
expect(result.get(imageUrl2)).toBeUndefined();
|
||||
expect(consoleLogSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,682 +1,59 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
||||
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
|
||||
import * as core from "@actions/core";
|
||||
import type { ParsedGitHubContext } from "../src/github/context";
|
||||
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
describe("prepareMcpConfig", () => {
|
||||
let consoleInfoSpy: any;
|
||||
let consoleWarningSpy: any;
|
||||
let setFailedSpy: any;
|
||||
let processExitSpy: any;
|
||||
|
||||
// Create a mock context for tests
|
||||
const mockContext: ParsedGitHubContext = {
|
||||
runId: "test-run-id",
|
||||
eventName: "issue_comment",
|
||||
eventAction: "created",
|
||||
repository: {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
full_name: "test-owner/test-repo",
|
||||
},
|
||||
actor: "test-actor",
|
||||
payload: {} as any,
|
||||
entityNumber: 123,
|
||||
isPR: false,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
branchPrefix: "",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
};
|
||||
|
||||
const mockPRContext: ParsedGitHubContext = {
|
||||
...mockContext,
|
||||
eventName: "pull_request",
|
||||
isPR: true,
|
||||
entityNumber: 456,
|
||||
};
|
||||
|
||||
const mockContextWithSigning: ParsedGitHubContext = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockPRContextWithSigning: ParsedGitHubContext = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {});
|
||||
consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
||||
setFailedSpy = spyOn(core, "setFailed").mockImplementation(() => {});
|
||||
processExitSpy = spyOn(process, "exit").mockImplementation(() => {
|
||||
throw new Error("Process exit");
|
||||
});
|
||||
|
||||
// Set up required environment variables
|
||||
if (!process.env.GITHUB_ACTION_PATH) {
|
||||
process.env.GITHUB_ACTION_PATH = "/test/action/path";
|
||||
}
|
||||
process.env.GITHUB_ACTION_PATH = "/action/path";
|
||||
process.env.GITHUB_WORKSPACE = "/workspace";
|
||||
process.env.GITEA_API_URL = "https://gitea.example.com/api/v1";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleInfoSpy.mockRestore();
|
||||
consoleWarningSpy.mockRestore();
|
||||
setFailedSpy.mockRestore();
|
||||
processExitSpy.mockRestore();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
test("should return comment server when commit signing is disabled", async () => {
|
||||
test("returns base gitea and local git MCP servers", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
githubToken: "token",
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
branch: "branch",
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
expect(parsed.mcpServers.github_comment.env.REPO_OWNER).toBe("test-owner");
|
||||
expect(parsed.mcpServers.github_comment.env.REPO_NAME).toBe("test-repo");
|
||||
});
|
||||
expect(Object.keys(parsed.mcpServers)).toEqual(["gitea", "local_git_ops"]);
|
||||
|
||||
test("should return file ops server when commit signing is enabled", async () => {
|
||||
const contextWithSigning = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
expect(parsed.mcpServers.github_file_ops.env.REPO_OWNER).toBe("test-owner");
|
||||
expect(parsed.mcpServers.github_file_ops.env.REPO_NAME).toBe("test-repo");
|
||||
expect(parsed.mcpServers.github_file_ops.env.BRANCH_NAME).toBe(
|
||||
"test-branch",
|
||||
);
|
||||
});
|
||||
|
||||
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",
|
||||
baseBranch: "main",
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).not.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 contextWithSigning = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__update_claude_comment",
|
||||
],
|
||||
context: contextWithSigning,
|
||||
});
|
||||
|
||||
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 comment server when no GitHub tools are allowed and signing disabled", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: ["Edit", "Read", "Write"],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
});
|
||||
|
||||
test("should return base config when additional config is empty string", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: "",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should return base config when additional config is whitespace only", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: " \n\t ",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should merge valid additional config with base config", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
custom_server: {
|
||||
command: "custom-command",
|
||||
args: ["arg1", "arg2"],
|
||||
env: {
|
||||
CUSTOM_ENV: "custom-value",
|
||||
},
|
||||
},
|
||||
expect(parsed.mcpServers.gitea).toEqual({
|
||||
command: "bun",
|
||||
args: ["run", "/action/path/src/mcp/gitea-mcp-server.ts"],
|
||||
env: {
|
||||
GITHUB_TOKEN: "token",
|
||||
REPO_OWNER: "owner",
|
||||
REPO_NAME: "repo",
|
||||
BRANCH_NAME: "branch",
|
||||
REPO_DIR: "/workspace",
|
||||
GITEA_API_URL: "https://gitea.example.com/api/v1",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
expect(parsed.mcpServers.local_git_ops.args[1]).toBe(
|
||||
"/action/path/src/mcp/local-git-ops-server.ts",
|
||||
);
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||
expect(parsed.mcpServers.custom_server.command).toBe("custom-command");
|
||||
expect(parsed.mcpServers.custom_server.args).toEqual(["arg1", "arg2"]);
|
||||
expect(parsed.mcpServers.custom_server.env.CUSTOM_ENV).toBe("custom-value");
|
||||
});
|
||||
|
||||
test("should override built-in servers when additional config has same server names", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
github: {
|
||||
command: "overridden-command",
|
||||
args: ["overridden-arg"],
|
||||
env: {
|
||||
OVERRIDDEN_ENV: "overridden-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
);
|
||||
expect(parsed.mcpServers.github.command).toBe("overridden-command");
|
||||
expect(parsed.mcpServers.github.args).toEqual(["overridden-arg"]);
|
||||
expect(parsed.mcpServers.github.env.OVERRIDDEN_ENV).toBe(
|
||||
"overridden-value",
|
||||
);
|
||||
expect(
|
||||
parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN,
|
||||
).toBeUndefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should merge additional root-level properties", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
customProperty: "custom-value",
|
||||
anotherProperty: {
|
||||
nested: "value",
|
||||
},
|
||||
mcpServers: {
|
||||
custom_server: {
|
||||
command: "custom",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.customProperty).toBe("custom-value");
|
||||
expect(parsed.anotherProperty).toEqual({ nested: "value" });
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle invalid JSON gracefully", async () => {
|
||||
const invalidJson = "{ invalid json }";
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: invalidJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle non-object JSON values", async () => {
|
||||
const nonObjectJson = JSON.stringify("string value");
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: nonObjectJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||
);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle null JSON value", async () => {
|
||||
const nullJson = JSON.stringify(null);
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: nullJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||
);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle array JSON value", async () => {
|
||||
const arrayJson = JSON.stringify([1, 2, 3]);
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: arrayJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
// Arrays are objects in JavaScript, so they pass the object check
|
||||
// But they'll fail when trying to spread or access mcpServers property
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
|
||||
expect(parsed[0]).toBe(1);
|
||||
expect(parsed[1]).toBe(2);
|
||||
expect(parsed[2]).toBe(3);
|
||||
});
|
||||
|
||||
test("should merge complex nested configurations", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
server1: {
|
||||
command: "cmd1",
|
||||
env: { KEY1: "value1" },
|
||||
},
|
||||
server2: {
|
||||
command: "cmd2",
|
||||
env: { KEY2: "value2" },
|
||||
},
|
||||
github_file_ops: {
|
||||
command: "overridden",
|
||||
env: { CUSTOM: "value" },
|
||||
},
|
||||
},
|
||||
otherConfig: {
|
||||
nested: {
|
||||
deeply: "value",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.server1).toBeDefined();
|
||||
expect(parsed.mcpServers.server2).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
|
||||
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
|
||||
expect(parsed.otherConfig.nested.deeply).toBe("value");
|
||||
});
|
||||
|
||||
test("should preserve GITHUB_ACTION_PATH in file_ops server args", async () => {
|
||||
const oldEnv = process.env.GITHUB_ACTION_PATH;
|
||||
process.env.GITHUB_ACTION_PATH = "/test/action/path";
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_file_ops.args[1]).toBe(
|
||||
"/test/action/path/src/mcp/github-file-ops-server.ts",
|
||||
);
|
||||
|
||||
process.env.GITHUB_ACTION_PATH = oldEnv;
|
||||
});
|
||||
|
||||
test("should use process.cwd() when GITHUB_WORKSPACE is not set", async () => {
|
||||
const oldEnv = process.env.GITHUB_WORKSPACE;
|
||||
test("falls back to process.cwd when workspace not provided", async () => {
|
||||
delete process.env.GITHUB_WORKSPACE;
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
githubToken: "token",
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
branch: "branch",
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_file_ops.env.REPO_DIR).toBe(process.cwd());
|
||||
|
||||
process.env.GITHUB_WORKSPACE = oldEnv;
|
||||
});
|
||||
|
||||
test("should include github_ci server when context.isPR is true and actions:read permission is granted", async () => {
|
||||
const oldEnv = process.env.ACTIONS_TOKEN;
|
||||
process.env.ACTIONS_TOKEN = "workflow-token";
|
||||
|
||||
const contextWithPermissions = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
additionalPermissions: new Map([["actions", "read"]]),
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
|
||||
expect(parsed.mcpServers.github_ci.env.PR_NUMBER).toBe("456");
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
|
||||
process.env.ACTIONS_TOKEN = oldEnv;
|
||||
});
|
||||
|
||||
test("should not include github_ci server when context.isPR is false", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should not include github_ci server when actions:read permission is not granted", async () => {
|
||||
const oldTokenEnv = process.env.ACTIONS_TOKEN;
|
||||
process.env.ACTIONS_TOKEN = "workflow-token";
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockPRContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
|
||||
process.env.ACTIONS_TOKEN = oldTokenEnv;
|
||||
});
|
||||
|
||||
test("should parse additional_permissions with multiple lines correctly", async () => {
|
||||
const oldTokenEnv = process.env.ACTIONS_TOKEN;
|
||||
process.env.ACTIONS_TOKEN = "workflow-token";
|
||||
|
||||
const contextWithPermissions = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
additionalPermissions: new Map([
|
||||
["actions", "read"],
|
||||
["future", "permission"],
|
||||
]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
|
||||
|
||||
process.env.ACTIONS_TOKEN = oldTokenEnv;
|
||||
});
|
||||
|
||||
test("should warn when actions:read is requested but token lacks permission", async () => {
|
||||
const oldTokenEnv = process.env.ACTIONS_TOKEN;
|
||||
process.env.ACTIONS_TOKEN = "invalid-token";
|
||||
|
||||
const contextWithPermissions = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
additionalPermissions: new Map([["actions", "read"]]),
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"The github_ci MCP server requires 'actions: read' permission",
|
||||
),
|
||||
);
|
||||
|
||||
process.env.ACTIONS_TOKEN = oldTokenEnv;
|
||||
expect(parsed.mcpServers.gitea.env.REPO_DIR).toBe(process.cwd());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,169 +1,63 @@
|
||||
import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test";
|
||||
import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import * as core from "@actions/core";
|
||||
import { checkWritePermissions } from "../src/github/validation/permissions";
|
||||
import type { ParsedGitHubContext } from "../src/github/context";
|
||||
|
||||
const baseContext: ParsedGitHubContext = {
|
||||
runId: "123",
|
||||
eventName: "issue_comment",
|
||||
eventAction: "created",
|
||||
repository: {
|
||||
owner: "owner",
|
||||
repo: "repo",
|
||||
full_name: "owner/repo",
|
||||
},
|
||||
actor: "tester",
|
||||
payload: {
|
||||
action: "created",
|
||||
issue: { number: 1, body: "", title: "", user: { login: "owner" } },
|
||||
comment: { id: 1, body: "@claude ping", user: { login: "tester" } },
|
||||
} as any,
|
||||
entityNumber: 1,
|
||||
isPR: false,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
};
|
||||
|
||||
describe("checkWritePermissions", () => {
|
||||
let coreInfoSpy: any;
|
||||
let coreWarningSpy: any;
|
||||
let coreErrorSpy: any;
|
||||
let infoSpy: any;
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// Spy on core methods
|
||||
coreInfoSpy = spyOn(core, "info").mockImplementation(() => {});
|
||||
coreWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
||||
coreErrorSpy = spyOn(core, "error").mockImplementation(() => {});
|
||||
infoSpy = spyOn(core, "info").mockImplementation(() => {});
|
||||
process.env.GITEA_API_URL = "https://gitea.example.com/api/v1";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
coreInfoSpy.mockRestore();
|
||||
coreWarningSpy.mockRestore();
|
||||
coreErrorSpy.mockRestore();
|
||||
infoSpy.mockRestore();
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
const createMockOctokit = (permission: string) => {
|
||||
return {
|
||||
repos: {
|
||||
getCollaboratorPermissionLevel: async () => ({
|
||||
data: { permission },
|
||||
}),
|
||||
},
|
||||
} as any;
|
||||
};
|
||||
|
||||
const createContext = (): ParsedGitHubContext => ({
|
||||
runId: "1234567890",
|
||||
eventName: "issue_comment",
|
||||
eventAction: "created",
|
||||
repository: {
|
||||
full_name: "test-owner/test-repo",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
},
|
||||
actor: "test-user",
|
||||
payload: {
|
||||
action: "created",
|
||||
issue: {
|
||||
number: 1,
|
||||
title: "Test Issue",
|
||||
body: "Test body",
|
||||
user: { login: "test-user" },
|
||||
},
|
||||
comment: {
|
||||
id: 123,
|
||||
body: "@claude test",
|
||||
user: { login: "test-user" },
|
||||
html_url:
|
||||
"https://github.com/test-owner/test-repo/issues/1#issuecomment-123",
|
||||
},
|
||||
} as any,
|
||||
entityNumber: 1,
|
||||
isPR: false,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
|
||||
test("should return true for admin permissions", async () => {
|
||||
const mockOctokit = createMockOctokit("admin");
|
||||
const context = createContext();
|
||||
|
||||
const result = await checkWritePermissions(mockOctokit, context);
|
||||
test("returns true immediately in Gitea environments", async () => {
|
||||
const client = { api: { getBaseUrl: () => "https://gitea.example.com/api/v1" } } as any;
|
||||
const result = await checkWritePermissions(client, baseContext);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(coreInfoSpy).toHaveBeenCalledWith(
|
||||
"Checking permissions for actor: test-user",
|
||||
expect(infoSpy).toHaveBeenCalledWith(
|
||||
"Detected Gitea environment (https://gitea.example.com/api/v1), assuming actor has permissions",
|
||||
);
|
||||
expect(coreInfoSpy).toHaveBeenCalledWith(
|
||||
"Permission level retrieved: admin",
|
||||
);
|
||||
expect(coreInfoSpy).toHaveBeenCalledWith("Actor has write access: admin");
|
||||
});
|
||||
|
||||
test("should return true for write permissions", async () => {
|
||||
const mockOctokit = createMockOctokit("write");
|
||||
const context = createContext();
|
||||
|
||||
const result = await checkWritePermissions(mockOctokit, context);
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(coreInfoSpy).toHaveBeenCalledWith("Actor has write access: write");
|
||||
});
|
||||
|
||||
test("should return false for read permissions", async () => {
|
||||
const mockOctokit = createMockOctokit("read");
|
||||
const context = createContext();
|
||||
|
||||
const result = await checkWritePermissions(mockOctokit, context);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||
"Actor has insufficient permissions: read",
|
||||
);
|
||||
});
|
||||
|
||||
test("should return false for none permissions", async () => {
|
||||
const mockOctokit = createMockOctokit("none");
|
||||
const context = createContext();
|
||||
|
||||
const result = await checkWritePermissions(mockOctokit, context);
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||
"Actor has insufficient permissions: none",
|
||||
);
|
||||
});
|
||||
|
||||
test("should throw error when permission check fails", async () => {
|
||||
const error = new Error("API error");
|
||||
const mockOctokit = {
|
||||
repos: {
|
||||
getCollaboratorPermissionLevel: async () => {
|
||||
throw error;
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const context = createContext();
|
||||
|
||||
await expect(checkWritePermissions(mockOctokit, context)).rejects.toThrow(
|
||||
"Failed to check permissions for test-user: Error: API error",
|
||||
);
|
||||
|
||||
expect(coreErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to check permissions: Error: API error",
|
||||
);
|
||||
});
|
||||
|
||||
test("should call API with correct parameters", async () => {
|
||||
let capturedParams: any;
|
||||
const mockOctokit = {
|
||||
repos: {
|
||||
getCollaboratorPermissionLevel: async (params: any) => {
|
||||
capturedParams = params;
|
||||
return { data: { permission: "write" } };
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
const context = createContext();
|
||||
|
||||
await checkWritePermissions(mockOctokit, context);
|
||||
|
||||
expect(capturedParams).toEqual({
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
username: "test-user",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,10 +69,19 @@ describe("parseEnvVarsWithContext", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("should throw error when CLAUDE_BRANCH is missing", () => {
|
||||
expect(() =>
|
||||
prepareContext(mockIssueCommentContext, "12345", "main"),
|
||||
).toThrow("CLAUDE_BRANCH is required for issue_comment event");
|
||||
test("should allow missing CLAUDE_BRANCH and omit it from event data", () => {
|
||||
const result = prepareContext(
|
||||
mockIssueCommentContext,
|
||||
"12345",
|
||||
"main",
|
||||
);
|
||||
|
||||
if (
|
||||
result.eventData.eventName === "issue_comment" &&
|
||||
!result.eventData.isPR
|
||||
) {
|
||||
expect(result.eventData.claudeBranch).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("should throw error when BASE_BRANCH is missing", () => {
|
||||
@@ -203,10 +212,12 @@ describe("parseEnvVarsWithContext", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("should throw error when CLAUDE_BRANCH is missing for issues", () => {
|
||||
expect(() =>
|
||||
prepareContext(mockIssueOpenedContext, "12345", "main"),
|
||||
).toThrow("CLAUDE_BRANCH is required for issues event");
|
||||
test("should allow issues event without CLAUDE_BRANCH", () => {
|
||||
const result = prepareContext(mockIssueOpenedContext, "12345", "main");
|
||||
|
||||
if (result.eventData.eventName === "issues") {
|
||||
expect(result.eventData.claudeBranch).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("should throw error when BASE_BRANCH is missing for issues", () => {
|
||||
|
||||
@@ -365,6 +365,343 @@ describe("checkContainsTrigger", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("pull request reviewer trigger", () => {
|
||||
it("should return true when PR has trigger user as requested reviewer (same as text mention)", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "opened",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [
|
||||
{ login: "claude", id: 1, type: "User" },
|
||||
{ login: "other-reviewer", id: 2, type: "User" },
|
||||
],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for synchronized PR with trigger user as reviewer", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "synchronized",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "synchronized",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [
|
||||
{ login: "claude", id: 1, type: "User" },
|
||||
],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when PR has no matching requested reviewers", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "opened",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [
|
||||
{ login: "other-reviewer", id: 2, type: "User" },
|
||||
],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle trigger phrase without @ symbol", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "opened",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [
|
||||
{ login: "claude", id: 1, type: "User" },
|
||||
],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "claude", // No @ symbol
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("should return true when PR has trigger user as requested reviewer for synchronized event", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "synchronized",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "synchronized",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [
|
||||
{ login: "claude", id: 1, type: "User" },
|
||||
],
|
||||
requested_teams: [],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when PR has no matching requested reviewers", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "opened",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [
|
||||
{ login: "other-reviewer", id: 2, type: "User" },
|
||||
],
|
||||
requested_teams: [],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle trigger phrase without @ symbol", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "opened",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [
|
||||
{ login: "claude", id: 1, type: "User" },
|
||||
],
|
||||
requested_teams: [],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "claude", // No @ symbol
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle empty requested_reviewers and requested_teams arrays", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "opened",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [],
|
||||
requested_teams: [],
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle missing requested_reviewers and requested_teams fields", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "opened",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "opened",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
// requested_reviewers and requested_teams are undefined
|
||||
},
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("comment trigger", () => {
|
||||
it("should return true for issue_comment with trigger phrase", () => {
|
||||
const context = mockIssueCommentContext;
|
||||
@@ -475,6 +812,119 @@ describe("checkContainsTrigger", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("pull request review_requested action", () => {
|
||||
it("should return true when trigger user is requested as reviewer", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "review_requested",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "review_requested",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
|
||||
requested_teams: [],
|
||||
},
|
||||
requested_reviewer: { login: "claude", id: 1, type: "User" },
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false when different user is requested as reviewer", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "review_requested",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "review_requested",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [{ login: "john", id: 2, type: "User" }],
|
||||
requested_teams: [],
|
||||
},
|
||||
requested_reviewer: { login: "john", id: 2, type: "User" },
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "@claude",
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle trigger phrase without @ symbol", () => {
|
||||
const context = createMockContext({
|
||||
eventName: "pull_request",
|
||||
eventAction: "review_requested",
|
||||
isPR: true,
|
||||
payload: {
|
||||
action: "review_requested",
|
||||
pull_request: {
|
||||
number: 123,
|
||||
title: "Test PR",
|
||||
body: "This PR fixes a bug",
|
||||
created_at: "2023-01-01T00:00:00Z",
|
||||
user: { login: "testuser" },
|
||||
requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
|
||||
requested_teams: [],
|
||||
},
|
||||
requested_reviewer: { login: "claude", id: 1, type: "User" },
|
||||
} as unknown as PullRequestEvent,
|
||||
inputs: {
|
||||
mode: "tag",
|
||||
triggerPhrase: "claude", // no @ symbol
|
||||
assigneeTrigger: "",
|
||||
labelTrigger: "",
|
||||
directPrompt: "",
|
||||
overridePrompt: "",
|
||||
allowedTools: [],
|
||||
disallowedTools: [],
|
||||
customInstructions: "",
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-matching events", () => {
|
||||
it("should return false for non-matching event type", () => {
|
||||
const context = createMockContext({
|
||||
@@ -485,7 +935,6 @@ describe("checkContainsTrigger", () => {
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("escapeRegExp", () => {
|
||||
it("should escape special regex characters", () => {
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { describe, test, expect, jest, beforeEach } from "bun:test";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import {
|
||||
updateClaudeComment,
|
||||
type UpdateClaudeCommentParams,
|
||||
} from "../src/github/operations/comments/update-claude-comment";
|
||||
|
||||
describe("updateClaudeComment", () => {
|
||||
let mockOctokit: Octokit;
|
||||
let mockOctokit: any;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOctokit = {
|
||||
@@ -18,7 +17,7 @@ describe("updateClaudeComment", () => {
|
||||
updateReviewComment: jest.fn(),
|
||||
},
|
||||
},
|
||||
} as any as Octokit;
|
||||
};
|
||||
});
|
||||
|
||||
test("should update issue comment successfully", async () => {
|
||||
@@ -31,7 +30,6 @@ describe("updateClaudeComment", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.updateComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
@@ -70,7 +68,6 @@ describe("updateClaudeComment", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.updateComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
@@ -109,7 +106,6 @@ describe("updateClaudeComment", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
@@ -151,11 +147,9 @@ describe("updateClaudeComment", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||
.fn()
|
||||
.mockRejectedValue(mockError);
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.updateComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
@@ -195,7 +189,6 @@ describe("updateClaudeComment", () => {
|
||||
const mockError = new Error("Internal Server Error") as any;
|
||||
mockError.status = 500;
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||
.fn()
|
||||
.mockRejectedValue(mockError);
|
||||
@@ -226,7 +219,6 @@ describe("updateClaudeComment", () => {
|
||||
test("should propagate error when issue comment update fails", async () => {
|
||||
const mockError = new Error("Forbidden");
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.updateComment = jest
|
||||
.fn()
|
||||
.mockRejectedValue(mockError);
|
||||
@@ -261,7 +253,6 @@ describe("updateClaudeComment", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.updateComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
@@ -294,7 +285,6 @@ describe("updateClaudeComment", () => {
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.updateComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
@@ -345,7 +335,6 @@ const code = "example";
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.issues.updateComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
@@ -388,7 +377,6 @@ const code = "example";
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||
.fn()
|
||||
.mockResolvedValue(mockResponse);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
|
||||
Reference in New Issue
Block a user