mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
21 Commits
enhance-te
...
ashwin/com
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
404e7dc841 | ||
|
|
787ba87628 | ||
|
|
547eb3464c | ||
|
|
23a694ae90 | ||
|
|
94c1288140 | ||
|
|
722b06e99b | ||
|
|
e409c57d90 | ||
|
|
f6e5597633 | ||
|
|
0cd44e50dd | ||
|
|
a8a36ced96 | ||
|
|
180a1b6680 | ||
|
|
8da47815ec | ||
|
|
35ad5fc467 | ||
|
|
fb7365fba9 | ||
|
|
5a787ed8ab | ||
|
|
507e4a6cd1 | ||
|
|
fcbdac91f2 | ||
|
|
03e5dcc3a1 | ||
|
|
52efa5e498 | ||
|
|
37c3c29341 | ||
|
|
9c9859aff1 |
3
.github/workflows/claude.yml
vendored
3
.github/workflows/claude.yml
vendored
@@ -34,3 +34,6 @@ jobs:
|
|||||||
uses: anthropics/claude-code-action@beta
|
uses: anthropics/claude-code-action@beta
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
|
||||||
|
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."
|
||||||
|
model: "claude-opus-4-20250514"
|
||||||
|
|||||||
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -99,6 +99,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
prompt_file: /tmp/claude-prompts/triage-prompt.txt
|
prompt_file: /tmp/claude-prompts/triage-prompt.txt
|
||||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||||
mcp_config_file: /tmp/mcp-config/mcp-servers.json
|
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||||
timeout_minutes: "5"
|
timeout_minutes: "5"
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|||||||
156
FAQ.md
Normal file
156
FAQ.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
|
This FAQ addresses common questions and gotchas when using the Claude Code GitHub Action.
|
||||||
|
|
||||||
|
## Triggering and Authentication
|
||||||
|
|
||||||
|
### Why doesn't tagging @claude from my automated workflow work?
|
||||||
|
|
||||||
|
The `github-actions` user cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user, or use a separate app token of your own. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow.
|
||||||
|
|
||||||
|
### Why does Claude say I don't have permission to trigger it?
|
||||||
|
|
||||||
|
Only users with **write permissions** to the repository can trigger Claude. This is a security feature to prevent unauthorized use. Make sure the user commenting has at least write access to the repository.
|
||||||
|
|
||||||
|
### Why am I getting OIDC authentication errors?
|
||||||
|
|
||||||
|
If you're using the default GitHub App authentication, you must add the `id-token: write` permission to your workflow:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # Required for OIDC authentication
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
## Claude's Capabilities and Limitations
|
||||||
|
|
||||||
|
### Why won't Claude update workflow files when I ask it to?
|
||||||
|
|
||||||
|
The GitHub App for Claude doesn't have workflow write access for security reasons. This prevents Claude from modifying CI/CD configurations that could potentially create unintended consequences. This is something we may reconsider in the future.
|
||||||
|
|
||||||
|
### Why won't Claude rebase my branch?
|
||||||
|
|
||||||
|
By default, Claude only uses commit tools for non-destructive changes to the branch. Claude is configured to:
|
||||||
|
|
||||||
|
- Never push to branches other than where it was invoked (either its own branch or the PR branch)
|
||||||
|
- Never force push or perform destructive operations
|
||||||
|
|
||||||
|
You can grant additional tools via the `allowed_tools` input if needed:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
allowed_tools: "Bash(git rebase:*)" # Use with caution
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why won't Claude create a pull request?
|
||||||
|
|
||||||
|
Claude doesn't create PRs by default. Instead, it pushes commits to a branch and provides a link to a pre-filled PR submission page. This approach ensures your repository's branch protection rules are still adhered to and gives you final control over PR creation.
|
||||||
|
|
||||||
|
### Why can't Claude run my tests or see CI results?
|
||||||
|
|
||||||
|
Claude cannot access GitHub Actions logs, test results, or other CI/CD outputs by default. It only has access to the repository files. If you need Claude to see test results, you can either:
|
||||||
|
|
||||||
|
1. Instruct Claude to run tests before making commits
|
||||||
|
2. Copy and paste CI results into a comment for Claude to analyze
|
||||||
|
|
||||||
|
This limitation exists for security reasons but may be reconsidered in the future based on user feedback.
|
||||||
|
|
||||||
|
### Why does Claude only update one comment instead of creating new ones?
|
||||||
|
|
||||||
|
Claude is configured to update a single comment to avoid cluttering PR/issue discussions. All of Claude's responses, including progress updates and final results, will appear in the same comment with checkboxes showing task progress.
|
||||||
|
|
||||||
|
## Branch and Commit Behavior
|
||||||
|
|
||||||
|
### Why did Claude create a new branch when commenting on a closed PR?
|
||||||
|
|
||||||
|
Claude's branch behavior depends on the context:
|
||||||
|
|
||||||
|
- **Open PRs**: Pushes directly to the existing PR branch
|
||||||
|
- **Closed/Merged PRs**: Creates a new branch (cannot push to closed PR branches)
|
||||||
|
- **Issues**: Always creates a new branch with a timestamp
|
||||||
|
|
||||||
|
### Why are my commits shallow/missing history?
|
||||||
|
|
||||||
|
For performance, Claude uses shallow clones:
|
||||||
|
|
||||||
|
- PRs: `--depth=20` (last 20 commits)
|
||||||
|
- New branches: `--depth=1` (single commit)
|
||||||
|
|
||||||
|
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
|
||||||
|
|
||||||
|
```
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
depth: 0 # will fetch full repo history
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration and Tools
|
||||||
|
|
||||||
|
### What's the difference between `direct_prompt` and `custom_instructions`?
|
||||||
|
|
||||||
|
These inputs serve different purposes in how Claude responds:
|
||||||
|
|
||||||
|
- **`direct_prompt`**: Bypasses trigger detection entirely. When provided, Claude executes this exact instruction regardless of comments or mentions. Perfect for automated workflows where you want Claude to perform a specific task on every run (e.g., "Update the API documentation based on changes in this PR").
|
||||||
|
|
||||||
|
- **`custom_instructions`**: Additional context added to Claude's system prompt while still respecting normal triggers. These instructions modify Claude's behavior but don't replace the triggering comment. Use this to give Claude standing instructions like "You have been granted additional tools for ...".
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Using direct_prompt - runs automatically without @claude mention
|
||||||
|
direct_prompt: "Review this PR for security vulnerabilities"
|
||||||
|
|
||||||
|
# Using custom_instructions - still requires @claude trigger
|
||||||
|
custom_instructions: "Focus on performance implications and suggest optimizations"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Why doesn't Claude execute my bash commands?
|
||||||
|
|
||||||
|
The Bash tool is **disabled by default** for security. To enable individual bash commands:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
allowed_tools: "Bash(npm:*),Bash(git:*)" # Allows only npm and git commands
|
||||||
|
```
|
||||||
|
|
||||||
|
### Can Claude work across multiple repositories?
|
||||||
|
|
||||||
|
No, Claude's GitHub app token is sandboxed to the current repository only. It cannot push to any other repositories. It can, however, read public repositories, but to get access to this, you must configure it with tools to do so.
|
||||||
|
|
||||||
|
## MCP Servers and Extended Functionality
|
||||||
|
|
||||||
|
### What MCP servers are available by default?
|
||||||
|
|
||||||
|
Claude Code Action automatically configures two MCP servers:
|
||||||
|
|
||||||
|
1. **GitHub MCP server**: For GitHub API operations
|
||||||
|
2. **File operations server**: For advanced file manipulation
|
||||||
|
|
||||||
|
However, tools from these servers still need to be explicitly allowed via `allowed_tools`.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### How can I debug what Claude is doing?
|
||||||
|
|
||||||
|
Check the GitHub Action log for Claude's run for the full execution trace.
|
||||||
|
|
||||||
|
### Why can't I trigger Claude with `@claude-mention` or `claude!`?
|
||||||
|
|
||||||
|
The trigger uses word boundaries, so `@claude` must be a complete word. Variations like `@claude-bot`, `@claude!`, or `claude@mention` won't work unless you customize the `trigger_phrase`.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Always specify permissions explicitly** in your workflow file
|
||||||
|
2. **Use GitHub Secrets** for API keys - never hardcode them
|
||||||
|
3. **Be specific with `allowed_tools`** - only enable what's necessary
|
||||||
|
4. **Test in a separate branch** before using on important PRs
|
||||||
|
5. **Monitor Claude's token usage** to avoid hitting API limits
|
||||||
|
6. **Review Claude's changes** carefully before merging
|
||||||
|
|
||||||
|
## Getting Help
|
||||||
|
|
||||||
|
If you encounter issues not covered here:
|
||||||
|
|
||||||
|
1. Check the [GitHub Issues](https://github.com/anthropics/claude-code-action/issues)
|
||||||
|
2. Review the [example workflows](https://github.com/anthropics/claude-code-action#examples)
|
||||||
|
|
||||||
|
[perms]: https://docs.anthropic.com/en/docs/claude-code/settings#permissions
|
||||||
60
README.md
60
README.md
@@ -33,6 +33,10 @@ This command will guide you through setting up the GitHub app and required secre
|
|||||||
2. Add `ANTHROPIC_API_KEY` to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions))
|
2. Add `ANTHROPIC_API_KEY` to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions))
|
||||||
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
|
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
|
||||||
|
|
||||||
|
## 📚 FAQ
|
||||||
|
|
||||||
|
Having issues or questions? Check out our [Frequently Asked Questions](./FAQ.md) for solutions to common problems and detailed explanations of Claude's capabilities and limitations.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
|
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
|
||||||
@@ -78,6 +82,7 @@ jobs:
|
|||||||
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
|
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
|
||||||
| `disallowed_tools` | Tools that Claude should never use | No | "" |
|
| `disallowed_tools` | Tools that Claude should never use | No | "" |
|
||||||
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
|
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
|
||||||
|
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
|
||||||
| `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 | - |
|
||||||
| `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` |
|
||||||
|
|
||||||
@@ -85,6 +90,61 @@ jobs:
|
|||||||
|
|
||||||
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
|
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
|
||||||
|
|
||||||
|
### Using Custom MCP Configuration
|
||||||
|
|
||||||
|
The `mcp_config` input allows you to add custom MCP (Model Context Protocol) servers to extend Claude's capabilities. These servers merge with the built-in GitHub MCP servers.
|
||||||
|
|
||||||
|
#### Basic Example: Adding a Sequential Thinking Server
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: anthropics/claude-code-action@beta
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
mcp_config: |
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"sequential-thinking": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"@modelcontextprotocol/server-sequential-thinking"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allowed_tools: "mcp__sequential-thinking__sequentialthinking" # Important: Each MCP tool from your server must be listed here, comma-separated
|
||||||
|
# ... other inputs
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Passing Secrets to MCP Servers
|
||||||
|
|
||||||
|
For MCP servers that require sensitive information like API keys or tokens, use GitHub Secrets in the environment variables:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: anthropics/claude-code-action@beta
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
mcp_config: |
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"custom-api-server": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-y", "@example/api-server"],
|
||||||
|
"env": {
|
||||||
|
"API_KEY": "${{ secrets.CUSTOM_API_KEY }}",
|
||||||
|
"BASE_URL": "https://api.example.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# ... other inputs
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important**:
|
||||||
|
|
||||||
|
- Always use GitHub Secrets (`${{ secrets.SECRET_NAME }}`) for sensitive values like API keys, tokens, or passwords. Never hardcode secrets directly in the workflow file.
|
||||||
|
- Your custom servers will override any built-in servers with the same name.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
### Ways to Tag @claude
|
### Ways to Tag @claude
|
||||||
|
|||||||
18
action.yml
18
action.yml
@@ -12,6 +12,9 @@ inputs:
|
|||||||
assignee_trigger:
|
assignee_trigger:
|
||||||
description: "The assignee username that triggers the action (e.g. @claude)"
|
description: "The assignee username that triggers the action (e.g. @claude)"
|
||||||
required: false
|
required: false
|
||||||
|
base_branch:
|
||||||
|
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
|
||||||
|
required: false
|
||||||
|
|
||||||
# Claude Code configuration
|
# Claude Code configuration
|
||||||
model:
|
model:
|
||||||
@@ -36,6 +39,10 @@ inputs:
|
|||||||
description: "Direct instruction for Claude (bypasses normal trigger detection)"
|
description: "Direct instruction for Claude (bypasses normal trigger detection)"
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
mcp_config:
|
||||||
|
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
# Auth configuration
|
# Auth configuration
|
||||||
anthropic_api_key:
|
anthropic_api_key:
|
||||||
@@ -85,16 +92,18 @@ runs:
|
|||||||
env:
|
env:
|
||||||
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||||
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
||||||
|
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||||
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
||||||
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
||||||
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
||||||
|
MCP_CONFIG: ${{ inputs.mcp_config }}
|
||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
id: claude-code
|
id: claude-code
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||||
uses: anthropics/claude-code-base-action@266585c92dd90d61d3806a3367582c4f6224e892 # https://github.com/anthropics/claude-code-base-action/releases/tag/v0.0.6
|
uses: anthropics/claude-code-base-action@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8
|
||||||
with:
|
with:
|
||||||
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
||||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||||
@@ -110,6 +119,9 @@ runs:
|
|||||||
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
# Provider configuration
|
||||||
|
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
||||||
|
|
||||||
# AWS configuration
|
# AWS configuration
|
||||||
AWS_REGION: ${{ env.AWS_REGION }}
|
AWS_REGION: ${{ env.AWS_REGION }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -143,10 +155,12 @@ runs:
|
|||||||
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_review_comment' }}
|
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
||||||
DEFAULT_BRANCH: ${{ steps.prepare.outputs.DEFAULT_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 || '' }}
|
||||||
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
||||||
|
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||||
|
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
||||||
|
|
||||||
- name: Display Claude Code Report
|
- name: Display Claude Code Report
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
||||||
|
|||||||
@@ -31,24 +31,13 @@ const BASE_ALLOWED_TOOLS = [
|
|||||||
"Write",
|
"Write",
|
||||||
"mcp__github_file_ops__commit_files",
|
"mcp__github_file_ops__commit_files",
|
||||||
"mcp__github_file_ops__delete_files",
|
"mcp__github_file_ops__delete_files",
|
||||||
|
"mcp__github_file_ops__update_claude_comment",
|
||||||
];
|
];
|
||||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||||
|
|
||||||
export function buildAllowedToolsString(
|
export function buildAllowedToolsString(customAllowedTools?: string): string {
|
||||||
eventData: EventData,
|
|
||||||
customAllowedTools?: string,
|
|
||||||
): string {
|
|
||||||
let baseTools = [...BASE_ALLOWED_TOOLS];
|
let baseTools = [...BASE_ALLOWED_TOOLS];
|
||||||
|
|
||||||
// Add the appropriate comment tool based on event type
|
|
||||||
if (eventData.eventName === "pull_request_review_comment") {
|
|
||||||
// For inline PR review comments, only use PR comment tool
|
|
||||||
baseTools.push("mcp__github__update_pull_request_comment");
|
|
||||||
} else {
|
|
||||||
// For all other events (issue comments, PR reviews, issues), use issue comment tool
|
|
||||||
baseTools.push("mcp__github__update_issue_comment");
|
|
||||||
}
|
|
||||||
|
|
||||||
let allAllowedTools = baseTools.join(",");
|
let allAllowedTools = baseTools.join(",");
|
||||||
if (customAllowedTools) {
|
if (customAllowedTools) {
|
||||||
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
|
allAllowedTools = `${allAllowedTools},${customAllowedTools}`;
|
||||||
@@ -58,10 +47,27 @@ export function buildAllowedToolsString(
|
|||||||
|
|
||||||
export function buildDisallowedToolsString(
|
export function buildDisallowedToolsString(
|
||||||
customDisallowedTools?: string,
|
customDisallowedTools?: string,
|
||||||
|
allowedTools?: string,
|
||||||
): string {
|
): string {
|
||||||
let allDisallowedTools = DISALLOWED_TOOLS.join(",");
|
let disallowedTools = [...DISALLOWED_TOOLS];
|
||||||
|
|
||||||
|
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
|
||||||
|
if (allowedTools) {
|
||||||
|
const allowedToolsArray = allowedTools
|
||||||
|
.split(",")
|
||||||
|
.map((tool) => tool.trim());
|
||||||
|
disallowedTools = disallowedTools.filter(
|
||||||
|
(tool) => !allowedToolsArray.includes(tool),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let allDisallowedTools = disallowedTools.join(",");
|
||||||
if (customDisallowedTools) {
|
if (customDisallowedTools) {
|
||||||
|
if (allDisallowedTools) {
|
||||||
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
|
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
|
||||||
|
} else {
|
||||||
|
allDisallowedTools = customDisallowedTools;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return allDisallowedTools;
|
return allDisallowedTools;
|
||||||
}
|
}
|
||||||
@@ -69,7 +75,7 @@ export function buildDisallowedToolsString(
|
|||||||
export function prepareContext(
|
export function prepareContext(
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
claudeCommentId: string,
|
claudeCommentId: string,
|
||||||
defaultBranch?: string,
|
baseBranch?: string,
|
||||||
claudeBranch?: string,
|
claudeBranch?: string,
|
||||||
): PreparedContext {
|
): PreparedContext {
|
||||||
const repository = context.repository.full_name;
|
const repository = context.repository.full_name;
|
||||||
@@ -147,7 +153,7 @@ export function prepareContext(
|
|||||||
...(commentId && { commentId }),
|
...(commentId && { commentId }),
|
||||||
commentBody,
|
commentBody,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(defaultBranch && { defaultBranch }),
|
...(baseBranch && { baseBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -169,7 +175,7 @@ export function prepareContext(
|
|||||||
prNumber,
|
prNumber,
|
||||||
commentBody,
|
commentBody,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(defaultBranch && { defaultBranch }),
|
...(baseBranch && { baseBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -194,13 +200,13 @@ export function prepareContext(
|
|||||||
prNumber,
|
prNumber,
|
||||||
commentBody,
|
commentBody,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(defaultBranch && { defaultBranch }),
|
...(baseBranch && { baseBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
} else if (!claudeBranch) {
|
} else if (!claudeBranch) {
|
||||||
throw new Error("CLAUDE_BRANCH is required for issue_comment event");
|
throw new Error("CLAUDE_BRANCH is required for issue_comment event");
|
||||||
} else if (!defaultBranch) {
|
} else if (!baseBranch) {
|
||||||
throw new Error("DEFAULT_BRANCH is required for issue_comment event");
|
throw new Error("BASE_BRANCH is required for issue_comment event");
|
||||||
} else if (!issueNumber) {
|
} else if (!issueNumber) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"ISSUE_NUMBER is required for issue_comment event for issues",
|
"ISSUE_NUMBER is required for issue_comment event for issues",
|
||||||
@@ -212,7 +218,7 @@ export function prepareContext(
|
|||||||
commentId,
|
commentId,
|
||||||
isPR: false,
|
isPR: false,
|
||||||
claudeBranch: claudeBranch,
|
claudeBranch: claudeBranch,
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
commentBody,
|
commentBody,
|
||||||
};
|
};
|
||||||
@@ -228,8 +234,8 @@ export function prepareContext(
|
|||||||
if (isPR) {
|
if (isPR) {
|
||||||
throw new Error("IS_PR must be false for issues event");
|
throw new Error("IS_PR must be false for issues event");
|
||||||
}
|
}
|
||||||
if (!defaultBranch) {
|
if (!baseBranch) {
|
||||||
throw new Error("DEFAULT_BRANCH is required for issues event");
|
throw new Error("BASE_BRANCH is required for issues event");
|
||||||
}
|
}
|
||||||
if (!claudeBranch) {
|
if (!claudeBranch) {
|
||||||
throw new Error("CLAUDE_BRANCH is required for issues event");
|
throw new Error("CLAUDE_BRANCH is required for issues event");
|
||||||
@@ -246,7 +252,7 @@ export function prepareContext(
|
|||||||
eventAction: "assigned",
|
eventAction: "assigned",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
assigneeTrigger,
|
assigneeTrigger,
|
||||||
};
|
};
|
||||||
@@ -256,7 +262,7 @@ export function prepareContext(
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -277,7 +283,7 @@ export function prepareContext(
|
|||||||
isPR: true,
|
isPR: true,
|
||||||
prNumber,
|
prNumber,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(defaultBranch && { defaultBranch }),
|
...(baseBranch && { baseBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -430,33 +436,15 @@ ${sanitizeContent(context.directPrompt)}
|
|||||||
</direct_prompt>`
|
</direct_prompt>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
${
|
${`<comment_tool_info>
|
||||||
eventData.eventName === "pull_request_review_comment"
|
IMPORTANT: You have been provided with the mcp__github_file_ops__update_claude_comment tool to update your comment. This tool automatically handles both issue and PR comments.
|
||||||
? `<comment_tool_info>
|
|
||||||
IMPORTANT: For this inline PR review comment, you have been provided with ONLY the mcp__github__update_pull_request_comment tool to update this specific review comment.
|
|
||||||
|
|
||||||
Tool usage example for mcp__github__update_pull_request_comment:
|
Tool usage example for mcp__github_file_ops__update_claude_comment:
|
||||||
{
|
{
|
||||||
"owner": "${context.repository.split("/")[0]}",
|
|
||||||
"repo": "${context.repository.split("/")[1]}",
|
|
||||||
"commentId": ${eventData.commentId || context.claudeCommentId},
|
|
||||||
"body": "Your comment text here"
|
"body": "Your comment text here"
|
||||||
}
|
}
|
||||||
All four parameters (owner, repo, commentId, body) are required.
|
Only the body parameter is required - the tool automatically knows which comment to update.
|
||||||
</comment_tool_info>`
|
</comment_tool_info>`}
|
||||||
: `<comment_tool_info>
|
|
||||||
IMPORTANT: For this event type, you have been provided with ONLY the mcp__github__update_issue_comment tool to update comments.
|
|
||||||
|
|
||||||
Tool usage example for mcp__github__update_issue_comment:
|
|
||||||
{
|
|
||||||
"owner": "${context.repository.split("/")[0]}",
|
|
||||||
"repo": "${context.repository.split("/")[1]}",
|
|
||||||
"commentId": ${context.claudeCommentId},
|
|
||||||
"body": "Your comment text here"
|
|
||||||
}
|
|
||||||
All four parameters (owner, repo, commentId, body) are required.
|
|
||||||
</comment_tool_info>`
|
|
||||||
}
|
|
||||||
|
|
||||||
Your task is to analyze the context, understand the request, and provide helpful responses and/or implement code changes as needed.
|
Your task is to analyze the context, understand the request, and provide helpful responses and/or implement code changes as needed.
|
||||||
|
|
||||||
@@ -470,7 +458,7 @@ Follow these steps:
|
|||||||
1. Create a Todo List:
|
1. Create a Todo List:
|
||||||
- Use your GitHub comment to maintain a detailed task list based on the request.
|
- Use your GitHub comment to maintain a detailed task list based on the request.
|
||||||
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
|
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
|
||||||
- Update the comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__github__update_pull_request_comment" : "mcp__github__update_issue_comment"} with each task completion.
|
- Update the comment using mcp__github_file_ops__update_claude_comment with each task completion.
|
||||||
|
|
||||||
2. Gather Context:
|
2. Gather Context:
|
||||||
- Analyze the pre-fetched data provided above.
|
- Analyze the pre-fetched data provided above.
|
||||||
@@ -500,11 +488,11 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
- Look for bugs, security issues, performance problems, and other issues
|
- Look for bugs, security issues, performance problems, and other issues
|
||||||
- Suggest improvements for readability and maintainability
|
- Suggest improvements for readability and maintainability
|
||||||
- Check for best practices and coding standards
|
- Check for best practices and coding standards
|
||||||
- Reference specific code sections with file paths and line numbers${eventData.isPR ? "\n - AFTER reading files and analyzing code, you MUST call mcp__github__update_issue_comment to post your review" : ""}
|
- Reference specific code sections with file paths and line numbers${eventData.isPR ? "\n - AFTER reading files and analyzing code, you MUST call mcp__github_file_ops__update_claude_comment to post your review" : ""}
|
||||||
- Formulate a concise, technical, and helpful response based on the context.
|
- Formulate a concise, technical, and helpful response based on the context.
|
||||||
- Reference specific code with inline formatting or code blocks.
|
- Reference specific code with inline formatting or code blocks.
|
||||||
- Include relevant file paths and line numbers when applicable.
|
- Include relevant file paths and line numbers when applicable.
|
||||||
- ${eventData.isPR ? "IMPORTANT: Submit your review feedback by updating the Claude comment. This will be displayed as your PR review." : "Remember that this feedback must be posted to the GitHub comment."}
|
- ${eventData.isPR ? "IMPORTANT: Submit your review feedback by updating the Claude comment using mcp__github_file_ops__update_claude_comment. This will be displayed as your PR review." : "Remember that this feedback must be posted to the GitHub comment using mcp__github_file_ops__update_claude_comment."}
|
||||||
|
|
||||||
B. For Straightforward Changes:
|
B. For Straightforward Changes:
|
||||||
- Use file system tools to make the change locally.
|
- Use file system tools to make the change locally.
|
||||||
@@ -524,13 +512,13 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
${
|
${
|
||||||
eventData.claudeBranch
|
eventData.claudeBranch
|
||||||
? `- Provide a URL to create a PR manually in this format:
|
? `- Provide a URL to create a PR manually in this format:
|
||||||
[Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.defaultBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
[Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
||||||
- IMPORTANT: Use THREE dots (...) between branch names, not two (..)
|
- IMPORTANT: Use THREE dots (...) between branch names, not two (..)
|
||||||
Example: ${GITHUB_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
|
Example: ${GITHUB_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
|
||||||
NOT: ${GITHUB_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect)
|
NOT: ${GITHUB_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect)
|
||||||
- IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces
|
- IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces
|
||||||
Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message"
|
Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message"
|
||||||
- The target-branch should be '${eventData.defaultBranch}'.
|
- The target-branch should be '${eventData.baseBranch}'.
|
||||||
- The branch-name is the current branch: ${eventData.claudeBranch}
|
- The branch-name is the current branch: ${eventData.claudeBranch}
|
||||||
- The body should include:
|
- The body should include:
|
||||||
- A clear description of the changes
|
- A clear description of the changes
|
||||||
@@ -559,8 +547,8 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
|
|
||||||
Important Notes:
|
Important Notes:
|
||||||
- All communication must happen through GitHub PR comments.
|
- All communication must happen through GitHub PR comments.
|
||||||
- Never create new comments. Only update the existing comment using ${eventData.eventName === "pull_request_review_comment" ? "mcp__github__update_pull_request_comment" : "mcp__github__update_issue_comment"} with comment_id: ${context.claudeCommentId}.
|
- Never create new comments. Only update the existing comment using mcp__github_file_ops__update_claude_comment.
|
||||||
- This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__github__update_issue_comment. Do NOT just respond with a normal response, the user will not see it." : ""}
|
- This includes ALL responses: code reviews, answers to questions, progress updates, and final results.${eventData.isPR ? "\n- PR CRITICAL: After reading files and forming your response, you MUST post it by calling mcp__github_file_ops__update_claude_comment. Do NOT just respond with a normal response, the user will not see it." : ""}
|
||||||
- You communicate exclusively by editing your single comment - not through any other means.
|
- You communicate exclusively by editing your single comment - not through any other means.
|
||||||
- Use this spinner HTML when work is in progress: <img src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
|
- Use this spinner HTML when work is in progress: <img src="https://github.com/user-attachments/assets/5ac382c7-e004-429b-8e35-7feb3e8f9c6f" width="14px" height="14px" style="vertical-align: middle; margin-left: 4px;" />
|
||||||
${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch || "the created branch"}). Never create new branches when triggered on issues or closed/merged PRs.`}
|
${eventData.isPR && !eventData.claudeBranch ? `- Always push to the existing branch when triggered on a PR.` : `- IMPORTANT: You are already on the correct branch (${eventData.claudeBranch || "the created branch"}). Never create new branches when triggered on issues or closed/merged PRs.`}
|
||||||
@@ -594,6 +582,11 @@ What You CANNOT Do:
|
|||||||
- Execute commands outside the repository context
|
- Execute commands outside the repository context
|
||||||
- Run arbitrary Bash commands (unless explicitly allowed via allowed_tools configuration)
|
- Run arbitrary Bash commands (unless explicitly allowed via allowed_tools configuration)
|
||||||
- Perform branch operations (cannot merge branches, rebase, or perform other git operations beyond pushing commits)
|
- Perform branch operations (cannot merge branches, rebase, or perform other git operations beyond pushing commits)
|
||||||
|
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
|
||||||
|
- View CI/CD results or workflow run outputs (cannot access GitHub Actions logs or test results)
|
||||||
|
|
||||||
|
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/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.
|
||||||
|
|
||||||
@@ -615,7 +608,7 @@ f. If you are unable to complete certain steps, such as running a linter or test
|
|||||||
|
|
||||||
export async function createPrompt(
|
export async function createPrompt(
|
||||||
claudeCommentId: number,
|
claudeCommentId: number,
|
||||||
defaultBranch: string | undefined,
|
baseBranch: string | undefined,
|
||||||
claudeBranch: string | undefined,
|
claudeBranch: string | undefined,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
@@ -624,7 +617,7 @@ export async function createPrompt(
|
|||||||
const preparedContext = prepareContext(
|
const preparedContext = prepareContext(
|
||||||
context,
|
context,
|
||||||
claudeCommentId.toString(),
|
claudeCommentId.toString(),
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -643,11 +636,11 @@ export async function createPrompt(
|
|||||||
|
|
||||||
// Set allowed tools
|
// Set allowed tools
|
||||||
const allAllowedTools = buildAllowedToolsString(
|
const allAllowedTools = buildAllowedToolsString(
|
||||||
preparedContext.eventData,
|
|
||||||
preparedContext.allowedTools,
|
preparedContext.allowedTools,
|
||||||
);
|
);
|
||||||
const allDisallowedTools = buildDisallowedToolsString(
|
const allDisallowedTools = buildDisallowedToolsString(
|
||||||
preparedContext.disallowedTools,
|
preparedContext.disallowedTools,
|
||||||
|
preparedContext.allowedTools,
|
||||||
);
|
);
|
||||||
|
|
||||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type PullRequestReviewCommentEvent = {
|
|||||||
commentId?: string; // May be present for review comments
|
commentId?: string; // May be present for review comments
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
defaultBranch?: string;
|
baseBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PullRequestReviewEvent = {
|
type PullRequestReviewEvent = {
|
||||||
@@ -25,7 +25,7 @@ type PullRequestReviewEvent = {
|
|||||||
prNumber: string;
|
prNumber: string;
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
defaultBranch?: string;
|
baseBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IssueCommentEvent = {
|
type IssueCommentEvent = {
|
||||||
@@ -33,7 +33,7 @@ type IssueCommentEvent = {
|
|||||||
commentId: string;
|
commentId: string;
|
||||||
issueNumber: string;
|
issueNumber: string;
|
||||||
isPR: false;
|
isPR: false;
|
||||||
defaultBranch: string;
|
baseBranch: string;
|
||||||
claudeBranch: string;
|
claudeBranch: string;
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ type PullRequestCommentEvent = {
|
|||||||
isPR: true;
|
isPR: true;
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
defaultBranch?: string;
|
baseBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IssueOpenedEvent = {
|
type IssueOpenedEvent = {
|
||||||
@@ -54,7 +54,7 @@ type IssueOpenedEvent = {
|
|||||||
eventAction: "opened";
|
eventAction: "opened";
|
||||||
isPR: false;
|
isPR: false;
|
||||||
issueNumber: string;
|
issueNumber: string;
|
||||||
defaultBranch: string;
|
baseBranch: string;
|
||||||
claudeBranch: string;
|
claudeBranch: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ type IssueAssignedEvent = {
|
|||||||
eventAction: "assigned";
|
eventAction: "assigned";
|
||||||
isPR: false;
|
isPR: false;
|
||||||
issueNumber: string;
|
issueNumber: string;
|
||||||
defaultBranch: string;
|
baseBranch: string;
|
||||||
claudeBranch: string;
|
claudeBranch: string;
|
||||||
assigneeTrigger: string;
|
assigneeTrigger: string;
|
||||||
};
|
};
|
||||||
@@ -74,7 +74,7 @@ type PullRequestEvent = {
|
|||||||
isPR: true;
|
isPR: true;
|
||||||
prNumber: string;
|
prNumber: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
defaultBranch?: string;
|
baseBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Union type for all possible event types
|
// Union type for all possible event types
|
||||||
|
|||||||
@@ -77,22 +77,28 @@ async function run() {
|
|||||||
// Step 10: Create prompt file
|
// Step 10: Create prompt file
|
||||||
await createPrompt(
|
await createPrompt(
|
||||||
commentId,
|
commentId,
|
||||||
branchInfo.defaultBranch,
|
branchInfo.baseBranch,
|
||||||
branchInfo.claudeBranch,
|
branchInfo.claudeBranch,
|
||||||
githubData,
|
githubData,
|
||||||
context,
|
context,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 11: Get MCP configuration
|
// Step 11: Get MCP configuration
|
||||||
const mcpConfig = await prepareMcpConfig(
|
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||||
|
const mcpConfig = await prepareMcpConfig({
|
||||||
githubToken,
|
githubToken,
|
||||||
context.repository.owner,
|
owner: context.repository.owner,
|
||||||
context.repository.repo,
|
repo: context.repository.repo,
|
||||||
branchInfo.currentBranch,
|
branch: branchInfo.currentBranch,
|
||||||
);
|
additionalMcpConfig,
|
||||||
|
claudeCommentId: commentId.toString(),
|
||||||
|
});
|
||||||
core.setOutput("mcp_config", mcpConfig);
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(`Prepare step failed with error: ${error}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
||||||
|
// Also output the clean error message for the action to capture
|
||||||
|
core.setOutput("prepare_error", errorMessage);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ import {
|
|||||||
} from "../github/context";
|
} from "../github/context";
|
||||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||||
import { checkAndDeleteEmptyBranch } from "../github/operations/branch-cleanup";
|
import { checkAndDeleteEmptyBranch } from "../github/operations/branch-cleanup";
|
||||||
|
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
|
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
|
||||||
const githubToken = process.env.GITHUB_TOKEN!;
|
const githubToken = process.env.GITHUB_TOKEN!;
|
||||||
const claudeBranch = process.env.CLAUDE_BRANCH;
|
const claudeBranch = process.env.CLAUDE_BRANCH;
|
||||||
const defaultBranch = process.env.DEFAULT_BRANCH || "main";
|
const baseBranch = process.env.BASE_BRANCH || "main";
|
||||||
const triggerUsername = process.env.TRIGGER_USERNAME;
|
const triggerUsername = process.env.TRIGGER_USERNAME;
|
||||||
|
|
||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
@@ -92,7 +93,7 @@ async function run() {
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if we need to add PR URL when we have a new branch
|
// Check if we need to add PR URL when we have a new branch
|
||||||
@@ -102,7 +103,7 @@ async function run() {
|
|||||||
// Check if comment already contains a PR URL
|
// Check if comment already contains a PR URL
|
||||||
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const prUrlPattern = new RegExp(
|
const prUrlPattern = new RegExp(
|
||||||
`${serverUrlPattern}\\/.+\\/compare\\/${defaultBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
|
`${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
|
||||||
);
|
);
|
||||||
const containsPRUrl = currentBody.match(prUrlPattern);
|
const containsPRUrl = currentBody.match(prUrlPattern);
|
||||||
|
|
||||||
@@ -113,7 +114,7 @@ async function run() {
|
|||||||
await octokit.rest.repos.compareCommitsWithBasehead({
|
await octokit.rest.repos.compareCommitsWithBasehead({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
basehead: `${defaultBranch}...${claudeBranch}`,
|
basehead: `${baseBranch}...${claudeBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there are changes (commits or file changes), add the PR URL
|
// If there are changes (commits or file changes), add the PR URL
|
||||||
@@ -128,7 +129,7 @@ async function run() {
|
|||||||
const prBody = encodeURIComponent(
|
const prBody = encodeURIComponent(
|
||||||
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
|
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
|
||||||
);
|
);
|
||||||
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${defaultBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
|
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
|
||||||
prLink = `\n[Create a PR](${prUrl})`;
|
prLink = `\n[Create a PR](${prUrl})`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -145,7 +146,16 @@ async function run() {
|
|||||||
duration_api_ms?: number;
|
duration_api_ms?: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
let actionFailed = false;
|
let actionFailed = false;
|
||||||
|
let errorDetails: string | undefined;
|
||||||
|
|
||||||
|
// First check if prepare step failed
|
||||||
|
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||||
|
const prepareError = process.env.PREPARE_ERROR;
|
||||||
|
|
||||||
|
if (!prepareSuccess && prepareError) {
|
||||||
|
actionFailed = true;
|
||||||
|
errorDetails = prepareError;
|
||||||
|
} else {
|
||||||
// Check for existence of output file and parse it if available
|
// Check for existence of output file and parse it if available
|
||||||
try {
|
try {
|
||||||
const outputFile = process.env.OUTPUT_FILE;
|
const outputFile = process.env.OUTPUT_FILE;
|
||||||
@@ -157,7 +167,7 @@ async function run() {
|
|||||||
if (Array.isArray(outputData) && outputData.length > 0) {
|
if (Array.isArray(outputData) && outputData.length > 0) {
|
||||||
const lastElement = outputData[outputData.length - 1];
|
const lastElement = outputData[outputData.length - 1];
|
||||||
if (
|
if (
|
||||||
lastElement.role === "system" &&
|
lastElement.type === "result" &&
|
||||||
"cost_usd" in lastElement &&
|
"cost_usd" in lastElement &&
|
||||||
"duration_ms" in lastElement
|
"duration_ms" in lastElement
|
||||||
) {
|
) {
|
||||||
@@ -170,7 +180,7 @@ async function run() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the action failed by looking at the exit code or error marker
|
// Check if the Claude action failed
|
||||||
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
||||||
actionFailed = !claudeSuccess;
|
actionFailed = !claudeSuccess;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -178,6 +188,7 @@ async function run() {
|
|||||||
// If we can't read the file, check for any failure markers
|
// If we can't read the file, check for any failure markers
|
||||||
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare input for updateCommentBody function
|
// Prepare input for updateCommentBody function
|
||||||
const commentInput: CommentUpdateInput = {
|
const commentInput: CommentUpdateInput = {
|
||||||
@@ -189,27 +200,19 @@ async function run() {
|
|||||||
prLink,
|
prLink,
|
||||||
branchName: shouldDeleteBranch ? undefined : claudeBranch,
|
branchName: shouldDeleteBranch ? undefined : claudeBranch,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
|
errorDetails,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedBody = updateCommentBody(commentInput);
|
const updatedBody = updateCommentBody(commentInput);
|
||||||
|
|
||||||
// Update the comment using the appropriate API
|
|
||||||
try {
|
try {
|
||||||
if (isPRReviewComment) {
|
await updateClaudeComment(octokit.rest, {
|
||||||
await octokit.rest.pulls.updateReviewComment({
|
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
comment_id: commentId,
|
commentId,
|
||||||
body: updatedBody,
|
body: updatedBody,
|
||||||
|
isPullRequestReviewComment: isPRReviewComment,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
await octokit.rest.issues.updateComment({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
comment_id: commentId,
|
|
||||||
body: updatedBody,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
|
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ export type Octokits = {
|
|||||||
|
|
||||||
export function createOctokit(token: string): Octokits {
|
export function createOctokit(token: string): Octokits {
|
||||||
return {
|
return {
|
||||||
rest: new Octokit({ auth: token }),
|
rest: new Octokit({
|
||||||
|
auth: token,
|
||||||
|
baseUrl: GITHUB_API_URL,
|
||||||
|
}),
|
||||||
graphql: graphql.defaults({
|
graphql: graphql.defaults({
|
||||||
baseUrl: GITHUB_API_URL,
|
baseUrl: GITHUB_API_URL,
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type ParsedGitHubContext = {
|
|||||||
disallowedTools: string;
|
disallowedTools: string;
|
||||||
customInstructions: string;
|
customInstructions: string;
|
||||||
directPrompt: string;
|
directPrompt: string;
|
||||||
|
baseBranch?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
disallowedTools: process.env.DISALLOWED_TOOLS ?? "",
|
disallowedTools: process.env.DISALLOWED_TOOLS ?? "",
|
||||||
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
||||||
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
||||||
|
baseBranch: process.env.BASE_BRANCH,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export async function checkAndDeleteEmptyBranch(
|
|||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
claudeBranch: string | undefined,
|
claudeBranch: string | undefined,
|
||||||
defaultBranch: string,
|
baseBranch: string,
|
||||||
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
|
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
|
||||||
let branchLink = "";
|
let branchLink = "";
|
||||||
let shouldDeleteBranch = false;
|
let shouldDeleteBranch = false;
|
||||||
@@ -18,7 +18,7 @@ export async function checkAndDeleteEmptyBranch(
|
|||||||
await octokit.rest.repos.compareCommitsWithBasehead({
|
await octokit.rest.repos.compareCommitsWithBasehead({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
basehead: `${defaultBranch}...${claudeBranch}`,
|
basehead: `${baseBranch}...${claudeBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there are no commits, mark branch for deletion
|
// If there are no commits, mark branch for deletion
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { Octokits } from "../api/client";
|
|||||||
import type { FetchDataResult } from "../data/fetcher";
|
import type { FetchDataResult } from "../data/fetcher";
|
||||||
|
|
||||||
export type BranchInfo = {
|
export type BranchInfo = {
|
||||||
defaultBranch: string;
|
baseBranch: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
currentBranch: string;
|
currentBranch: string;
|
||||||
};
|
};
|
||||||
@@ -26,15 +26,9 @@ export async function setupBranch(
|
|||||||
): Promise<BranchInfo> {
|
): Promise<BranchInfo> {
|
||||||
const { owner, repo } = context.repository;
|
const { owner, repo } = context.repository;
|
||||||
const entityNumber = context.entityNumber;
|
const entityNumber = context.entityNumber;
|
||||||
|
const { baseBranch } = context.inputs;
|
||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
// Get the default branch first
|
|
||||||
const repoResponse = await octokits.rest.repos.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
});
|
|
||||||
const defaultBranch = repoResponse.data.default_branch;
|
|
||||||
|
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
const prData = githubData.contextData as GitHubPullRequest;
|
const prData = githubData.contextData as GitHubPullRequest;
|
||||||
const prState = prData.state;
|
const prState = prData.state;
|
||||||
@@ -42,7 +36,7 @@ export async function setupBranch(
|
|||||||
// Check if PR is closed or merged
|
// Check if PR is closed or merged
|
||||||
if (prState === "CLOSED" || prState === "MERGED") {
|
if (prState === "CLOSED" || prState === "MERGED") {
|
||||||
console.log(
|
console.log(
|
||||||
`PR #${entityNumber} is ${prState}, creating new branch from default...`,
|
`PR #${entityNumber} is ${prState}, creating new branch from source...`,
|
||||||
);
|
);
|
||||||
// Fall through to create a new branch like we do for issues
|
// Fall through to create a new branch like we do for issues
|
||||||
} else {
|
} else {
|
||||||
@@ -58,17 +52,36 @@ export async function setupBranch(
|
|||||||
|
|
||||||
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
||||||
|
|
||||||
// For open PRs, return branch info
|
// For open PRs, we need to get the base branch of the PR
|
||||||
|
const baseBranch = prData.baseRefName;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultBranch,
|
baseBranch,
|
||||||
currentBranch: branchName,
|
currentBranch: branchName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine source branch - use baseBranch if provided, otherwise fetch default
|
||||||
|
let sourceBranch: string;
|
||||||
|
|
||||||
|
if (baseBranch) {
|
||||||
|
// Use provided base branch for source
|
||||||
|
sourceBranch = baseBranch;
|
||||||
|
} else {
|
||||||
|
// No base branch provided, fetch the default branch to use as source
|
||||||
|
const repoResponse = await octokits.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
});
|
||||||
|
sourceBranch = repoResponse.data.default_branch;
|
||||||
|
}
|
||||||
|
|
||||||
// Creating a new branch for either an issue or closed/merged PR
|
// Creating a new branch for either an issue or closed/merged PR
|
||||||
const entityType = isPR ? "pr" : "issue";
|
const entityType = isPR ? "pr" : "issue";
|
||||||
console.log(`Creating new branch for ${entityType} #${entityNumber}...`);
|
console.log(
|
||||||
|
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
||||||
|
);
|
||||||
|
|
||||||
const timestamp = new Date()
|
const timestamp = new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
@@ -80,14 +93,14 @@ export async function setupBranch(
|
|||||||
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the SHA of the default branch
|
// Get the SHA of the source branch
|
||||||
const defaultBranchRef = await octokits.rest.git.getRef({
|
const sourceBranchRef = await octokits.rest.git.getRef({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
ref: `heads/${defaultBranch}`,
|
ref: `heads/${sourceBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentSHA = defaultBranchRef.data.object.sha;
|
const currentSHA = sourceBranchRef.data.object.sha;
|
||||||
|
|
||||||
console.log(`Current SHA: ${currentSHA}`);
|
console.log(`Current SHA: ${currentSHA}`);
|
||||||
|
|
||||||
@@ -109,9 +122,9 @@ export async function setupBranch(
|
|||||||
|
|
||||||
// Set outputs for GitHub Actions
|
// Set outputs for GitHub Actions
|
||||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
core.setOutput("CLAUDE_BRANCH", newBranch);
|
||||||
core.setOutput("DEFAULT_BRANCH", defaultBranch);
|
core.setOutput("BASE_BRANCH", sourceBranch);
|
||||||
return {
|
return {
|
||||||
defaultBranch,
|
baseBranch: sourceBranch,
|
||||||
claudeBranch: newBranch,
|
claudeBranch: newBranch,
|
||||||
currentBranch: newBranch,
|
currentBranch: newBranch,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type CommentUpdateInput = {
|
|||||||
prLink?: string;
|
prLink?: string;
|
||||||
branchName?: string;
|
branchName?: string;
|
||||||
triggerUsername?: string;
|
triggerUsername?: string;
|
||||||
|
errorDetails?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ensureProperlyEncodedUrl(url: string): string | null {
|
export function ensureProperlyEncodedUrl(url: string): string | null {
|
||||||
@@ -75,6 +76,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
actionFailed,
|
actionFailed,
|
||||||
branchName,
|
branchName,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
|
errorDetails,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
// Extract content from the original comment body
|
// Extract content from the original comment body
|
||||||
@@ -177,7 +179,14 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the new body with blank line between header and separator
|
// Build the new body with blank line between header and separator
|
||||||
let newBody = `${header}${links}\n\n---\n`;
|
let newBody = `${header}${links}`;
|
||||||
|
|
||||||
|
// Add error details if available
|
||||||
|
if (actionFailed && errorDetails) {
|
||||||
|
newBody += `\n\n\`\`\`\n${errorDetails}\n\`\`\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
newBody += `\n\n---\n`;
|
||||||
|
|
||||||
// Clean up the body content
|
// Clean up the body content
|
||||||
// Remove any existing View job run, branch links from the bottom
|
// Remove any existing View job run, branch links from the bottom
|
||||||
|
|||||||
70
src/github/operations/comments/update-claude-comment.ts
Normal file
70
src/github/operations/comments/update-claude-comment.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
|
||||||
|
export type UpdateClaudeCommentParams = {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
commentId: number;
|
||||||
|
body: string;
|
||||||
|
isPullRequestReviewComment: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateClaudeCommentResult = {
|
||||||
|
id: number;
|
||||||
|
html_url: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a Claude comment on GitHub (either an issue/PR comment or a PR review comment)
|
||||||
|
*
|
||||||
|
* @param octokit - Authenticated Octokit instance
|
||||||
|
* @param params - Parameters for updating the comment
|
||||||
|
* @returns The updated comment details
|
||||||
|
* @throws Error if the update fails
|
||||||
|
*/
|
||||||
|
export async function updateClaudeComment(
|
||||||
|
octokit: Octokit,
|
||||||
|
params: UpdateClaudeCommentParams,
|
||||||
|
): Promise<UpdateClaudeCommentResult> {
|
||||||
|
const { owner, repo, commentId, body, isPullRequestReviewComment } = params;
|
||||||
|
|
||||||
|
let response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isPullRequestReviewComment) {
|
||||||
|
// Try PR review comment API first
|
||||||
|
response = await octokit.rest.pulls.updateReviewComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: commentId,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Use issue comment API (works for both issues and PR general comments)
|
||||||
|
response = await octokit.rest.issues.updateComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: commentId,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// If PR review comment update fails with 404, fall back to issue comment API
|
||||||
|
if (isPullRequestReviewComment && error.status === 404) {
|
||||||
|
response = await octokit.rest.issues.updateComment({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
comment_id: commentId,
|
||||||
|
body,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: response.data.id,
|
||||||
|
html_url: response.data.html_url,
|
||||||
|
updated_at: response.data.updated_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
isPullRequestReviewCommentEvent,
|
isPullRequestReviewCommentEvent,
|
||||||
type ParsedGitHubContext,
|
type ParsedGitHubContext,
|
||||||
} from "../../context";
|
} from "../../context";
|
||||||
|
import { updateClaudeComment } from "./update-claude-comment";
|
||||||
|
|
||||||
export async function updateTrackingComment(
|
export async function updateTrackingComment(
|
||||||
octokit: Octokits,
|
octokit: Octokits,
|
||||||
@@ -36,25 +37,19 @@ export async function updateTrackingComment(
|
|||||||
|
|
||||||
// Update the existing comment with the branch link
|
// Update the existing comment with the branch link
|
||||||
try {
|
try {
|
||||||
if (isPullRequestReviewCommentEvent(context)) {
|
const isPRReviewComment = isPullRequestReviewCommentEvent(context);
|
||||||
// For PR review comments (inline comments), use the pulls API
|
|
||||||
await octokit.rest.pulls.updateReviewComment({
|
await updateClaudeComment(octokit.rest, {
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
comment_id: commentId,
|
commentId,
|
||||||
body: updatedBody,
|
body: updatedBody,
|
||||||
|
isPullRequestReviewComment: isPRReviewComment,
|
||||||
});
|
});
|
||||||
console.log(`✅ Updated PR review comment ${commentId} with branch link`);
|
|
||||||
} else {
|
console.log(
|
||||||
// For all other comments, use the issues API
|
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with branch link`,
|
||||||
await octokit.rest.issues.updateComment({
|
);
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
comment_id: commentId,
|
|
||||||
body: updatedBody,
|
|
||||||
});
|
|
||||||
console.log(`✅ Updated issue comment ${commentId} with branch link`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating comment with branch link:", error);
|
console.error("Error updating comment with branch link:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { readFile } from "fs/promises";
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { GITHUB_API_URL } from "../github/api/config";
|
import { GITHUB_API_URL } from "../github/api/config";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||||
|
|
||||||
type GitHubRef = {
|
type GitHubRef = {
|
||||||
object: {
|
object: {
|
||||||
@@ -439,6 +441,69 @@ server.tool(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
server.tool(
|
||||||
|
"update_claude_comment",
|
||||||
|
"Update the Claude comment with progress and results (automatically handles both issue and PR comments)",
|
||||||
|
{
|
||||||
|
body: z.string().describe("The updated comment content"),
|
||||||
|
},
|
||||||
|
async ({ body }) => {
|
||||||
|
try {
|
||||||
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
|
const claudeCommentId = process.env.CLAUDE_COMMENT_ID;
|
||||||
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
||||||
|
|
||||||
|
if (!githubToken) {
|
||||||
|
throw new Error("GITHUB_TOKEN environment variable is required");
|
||||||
|
}
|
||||||
|
if (!claudeCommentId) {
|
||||||
|
throw new Error("CLAUDE_COMMENT_ID environment variable is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const owner = REPO_OWNER;
|
||||||
|
const repo = REPO_NAME;
|
||||||
|
const commentId = parseInt(claudeCommentId, 10);
|
||||||
|
|
||||||
|
const octokit = new Octokit({
|
||||||
|
auth: githubToken,
|
||||||
|
});
|
||||||
|
|
||||||
|
const isPullRequestReviewComment =
|
||||||
|
eventName === "pull_request_review_comment";
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(octokit, {
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
commentId,
|
||||||
|
body,
|
||||||
|
isPullRequestReviewComment,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: JSON.stringify(result, null, 2),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: `Error: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
error: errorMessage,
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
async function runServer() {
|
async function runServer() {
|
||||||
const transport = new StdioServerTransport();
|
const transport = new StdioServerTransport();
|
||||||
await server.connect(transport);
|
await server.connect(transport);
|
||||||
|
|||||||
@@ -1,13 +1,27 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
type PrepareConfigParams = {
|
||||||
|
githubToken: string;
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
branch: string;
|
||||||
|
additionalMcpConfig?: string;
|
||||||
|
claudeCommentId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export async function prepareMcpConfig(
|
export async function prepareMcpConfig(
|
||||||
githubToken: string,
|
params: PrepareConfigParams,
|
||||||
owner: string,
|
|
||||||
repo: string,
|
|
||||||
branch: string,
|
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
|
const {
|
||||||
|
githubToken,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branch,
|
||||||
|
additionalMcpConfig,
|
||||||
|
claudeCommentId,
|
||||||
|
} = params;
|
||||||
try {
|
try {
|
||||||
const mcpConfig = {
|
const baseMcpConfig = {
|
||||||
mcpServers: {
|
mcpServers: {
|
||||||
github: {
|
github: {
|
||||||
command: "docker",
|
command: "docker",
|
||||||
@@ -35,12 +49,47 @@ export async function prepareMcpConfig(
|
|||||||
REPO_NAME: repo,
|
REPO_NAME: repo,
|
||||||
BRANCH_NAME: branch,
|
BRANCH_NAME: branch,
|
||||||
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
||||||
|
...(claudeCommentId && { CLAUDE_COMMENT_ID: claudeCommentId }),
|
||||||
|
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
|
||||||
|
IS_PR: process.env.IS_PR || "false",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return JSON.stringify(mcpConfig, null, 2);
|
// Merge with additional MCP config if provided
|
||||||
|
if (additionalMcpConfig && additionalMcpConfig.trim()) {
|
||||||
|
try {
|
||||||
|
const additionalConfig = JSON.parse(additionalMcpConfig);
|
||||||
|
|
||||||
|
// Validate that parsed JSON is an object
|
||||||
|
if (typeof additionalConfig !== "object" || additionalConfig === null) {
|
||||||
|
throw new Error("MCP config must be a valid JSON object");
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
"Merging additional MCP server configuration with built-in servers",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Merge configurations with user config overriding built-in servers
|
||||||
|
const mergedConfig = {
|
||||||
|
...baseMcpConfig,
|
||||||
|
...additionalConfig,
|
||||||
|
mcpServers: {
|
||||||
|
...baseMcpConfig.mcpServers,
|
||||||
|
...additionalConfig.mcpServers,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return JSON.stringify(mergedConfig, null, 2);
|
||||||
|
} catch (parseError) {
|
||||||
|
core.warning(
|
||||||
|
`Failed to parse additional MCP config: ${parseError}. Using base config only.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.stringify(baseMcpConfig, null, 2);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(`Install MCP server failed with error: ${error}`);
|
core.setFailed(`Install MCP server failed with error: ${error}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -39,6 +39,25 @@ describe("updateCommentBody", () => {
|
|||||||
expect(result).toContain("**Claude encountered an error after 45s**");
|
expect(result).toContain("**Claude encountered an error after 45s**");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("includes error details when provided", () => {
|
||||||
|
const input = {
|
||||||
|
...baseInput,
|
||||||
|
currentBody: "Claude Code is working...",
|
||||||
|
actionFailed: true,
|
||||||
|
executionDetails: { duration_ms: 45000 },
|
||||||
|
errorDetails: "Failed to fetch issue data",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = updateCommentBody(input);
|
||||||
|
expect(result).toContain("**Claude encountered an error after 45s**");
|
||||||
|
expect(result).toContain("[View job]");
|
||||||
|
expect(result).toContain("```\nFailed to fetch issue data\n```");
|
||||||
|
// Ensure error details come after the header/links
|
||||||
|
const errorIndex = result.indexOf("```");
|
||||||
|
const headerIndex = result.indexOf("**Claude encountered an error");
|
||||||
|
expect(errorIndex).toBeGreaterThan(headerIndex);
|
||||||
|
});
|
||||||
|
|
||||||
it("handles username extraction from content when not provided", () => {
|
it("handles username extraction from content when not provided", () => {
|
||||||
const input = {
|
const input = {
|
||||||
...baseInput,
|
...baseInput,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
buildDisallowedToolsString,
|
buildDisallowedToolsString,
|
||||||
} from "../src/create-prompt";
|
} from "../src/create-prompt";
|
||||||
import type { PreparedContext } from "../src/create-prompt";
|
import type { PreparedContext } from "../src/create-prompt";
|
||||||
import type { EventData } from "../src/create-prompt/types";
|
|
||||||
|
|
||||||
describe("generatePrompt", () => {
|
describe("generatePrompt", () => {
|
||||||
const mockGitHubData = {
|
const mockGitHubData = {
|
||||||
@@ -127,7 +126,7 @@ describe("generatePrompt", () => {
|
|||||||
eventName: "issue_comment",
|
eventName: "issue_comment",
|
||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||||
issueNumber: "67890",
|
issueNumber: "67890",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
@@ -183,7 +182,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "789",
|
issueNumber: "789",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-789-20240101_120000",
|
claudeBranch: "claude/issue-789-20240101_120000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -210,7 +209,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "assigned",
|
eventAction: "assigned",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "999",
|
issueNumber: "999",
|
||||||
defaultBranch: "develop",
|
baseBranch: "develop",
|
||||||
claudeBranch: "claude/issue-999-20240101_120000",
|
claudeBranch: "claude/issue-999-20240101_120000",
|
||||||
assigneeTrigger: "claude-bot",
|
assigneeTrigger: "claude-bot",
|
||||||
},
|
},
|
||||||
@@ -238,7 +237,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "789",
|
issueNumber: "789",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-789-20240101_120000",
|
claudeBranch: "claude/issue-789-20240101_120000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -285,7 +284,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "123",
|
issueNumber: "123",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
},
|
},
|
||||||
@@ -307,7 +306,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "123",
|
issueNumber: "123",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
},
|
},
|
||||||
@@ -362,7 +361,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "789",
|
issueNumber: "789",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-789-20240101_120000",
|
claudeBranch: "claude/issue-789-20240101_120000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -400,7 +399,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "123",
|
issueNumber: "123",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-123-20240101_120000",
|
claudeBranch: "claude/issue-123-20240101_120000",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
},
|
},
|
||||||
@@ -432,7 +431,7 @@ describe("generatePrompt", () => {
|
|||||||
prNumber: "456",
|
prNumber: "456",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
claudeBranch: "claude/pr-456-20240101_120000",
|
claudeBranch: "claude/pr-456-20240101_120000",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -470,7 +469,7 @@ describe("generatePrompt", () => {
|
|||||||
isPR: true,
|
isPR: true,
|
||||||
prNumber: "456",
|
prNumber: "456",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
// No claudeBranch or defaultBranch for open PRs
|
// No claudeBranch or baseBranch for open PRs
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -503,7 +502,7 @@ describe("generatePrompt", () => {
|
|||||||
prNumber: "789",
|
prNumber: "789",
|
||||||
commentBody: "@claude please update this",
|
commentBody: "@claude please update this",
|
||||||
claudeBranch: "claude/pr-789-20240101_123000",
|
claudeBranch: "claude/pr-789-20240101_123000",
|
||||||
defaultBranch: "develop",
|
baseBranch: "develop",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -531,7 +530,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "review-comment-123",
|
commentId: "review-comment-123",
|
||||||
commentBody: "@claude fix this issue",
|
commentBody: "@claude fix this issue",
|
||||||
claudeBranch: "claude/pr-999-20240101_140000",
|
claudeBranch: "claude/pr-999-20240101_140000",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -559,7 +558,7 @@ describe("generatePrompt", () => {
|
|||||||
isPR: true,
|
isPR: true,
|
||||||
prNumber: "555",
|
prNumber: "555",
|
||||||
claudeBranch: "claude/pr-555-20240101_150000",
|
claudeBranch: "claude/pr-555-20240101_150000",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -604,7 +603,7 @@ describe("getEventTypeAndContext", () => {
|
|||||||
eventAction: "assigned",
|
eventAction: "assigned",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "999",
|
issueNumber: "999",
|
||||||
defaultBranch: "main",
|
baseBranch: "main",
|
||||||
claudeBranch: "claude/issue-999-20240101_120000",
|
claudeBranch: "claude/issue-999-20240101_120000",
|
||||||
assigneeTrigger: "claude-bot",
|
assigneeTrigger: "claude-bot",
|
||||||
},
|
},
|
||||||
@@ -619,15 +618,7 @@ describe("getEventTypeAndContext", () => {
|
|||||||
|
|
||||||
describe("buildAllowedToolsString", () => {
|
describe("buildAllowedToolsString", () => {
|
||||||
test("should return issue comment tool for regular events", () => {
|
test("should return issue comment tool for regular events", () => {
|
||||||
const mockEventData: EventData = {
|
const result = buildAllowedToolsString();
|
||||||
eventName: "issue_comment",
|
|
||||||
commentId: "123",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "456",
|
|
||||||
commentBody: "Test comment",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = buildAllowedToolsString(mockEventData);
|
|
||||||
|
|
||||||
// The base tools should be in the result
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
@@ -636,22 +627,15 @@ describe("buildAllowedToolsString", () => {
|
|||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
expect(result).toContain("Write");
|
||||||
expect(result).toContain("mcp__github__update_issue_comment");
|
expect(result).toContain("mcp__github_file_ops__update_claude_comment");
|
||||||
|
expect(result).not.toContain("mcp__github__update_issue_comment");
|
||||||
expect(result).not.toContain("mcp__github__update_pull_request_comment");
|
expect(result).not.toContain("mcp__github__update_pull_request_comment");
|
||||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should return PR comment tool for inline review comments", () => {
|
test("should return PR comment tool for inline review comments", () => {
|
||||||
const mockEventData: EventData = {
|
const result = buildAllowedToolsString();
|
||||||
eventName: "pull_request_review_comment",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "456",
|
|
||||||
commentBody: "Test review comment",
|
|
||||||
commentId: "789",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = buildAllowedToolsString(mockEventData);
|
|
||||||
|
|
||||||
// The base tools should be in the result
|
// The base tools should be in the result
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
@@ -660,23 +644,16 @@ describe("buildAllowedToolsString", () => {
|
|||||||
expect(result).toContain("LS");
|
expect(result).toContain("LS");
|
||||||
expect(result).toContain("Read");
|
expect(result).toContain("Read");
|
||||||
expect(result).toContain("Write");
|
expect(result).toContain("Write");
|
||||||
|
expect(result).toContain("mcp__github_file_ops__update_claude_comment");
|
||||||
expect(result).not.toContain("mcp__github__update_issue_comment");
|
expect(result).not.toContain("mcp__github__update_issue_comment");
|
||||||
expect(result).toContain("mcp__github__update_pull_request_comment");
|
expect(result).not.toContain("mcp__github__update_pull_request_comment");
|
||||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should append custom tools when provided", () => {
|
test("should append custom tools when provided", () => {
|
||||||
const mockEventData: EventData = {
|
|
||||||
eventName: "issue_comment",
|
|
||||||
commentId: "123",
|
|
||||||
isPR: true,
|
|
||||||
prNumber: "456",
|
|
||||||
commentBody: "Test comment",
|
|
||||||
};
|
|
||||||
|
|
||||||
const customTools = "Tool1,Tool2,Tool3";
|
const customTools = "Tool1,Tool2,Tool3";
|
||||||
const result = buildAllowedToolsString(mockEventData, customTools);
|
const result = buildAllowedToolsString(customTools);
|
||||||
|
|
||||||
// Base tools should be present
|
// Base tools should be present
|
||||||
expect(result).toContain("Edit");
|
expect(result).toContain("Edit");
|
||||||
@@ -722,4 +699,51 @@ describe("buildDisallowedToolsString", () => {
|
|||||||
expect(parts).toContain("BadTool1");
|
expect(parts).toContain("BadTool1");
|
||||||
expect(parts).toContain("BadTool2");
|
expect(parts).toContain("BadTool2");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
|
||||||
|
const customDisallowedTools = "BadTool1,BadTool2";
|
||||||
|
const allowedTools = "WebSearch,SomeOtherTool";
|
||||||
|
const result = buildDisallowedToolsString(
|
||||||
|
customDisallowedTools,
|
||||||
|
allowedTools,
|
||||||
|
);
|
||||||
|
|
||||||
|
// WebSearch should be removed from disallowed since it's in allowed
|
||||||
|
expect(result).not.toContain("WebSearch");
|
||||||
|
|
||||||
|
// WebFetch should still be disallowed since it's not in allowed
|
||||||
|
expect(result).toContain("WebFetch");
|
||||||
|
|
||||||
|
// Custom disallowed tools should still be present
|
||||||
|
expect(result).toContain("BadTool1");
|
||||||
|
expect(result).toContain("BadTool2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
|
||||||
|
const allowedTools = "WebSearch,WebFetch,SomeOtherTool";
|
||||||
|
const result = buildDisallowedToolsString(undefined, allowedTools);
|
||||||
|
|
||||||
|
// Both hardcoded disallowed tools should be removed
|
||||||
|
expect(result).not.toContain("WebSearch");
|
||||||
|
expect(result).not.toContain("WebFetch");
|
||||||
|
|
||||||
|
// Result should be empty since no custom disallowed tools provided
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
|
||||||
|
const customDisallowedTools = "BadTool1,BadTool2";
|
||||||
|
const allowedTools = "WebSearch,WebFetch";
|
||||||
|
const result = buildDisallowedToolsString(
|
||||||
|
customDisallowedTools,
|
||||||
|
allowedTools,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hardcoded tools should be removed
|
||||||
|
expect(result).not.toContain("WebSearch");
|
||||||
|
expect(result).not.toContain("WebFetch");
|
||||||
|
|
||||||
|
// Only custom disallowed tools should remain
|
||||||
|
expect(result).toBe("BadTool1,BadTool2");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
344
test/install-mcp-server.test.ts
Normal file
344
test/install-mcp-server.test.ts
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||||
|
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
describe("prepareMcpConfig", () => {
|
||||||
|
let consoleInfoSpy: any;
|
||||||
|
let consoleWarningSpy: any;
|
||||||
|
let setFailedSpy: any;
|
||||||
|
let processExitSpy: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {});
|
||||||
|
consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
||||||
|
setFailedSpy = spyOn(core, "setFailed").mockImplementation(() => {});
|
||||||
|
processExitSpy = spyOn(process, "exit").mockImplementation(() => {
|
||||||
|
throw new Error("Process exit");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
consoleInfoSpy.mockRestore();
|
||||||
|
consoleWarningSpy.mockRestore();
|
||||||
|
setFailedSpy.mockRestore();
|
||||||
|
processExitSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return base config when no additional config is provided", async () => {
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
||||||
|
"test-token",
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
||||||
|
"test-token",
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github_file_ops.env.REPO_OWNER).toBe("test-owner");
|
||||||
|
expect(parsed.mcpServers.github_file_ops.env.REPO_NAME).toBe("test-repo");
|
||||||
|
expect(parsed.mcpServers.github_file_ops.env.BRANCH_NAME).toBe(
|
||||||
|
"test-branch",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return base config when additional config is empty string", async () => {
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should return base config when additional config is whitespace only", async () => {
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: " \n\t ",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should merge valid additional config with base config", async () => {
|
||||||
|
const additionalConfig = JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
custom_server: {
|
||||||
|
command: "custom-command",
|
||||||
|
args: ["arg1", "arg2"],
|
||||||
|
env: {
|
||||||
|
CUSTOM_ENV: "custom-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: additionalConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||||
|
"Merging additional MCP server configuration with built-in servers",
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.custom_server.command).toBe("custom-command");
|
||||||
|
expect(parsed.mcpServers.custom_server.args).toEqual(["arg1", "arg2"]);
|
||||||
|
expect(parsed.mcpServers.custom_server.env.CUSTOM_ENV).toBe("custom-value");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should override built-in servers when additional config has same server names", async () => {
|
||||||
|
const additionalConfig = JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
github: {
|
||||||
|
command: "overridden-command",
|
||||||
|
args: ["overridden-arg"],
|
||||||
|
env: {
|
||||||
|
OVERRIDDEN_ENV: "overridden-value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: additionalConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||||
|
"Merging additional MCP server configuration with built-in servers",
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github.command).toBe("overridden-command");
|
||||||
|
expect(parsed.mcpServers.github.args).toEqual(["overridden-arg"]);
|
||||||
|
expect(parsed.mcpServers.github.env.OVERRIDDEN_ENV).toBe(
|
||||||
|
"overridden-value",
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN,
|
||||||
|
).toBeUndefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should merge additional root-level properties", async () => {
|
||||||
|
const additionalConfig = JSON.stringify({
|
||||||
|
customProperty: "custom-value",
|
||||||
|
anotherProperty: {
|
||||||
|
nested: "value",
|
||||||
|
},
|
||||||
|
mcpServers: {
|
||||||
|
custom_server: {
|
||||||
|
command: "custom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: additionalConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.customProperty).toBe("custom-value");
|
||||||
|
expect(parsed.anotherProperty).toEqual({ nested: "value" });
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle invalid JSON gracefully", async () => {
|
||||||
|
const invalidJson = "{ invalid json }";
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: invalidJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle non-object JSON values", async () => {
|
||||||
|
const nonObjectJson = JSON.stringify("string value");
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: nonObjectJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||||
|
);
|
||||||
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle null JSON value", async () => {
|
||||||
|
const nullJson = JSON.stringify(null);
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: nullJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||||
|
);
|
||||||
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle array JSON value", async () => {
|
||||||
|
const arrayJson = JSON.stringify([1, 2, 3]);
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: arrayJson,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
// Arrays are objects in JavaScript, so they pass the object check
|
||||||
|
// But they'll fail when trying to spread or access mcpServers property
|
||||||
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||||
|
"Merging additional MCP server configuration with built-in servers",
|
||||||
|
);
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||||
|
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
|
||||||
|
expect(parsed[0]).toBe(1);
|
||||||
|
expect(parsed[1]).toBe(2);
|
||||||
|
expect(parsed[2]).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should merge complex nested configurations", async () => {
|
||||||
|
const additionalConfig = JSON.stringify({
|
||||||
|
mcpServers: {
|
||||||
|
server1: {
|
||||||
|
command: "cmd1",
|
||||||
|
env: { KEY1: "value1" },
|
||||||
|
},
|
||||||
|
server2: {
|
||||||
|
command: "cmd2",
|
||||||
|
env: { KEY2: "value2" },
|
||||||
|
},
|
||||||
|
github_file_ops: {
|
||||||
|
command: "overridden",
|
||||||
|
env: { CUSTOM: "value" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
otherConfig: {
|
||||||
|
nested: {
|
||||||
|
deeply: "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
additionalMcpConfig: additionalConfig,
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers.server1).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.server2).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github).toBeDefined();
|
||||||
|
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
|
||||||
|
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
|
||||||
|
expect(parsed.otherConfig.nested.deeply).toBe("value");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should preserve GITHUB_ACTION_PATH in file_ops server args", async () => {
|
||||||
|
const oldEnv = process.env.GITHUB_ACTION_PATH;
|
||||||
|
process.env.GITHUB_ACTION_PATH = "/test/action/path";
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers.github_file_ops.args[1]).toBe(
|
||||||
|
"/test/action/path/src/mcp/github-file-ops-server.ts",
|
||||||
|
);
|
||||||
|
|
||||||
|
process.env.GITHUB_ACTION_PATH = oldEnv;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use process.cwd() when GITHUB_WORKSPACE is not set", async () => {
|
||||||
|
const oldEnv = process.env.GITHUB_WORKSPACE;
|
||||||
|
delete process.env.GITHUB_WORKSPACE;
|
||||||
|
|
||||||
|
const result = await prepareMcpConfig({
|
||||||
|
githubToken: "test-token",
|
||||||
|
owner: "test-owner",
|
||||||
|
repo: "test-repo",
|
||||||
|
branch: "test-branch",
|
||||||
|
});
|
||||||
|
|
||||||
|
const parsed = JSON.parse(result);
|
||||||
|
expect(parsed.mcpServers.github_file_ops.env.REPO_DIR).toBe(process.cwd());
|
||||||
|
|
||||||
|
process.env.GITHUB_WORKSPACE = oldEnv;
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,7 +34,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env = {
|
process.env = {
|
||||||
...BASE_ENV,
|
...BASE_ENV,
|
||||||
DEFAULT_BRANCH: "main",
|
BASE_BRANCH: "main",
|
||||||
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000",
|
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
expect(result.eventData.claudeBranch).toBe(
|
expect(result.eventData.claudeBranch).toBe(
|
||||||
"claude/issue-67890-20240101_120000",
|
"claude/issue-67890-20240101_120000",
|
||||||
);
|
);
|
||||||
expect(result.eventData.defaultBranch).toBe("main");
|
expect(result.eventData.baseBranch).toBe("main");
|
||||||
expect(result.eventData.commentBody).toBe(
|
expect(result.eventData.commentBody).toBe(
|
||||||
"@claude can you help explain how to configure the logging system?",
|
"@claude can you help explain how to configure the logging system?",
|
||||||
);
|
);
|
||||||
@@ -75,7 +75,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
).toThrow("CLAUDE_BRANCH is required for issue_comment event");
|
).toThrow("CLAUDE_BRANCH is required for issue_comment event");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error when DEFAULT_BRANCH is missing", () => {
|
test("should throw error when BASE_BRANCH is missing", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
prepareContext(
|
prepareContext(
|
||||||
mockIssueCommentContext,
|
mockIssueCommentContext,
|
||||||
@@ -83,7 +83,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
"claude/issue-67890-20240101_120000",
|
"claude/issue-67890-20240101_120000",
|
||||||
),
|
),
|
||||||
).toThrow("DEFAULT_BRANCH is required for issue_comment event");
|
).toThrow("BASE_BRANCH is required for issue_comment event");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env = {
|
process.env = {
|
||||||
...BASE_ENV,
|
...BASE_ENV,
|
||||||
DEFAULT_BRANCH: "main",
|
BASE_BRANCH: "main",
|
||||||
CLAUDE_BRANCH: "claude/issue-42-20240101_120000",
|
CLAUDE_BRANCH: "claude/issue-42-20240101_120000",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -172,7 +172,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
result.eventData.eventAction === "opened"
|
result.eventData.eventAction === "opened"
|
||||||
) {
|
) {
|
||||||
expect(result.eventData.issueNumber).toBe("42");
|
expect(result.eventData.issueNumber).toBe("42");
|
||||||
expect(result.eventData.defaultBranch).toBe("main");
|
expect(result.eventData.baseBranch).toBe("main");
|
||||||
expect(result.eventData.claudeBranch).toBe(
|
expect(result.eventData.claudeBranch).toBe(
|
||||||
"claude/issue-42-20240101_120000",
|
"claude/issue-42-20240101_120000",
|
||||||
);
|
);
|
||||||
@@ -195,7 +195,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
result.eventData.eventAction === "assigned"
|
result.eventData.eventAction === "assigned"
|
||||||
) {
|
) {
|
||||||
expect(result.eventData.issueNumber).toBe("123");
|
expect(result.eventData.issueNumber).toBe("123");
|
||||||
expect(result.eventData.defaultBranch).toBe("main");
|
expect(result.eventData.baseBranch).toBe("main");
|
||||||
expect(result.eventData.claudeBranch).toBe(
|
expect(result.eventData.claudeBranch).toBe(
|
||||||
"claude/issue-123-20240101_120000",
|
"claude/issue-123-20240101_120000",
|
||||||
);
|
);
|
||||||
@@ -209,7 +209,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
).toThrow("CLAUDE_BRANCH is required for issues event");
|
).toThrow("CLAUDE_BRANCH is required for issues event");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error when DEFAULT_BRANCH is missing for issues", () => {
|
test("should throw error when BASE_BRANCH is missing for issues", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
prepareContext(
|
prepareContext(
|
||||||
mockIssueOpenedContext,
|
mockIssueOpenedContext,
|
||||||
@@ -217,7 +217,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
"claude/issue-42-20240101_120000",
|
"claude/issue-42-20240101_120000",
|
||||||
),
|
),
|
||||||
).toThrow("DEFAULT_BRANCH is required for issues event");
|
).toThrow("BASE_BRANCH is required for issues event");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
413
test/update-claude-comment.test.ts
Normal file
413
test/update-claude-comment.test.ts
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
import { describe, test, expect, jest, beforeEach } from "bun:test";
|
||||||
|
import { Octokit } from "@octokit/rest";
|
||||||
|
import {
|
||||||
|
updateClaudeComment,
|
||||||
|
type UpdateClaudeCommentParams,
|
||||||
|
} from "../src/github/operations/comments/update-claude-comment";
|
||||||
|
|
||||||
|
describe("updateClaudeComment", () => {
|
||||||
|
let mockOctokit: Octokit;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockOctokit = {
|
||||||
|
rest: {
|
||||||
|
issues: {
|
||||||
|
updateComment: jest.fn(),
|
||||||
|
},
|
||||||
|
pulls: {
|
||||||
|
updateReviewComment: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as any as Octokit;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should update issue comment successfully", async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 123456,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/1#issuecomment-123456",
|
||||||
|
updated_at: "2024-01-01T00:00:00Z",
|
||||||
|
body: "Updated comment",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.updateComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 123456,
|
||||||
|
body: "Updated comment",
|
||||||
|
isPullRequestReviewComment: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 123456,
|
||||||
|
body: "Updated comment",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 123456,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/1#issuecomment-123456",
|
||||||
|
updated_at: "2024-01-01T00:00:00Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should update PR comment successfully", async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 789012,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/2#issuecomment-789012",
|
||||||
|
updated_at: "2024-01-02T00:00:00Z",
|
||||||
|
body: "Updated PR comment",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.updateComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 789012,
|
||||||
|
body: "Updated PR comment",
|
||||||
|
isPullRequestReviewComment: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 789012,
|
||||||
|
body: "Updated PR comment",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 789012,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/2#issuecomment-789012",
|
||||||
|
updated_at: "2024-01-02T00:00:00Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should update PR review comment successfully", async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 345678,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/3#discussion_r345678",
|
||||||
|
updated_at: "2024-01-03T00:00:00Z",
|
||||||
|
body: "Updated review comment",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 345678,
|
||||||
|
body: "Updated review comment",
|
||||||
|
isPullRequestReviewComment: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.pulls.updateReviewComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 345678,
|
||||||
|
body: "Updated review comment",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 345678,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/3#discussion_r345678",
|
||||||
|
updated_at: "2024-01-03T00:00:00Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should fallback to issue comment API when PR review comment update fails with 404", async () => {
|
||||||
|
const mockError = new Error("Not Found") as any;
|
||||||
|
mockError.status = 404;
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 456789,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/4#issuecomment-456789",
|
||||||
|
updated_at: "2024-01-04T00:00:00Z",
|
||||||
|
body: "Updated via fallback",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(mockError);
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.updateComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 456789,
|
||||||
|
body: "Updated via fallback",
|
||||||
|
isPullRequestReviewComment: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.pulls.updateReviewComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 456789,
|
||||||
|
body: "Updated via fallback",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 456789,
|
||||||
|
body: "Updated via fallback",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 456789,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/4#issuecomment-456789",
|
||||||
|
updated_at: "2024-01-04T00:00:00Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should propagate error when PR review comment update fails with non-404 error", async () => {
|
||||||
|
const mockError = new Error("Internal Server Error") as any;
|
||||||
|
mockError.status = 500;
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(mockError);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 567890,
|
||||||
|
body: "This will fail",
|
||||||
|
isPullRequestReviewComment: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(updateClaudeComment(mockOctokit, params)).rejects.toEqual(
|
||||||
|
mockError,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.pulls.updateReviewComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 567890,
|
||||||
|
body: "This will fail",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure fallback wasn't attempted
|
||||||
|
expect(mockOctokit.rest.issues.updateComment).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should propagate error when issue comment update fails", async () => {
|
||||||
|
const mockError = new Error("Forbidden");
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.updateComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockRejectedValue(mockError);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 678901,
|
||||||
|
body: "This will also fail",
|
||||||
|
isPullRequestReviewComment: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(updateClaudeComment(mockOctokit, params)).rejects.toEqual(
|
||||||
|
mockError,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 678901,
|
||||||
|
body: "This will also fail",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle empty body", async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 111222,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/5#issuecomment-111222",
|
||||||
|
updated_at: "2024-01-05T00:00:00Z",
|
||||||
|
body: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.updateComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 111222,
|
||||||
|
body: "",
|
||||||
|
isPullRequestReviewComment: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 111222,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/5#issuecomment-111222",
|
||||||
|
updated_at: "2024-01-05T00:00:00Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle very long body", async () => {
|
||||||
|
const longBody = "x".repeat(10000);
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 333444,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/6#issuecomment-333444",
|
||||||
|
updated_at: "2024-01-06T00:00:00Z",
|
||||||
|
body: longBody,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.updateComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 333444,
|
||||||
|
body: longBody,
|
||||||
|
isPullRequestReviewComment: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 333444,
|
||||||
|
body: longBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 333444,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/6#issuecomment-333444",
|
||||||
|
updated_at: "2024-01-06T00:00:00Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle markdown formatting in body", async () => {
|
||||||
|
const markdownBody = `
|
||||||
|
# Header
|
||||||
|
- List item 1
|
||||||
|
- List item 2
|
||||||
|
|
||||||
|
\`\`\`typescript
|
||||||
|
const code = "example";
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
[Link](https://example.com)
|
||||||
|
`.trim();
|
||||||
|
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 555666,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/7#issuecomment-555666",
|
||||||
|
updated_at: "2024-01-07T00:00:00Z",
|
||||||
|
body: markdownBody,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.issues.updateComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 555666,
|
||||||
|
body: markdownBody,
|
||||||
|
isPullRequestReviewComment: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
expect(mockOctokit.rest.issues.updateComment).toHaveBeenCalledWith({
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
comment_id: 555666,
|
||||||
|
body: markdownBody,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 555666,
|
||||||
|
html_url: "https://github.com/owner/repo/issues/7#issuecomment-555666",
|
||||||
|
updated_at: "2024-01-07T00:00:00Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should handle different response data fields", async () => {
|
||||||
|
const mockResponse = {
|
||||||
|
data: {
|
||||||
|
id: 777888,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/8#discussion_r777888",
|
||||||
|
updated_at: "2024-01-08T12:30:45Z",
|
||||||
|
body: "Updated",
|
||||||
|
// Additional fields that might be in the response
|
||||||
|
created_at: "2024-01-01T00:00:00Z",
|
||||||
|
user: { login: "bot" },
|
||||||
|
node_id: "MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDc3Nzg4OA==",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-expect-error Mock implementation doesn't match full type signature
|
||||||
|
mockOctokit.rest.pulls.updateReviewComment = jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue(mockResponse);
|
||||||
|
|
||||||
|
const params: UpdateClaudeCommentParams = {
|
||||||
|
owner: "testowner",
|
||||||
|
repo: "testrepo",
|
||||||
|
commentId: 777888,
|
||||||
|
body: "Updated",
|
||||||
|
isPullRequestReviewComment: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await updateClaudeComment(mockOctokit, params);
|
||||||
|
|
||||||
|
// Should only return the specific fields we care about
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 777888,
|
||||||
|
html_url: "https://github.com/owner/repo/pull/8#discussion_r777888",
|
||||||
|
updated_at: "2024-01-08T12:30:45Z",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user