mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
13 Commits
ashwin/tes
...
ashwin/all
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
374c1885f1 | ||
|
|
1a8e7d330a | ||
|
|
9975f36410 | ||
|
|
c1ffc8a0e8 | ||
|
|
13e47489f4 | ||
|
|
765fadc6a6 | ||
|
|
fd2c17f101 | ||
|
|
a4a723b927 | ||
|
|
d22fa6061b | ||
|
|
63f1c772bd | ||
|
|
fb823f6dd6 | ||
|
|
9e9123239f | ||
|
|
791fcb9fd1 |
6
.github/workflows/issue-triage.yml
vendored
6
.github/workflows/issue-triage.yml
vendored
@@ -78,7 +78,7 @@ jobs:
|
|||||||
4. Select appropriate labels from the available labels list provided above:
|
4. Select appropriate labels from the available labels list provided above:
|
||||||
- Choose labels that accurately reflect the issue's nature
|
- Choose labels that accurately reflect the issue's nature
|
||||||
- Be specific but comprehensive
|
- Be specific but comprehensive
|
||||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
|
||||||
- Consider platform labels (android, ios) if applicable
|
- Consider platform labels (android, ios) if applicable
|
||||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||||
|
|
||||||
@@ -97,10 +97,12 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
- name: Run Claude Code for Issue Triage
|
- name: Run Claude Code for Issue Triage
|
||||||
uses: anthropics/claude-code-base-action@v1
|
uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
|
prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
allowed_non_write_users: "*"
|
||||||
claude_args: |
|
claude_args: |
|
||||||
--allowedTools 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
|
--allowedTools 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
|
--mcp-config /tmp/mcp-config/mcp-servers.json
|
||||||
|
|||||||
17
action.yml
17
action.yml
@@ -27,6 +27,10 @@ inputs:
|
|||||||
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
|
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
allowed_non_write_users:
|
||||||
|
description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)."
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
# Claude Code configuration
|
# Claude Code configuration
|
||||||
prompt:
|
prompt:
|
||||||
@@ -73,6 +77,14 @@ inputs:
|
|||||||
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: "false"
|
||||||
|
bot_id:
|
||||||
|
description: "GitHub user ID to use for git operations (defaults to Claude's bot ID)"
|
||||||
|
required: false
|
||||||
|
default: "41898282" # Claude's bot ID - see src/github/constants.ts
|
||||||
|
bot_name:
|
||||||
|
description: "GitHub username to use for git operations (defaults to Claude's bot name)"
|
||||||
|
required: false
|
||||||
|
default: "claude[bot]"
|
||||||
track_progress:
|
track_progress:
|
||||||
description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events."
|
description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events."
|
||||||
required: false
|
required: false
|
||||||
@@ -140,10 +152,13 @@ runs:
|
|||||||
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
||||||
|
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||||
|
BOT_ID: ${{ inputs.bot_id }}
|
||||||
|
BOT_NAME: ${{ inputs.bot_name }}
|
||||||
TRACK_PROGRESS: ${{ inputs.track_progress }}
|
TRACK_PROGRESS: ${{ inputs.track_progress }}
|
||||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||||
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||||
@@ -162,7 +177,7 @@ runs:
|
|||||||
# Install Claude Code if no custom executable is provided
|
# Install Claude Code if no custom executable is provided
|
||||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||||
echo "Installing Claude Code..."
|
echo "Installing Claude Code..."
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.108
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
else
|
else
|
||||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ runs:
|
|||||||
run: |
|
run: |
|
||||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||||
echo "Installing Claude Code..."
|
echo "Installing Claude Code..."
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.108
|
||||||
else
|
else
|
||||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||||
# Add the directory containing the custom executable to PATH
|
# Add the directory containing the custom executable to PATH
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ This action supports the following GitHub events ([learn more GitHub event trigg
|
|||||||
- `issues` - When issues are opened or assigned
|
- `issues` - When issues are opened or assigned
|
||||||
- `pull_request_review` - When PR reviews are submitted
|
- `pull_request_review` - When PR reviews are submitted
|
||||||
- `pull_request_review_comment` - When comments are made on PR reviews
|
- `pull_request_review_comment` - When comments are made on PR reviews
|
||||||
- `repository_dispatch` - Custom events triggered via API (coming soon)
|
- `repository_dispatch` - Custom events triggered via API
|
||||||
- `workflow_dispatch` - Manual workflow triggers (coming soon)
|
- `workflow_dispatch` - Manual workflow triggers (coming soon)
|
||||||
|
|
||||||
## Automated Documentation Updates
|
## Automated Documentation Updates
|
||||||
|
|||||||
27
docs/faq.md
27
docs/faq.md
@@ -28,6 +28,33 @@ permissions:
|
|||||||
|
|
||||||
The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `github_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more.
|
The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `github_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more.
|
||||||
|
|
||||||
|
### Why am I getting '403 Resource not accessible by integration' errors?
|
||||||
|
|
||||||
|
This error occurs when the action tries to fetch the authenticated user information using a GitHub App installation token. GitHub App tokens have limited access and cannot access the `/user` endpoint, which causes this 403 error.
|
||||||
|
|
||||||
|
**Solution**: The action now includes `bot_id` and `bot_name` inputs that default to Claude's bot credentials. This avoids the need to fetch user information from the API.
|
||||||
|
|
||||||
|
For the default claude[bot]:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
# bot_id and bot_name have sensible defaults, no need to specify
|
||||||
|
```
|
||||||
|
|
||||||
|
For custom bots, specify both:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: anthropics/claude-code-action@v1
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
bot_id: "12345678" # Your bot's GitHub user ID
|
||||||
|
bot_name: "my-bot" # Your bot's username
|
||||||
|
```
|
||||||
|
|
||||||
|
This issue typically only affects agent/automation mode workflows. Interactive workflows (with @claude mentions) don't encounter this issue as they use the comment author's information.
|
||||||
|
|
||||||
## Claude's Capabilities and Limitations
|
## Claude's Capabilities and Limitations
|
||||||
|
|
||||||
### Why won't Claude update workflow files when I ask it to?
|
### Why won't Claude update workflow files when I ask it to?
|
||||||
|
|||||||
@@ -14,18 +14,19 @@ This guide helps you migrate from Claude Code Action v0.x to v1.0. The new versi
|
|||||||
|
|
||||||
The following inputs have been deprecated and replaced:
|
The following inputs have been deprecated and replaced:
|
||||||
|
|
||||||
| Deprecated Input | Replacement | Notes |
|
| Deprecated Input | Replacement | Notes |
|
||||||
| --------------------- | -------------------------------- | --------------------------------------------- |
|
| --------------------- | ------------------------------------ | --------------------------------------------- |
|
||||||
| `mode` | Auto-detected | Action automatically chooses based on context |
|
| `mode` | Auto-detected | Action automatically chooses based on context |
|
||||||
| `direct_prompt` | `prompt` | Direct drop-in replacement |
|
| `direct_prompt` | `prompt` | Direct drop-in replacement |
|
||||||
| `override_prompt` | `prompt` | Use GitHub context variables instead |
|
| `override_prompt` | `prompt` | Use GitHub context variables instead |
|
||||||
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
|
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
|
||||||
| `max_turns` | `claude_args: --max-turns` | Use CLI format |
|
| `max_turns` | `claude_args: --max-turns` | Use CLI format |
|
||||||
| `model` | `claude_args: --model` | Specify via CLI |
|
| `model` | `claude_args: --model` | Specify via CLI |
|
||||||
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
|
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
|
||||||
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
|
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
|
||||||
| `claude_env` | `settings` with env object | Use settings JSON |
|
| `claude_env` | `settings` with env object | Use settings JSON |
|
||||||
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
|
| `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
|
## Migration Examples
|
||||||
|
|
||||||
@@ -198,6 +199,30 @@ The `track_progress` input only works with these GitHub events:
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
## How Mode Detection Works
|
||||||
|
|
||||||
The action now automatically detects the appropriate mode:
|
The action now automatically detects the appropriate mode:
|
||||||
@@ -312,6 +337,7 @@ You can also pass MCP configuration from a file:
|
|||||||
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
|
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
|
||||||
- [ ] Move `claude_env` to `settings` JSON format
|
- [ ] Move `claude_env` to `settings` JSON format
|
||||||
- [ ] Move `mcp_config` to `claude_args` with `--mcp-config`
|
- [ ] 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
|
- [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode
|
||||||
- [ ] Test workflow in a non-production environment
|
- [ ] Test workflow in a non-production environment
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
|
|
||||||
- **Repository Access**: The action can only be triggered by users with write access to the repository
|
- **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
|
- **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
|
- **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
|
- **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
|
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
||||||
|
|||||||
@@ -47,28 +47,31 @@ jobs:
|
|||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
| Input | Description | Required | Default |
|
| Input | Description | Required | Default |
|
||||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------- |
|
||||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | 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 | - |
|
| `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` |
|
| `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 | "" |
|
| `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 | - |
|
| `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` |
|
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
|
||||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
| `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` |
|
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||||
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
|
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
|
||||||
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
||||||
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
|
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
|
||||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||||
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
|
| `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 | "" |
|
| `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 | "" |
|
| `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 | "" |
|
| `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` |
|
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||||
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
|
| `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 | "" |
|
||||||
|
|
||||||
### Deprecated Inputs
|
### Deprecated Inputs
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
issues: write
|
issues: write
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -72,5 +71,6 @@ jobs:
|
|||||||
- It's okay to not add any labels if none are clearly applicable
|
- It's okay to not add any labels if none are clearly applicable
|
||||||
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
claude_args: |
|
claude_args: |
|
||||||
--allowedTools "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"
|
--allowedTools "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"
|
||||||
|
|||||||
@@ -30,9 +30,13 @@ async function run() {
|
|||||||
|
|
||||||
// Step 3: Check write permissions (only for entity contexts)
|
// Step 3: Check write permissions (only for entity contexts)
|
||||||
if (isEntityContext(context)) {
|
if (isEntityContext(context)) {
|
||||||
|
// Check if github_token was provided as input (not from app)
|
||||||
|
const githubTokenProvided = !!process.env.OVERRIDE_GITHUB_TOKEN;
|
||||||
const hasWritePermissions = await checkWritePermissions(
|
const hasWritePermissions = await checkWritePermissions(
|
||||||
octokit.rest,
|
octokit.rest,
|
||||||
context,
|
context,
|
||||||
|
context.inputs.allowedNonWriteUsers,
|
||||||
|
githubTokenProvided,
|
||||||
);
|
);
|
||||||
if (!hasWritePermissions) {
|
if (!hasWritePermissions) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
|||||||
13
src/github/constants.ts
Normal file
13
src/github/constants.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* GitHub-related constants used throughout the application
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude App bot user ID
|
||||||
|
*/
|
||||||
|
export const CLAUDE_APP_BOT_ID = 41898282;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude bot username
|
||||||
|
*/
|
||||||
|
export const CLAUDE_BOT_LOGIN = "claude[bot]";
|
||||||
@@ -8,6 +8,7 @@ import type {
|
|||||||
PullRequestReviewCommentEvent,
|
PullRequestReviewCommentEvent,
|
||||||
WorkflowRunEvent,
|
WorkflowRunEvent,
|
||||||
} from "@octokit/webhooks-types";
|
} from "@octokit/webhooks-types";
|
||||||
|
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "./constants";
|
||||||
// Custom types for GitHub Actions events that aren't webhooks
|
// Custom types for GitHub Actions events that aren't webhooks
|
||||||
export type WorkflowDispatchEvent = {
|
export type WorkflowDispatchEvent = {
|
||||||
action?: never;
|
action?: never;
|
||||||
@@ -25,6 +26,20 @@ export type WorkflowDispatchEvent = {
|
|||||||
workflow: string;
|
workflow: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type RepositoryDispatchEvent = {
|
||||||
|
action: string;
|
||||||
|
client_payload?: Record<string, any>;
|
||||||
|
repository: {
|
||||||
|
name: string;
|
||||||
|
owner: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
sender: {
|
||||||
|
login: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type ScheduleEvent = {
|
export type ScheduleEvent = {
|
||||||
action?: never;
|
action?: never;
|
||||||
schedule?: string;
|
schedule?: string;
|
||||||
@@ -47,6 +62,7 @@ const ENTITY_EVENT_NAMES = [
|
|||||||
|
|
||||||
const AUTOMATION_EVENT_NAMES = [
|
const AUTOMATION_EVENT_NAMES = [
|
||||||
"workflow_dispatch",
|
"workflow_dispatch",
|
||||||
|
"repository_dispatch",
|
||||||
"schedule",
|
"schedule",
|
||||||
"workflow_run",
|
"workflow_run",
|
||||||
] as const;
|
] as const;
|
||||||
@@ -74,7 +90,10 @@ type BaseContext = {
|
|||||||
branchPrefix: string;
|
branchPrefix: string;
|
||||||
useStickyComment: boolean;
|
useStickyComment: boolean;
|
||||||
useCommitSigning: boolean;
|
useCommitSigning: boolean;
|
||||||
|
botId: string;
|
||||||
|
botName: string;
|
||||||
allowedBots: string;
|
allowedBots: string;
|
||||||
|
allowedNonWriteUsers: string;
|
||||||
trackProgress: boolean;
|
trackProgress: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -92,10 +111,14 @@ export type ParsedGitHubContext = BaseContext & {
|
|||||||
isPR: boolean;
|
isPR: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Context for automation events (workflow_dispatch, schedule, workflow_run)
|
// Context for automation events (workflow_dispatch, repository_dispatch, schedule, workflow_run)
|
||||||
export type AutomationContext = BaseContext & {
|
export type AutomationContext = BaseContext & {
|
||||||
eventName: AutomationEventName;
|
eventName: AutomationEventName;
|
||||||
payload: WorkflowDispatchEvent | ScheduleEvent | WorkflowRunEvent;
|
payload:
|
||||||
|
| WorkflowDispatchEvent
|
||||||
|
| RepositoryDispatchEvent
|
||||||
|
| ScheduleEvent
|
||||||
|
| WorkflowRunEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Union type for all contexts
|
// Union type for all contexts
|
||||||
@@ -122,7 +145,10 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||||
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
||||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||||
|
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
||||||
|
botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN,
|
||||||
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
||||||
|
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
|
||||||
trackProgress: process.env.TRACK_PROGRESS === "true",
|
trackProgress: process.env.TRACK_PROGRESS === "true",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -185,6 +211,13 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
payload: context.payload as unknown as WorkflowDispatchEvent,
|
payload: context.payload as unknown as WorkflowDispatchEvent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "repository_dispatch": {
|
||||||
|
return {
|
||||||
|
...commonFields,
|
||||||
|
eventName: "repository_dispatch",
|
||||||
|
payload: context.payload as unknown as RepositoryDispatchEvent,
|
||||||
|
};
|
||||||
|
}
|
||||||
case "schedule": {
|
case "schedule": {
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type GitUser = {
|
|||||||
export async function configureGitAuth(
|
export async function configureGitAuth(
|
||||||
githubToken: string,
|
githubToken: string,
|
||||||
context: GitHubContext,
|
context: GitHubContext,
|
||||||
user: GitUser | null,
|
user: GitUser,
|
||||||
) {
|
) {
|
||||||
console.log("Configuring git authentication for non-signing mode");
|
console.log("Configuring git authentication for non-signing mode");
|
||||||
|
|
||||||
@@ -28,20 +28,14 @@ export async function configureGitAuth(
|
|||||||
? "users.noreply.github.com"
|
? "users.noreply.github.com"
|
||||||
: `users.noreply.${serverUrl.hostname}`;
|
: `users.noreply.${serverUrl.hostname}`;
|
||||||
|
|
||||||
// Configure git user based on the comment creator
|
// Configure git user
|
||||||
console.log("Configuring git user...");
|
console.log("Configuring git user...");
|
||||||
if (user) {
|
const botName = user.login;
|
||||||
const botName = user.login;
|
const botId = user.id;
|
||||||
const botId = user.id;
|
console.log(`Setting git user as ${botName}...`);
|
||||||
console.log(`Setting git user as ${botName}...`);
|
await $`git config user.name "${botName}"`;
|
||||||
await $`git config user.name "${botName}"`;
|
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`;
|
||||||
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`;
|
console.log(`✓ Set git user as ${botName}`);
|
||||||
console.log(`✓ Set git user as ${botName}`);
|
|
||||||
} else {
|
|
||||||
console.log("No user data in comment, using default bot user");
|
|
||||||
await $`git config user.name "github-actions[bot]"`;
|
|
||||||
await $`git config user.email "41898282+github-actions[bot]@${noreplyDomain}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the authorization header that actions/checkout sets
|
// Remove the authorization header that actions/checkout sets
|
||||||
console.log("Removing existing git authentication headers...");
|
console.log("Removing existing git authentication headers...");
|
||||||
|
|||||||
@@ -6,17 +6,43 @@ import type { Octokit } from "@octokit/rest";
|
|||||||
* Check if the actor has write permissions to the repository
|
* Check if the actor has write permissions to the repository
|
||||||
* @param octokit - The Octokit REST client
|
* @param octokit - The Octokit REST client
|
||||||
* @param context - The GitHub context
|
* @param context - The GitHub context
|
||||||
|
* @param allowedNonWriteUsers - Comma-separated list of users allowed without write permissions, or '*' for all
|
||||||
|
* @param githubTokenProvided - Whether github_token was provided as input (not from app)
|
||||||
* @returns true if the actor has write permissions, false otherwise
|
* @returns true if the actor has write permissions, false otherwise
|
||||||
*/
|
*/
|
||||||
export async function checkWritePermissions(
|
export async function checkWritePermissions(
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
|
allowedNonWriteUsers?: string,
|
||||||
|
githubTokenProvided?: boolean,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const { repository, actor } = context;
|
const { repository, actor } = context;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
core.info(`Checking permissions for actor: ${actor}`);
|
core.info(`Checking permissions for actor: ${actor}`);
|
||||||
|
|
||||||
|
// Check if we should bypass permission checks for this user
|
||||||
|
if (allowedNonWriteUsers && githubTokenProvided) {
|
||||||
|
const allowedUsers = allowedNonWriteUsers.trim();
|
||||||
|
if (allowedUsers === "*") {
|
||||||
|
core.warning(
|
||||||
|
`⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else if (allowedUsers) {
|
||||||
|
const allowedUserList = allowedUsers
|
||||||
|
.split(",")
|
||||||
|
.map((u) => u.trim())
|
||||||
|
.filter((u) => u.length > 0);
|
||||||
|
if (allowedUserList.includes(actor)) {
|
||||||
|
core.warning(
|
||||||
|
`⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.`,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the actor is a GitHub App (bot user)
|
// Check if the actor is a GitHub App (bot user)
|
||||||
if (actor.endsWith("[bot]")) {
|
if (actor.endsWith("[bot]")) {
|
||||||
core.info(`Actor is a GitHub App: ${actor}`);
|
core.info(`Actor is a GitHub App: ${actor}`);
|
||||||
|
|||||||
@@ -74,10 +74,6 @@ export async function prepareMcpConfig(
|
|||||||
tool.startsWith("mcp__github_inline_comment__"),
|
tool.startsWith("mcp__github_inline_comment__"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const hasGitHubCommentTools = allowedToolsList.some((tool) =>
|
|
||||||
tool.startsWith("mcp__github_comment__"),
|
|
||||||
);
|
|
||||||
|
|
||||||
const hasGitHubCITools = allowedToolsList.some((tool) =>
|
const hasGitHubCITools = allowedToolsList.some((tool) =>
|
||||||
tool.startsWith("mcp__github_ci__"),
|
tool.startsWith("mcp__github_ci__"),
|
||||||
);
|
);
|
||||||
@@ -89,7 +85,7 @@ export async function prepareMcpConfig(
|
|||||||
// Include comment server:
|
// Include comment server:
|
||||||
// - Always in tag mode (for updating Claude comments)
|
// - Always in tag mode (for updating Claude comments)
|
||||||
// - Only with explicit tools in agent mode
|
// - Only with explicit tools in agent mode
|
||||||
const shouldIncludeCommentServer = !isAgentMode || hasGitHubCommentTools;
|
const shouldIncludeCommentServer = !isAgentMode;
|
||||||
|
|
||||||
if (shouldIncludeCommentServer) {
|
if (shouldIncludeCommentServer) {
|
||||||
baseMcpConfig.mcpServers.github_comment = {
|
baseMcpConfig.mcpServers.github_comment = {
|
||||||
|
|||||||
@@ -77,22 +77,16 @@ export const agentMode: Mode = {
|
|||||||
return false;
|
return false;
|
||||||
},
|
},
|
||||||
|
|
||||||
async prepare({
|
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
||||||
context,
|
|
||||||
githubToken,
|
|
||||||
octokit,
|
|
||||||
}: ModeOptions): Promise<ModeResult> {
|
|
||||||
// Configure git authentication for agent mode (same as tag mode)
|
// Configure git authentication for agent mode (same as tag mode)
|
||||||
if (!context.inputs.useCommitSigning) {
|
if (!context.inputs.useCommitSigning) {
|
||||||
try {
|
// Use bot_id and bot_name from inputs directly
|
||||||
// Get the authenticated user (will be claude[bot] when using Claude App token)
|
const user = {
|
||||||
const { data: authenticatedUser } =
|
login: context.inputs.botName,
|
||||||
await octokit.rest.users.getAuthenticated();
|
id: parseInt(context.inputs.botId),
|
||||||
const user = {
|
};
|
||||||
login: authenticatedUser.login,
|
|
||||||
id: authenticatedUser.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
try {
|
||||||
// Use the shared git configuration function
|
// Use the shared git configuration function
|
||||||
await configureGitAuth(githubToken, context, user);
|
await configureGitAuth(githubToken, context, user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -44,6 +44,10 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
|
|||||||
|
|
||||||
// Issue events
|
// Issue events
|
||||||
if (isEntityContext(context) && isIssuesEvent(context)) {
|
if (isEntityContext(context) && isIssuesEvent(context)) {
|
||||||
|
// If prompt is provided, use agent mode (same as PR events)
|
||||||
|
if (context.inputs.prompt) {
|
||||||
|
return "agent";
|
||||||
|
}
|
||||||
// Check for @claude mentions or labels/assignees
|
// Check for @claude mentions or labels/assignees
|
||||||
if (checkContainsTrigger(context)) {
|
if (checkContainsTrigger(context)) {
|
||||||
return "tag";
|
return "tag";
|
||||||
|
|||||||
@@ -89,8 +89,14 @@ export const tagMode: Mode = {
|
|||||||
|
|
||||||
// Configure git authentication if not using commit signing
|
// Configure git authentication if not using commit signing
|
||||||
if (!context.inputs.useCommitSigning) {
|
if (!context.inputs.useCommitSigning) {
|
||||||
|
// Use bot_id and bot_name from inputs directly
|
||||||
|
const user = {
|
||||||
|
login: context.inputs.botName,
|
||||||
|
id: parseInt(context.inputs.botId),
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await configureGitAuth(githubToken, context, commentData.user);
|
await configureGitAuth(githubToken, context, user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to configure git authentication:", error);
|
console.error("Failed to configure git authentication:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
|||||||
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
|
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import type { ParsedGitHubContext } from "../src/github/context";
|
import type { ParsedGitHubContext } from "../src/github/context";
|
||||||
|
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants";
|
||||||
|
|
||||||
describe("prepareMcpConfig", () => {
|
describe("prepareMcpConfig", () => {
|
||||||
let consoleInfoSpy: any;
|
let consoleInfoSpy: any;
|
||||||
@@ -31,7 +32,10 @@ describe("prepareMcpConfig", () => {
|
|||||||
branchPrefix: "",
|
branchPrefix: "",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
botId: String(CLAUDE_APP_BOT_ID),
|
||||||
|
botName: CLAUDE_BOT_LOGIN,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
|
allowedNonWriteUsers: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
ParsedGitHubContext,
|
ParsedGitHubContext,
|
||||||
AutomationContext,
|
AutomationContext,
|
||||||
|
RepositoryDispatchEvent,
|
||||||
} from "../src/github/context";
|
} from "../src/github/context";
|
||||||
import type {
|
import type {
|
||||||
IssuesEvent,
|
IssuesEvent,
|
||||||
@@ -9,6 +10,7 @@ import type {
|
|||||||
PullRequestReviewEvent,
|
PullRequestReviewEvent,
|
||||||
PullRequestReviewCommentEvent,
|
PullRequestReviewCommentEvent,
|
||||||
} from "@octokit/webhooks-types";
|
} from "@octokit/webhooks-types";
|
||||||
|
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants";
|
||||||
|
|
||||||
const defaultInputs = {
|
const defaultInputs = {
|
||||||
prompt: "",
|
prompt: "",
|
||||||
@@ -18,7 +20,10 @@ const defaultInputs = {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
botId: String(CLAUDE_APP_BOT_ID),
|
||||||
|
botName: CLAUDE_BOT_LOGIN,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
|
allowedNonWriteUsers: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -78,6 +83,33 @@ export const createMockAutomationContext = (
|
|||||||
return { ...baseContext, ...overrides, inputs: mergedInputs };
|
return { ...baseContext, ...overrides, inputs: mergedInputs };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mockRepositoryDispatchContext: AutomationContext = {
|
||||||
|
runId: "1234567890",
|
||||||
|
eventName: "repository_dispatch",
|
||||||
|
eventAction: undefined,
|
||||||
|
repository: defaultRepository,
|
||||||
|
actor: "automation-user",
|
||||||
|
payload: {
|
||||||
|
action: "trigger-analysis",
|
||||||
|
client_payload: {
|
||||||
|
source: "issue-detective",
|
||||||
|
issue_number: 42,
|
||||||
|
repository_name: "test-owner/test-repo",
|
||||||
|
analysis_type: "bug-report",
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
name: "test-repo",
|
||||||
|
owner: {
|
||||||
|
login: "test-owner",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
login: "automation-user",
|
||||||
|
},
|
||||||
|
} as RepositoryDispatchEvent,
|
||||||
|
inputs: defaultInputs,
|
||||||
|
};
|
||||||
|
|
||||||
export const mockIssueOpenedContext: ParsedGitHubContext = {
|
export const mockIssueOpenedContext: ParsedGitHubContext = {
|
||||||
runId: "1234567890",
|
runId: "1234567890",
|
||||||
eventName: "issues",
|
eventName: "issues",
|
||||||
|
|||||||
@@ -1,13 +1,23 @@
|
|||||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
import {
|
||||||
|
describe,
|
||||||
|
test,
|
||||||
|
expect,
|
||||||
|
beforeEach,
|
||||||
|
afterEach,
|
||||||
|
spyOn,
|
||||||
|
mock,
|
||||||
|
} from "bun:test";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
import type { GitHubContext } from "../../src/github/context";
|
import type { GitHubContext } from "../../src/github/context";
|
||||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
import * as gitConfig from "../../src/github/operations/git-config";
|
||||||
|
|
||||||
describe("Agent Mode", () => {
|
describe("Agent Mode", () => {
|
||||||
let mockContext: GitHubContext;
|
let mockContext: GitHubContext;
|
||||||
let exportVariableSpy: any;
|
let exportVariableSpy: any;
|
||||||
let setOutputSpy: any;
|
let setOutputSpy: any;
|
||||||
|
let configureGitAuthSpy: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockContext = createMockAutomationContext({
|
mockContext = createMockAutomationContext({
|
||||||
@@ -17,13 +27,22 @@ describe("Agent Mode", () => {
|
|||||||
() => {},
|
() => {},
|
||||||
);
|
);
|
||||||
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
||||||
|
// Mock configureGitAuth to prevent actual git commands from running
|
||||||
|
configureGitAuthSpy = spyOn(
|
||||||
|
gitConfig,
|
||||||
|
"configureGitAuth",
|
||||||
|
).mockImplementation(async () => {
|
||||||
|
// Do nothing - prevent actual git config modifications
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
exportVariableSpy?.mockClear();
|
exportVariableSpy?.mockClear();
|
||||||
setOutputSpy?.mockClear();
|
setOutputSpy?.mockClear();
|
||||||
|
configureGitAuthSpy?.mockClear();
|
||||||
exportVariableSpy?.mockRestore();
|
exportVariableSpy?.mockRestore();
|
||||||
setOutputSpy?.mockRestore();
|
setOutputSpy?.mockRestore();
|
||||||
|
configureGitAuthSpy?.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("agent mode has correct properties", () => {
|
test("agent mode has correct properties", () => {
|
||||||
@@ -57,6 +76,11 @@ describe("Agent Mode", () => {
|
|||||||
});
|
});
|
||||||
expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
|
expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
|
||||||
|
|
||||||
|
const repositoryDispatchContext = createMockAutomationContext({
|
||||||
|
eventName: "repository_dispatch",
|
||||||
|
});
|
||||||
|
expect(agentMode.shouldTrigger(repositoryDispatchContext)).toBe(false);
|
||||||
|
|
||||||
// Should NOT trigger for entity events without prompt
|
// Should NOT trigger for entity events without prompt
|
||||||
const entityEvents = [
|
const entityEvents = [
|
||||||
"issue_comment",
|
"issue_comment",
|
||||||
@@ -73,6 +97,7 @@ describe("Agent Mode", () => {
|
|||||||
// Should trigger for ANY event when prompt is provided
|
// Should trigger for ANY event when prompt is provided
|
||||||
const allEvents = [
|
const allEvents = [
|
||||||
"workflow_dispatch",
|
"workflow_dispatch",
|
||||||
|
"repository_dispatch",
|
||||||
"schedule",
|
"schedule",
|
||||||
"issue_comment",
|
"issue_comment",
|
||||||
"pull_request",
|
"pull_request",
|
||||||
@@ -82,7 +107,9 @@ describe("Agent Mode", () => {
|
|||||||
|
|
||||||
allEvents.forEach((eventName) => {
|
allEvents.forEach((eventName) => {
|
||||||
const contextWithPrompt =
|
const contextWithPrompt =
|
||||||
eventName === "workflow_dispatch" || eventName === "schedule"
|
eventName === "workflow_dispatch" ||
|
||||||
|
eventName === "repository_dispatch" ||
|
||||||
|
eventName === "schedule"
|
||||||
? createMockAutomationContext({
|
? createMockAutomationContext({
|
||||||
eventName,
|
eventName,
|
||||||
inputs: { prompt: "Do something" },
|
inputs: { prompt: "Do something" },
|
||||||
@@ -113,7 +140,22 @@ describe("Agent Mode", () => {
|
|||||||
// Set CLAUDE_ARGS environment variable
|
// Set CLAUDE_ARGS environment variable
|
||||||
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
|
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
|
||||||
|
|
||||||
const mockOctokit = {} as any;
|
const mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
users: {
|
||||||
|
getAuthenticated: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "test-user", id: 12345 },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
getByUsername: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "test-user", id: 12345 },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
const result = await agentMode.prepare({
|
const result = await agentMode.prepare({
|
||||||
context: contextWithCustomArgs,
|
context: contextWithCustomArgs,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
@@ -152,7 +194,22 @@ describe("Agent Mode", () => {
|
|||||||
// In v1-dev, we only have the unified prompt field
|
// In v1-dev, we only have the unified prompt field
|
||||||
contextWithPrompts.inputs.prompt = "Custom prompt content";
|
contextWithPrompts.inputs.prompt = "Custom prompt content";
|
||||||
|
|
||||||
const mockOctokit = {} as any;
|
const mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
users: {
|
||||||
|
getAuthenticated: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "test-user", id: 12345 },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
getByUsername: mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
data: { login: "test-user", id: 12345 },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
await agentMode.prepare({
|
await agentMode.prepare({
|
||||||
context: contextWithPrompts,
|
context: contextWithPrompts,
|
||||||
octokit: mockOctokit,
|
octokit: mockOctokit,
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { describe, test, expect } from "bun:test";
|
|||||||
import { getMode, isValidMode } from "../../src/modes/registry";
|
import { getMode, isValidMode } from "../../src/modes/registry";
|
||||||
import { agentMode } from "../../src/modes/agent";
|
import { agentMode } from "../../src/modes/agent";
|
||||||
import { tagMode } from "../../src/modes/tag";
|
import { tagMode } from "../../src/modes/tag";
|
||||||
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
import {
|
||||||
|
createMockContext,
|
||||||
|
createMockAutomationContext,
|
||||||
|
mockRepositoryDispatchContext,
|
||||||
|
} from "../mockContext";
|
||||||
|
|
||||||
describe("Mode Registry", () => {
|
describe("Mode Registry", () => {
|
||||||
const mockContext = createMockContext({
|
const mockContext = createMockContext({
|
||||||
@@ -50,6 +54,34 @@ describe("Mode Registry", () => {
|
|||||||
expect(mode.name).toBe("agent");
|
expect(mode.name).toBe("agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("getMode auto-detects agent for repository_dispatch event", () => {
|
||||||
|
const mode = getMode(mockRepositoryDispatchContext);
|
||||||
|
expect(mode).toBe(agentMode);
|
||||||
|
expect(mode.name).toBe("agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("getMode auto-detects agent for repository_dispatch with client_payload", () => {
|
||||||
|
const contextWithPayload = createMockAutomationContext({
|
||||||
|
eventName: "repository_dispatch",
|
||||||
|
payload: {
|
||||||
|
action: "trigger-analysis",
|
||||||
|
client_payload: {
|
||||||
|
source: "external-system",
|
||||||
|
metadata: { priority: "high" },
|
||||||
|
},
|
||||||
|
repository: {
|
||||||
|
name: "test-repo",
|
||||||
|
owner: { login: "test-owner" },
|
||||||
|
},
|
||||||
|
sender: { login: "automation-user" },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mode = getMode(contextWithPayload);
|
||||||
|
expect(mode).toBe(agentMode);
|
||||||
|
expect(mode.name).toBe("agent");
|
||||||
|
});
|
||||||
|
|
||||||
// Removed test - legacy mode names no longer supported in v1.0
|
// Removed test - legacy mode names no longer supported in v1.0
|
||||||
|
|
||||||
test("getMode auto-detects agent mode for PR opened", () => {
|
test("getMode auto-detects agent mode for PR opened", () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test";
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { checkWritePermissions } from "../src/github/validation/permissions";
|
import { checkWritePermissions } from "../src/github/validation/permissions";
|
||||||
import type { ParsedGitHubContext } from "../src/github/context";
|
import type { ParsedGitHubContext } from "../src/github/context";
|
||||||
|
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "../src/github/constants";
|
||||||
|
|
||||||
describe("checkWritePermissions", () => {
|
describe("checkWritePermissions", () => {
|
||||||
let coreInfoSpy: any;
|
let coreInfoSpy: any;
|
||||||
@@ -67,7 +68,10 @@ describe("checkWritePermissions", () => {
|
|||||||
branchPrefix: "claude/",
|
branchPrefix: "claude/",
|
||||||
useStickyComment: false,
|
useStickyComment: false,
|
||||||
useCommitSigning: false,
|
useCommitSigning: false,
|
||||||
|
botId: String(CLAUDE_APP_BOT_ID),
|
||||||
|
botName: CLAUDE_BOT_LOGIN,
|
||||||
allowedBots: "",
|
allowedBots: "",
|
||||||
|
allowedNonWriteUsers: "",
|
||||||
trackProgress: false,
|
trackProgress: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -172,4 +176,126 @@ describe("checkWritePermissions", () => {
|
|||||||
username: "test-user",
|
username: "test-user",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("allowed_non_write_users bypass", () => {
|
||||||
|
test("should bypass permission check for specific user when github_token provided", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("read");
|
||||||
|
const context = createContext();
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(
|
||||||
|
mockOctokit,
|
||||||
|
context,
|
||||||
|
"test-user,other-user",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||||
|
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should bypass permission check for all users with wildcard", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("read");
|
||||||
|
const context = createContext();
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(
|
||||||
|
mockOctokit,
|
||||||
|
context,
|
||||||
|
"*",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||||
|
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should NOT bypass permission check when user not in allowed list", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("read");
|
||||||
|
const context = createContext();
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(
|
||||||
|
mockOctokit,
|
||||||
|
context,
|
||||||
|
"other-user,another-user",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||||
|
"Actor has insufficient permissions: read",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should NOT bypass permission check when github_token not provided", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("read");
|
||||||
|
const context = createContext();
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(
|
||||||
|
mockOctokit,
|
||||||
|
context,
|
||||||
|
"test-user",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||||
|
"Actor has insufficient permissions: read",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should NOT bypass permission check when allowed_non_write_users is empty", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("read");
|
||||||
|
const context = createContext();
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(
|
||||||
|
mockOctokit,
|
||||||
|
context,
|
||||||
|
"",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||||
|
"Actor has insufficient permissions: read",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle whitespace in allowed_non_write_users list", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("read");
|
||||||
|
const context = createContext();
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(
|
||||||
|
mockOctokit,
|
||||||
|
context,
|
||||||
|
" test-user , other-user ",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(coreWarningSpy).toHaveBeenCalledWith(
|
||||||
|
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should bypass for bot users even when allowed_non_write_users is set", async () => {
|
||||||
|
const mockOctokit = createMockOctokit("none");
|
||||||
|
const context = createContext();
|
||||||
|
context.actor = "test-bot[bot]";
|
||||||
|
|
||||||
|
const result = await checkWritePermissions(
|
||||||
|
mockOctokit,
|
||||||
|
context,
|
||||||
|
"some-user",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(coreInfoSpy).toHaveBeenCalledWith(
|
||||||
|
"Actor is a GitHub App: test-bot[bot]",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -113,6 +113,33 @@ describe("detectMode with enhanced routing", () => {
|
|||||||
|
|
||||||
expect(detectMode(context)).toBe("agent");
|
expect(detectMode(context)).toBe("agent");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use agent mode for issues with explicit prompt", () => {
|
||||||
|
const context: GitHubContext = {
|
||||||
|
...baseContext,
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "opened",
|
||||||
|
payload: { issue: { number: 1, body: "Test issue" } } as any,
|
||||||
|
entityNumber: 1,
|
||||||
|
isPR: false,
|
||||||
|
inputs: { ...baseContext.inputs, prompt: "Analyze this issue" },
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(detectMode(context)).toBe("agent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use tag mode for issues with @claude mention and no prompt", () => {
|
||||||
|
const context: GitHubContext = {
|
||||||
|
...baseContext,
|
||||||
|
eventName: "issues",
|
||||||
|
eventAction: "opened",
|
||||||
|
payload: { issue: { number: 1, body: "@claude help" } } as any,
|
||||||
|
entityNumber: 1,
|
||||||
|
isPR: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(detectMode(context)).toBe("tag");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Comment Events (unchanged behavior)", () => {
|
describe("Comment Events (unchanged behavior)", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user