mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
13 Commits
add-claude
...
ashwin/bra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b12957001c | ||
|
|
60112e2967 | ||
|
|
ddb3ef092b | ||
|
|
35c0ca2406 | ||
|
|
1a49e00b3e | ||
|
|
e758a32de7 | ||
|
|
6246ecdcb5 | ||
|
|
32e7aeee0e | ||
|
|
40c2c1e7b4 | ||
|
|
9c4bc5dc35 | ||
|
|
b5de2b9913 | ||
|
|
4991c0c947 | ||
|
|
726d5808b5 |
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -9,7 +9,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
prettier:
|
prettier:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v1
|
- uses: oven-sh/setup-bun@v1
|
||||||
with:
|
with:
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
typecheck:
|
typecheck:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: oven-sh/setup-bun@v2
|
- uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
|
|||||||
57
.github/workflows/claude-code-review.yml
vendored
57
.github/workflows/claude-code-review.yml
vendored
@@ -1,57 +0,0 @@
|
|||||||
name: Claude Code Review
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize]
|
|
||||||
# Optional: Only run on specific file changes
|
|
||||||
# paths:
|
|
||||||
# - "src/**/*.ts"
|
|
||||||
# - "src/**/*.tsx"
|
|
||||||
# - "src/**/*.js"
|
|
||||||
# - "src/**/*.jsx"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
claude-review:
|
|
||||||
# Optional: Filter by PR author
|
|
||||||
# if: |
|
|
||||||
# github.event.pull_request.user.login == 'external-contributor' ||
|
|
||||||
# github.event.pull_request.user.login == 'new-developer' ||
|
|
||||||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: read
|
|
||||||
issues: read
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Run Claude Code Review
|
|
||||||
id: claude-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 and provide feedback on:
|
|
||||||
- Code quality and best practices
|
|
||||||
- Potential bugs or issues
|
|
||||||
- Performance considerations
|
|
||||||
- Security concerns
|
|
||||||
- Test coverage
|
|
||||||
|
|
||||||
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
|
|
||||||
|
|
||||||
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
|
|
||||||
|
|
||||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
||||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
|
||||||
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'
|
|
||||||
|
|
||||||
2
.github/workflows/claude-review.yml
vendored
2
.github/workflows/claude-review.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
38
.github/workflows/claude-test.yml
vendored
Normal file
38
.github/workflows/claude-test.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Test workflow for km-anthropic fork (v1-dev branch)
|
||||||
|
# This tests the fork implementation, not the main repo
|
||||||
|
name: Claude Code (Fork Test)
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created]
|
||||||
|
issues:
|
||||||
|
types: [opened, assigned]
|
||||||
|
pull_request_review:
|
||||||
|
types: [submitted]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude:
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||||
|
(github.event_name == 'issues' && (
|
||||||
|
contains(github.event.issue.body, '@claude') ||
|
||||||
|
contains(github.event.issue.title, '@claude')
|
||||||
|
))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
id-token: write # Required for OIDC token exchange
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Run Claude Code
|
||||||
|
uses: km-anthropic/claude-code-action@v1-dev
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
17
.github/workflows/claude.yml
vendored
17
.github/workflows/claude.yml
vendored
@@ -23,7 +23,6 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
issues: read
|
issues: read
|
||||||
id-token: write
|
id-token: write
|
||||||
actions: read # Required for Claude to read CI results on PRs
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -35,16 +34,6 @@ jobs:
|
|||||||
uses: anthropics/claude-code-action@v1
|
uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
claude_args: |
|
||||||
# This is an optional setting that allows Claude to read CI results on PRs
|
--allowedTools "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
|
||||||
additional_permissions: |
|
--model "claude-opus-4-1-20250805"
|
||||||
actions: read
|
|
||||||
|
|
||||||
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
|
|
||||||
# prompt: 'Update the pull request description to include a summary of changes.'
|
|
||||||
|
|
||||||
# Optional: Add claude_args to customize behavior and configuration
|
|
||||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
|
||||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
|
||||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
next_version: ${{ steps.next_version.outputs.next_version }}
|
next_version: ${{ steps.next_version.outputs.next_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ jobs:
|
|||||||
environment: production
|
environment: production
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout base-action repo
|
- name: Checkout base-action repo
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
repository: anthropics/claude-code-base-action
|
repository: anthropics/claude-code-base-action
|
||||||
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||||
|
|||||||
4
.github/workflows/test-settings.yml
vendored
4
.github/workflows/test-settings.yml
vendored
@@ -67,7 +67,7 @@ jobs:
|
|||||||
uses: ./base-action
|
uses: ./base-action
|
||||||
with:
|
with:
|
||||||
prompt: |
|
prompt: |
|
||||||
Run the command `echo $HOME` to check the home directory path
|
Use Bash to echo "This should not work"
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
settings: |
|
settings: |
|
||||||
{
|
{
|
||||||
@@ -163,7 +163,7 @@ jobs:
|
|||||||
uses: ./base-action
|
uses: ./base-action
|
||||||
with:
|
with:
|
||||||
prompt: |
|
prompt: |
|
||||||
Run the command `echo $HOME` to check the home directory path
|
Use Bash to echo "This should not work from file"
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
settings: "test-settings.json"
|
settings: "test-settings.json"
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ inputs:
|
|||||||
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
||||||
required: false
|
required: false
|
||||||
default: "claude/"
|
default: "claude/"
|
||||||
|
branch_name_template:
|
||||||
|
description: "Template for branch naming. Available variables: {{prefix}}, {{entityType}}, {{entityNumber}}, {{timestamp}}, {{sha}}, {{label}}, {{description}}. {{label}} will be first label from the issue/PR, or {{entityType}} as a fallback. {{description}} will be the first 3 words of the issue/PR title in kebab-case. Default: '{{prefix}}{{entityType}}-{{entityNumber}}-{{timestamp}}'"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
allowed_bots:
|
allowed_bots:
|
||||||
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
|
||||||
@@ -150,6 +154,7 @@ runs:
|
|||||||
LABEL_TRIGGER: ${{ inputs.label_trigger }}
|
LABEL_TRIGGER: ${{ inputs.label_trigger }}
|
||||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||||
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
||||||
|
BRANCH_NAME_TEMPLATE: ${{ inputs.branch_name_template }}
|
||||||
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 }}
|
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
|
||||||
@@ -177,7 +182,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 2.0.24
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.113
|
||||||
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 }}"
|
||||||
@@ -259,7 +264,7 @@ runs:
|
|||||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||||
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
|
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
|
||||||
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
||||||
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review_comment' }}
|
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
||||||
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
||||||
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
||||||
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
||||||
|
|||||||
@@ -336,7 +336,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -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 2.0.24
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.113
|
||||||
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
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
"--rm",
|
"--rm",
|
||||||
"-e",
|
"-e",
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||||
"ghcr.io/github/github-mcp-server:sha-23fa0dd"
|
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||||
],
|
],
|
||||||
"env": {
|
"env": {
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ The action automatically detects which mode to use based on your configuration:
|
|||||||
|
|
||||||
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)):
|
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
|
- `pull_request` - When PRs are opened or synchronized
|
||||||
- `issue_comment` - When comments are created on issues or PRs
|
- `issue_comment` - When comments are created on issues or PRs
|
||||||
- `pull_request_comment` - When comments are made on PR diffs
|
- `pull_request_comment` - When comments are made on PR diffs
|
||||||
- `issues` - When issues are opened or assigned
|
- `issues` - When issues are opened or assigned
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ For performance, Claude uses shallow clones:
|
|||||||
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
|
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
|
||||||
|
|
||||||
```
|
```
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
depth: 0 # will fetch full repo history
|
depth: 0 # will fetch full repo history
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -13,28 +13,13 @@
|
|||||||
- **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
|
||||||
|
|
||||||
## ⚠️ Prompt Injection Risks
|
|
||||||
|
|
||||||
**Beware of potential hidden markdown when tagging Claude on untrusted content.** External contributors may include hidden instructions through HTML comments, invisible characters, hidden attributes, or other techniques. The action sanitizes content by stripping HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities, but new bypass techniques may emerge. We recommend reviewing the raw content of all input coming from external contributors before allowing Claude to process it.
|
|
||||||
|
|
||||||
## GitHub App Permissions
|
## GitHub App Permissions
|
||||||
|
|
||||||
The [Claude Code GitHub app](https://github.com/apps/claude) requests the following permissions:
|
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
|
||||||
|
|
||||||
### Currently Used Permissions
|
- **Pull Requests**: Read and write to create PRs and push changes
|
||||||
|
- **Issues**: Read and write to respond to issues
|
||||||
- **Contents** (Read & Write): For reading repository files and creating branches
|
- **Contents**: Read and write to modify repository files
|
||||||
- **Pull Requests** (Read & Write): For reading PR data and creating/updating pull requests
|
|
||||||
- **Issues** (Read & Write): For reading issue data and updating issue comments
|
|
||||||
|
|
||||||
### Permissions for Future Features
|
|
||||||
|
|
||||||
The following permissions are requested but not yet actively used. These will enable planned features in future releases:
|
|
||||||
|
|
||||||
- **Discussions** (Read & Write): For interaction with GitHub Discussions
|
|
||||||
- **Actions** (Read): For accessing workflow run data and logs
|
|
||||||
- **Checks** (Read): For reading check run results
|
|
||||||
- **Workflows** (Read & Write): For triggering and managing GitHub Actions workflows
|
|
||||||
|
|
||||||
## Commit Signing
|
## Commit Signing
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -211,7 +211,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -268,7 +268,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
@@ -344,7 +344,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -456,7 +456,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@@ -513,7 +513,7 @@ jobs:
|
|||||||
security-events: write
|
security-events: write
|
||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ jobs:
|
|||||||
| `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 | "" |
|
||||||
| `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` |
|
||||||
@@ -89,7 +90,6 @@ These inputs are deprecated and will be removed in a future version:
|
|||||||
| `fallback_model` | **DEPRECATED**: Use `claude_args` with fallback configuration | Configure fallback in `claude_args` or `settings` |
|
| `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"` |
|
| `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"` |
|
| `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 |
|
| `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)
|
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.workflow_run.head_branch }}
|
ref: ${{ github.event.workflow_run.head_branch }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ jobs:
|
|||||||
actions: read # Required for Claude to read CI results on PRs
|
actions: read # Required for Claude to read CI results on PRs
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
|||||||
@@ -384,7 +384,6 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
|||||||
};
|
};
|
||||||
|
|
||||||
case "pull_request":
|
case "pull_request":
|
||||||
case "pull_request_target":
|
|
||||||
return {
|
return {
|
||||||
eventType: "PULL_REQUEST",
|
eventType: "PULL_REQUEST",
|
||||||
triggerContext: eventData.eventAction
|
triggerContext: eventData.eventAction
|
||||||
@@ -709,7 +708,7 @@ What You CANNOT Do:
|
|||||||
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
|
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
|
||||||
|
|
||||||
When users ask you to perform actions you cannot do, politely explain the limitation and, when applicable, direct them to the FAQ for more information and workarounds:
|
When users ask you to perform actions you cannot do, politely explain the limitation and, when applicable, direct them to the FAQ for more information and workarounds:
|
||||||
"I'm unable to [specific action] due to [reason]. You can find more information and potential workarounds in the [FAQ](https://github.com/anthropics/claude-code-action/blob/main/docs/faq.md)."
|
"I'm unable to [specific action] due to [reason]. You can find more information and potential workarounds in the [FAQ](https://github.com/anthropics/claude-code-action/blob/main/FAQ.md)."
|
||||||
|
|
||||||
If a user asks for something outside these capabilities (and you have no other tools provided), politely explain that you cannot perform that action and suggest an alternative approach if possible.
|
If a user asks for something outside these capabilities (and you have no other tools provided), politely explain that you cannot perform that action and suggest an alternative approach if possible.
|
||||||
|
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ type IssueLabeledEvent = {
|
|||||||
labelTrigger: string;
|
labelTrigger: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PullRequestBaseEvent = {
|
type PullRequestEvent = {
|
||||||
|
eventName: "pull_request";
|
||||||
eventAction?: string; // opened, synchronize, etc.
|
eventAction?: string; // opened, synchronize, etc.
|
||||||
isPR: true;
|
isPR: true;
|
||||||
prNumber: string;
|
prNumber: string;
|
||||||
@@ -86,14 +87,6 @@ type PullRequestBaseEvent = {
|
|||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PullRequestEvent = PullRequestBaseEvent & {
|
|
||||||
eventName: "pull_request";
|
|
||||||
};
|
|
||||||
|
|
||||||
type PullRequestTargetEvent = PullRequestBaseEvent & {
|
|
||||||
eventName: "pull_request_target";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Union type for all possible event types
|
// Union type for all possible event types
|
||||||
export type EventData =
|
export type EventData =
|
||||||
| PullRequestReviewCommentEvent
|
| PullRequestReviewCommentEvent
|
||||||
@@ -103,8 +96,7 @@ export type EventData =
|
|||||||
| IssueOpenedEvent
|
| IssueOpenedEvent
|
||||||
| IssueAssignedEvent
|
| IssueAssignedEvent
|
||||||
| IssueLabeledEvent
|
| IssueLabeledEvent
|
||||||
| PullRequestEvent
|
| PullRequestEvent;
|
||||||
| PullRequestTargetEvent;
|
|
||||||
|
|
||||||
// Combined type with separate eventData field
|
// Combined type with separate eventData field
|
||||||
export type PreparedContext = CommonFields & {
|
export type PreparedContext = CommonFields & {
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ export const PR_QUERY = `
|
|||||||
additions
|
additions
|
||||||
deletions
|
deletions
|
||||||
state
|
state
|
||||||
|
labels(first: 1) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
commits(first: 100) {
|
commits(first: 100) {
|
||||||
totalCount
|
totalCount
|
||||||
nodes {
|
nodes {
|
||||||
@@ -97,6 +102,11 @@ export const ISSUE_QUERY = `
|
|||||||
}
|
}
|
||||||
createdAt
|
createdAt
|
||||||
state
|
state
|
||||||
|
labels(first: 1) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
comments(first: 100) {
|
comments(first: 100) {
|
||||||
nodes {
|
nodes {
|
||||||
id
|
id
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ type BaseContext = {
|
|||||||
labelTrigger: string;
|
labelTrigger: string;
|
||||||
baseBranch?: string;
|
baseBranch?: string;
|
||||||
branchPrefix: string;
|
branchPrefix: string;
|
||||||
|
branchNameTemplate?: string;
|
||||||
useStickyComment: boolean;
|
useStickyComment: boolean;
|
||||||
useCommitSigning: boolean;
|
useCommitSigning: boolean;
|
||||||
botId: string;
|
botId: string;
|
||||||
@@ -143,6 +144,7 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
||||||
baseBranch: process.env.BASE_BRANCH,
|
baseBranch: process.env.BASE_BRANCH,
|
||||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||||
|
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
|
||||||
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),
|
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
||||||
@@ -174,8 +176,7 @@ export function parseGitHubContext(): GitHubContext {
|
|||||||
isPR: Boolean(payload.issue.pull_request),
|
isPR: Boolean(payload.issue.pull_request),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
case "pull_request":
|
case "pull_request": {
|
||||||
case "pull_request_target": {
|
|
||||||
const payload = context.payload as PullRequestEvent;
|
const payload = context.payload as PullRequestEvent;
|
||||||
return {
|
return {
|
||||||
...commonFields,
|
...commonFields,
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import type { ParsedGitHubContext } from "../context";
|
|||||||
import type { GitHubPullRequest } from "../types";
|
import type { GitHubPullRequest } from "../types";
|
||||||
import type { Octokits } from "../api/client";
|
import type { Octokits } from "../api/client";
|
||||||
import type { FetchDataResult } from "../data/fetcher";
|
import type { FetchDataResult } from "../data/fetcher";
|
||||||
|
import { generateBranchName } from "../../utils/branch-template";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the first label from GitHub data, or returns undefined if no labels exist
|
||||||
|
*/
|
||||||
|
function extractFirstLabel(githubData: FetchDataResult): string | undefined {
|
||||||
|
const labels = githubData.contextData.labels?.nodes;
|
||||||
|
return labels && labels.length > 0 ? labels[0]?.name : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export type BranchInfo = {
|
export type BranchInfo = {
|
||||||
baseBranch: string;
|
baseBranch: string;
|
||||||
@@ -26,7 +35,7 @@ export async function setupBranch(
|
|||||||
): Promise<BranchInfo> {
|
): Promise<BranchInfo> {
|
||||||
const { owner, repo } = context.repository;
|
const { owner, repo } = context.repository;
|
||||||
const entityNumber = context.entityNumber;
|
const entityNumber = context.entityNumber;
|
||||||
const { baseBranch, branchPrefix } = context.inputs;
|
const { baseBranch, branchPrefix, branchNameTemplate } = context.inputs;
|
||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
@@ -87,17 +96,8 @@ export async function setupBranch(
|
|||||||
// Generate branch name for either an issue or closed/merged PR
|
// Generate branch name for either an issue or closed/merged PR
|
||||||
const entityType = isPR ? "pr" : "issue";
|
const entityType = isPR ? "pr" : "issue";
|
||||||
|
|
||||||
// Create Kubernetes-compatible timestamp: lowercase, hyphens only, shorter format
|
// Get the SHA of the source branch to use in template
|
||||||
const now = new Date();
|
let sourceSHA: string | undefined;
|
||||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
|
||||||
|
|
||||||
// Ensure branch name is Kubernetes-compatible:
|
|
||||||
// - Lowercase only
|
|
||||||
// - Alphanumeric with hyphens
|
|
||||||
// - No underscores
|
|
||||||
// - Max 50 chars (to allow for prefixes)
|
|
||||||
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
|
|
||||||
const newBranch = branchName.toLowerCase().substring(0, 50);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the SHA of the source branch to verify it exists
|
// Get the SHA of the source branch to verify it exists
|
||||||
@@ -107,8 +107,46 @@ export async function setupBranch(
|
|||||||
ref: `heads/${sourceBranch}`,
|
ref: `heads/${sourceBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentSHA = sourceBranchRef.data.object.sha;
|
sourceSHA = sourceBranchRef.data.object.sha;
|
||||||
console.log(`Source branch SHA: ${currentSHA}`);
|
console.log(`Source branch SHA: ${sourceSHA}`);
|
||||||
|
|
||||||
|
// Extract first label from GitHub data
|
||||||
|
const firstLabel = extractFirstLabel(githubData);
|
||||||
|
|
||||||
|
// Extract title from GitHub data
|
||||||
|
const title = githubData.contextData.title;
|
||||||
|
|
||||||
|
// Generate branch name using template or default format
|
||||||
|
let newBranch = generateBranchName(
|
||||||
|
branchNameTemplate,
|
||||||
|
branchPrefix,
|
||||||
|
entityType,
|
||||||
|
entityNumber,
|
||||||
|
sourceSHA,
|
||||||
|
firstLabel,
|
||||||
|
title,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if generated branch already exists on remote
|
||||||
|
try {
|
||||||
|
await $`git ls-remote --exit-code origin refs/heads/${newBranch}`.quiet();
|
||||||
|
|
||||||
|
// If we get here, branch exists (exit code 0)
|
||||||
|
console.log(
|
||||||
|
`Branch '${newBranch}' already exists, falling back to default format`,
|
||||||
|
);
|
||||||
|
newBranch = generateBranchName(
|
||||||
|
undefined, // Force default template
|
||||||
|
branchPrefix,
|
||||||
|
entityType,
|
||||||
|
entityNumber,
|
||||||
|
sourceSHA,
|
||||||
|
firstLabel,
|
||||||
|
title,
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
// Branch doesn't exist (non-zero exit code), continue with generated name
|
||||||
|
}
|
||||||
|
|
||||||
// For commit signing, defer branch creation to the file ops server
|
// For commit signing, defer branch creation to the file ops server
|
||||||
if (context.inputs.useCommitSigning) {
|
if (context.inputs.useCommitSigning) {
|
||||||
|
|||||||
@@ -61,6 +61,11 @@ export type GitHubPullRequest = {
|
|||||||
additions: number;
|
additions: number;
|
||||||
deletions: number;
|
deletions: number;
|
||||||
state: string;
|
state: string;
|
||||||
|
labels: {
|
||||||
|
nodes: Array<{
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
commits: {
|
commits: {
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
nodes: Array<{
|
nodes: Array<{
|
||||||
@@ -84,6 +89,11 @@ export type GitHubIssue = {
|
|||||||
author: GitHubAuthor;
|
author: GitHubAuthor;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
state: string;
|
state: string;
|
||||||
|
labels: {
|
||||||
|
nodes: Array<{
|
||||||
|
name: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
comments: {
|
comments: {
|
||||||
nodes: GitHubComment[];
|
nodes: GitHubComment[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export async function prepareMcpConfig(
|
|||||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||||
"-e",
|
"-e",
|
||||||
"GITHUB_HOST",
|
"GITHUB_HOST",
|
||||||
"ghcr.io/github/github-mcp-server:sha-23fa0dd", // https://github.com/github/github-mcp-server/releases/tag/v0.17.1
|
"ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
|
||||||
],
|
],
|
||||||
env: {
|
env: {
|
||||||
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export function parseAllowedTools(claudeArgs: string): string[] {
|
export function parseAllowedTools(claudeArgs: string): string[] {
|
||||||
// Match --allowedTools or --allowed-tools followed by the value
|
// Match --allowedTools followed by the value
|
||||||
// Handle both quoted and unquoted values
|
// Handle both quoted and unquoted values
|
||||||
const patterns = [
|
const patterns = [
|
||||||
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/, // Double quoted
|
/--allowedTools\s+"([^"]+)"/, // Double quoted
|
||||||
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/, // Single quoted
|
/--allowedTools\s+'([^']+)'/, // Single quoted
|
||||||
/--(?:allowedTools|allowed-tools)\s+([^\s]+)/, // Unquoted
|
/--allowedTools\s+([^\s]+)/, // Unquoted
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
|
|||||||
94
src/utils/branch-template.ts
Normal file
94
src/utils/branch-template.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Branch name template parsing and variable substitution utilities
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the first three words from a title and converts them to kebab-case
|
||||||
|
*/
|
||||||
|
function extractDescription(title: string): string {
|
||||||
|
if (!title || title.trim() === "") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return title
|
||||||
|
.trim() // Remove leading/trailing whitespace
|
||||||
|
.split(/\s+/) // Split on whitespace
|
||||||
|
.slice(0, 3) // Take first 3 words
|
||||||
|
.join("-") // Join with hyphens
|
||||||
|
.toLowerCase() // Convert to lowercase
|
||||||
|
.replace(/[^a-z0-9-]/g, "") // Remove non-alphanumeric except hyphens
|
||||||
|
.replace(/-+/g, "-") // Replace multiple hyphens with single
|
||||||
|
.replace(/^-|-$/g, ""); // Remove leading/trailing hyphens
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BranchTemplateVariables {
|
||||||
|
prefix: string;
|
||||||
|
entityType: string;
|
||||||
|
entityNumber: number;
|
||||||
|
timestamp: string;
|
||||||
|
sha?: string;
|
||||||
|
label?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces template variables in a branch name template
|
||||||
|
* Template format: {{variableName}}
|
||||||
|
*/
|
||||||
|
export function applyBranchTemplate(
|
||||||
|
template: string,
|
||||||
|
variables: BranchTemplateVariables,
|
||||||
|
): string {
|
||||||
|
let result = template;
|
||||||
|
|
||||||
|
// Replace each variable
|
||||||
|
Object.entries(variables).forEach(([key, value]) => {
|
||||||
|
const placeholder = `{{${key}}}`;
|
||||||
|
const replacement = value ? String(value) : "";
|
||||||
|
result = result.replaceAll(placeholder, replacement);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a branch name from the provided `template` and set of `variables`. Uses a default format if the template is empty or produces an empty result.
|
||||||
|
*/
|
||||||
|
export function generateBranchName(
|
||||||
|
template: string | undefined,
|
||||||
|
branchPrefix: string,
|
||||||
|
entityType: string,
|
||||||
|
entityNumber: number,
|
||||||
|
sha?: string,
|
||||||
|
label?: string,
|
||||||
|
title?: string,
|
||||||
|
): string {
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
const variables: BranchTemplateVariables = {
|
||||||
|
prefix: branchPrefix,
|
||||||
|
entityType,
|
||||||
|
entityNumber,
|
||||||
|
timestamp: `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`,
|
||||||
|
sha: sha?.substring(0, 8), // First 8 characters of SHA
|
||||||
|
label: label || entityType, // Fall back to entityType if no label
|
||||||
|
description: title ? extractDescription(title) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (template?.trim()) {
|
||||||
|
const branchName = applyBranchTemplate(template, variables);
|
||||||
|
|
||||||
|
// Some templates could produce empty results- validate
|
||||||
|
if (branchName.trim().length > 0) return branchName;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Branch template '${template}' generated empty result, falling back to default format`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${variables.timestamp}`;
|
||||||
|
// Kubernetes compatible: lowercase, max 50 chars, alphanumeric and hyphens only
|
||||||
|
return branchName.toLowerCase().substring(0, 50);
|
||||||
|
}
|
||||||
232
test/branch-template.test.ts
Normal file
232
test/branch-template.test.ts
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import {
|
||||||
|
applyBranchTemplate,
|
||||||
|
generateBranchName,
|
||||||
|
} from "../src/utils/branch-template";
|
||||||
|
|
||||||
|
describe("branch template utilities", () => {
|
||||||
|
describe("applyBranchTemplate", () => {
|
||||||
|
it("should replace all template variables", () => {
|
||||||
|
const template =
|
||||||
|
"{{prefix}}{{entityType}}-{{entityNumber}}-{{timestamp}}";
|
||||||
|
const variables = {
|
||||||
|
prefix: "feat/",
|
||||||
|
entityType: "issue",
|
||||||
|
entityNumber: 123,
|
||||||
|
timestamp: "20240301-1430",
|
||||||
|
sha: "abcd1234",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyBranchTemplate(template, variables);
|
||||||
|
expect(result).toBe("feat/issue-123-20240301-1430");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle custom templates with multiple variables", () => {
|
||||||
|
const template =
|
||||||
|
"{{prefix}}fix/{{entityType}}_{{entityNumber}}_{{timestamp}}_{{sha}}";
|
||||||
|
const variables = {
|
||||||
|
prefix: "claude-",
|
||||||
|
entityType: "pr",
|
||||||
|
entityNumber: 456,
|
||||||
|
timestamp: "20240301-1430",
|
||||||
|
sha: "abcd1234",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyBranchTemplate(template, variables);
|
||||||
|
expect(result).toBe("claude-fix/pr_456_20240301-1430_abcd1234");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle templates with missing variables gracefully", () => {
|
||||||
|
const template = "{{prefix}}{{entityType}}-{{missing}}-{{entityNumber}}";
|
||||||
|
const variables = {
|
||||||
|
prefix: "feat/",
|
||||||
|
entityType: "issue",
|
||||||
|
entityNumber: 123,
|
||||||
|
timestamp: "20240301-1430",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = applyBranchTemplate(template, variables);
|
||||||
|
expect(result).toBe("feat/issue-{{missing}}-123");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateBranchName", () => {
|
||||||
|
it("should use custom template when provided", () => {
|
||||||
|
const template = "{{prefix}}custom-{{entityType}}_{{entityNumber}}";
|
||||||
|
const result = generateBranchName(template, "feature/", "issue", 123);
|
||||||
|
|
||||||
|
expect(result).toBe("feature/custom-issue_123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use default format when template is empty", () => {
|
||||||
|
const result = generateBranchName("", "claude/", "issue", 123);
|
||||||
|
|
||||||
|
expect(result).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use default format when template is undefined", () => {
|
||||||
|
const result = generateBranchName(undefined, "claude/", "pr", 456);
|
||||||
|
|
||||||
|
expect(result).toMatch(/^claude\/pr-456-\d{8}-\d{4}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve custom template formatting (no automatic lowercase/truncation)", () => {
|
||||||
|
const template = "{{prefix}}UPPERCASE_Branch-Name_{{entityNumber}}";
|
||||||
|
const result = generateBranchName(template, "Feature/", "issue", 123);
|
||||||
|
|
||||||
|
expect(result).toBe("Feature/UPPERCASE_Branch-Name_123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not truncate custom template results", () => {
|
||||||
|
const template =
|
||||||
|
"{{prefix}}very-long-branch-name-that-exceeds-the-maximum-allowed-length-{{entityNumber}}";
|
||||||
|
const result = generateBranchName(template, "feature/", "issue", 123);
|
||||||
|
|
||||||
|
expect(result).toBe(
|
||||||
|
"feature/very-long-branch-name-that-exceeds-the-maximum-allowed-length-123",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should apply Kubernetes-compatible transformations to default template only", () => {
|
||||||
|
const result = generateBranchName(undefined, "Feature/", "issue", 123);
|
||||||
|
|
||||||
|
expect(result).toMatch(/^feature\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
expect(result.length).toBeLessThanOrEqual(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle SHA in template", () => {
|
||||||
|
const template = "{{prefix}}{{entityType}}-{{entityNumber}}-{{sha}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"fix/",
|
||||||
|
"pr",
|
||||||
|
789,
|
||||||
|
"abcdef123456",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("fix/pr-789-abcdef12");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use label in template when provided", () => {
|
||||||
|
const template = "{{prefix}}{{label}}/{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"feature/",
|
||||||
|
"issue",
|
||||||
|
123,
|
||||||
|
undefined,
|
||||||
|
"bug",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("feature/bug/123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fallback to entityType when label template is used but no label provided", () => {
|
||||||
|
const template = "{{prefix}}{{label}}-{{entityNumber}}";
|
||||||
|
const result = generateBranchName(template, "fix/", "pr", 456);
|
||||||
|
|
||||||
|
expect(result).toBe("fix/pr-456");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle template with both label and entityType", () => {
|
||||||
|
const template = "{{prefix}}{{label}}-{{entityType}}_{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"dev/",
|
||||||
|
"issue",
|
||||||
|
789,
|
||||||
|
undefined,
|
||||||
|
"enhancement",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("dev/enhancement-issue_789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use description in template when provided", () => {
|
||||||
|
const template = "{{prefix}}{{description}}/{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"feature/",
|
||||||
|
"issue",
|
||||||
|
123,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"Fix login bug with OAuth",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("feature/fix-login-bug/123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle template with multiple variables including description", () => {
|
||||||
|
const template =
|
||||||
|
"{{prefix}}{{label}}/{{description}}-{{entityType}}_{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"dev/",
|
||||||
|
"issue",
|
||||||
|
456,
|
||||||
|
undefined,
|
||||||
|
"bug",
|
||||||
|
"User authentication fails completely",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("dev/bug/user-authentication-fails-issue_456");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle description with special characters in template", () => {
|
||||||
|
const template = "{{prefix}}{{description}}-{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"fix/",
|
||||||
|
"pr",
|
||||||
|
789,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"Add: User Registration & Email Validation",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("fix/add-user-registration-789");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty description in template", () => {
|
||||||
|
const template = "{{prefix}}{{description}}-{{entityNumber}}";
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"test/",
|
||||||
|
"issue",
|
||||||
|
101,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("test/-101");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fallback to default format when template produces empty result", () => {
|
||||||
|
const template = "{{description}}"; // Will be empty if no title provided
|
||||||
|
const result = generateBranchName(template, "claude/", "issue", 123);
|
||||||
|
|
||||||
|
expect(result).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
|
||||||
|
expect(result.length).toBeLessThanOrEqual(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fallback to default format when template produces only whitespace", () => {
|
||||||
|
const template = " {{description}} "; // Will be " " if description is empty
|
||||||
|
const result = generateBranchName(
|
||||||
|
template,
|
||||||
|
"fix/",
|
||||||
|
"pr",
|
||||||
|
456,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toMatch(/^fix\/pr-456-\d{8}-\d{4}$/);
|
||||||
|
expect(result.length).toBeLessThanOrEqual(50);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -61,6 +61,7 @@ describe("generatePrompt", () => {
|
|||||||
body: "This is a test PR",
|
body: "This is a test PR",
|
||||||
author: { login: "testuser" },
|
author: { login: "testuser" },
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
|
labels: { nodes: [] },
|
||||||
createdAt: "2023-01-01T00:00:00Z",
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
additions: 15,
|
additions: 15,
|
||||||
deletions: 5,
|
deletions: 5,
|
||||||
@@ -475,6 +476,7 @@ describe("generatePrompt", () => {
|
|||||||
body: "The login form is not working",
|
body: "The login form is not working",
|
||||||
author: { login: "testuser" },
|
author: { login: "testuser" },
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
|
labels: { nodes: [] },
|
||||||
createdAt: "2023-01-01T00:00:00Z",
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
comments: {
|
comments: {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
|
|||||||
@@ -28,6 +28,9 @@ describe("formatContext", () => {
|
|||||||
additions: 50,
|
additions: 50,
|
||||||
deletions: 30,
|
deletions: 30,
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
|
labels: {
|
||||||
|
nodes: [],
|
||||||
|
},
|
||||||
commits: {
|
commits: {
|
||||||
totalCount: 3,
|
totalCount: 3,
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@@ -63,6 +66,9 @@ Changed Files: 2 files`,
|
|||||||
author: { login: "test-user" },
|
author: { login: "test-user" },
|
||||||
createdAt: "2023-01-01T00:00:00Z",
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
state: "OPEN",
|
state: "OPEN",
|
||||||
|
labels: {
|
||||||
|
nodes: [],
|
||||||
|
},
|
||||||
comments: {
|
comments: {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -68,20 +68,4 @@ describe("parseAllowedTools", () => {
|
|||||||
"mcp__github_comment__update",
|
"mcp__github_comment__update",
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("parses kebab-case --allowed-tools", () => {
|
|
||||||
const args = "--allowed-tools mcp__github__*,mcp__github_comment__*";
|
|
||||||
expect(parseAllowedTools(args)).toEqual([
|
|
||||||
"mcp__github__*",
|
|
||||||
"mcp__github_comment__*",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("parses quoted kebab-case --allowed-tools", () => {
|
|
||||||
const args = '--allowed-tools "mcp__github__*,mcp__github_comment__*"';
|
|
||||||
expect(parseAllowedTools(args)).toEqual([
|
|
||||||
"mcp__github__*",
|
|
||||||
"mcp__github_comment__*",
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,504 +0,0 @@
|
|||||||
#!/usr/bin/env bun
|
|
||||||
|
|
||||||
import { describe, test, expect } from "bun:test";
|
|
||||||
import {
|
|
||||||
getEventTypeAndContext,
|
|
||||||
generatePrompt,
|
|
||||||
generateDefaultPrompt,
|
|
||||||
} from "../src/create-prompt";
|
|
||||||
import type { PreparedContext } from "../src/create-prompt";
|
|
||||||
import type { Mode } from "../src/modes/types";
|
|
||||||
|
|
||||||
describe("pull_request_target event support", () => {
|
|
||||||
// Mock tag mode for testing
|
|
||||||
const mockTagMode: Mode = {
|
|
||||||
name: "tag",
|
|
||||||
description: "Tag mode",
|
|
||||||
shouldTrigger: () => true,
|
|
||||||
prepareContext: (context) => ({ mode: "tag", githubContext: context }),
|
|
||||||
getAllowedTools: () => [],
|
|
||||||
getDisallowedTools: () => [],
|
|
||||||
shouldCreateTrackingComment: () => true,
|
|
||||||
generatePrompt: (context, githubData, useCommitSigning) =>
|
|
||||||
generateDefaultPrompt(context, githubData, useCommitSigning),
|
|
||||||
prepare: async () => ({
|
|
||||||
commentId: 123,
|
|
||||||
branchInfo: {
|
|
||||||
baseBranch: "main",
|
|
||||||
currentBranch: "main",
|
|
||||||
claudeBranch: undefined,
|
|
||||||
},
|
|
||||||
mcpConfig: "{}",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockGitHubData = {
|
|
||||||
contextData: {
|
|
||||||
title: "External PR via pull_request_target",
|
|
||||||
body: "This PR comes from a forked repository",
|
|
||||||
author: { login: "external-contributor" },
|
|
||||||
state: "OPEN",
|
|
||||||
createdAt: "2023-01-01T00:00:00Z",
|
|
||||||
additions: 25,
|
|
||||||
deletions: 3,
|
|
||||||
baseRefName: "main",
|
|
||||||
headRefName: "feature-branch",
|
|
||||||
headRefOid: "abc123",
|
|
||||||
commits: {
|
|
||||||
totalCount: 2,
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
commit: {
|
|
||||||
oid: "commit1",
|
|
||||||
message: "Initial feature implementation",
|
|
||||||
author: {
|
|
||||||
name: "External Dev",
|
|
||||||
email: "external@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
commit: {
|
|
||||||
oid: "commit2",
|
|
||||||
message: "Fix typos and formatting",
|
|
||||||
author: {
|
|
||||||
name: "External Dev",
|
|
||||||
email: "external@example.com",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
files: {
|
|
||||||
nodes: [
|
|
||||||
{
|
|
||||||
path: "src/feature.ts",
|
|
||||||
additions: 20,
|
|
||||||
deletions: 2,
|
|
||||||
changeType: "MODIFIED",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "tests/feature.test.ts",
|
|
||||||
additions: 5,
|
|
||||||
deletions: 1,
|
|
||||||
changeType: "ADDED",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
comments: { nodes: [] },
|
|
||||||
reviews: { nodes: [] },
|
|
||||||
},
|
|
||||||
comments: [],
|
|
||||||
changedFiles: [],
|
|
||||||
changedFilesWithSHA: [
|
|
||||||
{
|
|
||||||
path: "src/feature.ts",
|
|
||||||
additions: 20,
|
|
||||||
deletions: 2,
|
|
||||||
changeType: "MODIFIED",
|
|
||||||
sha: "abc123",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "tests/feature.test.ts",
|
|
||||||
additions: 5,
|
|
||||||
deletions: 1,
|
|
||||||
changeType: "ADDED",
|
|
||||||
sha: "abc123",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
reviewData: { nodes: [] },
|
|
||||||
imageUrlMap: new Map<string, string>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("prompt generation for pull_request_target", () => {
|
|
||||||
test("should generate correct prompt for pull_request_target event", () => {
|
|
||||||
const envVars: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: "opened",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "123",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = generatePrompt(
|
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain pull request event type and metadata
|
|
||||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
|
||||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
|
||||||
expect(prompt).toContain("<pr_number>123</pr_number>");
|
|
||||||
expect(prompt).toContain(
|
|
||||||
"<trigger_context>pull request opened</trigger_context>",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should contain PR-specific information
|
|
||||||
expect(prompt).toContain(
|
|
||||||
"- src/feature.ts (MODIFIED) +20/-2 SHA: abc123",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain(
|
|
||||||
"- tests/feature.test.ts (ADDED) +5/-1 SHA: abc123",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("external-contributor");
|
|
||||||
expect(prompt).toContain("<repository>owner/repo</repository>");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle pull_request_target with commit signing disabled", () => {
|
|
||||||
const envVars: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: "synchronize",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "456",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = generatePrompt(
|
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should include git commands for non-commit-signing mode
|
|
||||||
expect(prompt).toContain("git push");
|
|
||||||
expect(prompt).toContain(
|
|
||||||
"Always push to the existing branch when triggered on a PR",
|
|
||||||
);
|
|
||||||
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
|
|
||||||
|
|
||||||
// Should not include commit signing tools
|
|
||||||
expect(prompt).not.toContain("mcp__github_file_ops__commit_files");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle pull_request_target with commit signing enabled", () => {
|
|
||||||
const envVars: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: "synchronize",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "456",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
|
|
||||||
|
|
||||||
// Should include commit signing tools
|
|
||||||
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
|
||||||
expect(prompt).toContain("mcp__github_file_ops__delete_files");
|
|
||||||
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
|
|
||||||
|
|
||||||
// Should not include git command instructions
|
|
||||||
expect(prompt).not.toContain("Use git commands via the Bash tool");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should treat pull_request_target same as pull_request in prompt generation", () => {
|
|
||||||
const baseContext: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: "opened",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "123",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate prompt for pull_request
|
|
||||||
const pullRequestContext: PreparedContext = {
|
|
||||||
...baseContext,
|
|
||||||
eventData: {
|
|
||||||
...baseContext.eventData,
|
|
||||||
eventName: "pull_request",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "123",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Generate prompt for pull_request_target
|
|
||||||
const pullRequestTargetContext: PreparedContext = {
|
|
||||||
...baseContext,
|
|
||||||
eventData: {
|
|
||||||
...baseContext.eventData,
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "123",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const pullRequestPrompt = generatePrompt(
|
|
||||||
pullRequestContext,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
const pullRequestTargetPrompt = generatePrompt(
|
|
||||||
pullRequestTargetContext,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Both should have the same event type and structure
|
|
||||||
expect(pullRequestPrompt).toContain(
|
|
||||||
"<event_type>PULL_REQUEST</event_type>",
|
|
||||||
);
|
|
||||||
expect(pullRequestTargetPrompt).toContain(
|
|
||||||
"<event_type>PULL_REQUEST</event_type>",
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(pullRequestPrompt).toContain(
|
|
||||||
"<trigger_context>pull request opened</trigger_context>",
|
|
||||||
);
|
|
||||||
expect(pullRequestTargetPrompt).toContain(
|
|
||||||
"<trigger_context>pull request opened</trigger_context>",
|
|
||||||
);
|
|
||||||
|
|
||||||
// Both should contain PR-specific instructions
|
|
||||||
expect(pullRequestPrompt).toContain(
|
|
||||||
"Always push to the existing branch when triggered on a PR",
|
|
||||||
);
|
|
||||||
expect(pullRequestTargetPrompt).toContain(
|
|
||||||
"Always push to the existing branch when triggered on a PR",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle pull_request_target in agent mode with custom prompt", () => {
|
|
||||||
const envVars: PreparedContext = {
|
|
||||||
repository: "test/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
prompt: "Review this pull_request_target PR for security issues",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: "opened",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "789",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Use agent mode which passes through the prompt as-is
|
|
||||||
const mockAgentMode: Mode = {
|
|
||||||
name: "agent",
|
|
||||||
description: "Agent mode",
|
|
||||||
shouldTrigger: () => true,
|
|
||||||
prepareContext: (context) => ({
|
|
||||||
mode: "agent",
|
|
||||||
githubContext: context,
|
|
||||||
}),
|
|
||||||
getAllowedTools: () => [],
|
|
||||||
getDisallowedTools: () => [],
|
|
||||||
shouldCreateTrackingComment: () => true,
|
|
||||||
generatePrompt: (context) => context.prompt || "default prompt",
|
|
||||||
prepare: async () => ({
|
|
||||||
commentId: 123,
|
|
||||||
branchInfo: {
|
|
||||||
baseBranch: "main",
|
|
||||||
currentBranch: "main",
|
|
||||||
claudeBranch: undefined,
|
|
||||||
},
|
|
||||||
mcpConfig: "{}",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = generatePrompt(
|
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockAgentMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(prompt).toBe(
|
|
||||||
"Review this pull_request_target PR for security issues",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle pull_request_target with no custom prompt", () => {
|
|
||||||
const envVars: PreparedContext = {
|
|
||||||
repository: "test/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: "synchronize",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "456",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = generatePrompt(
|
|
||||||
envVars,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should generate default prompt structure
|
|
||||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
|
||||||
expect(prompt).toContain("<pr_number>456</pr_number>");
|
|
||||||
expect(prompt).toContain(
|
|
||||||
"Always push to the existing branch when triggered on a PR",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("pull_request_target vs pull_request behavior consistency", () => {
|
|
||||||
test("should produce identical event processing for both event types", () => {
|
|
||||||
const baseEventData = {
|
|
||||||
eventAction: "opened",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "100",
|
|
||||||
};
|
|
||||||
|
|
||||||
const pullRequestEvent: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
...baseEventData,
|
|
||||||
eventName: "pull_request",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "100",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const pullRequestTargetEvent: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
...baseEventData,
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "100",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Both should have identical event type detection
|
|
||||||
const prResult = getEventTypeAndContext(pullRequestEvent);
|
|
||||||
const prtResult = getEventTypeAndContext(pullRequestTargetEvent);
|
|
||||||
|
|
||||||
expect(prResult.eventType).toBe(prtResult.eventType);
|
|
||||||
expect(prResult.triggerContext).toBe(prtResult.triggerContext);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle edge cases in pull_request_target events", () => {
|
|
||||||
// Test with minimal event data
|
|
||||||
const minimalContext: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "1",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = getEventTypeAndContext(minimalContext);
|
|
||||||
expect(result.eventType).toBe("PULL_REQUEST");
|
|
||||||
expect(result.triggerContext).toBe("pull request event");
|
|
||||||
|
|
||||||
// Should not throw when generating prompt
|
|
||||||
expect(() => {
|
|
||||||
generatePrompt(minimalContext, mockGitHubData, false, mockTagMode);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle all valid pull_request_target actions", () => {
|
|
||||||
const actions = ["opened", "synchronize", "reopened", "closed", "edited"];
|
|
||||||
|
|
||||||
actions.forEach((action) => {
|
|
||||||
const context: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: action,
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "1",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = getEventTypeAndContext(context);
|
|
||||||
expect(result.eventType).toBe("PULL_REQUEST");
|
|
||||||
expect(result.triggerContext).toBe(`pull request ${action}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("security considerations for pull_request_target", () => {
|
|
||||||
test("should maintain same prompt structure regardless of event source", () => {
|
|
||||||
// Test that external PRs don't get different treatment in prompts
|
|
||||||
const internalPR: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request",
|
|
||||||
eventAction: "opened",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "1",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const externalPR: PreparedContext = {
|
|
||||||
repository: "owner/repo",
|
|
||||||
claudeCommentId: "12345",
|
|
||||||
triggerPhrase: "@claude",
|
|
||||||
eventData: {
|
|
||||||
eventName: "pull_request_target",
|
|
||||||
eventAction: "opened",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "1",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const internalPrompt = generatePrompt(
|
|
||||||
internalPR,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
const externalPrompt = generatePrompt(
|
|
||||||
externalPR,
|
|
||||||
mockGitHubData,
|
|
||||||
false,
|
|
||||||
mockTagMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should have same tool access patterns
|
|
||||||
expect(
|
|
||||||
internalPrompt.includes("mcp__github_comment__update_claude_comment"),
|
|
||||||
).toBe(
|
|
||||||
externalPrompt.includes("mcp__github_comment__update_claude_comment"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should have same branch handling instructions
|
|
||||||
expect(
|
|
||||||
internalPrompt.includes(
|
|
||||||
"Always push to the existing branch when triggered on a PR",
|
|
||||||
),
|
|
||||||
).toBe(
|
|
||||||
externalPrompt.includes(
|
|
||||||
"Always push to the existing branch when triggered on a PR",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user