mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
35 Commits
v0.0.30
...
ashwin/tes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d909172ebf | ||
|
|
b04be4e195 | ||
|
|
c23800108f | ||
|
|
23b54ce0d2 | ||
|
|
bf2400d475 | ||
|
|
ed1e708634 | ||
|
|
37f5543283 | ||
|
|
533ec5356f | ||
|
|
f1e95926d6 | ||
|
|
4e2cfbac36 | ||
|
|
018533dc9a | ||
|
|
a9d9ad3612 | ||
|
|
4824494f4d | ||
|
|
c09fc691c5 | ||
|
|
e5b28393c7 | ||
|
|
168e891554 | ||
|
|
7673148cfb | ||
|
|
af74c779a5 | ||
|
|
2877ea975e | ||
|
|
c61f7b0167 | ||
|
|
b16ea06ada | ||
|
|
1eab4a208c | ||
|
|
b938e69075 | ||
|
|
ba5d64171b | ||
|
|
5b3ce5ec6d | ||
|
|
b3c6de94ea | ||
|
|
c6e906e3ba | ||
|
|
dbf69fe645 | ||
|
|
b49014e105 | ||
|
|
b92e56a96b | ||
|
|
b6868bfc27 | ||
|
|
0f9a2c4dc3 | ||
|
|
cefe963a6b | ||
|
|
eda5af4e69 | ||
|
|
87facd7051 |
1
.github/workflows/claude-review.yml
vendored
1
.github/workflows/claude-review.yml
vendored
@@ -26,6 +26,7 @@ jobs:
|
||||
- Potential bugs or issues
|
||||
- Suggestions for improvements
|
||||
- Overall architecture and design decisions
|
||||
- Documentation consistency: Verify that README.md and other documentation files are updated to reflect any code changes (especially new inputs, features, or configuration options)
|
||||
|
||||
Be constructive and specific in your feedback. Give inline comments where applicable.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
7
.github/workflows/claude.yml
vendored
7
.github/workflows/claude.yml
vendored
@@ -31,9 +31,14 @@ jobs:
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
uses: ./
|
||||
with:
|
||||
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"
|
||||
# Test network restrictions
|
||||
allowed_domains: |
|
||||
.anthropic.com
|
||||
.github.com
|
||||
.githubusercontent.com
|
||||
|
||||
209
README.md
209
README.md
@@ -35,6 +35,86 @@ This command will guide you through setting up the GitHub app and required secre
|
||||
- Or `CLAUDE_CODE_OAUTH_TOKEN` for OAuth token authentication (Pro and Max users can generate this by running `claude setup-token` locally)
|
||||
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
|
||||
|
||||
### Using a Custom GitHub App
|
||||
|
||||
If you prefer not to install the official Claude app, you can create your own GitHub App to use with this action. This gives you complete control over permissions and access.
|
||||
|
||||
**When you may want to use a custom GitHub App:**
|
||||
|
||||
- You need more restrictive permissions than the official app
|
||||
- Organization policies prevent installing third-party apps
|
||||
- You're using AWS Bedrock or Google Vertex AI
|
||||
|
||||
**Steps to create and use a custom GitHub App:**
|
||||
|
||||
1. **Create a new GitHub App:**
|
||||
|
||||
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
|
||||
- Click "New GitHub App"
|
||||
- Configure the app with these minimum permissions:
|
||||
- **Repository permissions:**
|
||||
- Contents: Read & Write
|
||||
- Issues: Read & Write
|
||||
- Pull requests: Read & Write
|
||||
- **Account permissions:** None required
|
||||
- Set "Where can this GitHub App be installed?" to your preference
|
||||
- Create the app
|
||||
|
||||
2. **Generate and download a private key:**
|
||||
|
||||
- After creating the app, scroll down to "Private keys"
|
||||
- Click "Generate a private key"
|
||||
- Download the `.pem` file (keep this secure!)
|
||||
|
||||
3. **Install the app on your repository:**
|
||||
|
||||
- Go to the app's settings page
|
||||
- Click "Install App"
|
||||
- Select the repositories where you want to use Claude
|
||||
|
||||
4. **Add the app credentials to your repository secrets:**
|
||||
|
||||
- Go to your repository's Settings → Secrets and variables → Actions
|
||||
- Add these secrets:
|
||||
- `APP_ID`: Your GitHub App's ID (found in the app settings)
|
||||
- `APP_PRIVATE_KEY`: The contents of the downloaded `.pem` file
|
||||
|
||||
5. **Update your workflow to use the custom app:**
|
||||
|
||||
```yaml
|
||||
name: Claude with Custom App
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
# ... other triggers
|
||||
|
||||
jobs:
|
||||
claude-response:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Generate a token from your custom app
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Use Claude with your custom app's token
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
# ... other configuration
|
||||
```
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- The custom app must have read/write permissions for Issues, Pull Requests, and Contents
|
||||
- Your app's token will have the exact permissions you configured, nothing more
|
||||
|
||||
For more information on creating GitHub Apps, see the [GitHub documentation](https://docs.github.com/en/apps/creating-github-apps).
|
||||
|
||||
## 📚 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.
|
||||
@@ -86,7 +166,7 @@ jobs:
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
||||
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
|
||||
@@ -109,7 +189,10 @@ jobs:
|
||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
|
||||
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
|
||||
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
|
||||
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
||||
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
||||
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||
|
||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||
|
||||
@@ -491,6 +574,130 @@ Use a specific Claude model:
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
### Network Restrictions
|
||||
|
||||
For enhanced security, you can restrict Claude's network access to specific domains only. This feature is particularly useful for:
|
||||
|
||||
- Enterprise environments with strict security policies
|
||||
- Preventing access to external services
|
||||
- Limiting Claude to only your internal APIs and services
|
||||
|
||||
When `experimental_allowed_domains` is set, Claude can only access the domains you explicitly list. You'll need to include the appropriate provider domains based on your authentication method.
|
||||
|
||||
#### Provider-Specific Examples
|
||||
|
||||
##### If using Anthropic API or subscription
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Or: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
experimental_allowed_domains: |
|
||||
.anthropic.com
|
||||
```
|
||||
|
||||
##### If using AWS Bedrock
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
experimental_allowed_domains: |
|
||||
bedrock.*.amazonaws.com
|
||||
bedrock-runtime.*.amazonaws.com
|
||||
```
|
||||
|
||||
##### If using Google Vertex AI
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
use_vertex: "true"
|
||||
experimental_allowed_domains: |
|
||||
*.googleapis.com
|
||||
vertexai.googleapis.com
|
||||
```
|
||||
|
||||
#### Common GitHub Domains
|
||||
|
||||
In addition to your provider domains, you may need to include GitHub-related domains. For GitHub.com users, common domains include:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
experimental_allowed_domains: |
|
||||
.anthropic.com # For Anthropic API
|
||||
.github.com
|
||||
.githubusercontent.com
|
||||
ghcr.io
|
||||
.blob.core.windows.net
|
||||
```
|
||||
|
||||
For GitHub Enterprise users, replace the GitHub.com domains above with your enterprise domains (e.g., `.github.company.com`, `packages.company.com`, etc.).
|
||||
|
||||
To determine which domains your workflow needs, you can temporarily run without restrictions and monitor the network requests, or check your GitHub Enterprise configuration for the specific services you use.
|
||||
|
||||
### Claude Code Settings
|
||||
|
||||
You can provide Claude Code settings to customize behavior such as model selection, environment variables, permissions, and hooks. Settings can be provided either as a JSON string or a path to a settings file.
|
||||
|
||||
#### Option 1: Settings File
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
settings: "path/to/settings.json"
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
#### Option 2: Inline Settings
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
settings: |
|
||||
{
|
||||
"model": "claude-opus-4-20250514",
|
||||
"env": {
|
||||
"DEBUG": "true",
|
||||
"API_URL": "https://api.example.com"
|
||||
},
|
||||
"permissions": {
|
||||
"allow": ["Bash", "Read"],
|
||||
"deny": ["WebFetch"]
|
||||
},
|
||||
"hooks": {
|
||||
"PreToolUse": [{
|
||||
"matcher": "Bash",
|
||||
"hooks": [{
|
||||
"type": "command",
|
||||
"command": "echo Running bash command..."
|
||||
}]
|
||||
}]
|
||||
}
|
||||
}
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
The settings support all Claude Code settings options including:
|
||||
|
||||
- `model`: Override the default model
|
||||
- `env`: Environment variables for the session
|
||||
- `permissions`: Tool usage permissions
|
||||
- `hooks`: Pre/post tool execution hooks
|
||||
- And more...
|
||||
|
||||
For a complete list of available settings and their descriptions, see the [Claude Code settings documentation](https://docs.anthropic.com/en/docs/claude-code/settings).
|
||||
|
||||
**Notes**:
|
||||
|
||||
- The `enableAllProjectMcpServers` setting is always set to `true` by this action to ensure MCP servers work correctly.
|
||||
- If both the `model` input parameter and a `model` in settings are provided, the `model` input parameter takes precedence.
|
||||
- The `allowed_tools` and `disallowed_tools` input parameters take precedence over `permissions` in settings.
|
||||
- In a future version, we may deprecate individual input parameters in favor of using the settings file for all configuration.
|
||||
|
||||
## Cloud Providers
|
||||
|
||||
You can authenticate with Claude using any of these three methods:
|
||||
|
||||
52
action.yml
52
action.yml
@@ -60,6 +60,10 @@ inputs:
|
||||
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
|
||||
required: false
|
||||
default: ""
|
||||
settings:
|
||||
description: "Claude Code settings as JSON string or path to settings JSON file"
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
# Auth configuration
|
||||
anthropic_api_key:
|
||||
@@ -92,11 +96,22 @@ inputs:
|
||||
description: "Use just one comment to deliver issue/PR comments"
|
||||
required: false
|
||||
default: "false"
|
||||
use_commit_signing:
|
||||
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
||||
required: false
|
||||
default: "false"
|
||||
experimental_allowed_domains:
|
||||
description: "Restrict network access to these domains only (newline-separated). If not set, no restrictions are applied. Provider domains are auto-detected."
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
outputs:
|
||||
execution_file:
|
||||
description: "Path to the Claude Code execution output file"
|
||||
value: ${{ steps.claude-code.outputs.execution_file }}
|
||||
branch_name:
|
||||
description: "The branch created by Claude Code for this execution"
|
||||
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -133,11 +148,44 @@ runs:
|
||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||
ACTIONS_TOKEN: ${{ github.token }}
|
||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||
|
||||
- name: Setup Network Restrictions
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||
shell: bash
|
||||
run: |
|
||||
# Install and configure Squid proxy
|
||||
sudo apt-get update && sudo apt-get install -y squid
|
||||
|
||||
echo "${{ inputs.experimental_allowed_domains }}" > $RUNNER_TEMP/whitelist.txt
|
||||
|
||||
# Configure Squid
|
||||
sudo tee /etc/squid/squid.conf << EOF
|
||||
http_port 127.0.0.1:3128
|
||||
acl whitelist dstdomain "$RUNNER_TEMP/whitelist.txt"
|
||||
acl localhost src 127.0.0.1/32
|
||||
http_access allow localhost whitelist
|
||||
http_access deny all
|
||||
cache deny all
|
||||
EOF
|
||||
|
||||
# Stop any existing squid instance and start with our config
|
||||
sudo squid -k shutdown || true
|
||||
sleep 2
|
||||
sudo rm -f /run/squid.pid
|
||||
sudo squid -N -d 1 &
|
||||
sleep 5
|
||||
|
||||
# Set proxy environment variables
|
||||
echo "http_proxy=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
echo "https_proxy=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
echo "HTTP_PROXY=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
echo "HTTPS_PROXY=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude-code
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
uses: anthropics/claude-code-base-action@3560d21b41bd19b1d3ac6c9000af378903d8df0e # v0.0.32
|
||||
uses: anthropics/claude-code-base-action@503cc7080e62d63d2cc1d80035ed04617d5efb47 # v0.0.35
|
||||
with:
|
||||
prompt_file: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||
@@ -152,6 +200,7 @@ runs:
|
||||
anthropic_api_key: ${{ inputs.anthropic_api_key }}
|
||||
claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }}
|
||||
claude_env: ${{ inputs.claude_env }}
|
||||
settings: ${{ inputs.settings }}
|
||||
env:
|
||||
# Model configuration
|
||||
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||
@@ -201,6 +250,7 @@ runs:
|
||||
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||
|
||||
- name: Display Claude Code Report
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
||||
|
||||
@@ -36,3 +36,12 @@ jobs:
|
||||
# Or use OAuth token instead:
|
||||
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
timeout_minutes: "60"
|
||||
# Optional: Restrict network access to specific domains only
|
||||
# experimental_allowed_domains: |
|
||||
# .anthropic.com
|
||||
# .github.com
|
||||
# api.github.com
|
||||
# .githubusercontent.com
|
||||
# bun.sh
|
||||
# registry.npmjs.org
|
||||
# .blob.core.windows.net
|
||||
|
||||
@@ -30,18 +30,40 @@ const BASE_ALLOWED_TOOLS = [
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__delete_files",
|
||||
"mcp__github_file_ops__update_claude_comment",
|
||||
];
|
||||
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
|
||||
|
||||
export function buildAllowedToolsString(
|
||||
customAllowedTools?: string[],
|
||||
includeActionsTools: boolean = false,
|
||||
useCommitSigning: boolean = false,
|
||||
): string {
|
||||
let baseTools = [...BASE_ALLOWED_TOOLS];
|
||||
|
||||
// Always include the comment update tool from the comment server
|
||||
baseTools.push("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Add commit signing tools if enabled
|
||||
if (useCommitSigning) {
|
||||
baseTools.push(
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__delete_files",
|
||||
);
|
||||
} else {
|
||||
// When not using commit signing, add specific Bash git commands only
|
||||
baseTools.push(
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git rm:*)",
|
||||
"Bash(git config user.name:*)",
|
||||
"Bash(git config user.email:*)",
|
||||
);
|
||||
}
|
||||
|
||||
// Add GitHub Actions MCP tools if enabled
|
||||
if (includeActionsTools) {
|
||||
baseTools.push(
|
||||
@@ -380,9 +402,68 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
||||
}
|
||||
}
|
||||
|
||||
function getCommitInstructions(
|
||||
eventData: EventData,
|
||||
githubData: FetchDataResult,
|
||||
context: PreparedContext,
|
||||
useCommitSigning: boolean,
|
||||
): string {
|
||||
const coAuthorLine =
|
||||
(githubData.triggerDisplayName ?? context.triggerUsername !== "Unknown")
|
||||
? `Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>`
|
||||
: "";
|
||||
|
||||
if (useCommitSigning) {
|
||||
if (eventData.isPR && !eventData.claudeBranch) {
|
||||
return `
|
||||
- Push directly using mcp__github_file_ops__commit_files to the existing branch (works for both new and existing files).
|
||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||
- When pushing changes with this tool and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
||||
- Use: "${coAuthorLine}"`;
|
||||
} else {
|
||||
return `
|
||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
||||
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||
- When pushing changes and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
||||
- Use: "${coAuthorLine}"`;
|
||||
}
|
||||
} else {
|
||||
// Non-signing instructions
|
||||
if (eventData.isPR && !eventData.claudeBranch) {
|
||||
return `
|
||||
- Use git commands via the Bash tool to commit and push your changes:
|
||||
- Stage files: Bash(git add <files>)
|
||||
- Commit with a descriptive message: Bash(git commit -m "<message>")
|
||||
${
|
||||
coAuthorLine
|
||||
? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer:
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin HEAD)`;
|
||||
} else {
|
||||
const branchName = eventData.claudeBranch || eventData.baseBranch;
|
||||
return `
|
||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
||||
- Use git commands via the Bash tool to commit and push your changes:
|
||||
- Stage files: Bash(git add <files>)
|
||||
- Commit with a descriptive message: Bash(git commit -m "<message>")
|
||||
${
|
||||
coAuthorLine
|
||||
? `- When committing and the trigger user is not "Unknown", include a Co-authored-by trailer:
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin ${branchName})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
): string {
|
||||
const {
|
||||
contextData,
|
||||
@@ -471,9 +552,9 @@ ${sanitizeContent(context.directPrompt)}
|
||||
: ""
|
||||
}
|
||||
${`<comment_tool_info>
|
||||
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.
|
||||
IMPORTANT: You have been provided with the mcp__github_comment__update_claude_comment tool to update your comment. This tool automatically handles both issue and PR comments.
|
||||
|
||||
Tool usage example for mcp__github_file_ops__update_claude_comment:
|
||||
Tool usage example for mcp__github_comment__update_claude_comment:
|
||||
{
|
||||
"body": "Your comment text here"
|
||||
}
|
||||
@@ -492,7 +573,7 @@ Follow these steps:
|
||||
1. Create a Todo List:
|
||||
- Use your GitHub comment to maintain a detailed task list based on the request.
|
||||
- Format todos as a checklist (- [ ] for incomplete, - [x] for complete).
|
||||
- Update the comment using mcp__github_file_ops__update_claude_comment with each task completion.
|
||||
- Update the comment using mcp__github_comment__update_claude_comment with each task completion.
|
||||
|
||||
2. Gather Context:
|
||||
- Analyze the pre-fetched data provided above.
|
||||
@@ -523,29 +604,16 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
||||
- Look for bugs, security issues, performance problems, and other issues
|
||||
- Suggest improvements for readability and maintainability
|
||||
- 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_file_ops__update_claude_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_comment__update_claude_comment to post your review` : ""}
|
||||
- Formulate a concise, technical, and helpful response based on the context.
|
||||
- Reference specific code with inline formatting or code blocks.
|
||||
- Include relevant file paths and line numbers when applicable.
|
||||
- ${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."}
|
||||
- ${eventData.isPR ? `IMPORTANT: Submit your review feedback by updating the Claude comment using mcp__github_comment__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_comment__update_claude_comment.`}
|
||||
|
||||
B. For Straightforward Changes:
|
||||
- Use file system tools to make the change locally.
|
||||
- If you discover related tasks (e.g., updating tests), add them to the todo list.
|
||||
- Mark each subtask as completed as you progress.
|
||||
${
|
||||
eventData.isPR && !eventData.claudeBranch
|
||||
? `
|
||||
- Push directly using mcp__github_file_ops__commit_files to the existing branch (works for both new and existing files).
|
||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||
- When pushing changes with this tool and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
||||
- Use: "Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>"`
|
||||
: `
|
||||
- You are already on the correct branch (${eventData.claudeBranch || "the PR branch"}). Do not create a new branch.
|
||||
- Push changes directly to the current branch using mcp__github_file_ops__commit_files (works for both new and existing files)
|
||||
- Use mcp__github_file_ops__commit_files to commit files atomically in a single commit (supports single or multiple files).
|
||||
- When pushing changes and the trigger user is not "Unknown", include a Co-authored-by trailer in the commit message.
|
||||
- Use: "Co-authored-by: ${githubData.triggerDisplayName ?? context.triggerUsername} <${context.triggerUsername}@users.noreply.github.com>"
|
||||
- Mark each subtask as completed as you progress.${getCommitInstructions(eventData, githubData, context, useCommitSigning)}
|
||||
${
|
||||
eventData.claudeBranch
|
||||
? `- Provide a URL to create a PR manually in this format:
|
||||
@@ -563,7 +631,6 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
||||
- The signature: "Generated with [Claude Code](https://claude.ai/code)"
|
||||
- Just include the markdown link with text "Create a PR" - do not add explanatory text before it like "You can create a PR using this link"`
|
||||
: ""
|
||||
}`
|
||||
}
|
||||
|
||||
C. For Complex Changes:
|
||||
@@ -579,20 +646,31 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
||||
- Always update the GitHub comment to reflect the current todo state.
|
||||
- When all todos are completed, remove the spinner and add a brief summary of what was accomplished, and what was not done.
|
||||
- Note: If you see previous Claude comments with headers like "**Claude finished @user's task**" followed by "---", do not include this in your comment. The system adds this automatically.
|
||||
- If you changed any files locally, you must update them in the remote branch via mcp__github_file_ops__commit_files before saying that you're done.
|
||||
- If you changed any files locally, you must update them in the remote branch via ${useCommitSigning ? "mcp__github_file_ops__commit_files" : "git commands (add, commit, push)"} before saying that you're done.
|
||||
${eventData.claudeBranch ? `- If you created anything in your branch, your comment must include the PR URL with prefilled title and body mentioned above.` : ""}
|
||||
|
||||
Important Notes:
|
||||
- All communication must happen through GitHub PR comments.
|
||||
- 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_file_ops__update_claude_comment. Do NOT just respond with a normal response, the user will not see it." : ""}
|
||||
- Never create new comments. Only update the existing comment using mcp__github_comment__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_comment__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.
|
||||
- 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.`}
|
||||
- Use mcp__github_file_ops__commit_files for making commits (works for both new and existing files, single or multiple). Use mcp__github_file_ops__delete_files for deleting files (supports deleting single or multiple files atomically), or mcp__github__delete_file for deleting a single file. Edit files locally, and the tool will read the content from the same path on disk.
|
||||
${
|
||||
useCommitSigning
|
||||
? `- Use mcp__github_file_ops__commit_files for making commits (works for both new and existing files, single or multiple). Use mcp__github_file_ops__delete_files for deleting files (supports deleting single or multiple files atomically), or mcp__github__delete_file for deleting a single file. Edit files locally, and the tool will read the content from the same path on disk.
|
||||
Tool usage examples:
|
||||
- mcp__github_file_ops__commit_files: {"files": ["path/to/file1.js", "path/to/file2.py"], "message": "feat: add new feature"}
|
||||
- mcp__github_file_ops__delete_files: {"files": ["path/to/old.js"], "message": "chore: remove deprecated file"}
|
||||
- mcp__github_file_ops__delete_files: {"files": ["path/to/old.js"], "message": "chore: remove deprecated file"}`
|
||||
: `- Use git commands via the Bash tool for version control (you have access to specific git commands only):
|
||||
- Stage files: Bash(git add <files>)
|
||||
- Commit changes: Bash(git commit -m "<message>")
|
||||
- Push to remote: Bash(git push origin <branch>) (NEVER force push)
|
||||
- Delete files: Bash(git rm <files>) followed by commit and push
|
||||
- Check status: Bash(git status)
|
||||
- View diff: Bash(git diff)
|
||||
- Configure git user: Bash(git config user.name "...") and Bash(git config user.email "...")`
|
||||
}
|
||||
- Display the todo list as a checklist in the GitHub comment and mark things off as you go.
|
||||
- REPOSITORY SETUP INSTRUCTIONS: The repository's CLAUDE.md file(s) contain critical repo-specific setup instructions, development guidelines, and preferences. Always read and follow these files, particularly the root CLAUDE.md, as they provide essential context for working with the codebase effectively.
|
||||
- Use h3 headers (###) for section titles in your comments, not h1 headers (#).
|
||||
@@ -663,7 +741,11 @@ export async function createPrompt(
|
||||
});
|
||||
|
||||
// Generate the prompt
|
||||
const promptContent = generatePrompt(preparedContext, githubData);
|
||||
const promptContent = generatePrompt(
|
||||
preparedContext,
|
||||
githubData,
|
||||
context.inputs.useCommitSigning,
|
||||
);
|
||||
|
||||
// Log the final prompt to console
|
||||
console.log("===== FINAL PROMPT =====");
|
||||
@@ -683,6 +765,7 @@ export async function createPrompt(
|
||||
const allAllowedTools = buildAllowedToolsString(
|
||||
context.inputs.allowedTools,
|
||||
hasActionsReadPermission,
|
||||
context.inputs.useCommitSigning,
|
||||
);
|
||||
const allDisallowedTools = buildDisallowedToolsString(
|
||||
context.inputs.disallowedTools,
|
||||
|
||||
@@ -12,7 +12,7 @@ import { checkHumanActor } from "../github/validation/actor";
|
||||
import { checkWritePermissions } from "../github/validation/permissions";
|
||||
import { createInitialComment } from "../github/operations/comments/create-initial";
|
||||
import { setupBranch } from "../github/operations/branch";
|
||||
import { updateTrackingComment } from "../github/operations/comments/update-with-branch";
|
||||
import { configureGitAuth } from "../github/operations/git-config";
|
||||
import { prepareMcpConfig } from "../mcp/install-mcp-server";
|
||||
import { createPrompt } from "../create-prompt";
|
||||
import { createOctokit } from "../github/api/client";
|
||||
@@ -51,7 +51,8 @@ async function run() {
|
||||
await checkHumanActor(octokit.rest, context);
|
||||
|
||||
// Step 6: Create initial tracking comment
|
||||
const commentId = await createInitialComment(octokit.rest, context);
|
||||
const commentData = await createInitialComment(octokit.rest, context);
|
||||
const commentId = commentData.id;
|
||||
|
||||
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
|
||||
const githubData = await fetchGitHubData({
|
||||
@@ -65,14 +66,14 @@ async function run() {
|
||||
// Step 8: Setup branch
|
||||
const branchInfo = await setupBranch(octokit, githubData, context);
|
||||
|
||||
// Step 9: Update initial comment with branch link (only for issues that created a new branch)
|
||||
if (branchInfo.claudeBranch) {
|
||||
await updateTrackingComment(
|
||||
octokit,
|
||||
context,
|
||||
commentId,
|
||||
branchInfo.claudeBranch,
|
||||
);
|
||||
// Step 9: Configure git authentication if not using commit signing
|
||||
if (!context.inputs.useCommitSigning) {
|
||||
try {
|
||||
await configureGitAuth(githubToken, context, commentData.user);
|
||||
} catch (error) {
|
||||
console.error("Failed to configure git authentication:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 10: Create prompt file
|
||||
@@ -90,7 +91,8 @@ async function run() {
|
||||
githubToken,
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: branchInfo.currentBranch,
|
||||
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
additionalMcpConfig,
|
||||
claudeCommentId: commentId.toString(),
|
||||
allowedTools: context.inputs.allowedTools,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
isPullRequestReviewCommentEvent,
|
||||
} from "../github/context";
|
||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import { checkAndDeleteEmptyBranch } from "../github/operations/branch-cleanup";
|
||||
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
|
||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||
|
||||
async function run() {
|
||||
@@ -88,12 +88,15 @@ async function run() {
|
||||
const currentBody = comment.body ?? "";
|
||||
|
||||
// Check if we need to add branch link for new branches
|
||||
const { shouldDeleteBranch, branchLink } = await checkAndDeleteEmptyBranch(
|
||||
const useCommitSigning = process.env.USE_COMMIT_SIGNING === "true";
|
||||
const { shouldDeleteBranch, branchLink } =
|
||||
await checkAndCommitOrDeleteBranch(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
claudeBranch,
|
||||
baseBranch,
|
||||
useCommitSigning,
|
||||
);
|
||||
|
||||
// Check if we need to add PR URL when we have a new branch
|
||||
@@ -198,7 +201,7 @@ async function run() {
|
||||
jobUrl,
|
||||
branchLink,
|
||||
prLink,
|
||||
branchName: shouldDeleteBranch ? undefined : claudeBranch,
|
||||
branchName: shouldDeleteBranch || !branchLink ? undefined : claudeBranch,
|
||||
triggerUsername,
|
||||
errorDetails,
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ export type ParsedGitHubContext = {
|
||||
branchPrefix: string;
|
||||
useStickyComment: boolean;
|
||||
additionalPermissions: Map<string, string>;
|
||||
useCommitSigning: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -68,6 +69,7 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
||||
additionalPermissions: parseAdditionalPermissions(
|
||||
process.env.ADDITIONAL_PERMISSIONS ?? "",
|
||||
),
|
||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,17 +1,44 @@
|
||||
import type { Octokits } from "../api/client";
|
||||
import { GITHUB_SERVER_URL } from "../api/config";
|
||||
import { $ } from "bun";
|
||||
|
||||
export async function checkAndDeleteEmptyBranch(
|
||||
export async function checkAndCommitOrDeleteBranch(
|
||||
octokit: Octokits,
|
||||
owner: string,
|
||||
repo: string,
|
||||
claudeBranch: string | undefined,
|
||||
baseBranch: string,
|
||||
useCommitSigning: boolean,
|
||||
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
|
||||
let branchLink = "";
|
||||
let shouldDeleteBranch = false;
|
||||
|
||||
if (claudeBranch) {
|
||||
// First check if the branch exists remotely
|
||||
let branchExistsRemotely = false;
|
||||
try {
|
||||
await octokit.rest.repos.getBranch({
|
||||
owner,
|
||||
repo,
|
||||
branch: claudeBranch,
|
||||
});
|
||||
branchExistsRemotely = true;
|
||||
} catch (error: any) {
|
||||
if (error.status === 404) {
|
||||
console.log(`Branch ${claudeBranch} does not exist remotely`);
|
||||
} else {
|
||||
console.error("Error checking if branch exists:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Only proceed if branch exists remotely
|
||||
if (!branchExistsRemotely) {
|
||||
console.log(
|
||||
`Branch ${claudeBranch} does not exist remotely, no branch link will be added`,
|
||||
);
|
||||
return { shouldDeleteBranch: false, branchLink: "" };
|
||||
}
|
||||
|
||||
// Check if Claude made any commits to the branch
|
||||
try {
|
||||
const { data: comparison } =
|
||||
@@ -21,20 +48,66 @@ export async function checkAndDeleteEmptyBranch(
|
||||
basehead: `${baseBranch}...${claudeBranch}`,
|
||||
});
|
||||
|
||||
// If there are no commits, mark branch for deletion
|
||||
// If there are no commits, check for uncommitted changes if not using commit signing
|
||||
if (comparison.total_commits === 0) {
|
||||
if (!useCommitSigning) {
|
||||
console.log(
|
||||
`Branch ${claudeBranch} has no commits from Claude, checking for uncommitted changes...`,
|
||||
);
|
||||
|
||||
// Check for uncommitted changes using git status
|
||||
try {
|
||||
const gitStatus = await $`git status --porcelain`.quiet();
|
||||
const hasUncommittedChanges =
|
||||
gitStatus.stdout.toString().trim().length > 0;
|
||||
|
||||
if (hasUncommittedChanges) {
|
||||
console.log("Found uncommitted changes, committing them...");
|
||||
|
||||
// Add all changes
|
||||
await $`git add -A`;
|
||||
|
||||
// Commit with a descriptive message
|
||||
const runId = process.env.GITHUB_RUN_ID || "unknown";
|
||||
const commitMessage = `Auto-commit: Save uncommitted changes from Claude\n\nRun ID: ${runId}`;
|
||||
await $`git commit -m ${commitMessage}`;
|
||||
|
||||
// Push the changes
|
||||
await $`git push origin ${claudeBranch}`;
|
||||
|
||||
console.log(
|
||||
"✅ Successfully committed and pushed uncommitted changes",
|
||||
);
|
||||
|
||||
// Set branch link since we now have commits
|
||||
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
|
||||
branchLink = `\n[View branch](${branchUrl})`;
|
||||
} else {
|
||||
console.log(
|
||||
"No uncommitted changes found, marking branch for deletion",
|
||||
);
|
||||
shouldDeleteBranch = true;
|
||||
}
|
||||
} catch (gitError) {
|
||||
console.error("Error checking/committing changes:", gitError);
|
||||
// If we can't check git status, assume the branch might have changes
|
||||
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
|
||||
branchLink = `\n[View branch](${branchUrl})`;
|
||||
}
|
||||
} else {
|
||||
console.log(
|
||||
`Branch ${claudeBranch} has no commits from Claude, will delete it`,
|
||||
);
|
||||
shouldDeleteBranch = true;
|
||||
}
|
||||
} else {
|
||||
// Only add branch link if there are commits
|
||||
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
|
||||
branchLink = `\n[View branch](${branchUrl})`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking for commits on Claude branch:", error);
|
||||
// If we can't check, assume the branch has commits to be safe
|
||||
console.error("Error comparing commits on Claude branch:", error);
|
||||
// If we can't compare but the branch exists remotely, include the branch link
|
||||
const branchUrl = `${GITHUB_SERVER_URL}/${owner}/${repo}/tree/${claudeBranch}`;
|
||||
branchLink = `\n[View branch](${branchUrl})`;
|
||||
}
|
||||
|
||||
@@ -84,23 +84,23 @@ export async function setupBranch(
|
||||
sourceBranch = repoResponse.data.default_branch;
|
||||
}
|
||||
|
||||
// Creating a new branch for either an issue or closed/merged PR
|
||||
// Generate branch name for either an issue or closed/merged PR
|
||||
const entityType = isPR ? "pr" : "issue";
|
||||
console.log(
|
||||
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
||||
);
|
||||
|
||||
const timestamp = new Date()
|
||||
.toISOString()
|
||||
.replace(/[:-]/g, "")
|
||||
.replace(/\.\d{3}Z/, "")
|
||||
.split("T")
|
||||
.join("_");
|
||||
// Create Kubernetes-compatible timestamp: lowercase, hyphens only, shorter format
|
||||
const now = new Date();
|
||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
const newBranch = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
|
||||
// Ensure branch name is Kubernetes-compatible:
|
||||
// - Lowercase only
|
||||
// - Alphanumeric with hyphens
|
||||
// - No underscores
|
||||
// - Max 50 chars (to allow for prefixes)
|
||||
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
|
||||
const newBranch = branchName.toLowerCase().substring(0, 50);
|
||||
|
||||
try {
|
||||
// Get the SHA of the source branch
|
||||
// Get the SHA of the source branch to verify it exists
|
||||
const sourceBranchRef = await octokits.rest.git.getRef({
|
||||
owner,
|
||||
repo,
|
||||
@@ -108,23 +108,34 @@ export async function setupBranch(
|
||||
});
|
||||
|
||||
const currentSHA = sourceBranchRef.data.object.sha;
|
||||
console.log(`Source branch SHA: ${currentSHA}`);
|
||||
|
||||
console.log(`Current SHA: ${currentSHA}`);
|
||||
// For commit signing, defer branch creation to the file ops server
|
||||
if (context.inputs.useCommitSigning) {
|
||||
console.log(
|
||||
`Branch name generated: ${newBranch} (will be created by file ops server on first commit)`,
|
||||
);
|
||||
|
||||
// Create branch using GitHub API
|
||||
await octokits.rest.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/heads/${newBranch}`,
|
||||
sha: currentSHA,
|
||||
});
|
||||
// Set outputs for GitHub Actions
|
||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
||||
core.setOutput("BASE_BRANCH", sourceBranch);
|
||||
return {
|
||||
baseBranch: sourceBranch,
|
||||
claudeBranch: newBranch,
|
||||
currentBranch: sourceBranch, // Stay on source branch for now
|
||||
};
|
||||
}
|
||||
|
||||
// Checkout the new branch (shallow fetch for performance)
|
||||
await $`git fetch origin --depth=1 ${newBranch}`;
|
||||
await $`git checkout ${newBranch}`;
|
||||
// For non-signing case, create and checkout the branch locally only
|
||||
console.log(
|
||||
`Creating local branch ${newBranch} for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
||||
);
|
||||
|
||||
// Create and checkout the new branch locally
|
||||
await $`git checkout -b ${newBranch}`;
|
||||
|
||||
console.log(
|
||||
`Successfully created and checked out new branch: ${newBranch}`,
|
||||
`Successfully created and checked out local branch: ${newBranch}`,
|
||||
);
|
||||
|
||||
// Set outputs for GitHub Actions
|
||||
@@ -136,7 +147,7 @@ export async function setupBranch(
|
||||
currentBranch: newBranch,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error creating branch:", error);
|
||||
console.error("Error in branch setup:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export async function createInitialComment(
|
||||
const githubOutput = process.env.GITHUB_OUTPUT!;
|
||||
appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`);
|
||||
console.log(`✅ Created initial comment with ID: ${response.data.id}`);
|
||||
return response.data.id;
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error in initial comment:", error);
|
||||
|
||||
@@ -102,7 +102,7 @@ export async function createInitialComment(
|
||||
const githubOutput = process.env.GITHUB_OUTPUT!;
|
||||
appendFileSync(githubOutput, `claude_comment_id=${response.data.id}\n`);
|
||||
console.log(`✅ Created fallback comment with ID: ${response.data.id}`);
|
||||
return response.data.id;
|
||||
return response.data;
|
||||
} catch (fallbackError) {
|
||||
console.error("Error creating fallback comment:", fallbackError);
|
||||
throw fallbackError;
|
||||
|
||||
56
src/github/operations/git-config.ts
Normal file
56
src/github/operations/git-config.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Configure git authentication for non-signing mode
|
||||
* Sets up git user and authentication to work with GitHub App tokens
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import { GITHUB_SERVER_URL } from "../api/config";
|
||||
|
||||
type GitUser = {
|
||||
login: string;
|
||||
id: number;
|
||||
};
|
||||
|
||||
export async function configureGitAuth(
|
||||
githubToken: string,
|
||||
context: ParsedGitHubContext,
|
||||
user: GitUser | null,
|
||||
) {
|
||||
console.log("Configuring git authentication for non-signing mode");
|
||||
|
||||
// Configure git user based on the comment creator
|
||||
console.log("Configuring git user...");
|
||||
if (user) {
|
||||
const botName = user.login;
|
||||
const botId = user.id;
|
||||
console.log(`Setting git user as ${botName}...`);
|
||||
await $`git config user.name "${botName}"`;
|
||||
await $`git config user.email "${botId}+${botName}@users.noreply.github.com"`;
|
||||
console.log(`✓ Set git user as ${botName}`);
|
||||
} else {
|
||||
console.log("No user data in comment, using default bot user");
|
||||
await $`git config user.name "github-actions[bot]"`;
|
||||
await $`git config user.email "41898282+github-actions[bot]@users.noreply.github.com"`;
|
||||
}
|
||||
|
||||
// Remove the authorization header that actions/checkout sets
|
||||
console.log("Removing existing git authentication headers...");
|
||||
try {
|
||||
await $`git config --unset-all http.${GITHUB_SERVER_URL}/.extraheader`;
|
||||
console.log("✓ Removed existing authentication headers");
|
||||
} catch (e) {
|
||||
console.log("No existing authentication headers to remove");
|
||||
}
|
||||
|
||||
// Update the remote URL to include the token for authentication
|
||||
console.log("Updating remote URL with authentication...");
|
||||
const serverUrl = new URL(GITHUB_SERVER_URL);
|
||||
const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
|
||||
await $`git remote set-url origin ${remoteUrl}`;
|
||||
console.log("✓ Updated remote URL with authentication token");
|
||||
|
||||
console.log("Git authentication configured successfully");
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
@@ -54,6 +55,7 @@ server.tool(
|
||||
try {
|
||||
const client = new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
baseUrl: GITHUB_API_URL,
|
||||
});
|
||||
|
||||
// Get the PR to find the head SHA
|
||||
@@ -142,6 +144,7 @@ server.tool(
|
||||
try {
|
||||
const client = new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
baseUrl: GITHUB_API_URL,
|
||||
});
|
||||
|
||||
// Get jobs for this workflow run
|
||||
@@ -209,6 +212,7 @@ server.tool(
|
||||
try {
|
||||
const client = new Octokit({
|
||||
auth: GITHUB_TOKEN,
|
||||
baseUrl: GITHUB_API_URL,
|
||||
});
|
||||
|
||||
const response = await client.actions.downloadJobLogsForWorkflowRun({
|
||||
|
||||
98
src/mcp/github-comment-server.ts
Normal file
98
src/mcp/github-comment-server.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env node
|
||||
// GitHub Comment MCP Server - Minimal server that only provides comment update functionality
|
||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||
|
||||
// Get repository information from environment variables
|
||||
const REPO_OWNER = process.env.REPO_OWNER;
|
||||
const REPO_NAME = process.env.REPO_NAME;
|
||||
|
||||
if (!REPO_OWNER || !REPO_NAME) {
|
||||
console.error(
|
||||
"Error: REPO_OWNER and REPO_NAME environment variables are required",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const server = new McpServer({
|
||||
name: "GitHub Comment Server",
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
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,
|
||||
baseUrl: GITHUB_API_URL,
|
||||
});
|
||||
|
||||
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() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
process.on("exit", () => {
|
||||
server.close();
|
||||
});
|
||||
}
|
||||
|
||||
runServer().catch(console.error);
|
||||
@@ -7,8 +7,6 @@ import { readFile } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import fetch from "node-fetch";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||
import { retryWithBackoff } from "../utils/retry";
|
||||
|
||||
type GitHubRef = {
|
||||
@@ -54,6 +52,116 @@ const server = new McpServer({
|
||||
version: "0.0.1",
|
||||
});
|
||||
|
||||
// Helper function to get or create branch reference
|
||||
async function getOrCreateBranchRef(
|
||||
owner: string,
|
||||
repo: string,
|
||||
branch: string,
|
||||
githubToken: string,
|
||||
): Promise<string> {
|
||||
// Try to get the branch reference
|
||||
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||
const refResponse = await fetch(refUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (refResponse.ok) {
|
||||
const refData = (await refResponse.json()) as GitHubRef;
|
||||
return refData.object.sha;
|
||||
}
|
||||
|
||||
if (refResponse.status !== 404) {
|
||||
throw new Error(`Failed to get branch reference: ${refResponse.status}`);
|
||||
}
|
||||
|
||||
const baseBranch = process.env.BASE_BRANCH!;
|
||||
|
||||
// Get the SHA of the base branch
|
||||
const baseRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${baseBranch}`;
|
||||
const baseRefResponse = await fetch(baseRefUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
let baseSha: string;
|
||||
|
||||
if (!baseRefResponse.ok) {
|
||||
// If base branch doesn't exist, try default branch
|
||||
const repoUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}`;
|
||||
const repoResponse = await fetch(repoUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!repoResponse.ok) {
|
||||
throw new Error(`Failed to get repository info: ${repoResponse.status}`);
|
||||
}
|
||||
|
||||
const repoData = (await repoResponse.json()) as {
|
||||
default_branch: string;
|
||||
};
|
||||
const defaultBranch = repoData.default_branch;
|
||||
|
||||
// Try default branch
|
||||
const defaultRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${defaultBranch}`;
|
||||
const defaultRefResponse = await fetch(defaultRefUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!defaultRefResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to get default branch reference: ${defaultRefResponse.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const defaultRefData = (await defaultRefResponse.json()) as GitHubRef;
|
||||
baseSha = defaultRefData.object.sha;
|
||||
} else {
|
||||
const baseRefData = (await baseRefResponse.json()) as GitHubRef;
|
||||
baseSha = baseRefData.object.sha;
|
||||
}
|
||||
|
||||
// Create the new branch using the same pattern as octokit
|
||||
const createRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs`;
|
||||
const createRefResponse = await fetch(createRefUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
ref: `refs/heads/${branch}`,
|
||||
sha: baseSha,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!createRefResponse.ok) {
|
||||
const errorText = await createRefResponse.text();
|
||||
throw new Error(
|
||||
`Failed to create branch: ${createRefResponse.status} - ${errorText}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Successfully created branch ${branch}`);
|
||||
return baseSha;
|
||||
}
|
||||
|
||||
// Commit files tool
|
||||
server.tool(
|
||||
"commit_files",
|
||||
@@ -83,24 +191,13 @@ server.tool(
|
||||
return filePath;
|
||||
});
|
||||
|
||||
// 1. Get the branch reference
|
||||
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||
const refResponse = await fetch(refUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!refResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to get branch reference: ${refResponse.status}`,
|
||||
// 1. Get the branch reference (create if doesn't exist)
|
||||
const baseSha = await getOrCreateBranchRef(
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
|
||||
const refData = (await refResponse.json()) as GitHubRef;
|
||||
const baseSha = refData.object.sha;
|
||||
|
||||
// 2. Get the base commit
|
||||
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
|
||||
@@ -262,7 +359,6 @@ server.tool(
|
||||
|
||||
// Only retry on 403 errors - these are the intermittent failures we're targeting
|
||||
if (updateRefResponse.status === 403) {
|
||||
console.log("Received 403 error, will retry...");
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -355,24 +451,13 @@ server.tool(
|
||||
return filePath;
|
||||
});
|
||||
|
||||
// 1. Get the branch reference
|
||||
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||
const refResponse = await fetch(refUrl, {
|
||||
headers: {
|
||||
Accept: "application/vnd.github+json",
|
||||
Authorization: `Bearer ${githubToken}`,
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
});
|
||||
|
||||
if (!refResponse.ok) {
|
||||
throw new Error(
|
||||
`Failed to get branch reference: ${refResponse.status}`,
|
||||
// 1. Get the branch reference (create if doesn't exist)
|
||||
const baseSha = await getOrCreateBranchRef(
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
githubToken,
|
||||
);
|
||||
}
|
||||
|
||||
const refData = (await refResponse.json()) as GitHubRef;
|
||||
const baseSha = refData.object.sha;
|
||||
|
||||
// 2. Get the base commit
|
||||
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
|
||||
@@ -535,70 +620,6 @@ 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,
|
||||
baseUrl: GITHUB_API_URL,
|
||||
});
|
||||
|
||||
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() {
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
|
||||
@@ -8,6 +8,7 @@ type PrepareConfigParams = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
branch: string;
|
||||
baseBranch: string;
|
||||
additionalMcpConfig?: string;
|
||||
claudeCommentId?: string;
|
||||
allowedTools: string[];
|
||||
@@ -20,7 +21,7 @@ async function checkActionsReadPermission(
|
||||
repo: string,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const client = new Octokit({ auth: token });
|
||||
const client = new Octokit({ auth: token, baseUrl: GITHUB_API_URL });
|
||||
|
||||
// Try to list workflow runs - this requires actions:read
|
||||
// We use per_page=1 to minimize the response size
|
||||
@@ -54,6 +55,7 @@ export async function prepareMcpConfig(
|
||||
owner,
|
||||
repo,
|
||||
branch,
|
||||
baseBranch,
|
||||
additionalMcpConfig,
|
||||
claudeCommentId,
|
||||
allowedTools,
|
||||
@@ -67,8 +69,29 @@ export async function prepareMcpConfig(
|
||||
);
|
||||
|
||||
const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
|
||||
mcpServers: {
|
||||
github_file_ops: {
|
||||
mcpServers: {},
|
||||
};
|
||||
|
||||
// Always include comment server for updating Claude comments
|
||||
baseMcpConfig.mcpServers.github_comment = {
|
||||
command: "bun",
|
||||
args: [
|
||||
"run",
|
||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/github-comment-server.ts`,
|
||||
],
|
||||
env: {
|
||||
GITHUB_TOKEN: githubToken,
|
||||
REPO_OWNER: owner,
|
||||
REPO_NAME: repo,
|
||||
...(claudeCommentId && { CLAUDE_COMMENT_ID: claudeCommentId }),
|
||||
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
|
||||
GITHUB_API_URL: GITHUB_API_URL,
|
||||
},
|
||||
};
|
||||
|
||||
// Include file ops server when commit signing is enabled
|
||||
if (context.inputs.useCommitSigning) {
|
||||
baseMcpConfig.mcpServers.github_file_ops = {
|
||||
command: "bun",
|
||||
args: [
|
||||
"run",
|
||||
@@ -79,15 +102,14 @@ export async function prepareMcpConfig(
|
||||
REPO_OWNER: owner,
|
||||
REPO_NAME: repo,
|
||||
BRANCH_NAME: branch,
|
||||
BASE_BRANCH: baseBranch,
|
||||
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",
|
||||
GITHUB_API_URL: GITHUB_API_URL,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Only add CI server if we have actions:read permission and we're in a PR context
|
||||
const hasActionsReadPermission =
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
||||
import { checkAndDeleteEmptyBranch } from "../src/github/operations/branch-cleanup";
|
||||
import { checkAndCommitOrDeleteBranch } from "../src/github/operations/branch-cleanup";
|
||||
import type { Octokits } from "../src/github/api/client";
|
||||
import { GITHUB_SERVER_URL } from "../src/github/api/config";
|
||||
|
||||
describe("checkAndDeleteEmptyBranch", () => {
|
||||
describe("checkAndCommitOrDeleteBranch", () => {
|
||||
let consoleLogSpy: any;
|
||||
let consoleErrorSpy: any;
|
||||
|
||||
@@ -21,6 +21,7 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
const createMockOctokit = (
|
||||
compareResponse?: any,
|
||||
deleteRefError?: Error,
|
||||
branchExists: boolean = true,
|
||||
): Octokits => {
|
||||
return {
|
||||
rest: {
|
||||
@@ -28,6 +29,14 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
compareCommitsWithBasehead: async () => ({
|
||||
data: compareResponse || { total_commits: 0 },
|
||||
}),
|
||||
getBranch: async () => {
|
||||
if (!branchExists) {
|
||||
const error: any = new Error("Not Found");
|
||||
error.status = 404;
|
||||
throw error;
|
||||
}
|
||||
return { data: {} };
|
||||
},
|
||||
},
|
||||
git: {
|
||||
deleteRef: async () => {
|
||||
@@ -43,12 +52,13 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
|
||||
test("should return no branch link and not delete when branch is undefined", async () => {
|
||||
const mockOctokit = createMockOctokit();
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
const result = await checkAndCommitOrDeleteBranch(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
undefined,
|
||||
"main",
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(false);
|
||||
@@ -56,39 +66,38 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
expect(consoleLogSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should delete branch and return no link when branch has no commits", async () => {
|
||||
test("should mark branch for deletion when commit signing is enabled and no commits", async () => {
|
||||
const mockOctokit = createMockOctokit({ total_commits: 0 });
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
const result = await checkAndCommitOrDeleteBranch(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"claude/issue-123-20240101-1234",
|
||||
"main",
|
||||
true, // commit signing enabled
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(true);
|
||||
expect(result.branchLink).toBe("");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Branch claude/issue-123-20240101_123456 has no commits from Claude, will delete it",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"✅ Deleted empty branch: claude/issue-123-20240101_123456",
|
||||
"Branch claude/issue-123-20240101-1234 has no commits from Claude, will delete it",
|
||||
);
|
||||
});
|
||||
|
||||
test("should not delete branch and return link when branch has commits", async () => {
|
||||
const mockOctokit = createMockOctokit({ total_commits: 3 });
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
const result = await checkAndCommitOrDeleteBranch(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"claude/issue-123-20240101-1234",
|
||||
"main",
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(false);
|
||||
expect(result.branchLink).toBe(
|
||||
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101_123456)`,
|
||||
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101-1234)`,
|
||||
);
|
||||
expect(consoleLogSpy).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining("has no commits"),
|
||||
@@ -102,6 +111,7 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
compareCommitsWithBasehead: async () => {
|
||||
throw new Error("API error");
|
||||
},
|
||||
getBranch: async () => ({ data: {} }), // Branch exists
|
||||
},
|
||||
git: {
|
||||
deleteRef: async () => ({ data: {} }),
|
||||
@@ -109,20 +119,21 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
},
|
||||
} as any as Octokits;
|
||||
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
const result = await checkAndCommitOrDeleteBranch(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"claude/issue-123-20240101-1234",
|
||||
"main",
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(false);
|
||||
expect(result.branchLink).toBe(
|
||||
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101_123456)`,
|
||||
`\n[View branch](${GITHUB_SERVER_URL}/owner/repo/tree/claude/issue-123-20240101-1234)`,
|
||||
);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Error checking for commits on Claude branch:",
|
||||
"Error comparing commits on Claude branch:",
|
||||
expect.any(Error),
|
||||
);
|
||||
});
|
||||
@@ -131,19 +142,46 @@ describe("checkAndDeleteEmptyBranch", () => {
|
||||
const deleteError = new Error("Delete failed");
|
||||
const mockOctokit = createMockOctokit({ total_commits: 0 }, deleteError);
|
||||
|
||||
const result = await checkAndDeleteEmptyBranch(
|
||||
const result = await checkAndCommitOrDeleteBranch(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101_123456",
|
||||
"claude/issue-123-20240101-1234",
|
||||
"main",
|
||||
true, // commit signing enabled - will try to delete
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(true);
|
||||
expect(result.branchLink).toBe("");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
"Failed to delete branch claude/issue-123-20240101_123456:",
|
||||
"Failed to delete branch claude/issue-123-20240101-1234:",
|
||||
deleteError,
|
||||
);
|
||||
});
|
||||
|
||||
test("should return no branch link when branch doesn't exist remotely", async () => {
|
||||
const mockOctokit = createMockOctokit(
|
||||
{ total_commits: 0 },
|
||||
undefined,
|
||||
false, // branch doesn't exist
|
||||
);
|
||||
|
||||
const result = await checkAndCommitOrDeleteBranch(
|
||||
mockOctokit,
|
||||
"owner",
|
||||
"repo",
|
||||
"claude/issue-123-20240101-1234",
|
||||
"main",
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result.shouldDeleteBranch).toBe(false);
|
||||
expect(result.branchLink).toBe("");
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Branch claude/issue-123-20240101-1234 does not exist remotely",
|
||||
);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
"Branch claude/issue-123-20240101-1234 does not exist remotely, no branch link will be added",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, it, expect } from "bun:test";
|
||||
import { updateCommentBody } from "../src/github/operations/comment-logic";
|
||||
import {
|
||||
updateCommentBody,
|
||||
type CommentUpdateInput,
|
||||
} from "../src/github/operations/comment-logic";
|
||||
|
||||
describe("updateCommentBody", () => {
|
||||
const baseInput = {
|
||||
@@ -100,12 +103,12 @@ describe("updateCommentBody", () => {
|
||||
it("adds branch name with link to header when provided", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
branchName: "claude/issue-123-20240101_120000",
|
||||
branchName: "claude/issue-123-20240101-1200",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
expect(result).toContain(
|
||||
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)",
|
||||
"• [`claude/issue-123-20240101-1200`](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -381,9 +384,9 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working… <img src='spinner.gif' />",
|
||||
branchName: "claude/pr-456-20240101_120000",
|
||||
branchName: "claude/pr-456-20240101-1200",
|
||||
prLink:
|
||||
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
|
||||
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101-1200)",
|
||||
triggerUsername: "jane-doe",
|
||||
};
|
||||
|
||||
@@ -391,7 +394,7 @@ describe("updateCommentBody", () => {
|
||||
|
||||
// Should include the PR link in the formatted style
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
|
||||
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101-1200)",
|
||||
);
|
||||
expect(result).toContain("**Claude finished @jane-doe's task**");
|
||||
});
|
||||
@@ -400,22 +403,44 @@ describe("updateCommentBody", () => {
|
||||
const input = {
|
||||
...baseInput,
|
||||
currentBody: "Claude Code is working…",
|
||||
branchName: "claude/issue-123-20240101_120000",
|
||||
branchName: "claude/issue-123-20240101-1200",
|
||||
branchLink:
|
||||
"\n[View branch](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)",
|
||||
"\n[View branch](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
|
||||
prLink:
|
||||
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
|
||||
"\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101-1200)",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
|
||||
// Should include both links in formatted style
|
||||
expect(result).toContain(
|
||||
"• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/tree/claude/issue-123-20240101_120000)",
|
||||
"• [`claude/issue-123-20240101-1200`](https://github.com/owner/repo/tree/claude/issue-123-20240101-1200)",
|
||||
);
|
||||
expect(result).toContain(
|
||||
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
|
||||
"• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101-1200)",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not show branch name when branch doesn't exist remotely", () => {
|
||||
const input: CommentUpdateInput = {
|
||||
currentBody: "@claude can you help with this?",
|
||||
actionFailed: false,
|
||||
executionDetails: { duration_ms: 90000 },
|
||||
jobUrl: "https://github.com/owner/repo/actions/runs/123",
|
||||
branchLink: "", // Empty branch link means branch doesn't exist remotely
|
||||
branchName: undefined, // Should be undefined when branchLink is empty
|
||||
triggerUsername: "claude",
|
||||
prLink: "",
|
||||
};
|
||||
|
||||
const result = updateCommentBody(input);
|
||||
|
||||
expect(result).toContain("Claude finished @claude's task in 1m 30s");
|
||||
expect(result).toContain(
|
||||
"[View job](https://github.com/owner/repo/actions/runs/123)",
|
||||
);
|
||||
expect(result).not.toContain("claude/issue-123");
|
||||
expect(result).not.toContain("tree/claude/issue-123");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -127,13 +127,13 @@ describe("generatePrompt", () => {
|
||||
commentId: "67890",
|
||||
isPR: false,
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||
claudeBranch: "claude/issue-67890-20240101-1200",
|
||||
issueNumber: "67890",
|
||||
commentBody: "@claude please fix this",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("You are Claude, an AI assistant");
|
||||
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
|
||||
@@ -161,7 +161,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -183,11 +183,11 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "789",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-789-20240101_120000",
|
||||
claudeBranch: "claude/issue-789-20240101-1200",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -210,12 +210,12 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "999",
|
||||
baseBranch: "develop",
|
||||
claudeBranch: "claude/issue-999-20240101_120000",
|
||||
claudeBranch: "claude/issue-999-20240101-1200",
|
||||
assigneeTrigger: "claude-bot",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -237,12 +237,12 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "888",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-888-20240101_120000",
|
||||
claudeBranch: "claude/issue-888-20240101-1200",
|
||||
labelTrigger: "claude-task",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
|
||||
expect(prompt).toContain(
|
||||
@@ -265,11 +265,11 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "789",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-789-20240101_120000",
|
||||
claudeBranch: "claude/issue-789-20240101-1200",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<direct_prompt>");
|
||||
expect(prompt).toContain("Fix the bug in the login form");
|
||||
@@ -292,7 +292,7 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
|
||||
expect(prompt).toContain("<is_pr>true</is_pr>");
|
||||
@@ -312,12 +312,12 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "123",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||
claudeBranch: "claude/issue-67890-20240101-1200",
|
||||
commentBody: "@claude please fix this",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("CUSTOM INSTRUCTIONS:\nAlways use TypeScript");
|
||||
});
|
||||
@@ -334,16 +334,17 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "123",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||
claudeBranch: "claude/issue-67890-20240101-1200",
|
||||
commentBody: "@claude please fix this",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
|
||||
// With commit signing disabled, co-author info appears in git commit instructions
|
||||
expect(prompt).toContain(
|
||||
'Use: "Co-authored-by: johndoe <johndoe@users.noreply.github.com>"',
|
||||
"Co-authored-by: johndoe <johndoe@users.noreply.github.com>",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -360,12 +361,10 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain PR-specific instructions
|
||||
expect(prompt).toContain(
|
||||
"Push directly using mcp__github_file_ops__commit_files to the existing branch",
|
||||
);
|
||||
// Should contain PR-specific instructions (git commands when not using signing)
|
||||
expect(prompt).toContain("git push");
|
||||
expect(prompt).toContain(
|
||||
"Always push to the existing branch when triggered on a PR",
|
||||
);
|
||||
@@ -389,18 +388,18 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "789",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-789-20240101_120000",
|
||||
claudeBranch: "claude/issue-789-20240101-1200",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain Issue-specific instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/issue-789-20240101_120000)",
|
||||
"You are already on the correct branch (claude/issue-789-20240101-1200)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"IMPORTANT: You are already on the correct branch (claude/issue-789-20240101_120000)",
|
||||
"IMPORTANT: You are already on the correct branch (claude/issue-789-20240101-1200)",
|
||||
);
|
||||
expect(prompt).toContain("Create a PR](https://github.com/");
|
||||
expect(prompt).toContain(
|
||||
@@ -427,22 +426,22 @@ describe("generatePrompt", () => {
|
||||
isPR: false,
|
||||
issueNumber: "123",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-123-20240101_120000",
|
||||
claudeBranch: "claude/issue-123-20240101-1200",
|
||||
commentBody: "@claude please fix this",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain the actual branch name with timestamp
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/issue-123-20240101_120000)",
|
||||
"You are already on the correct branch (claude/issue-123-20240101-1200)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"IMPORTANT: You are already on the correct branch (claude/issue-123-20240101_120000)",
|
||||
"IMPORTANT: You are already on the correct branch (claude/issue-123-20240101-1200)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"The branch-name is the current branch: claude/issue-123-20240101_120000",
|
||||
"The branch-name is the current branch: claude/issue-123-20240101-1200",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -457,22 +456,22 @@ describe("generatePrompt", () => {
|
||||
isPR: true,
|
||||
prNumber: "456",
|
||||
commentBody: "@claude please fix this",
|
||||
claudeBranch: "claude/pr-456-20240101_120000",
|
||||
claudeBranch: "claude/pr-456-20240101-1200",
|
||||
baseBranch: "main",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain branch-specific instructions like issues
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-456-20240101_120000)",
|
||||
"You are already on the correct branch (claude/pr-456-20240101-1200)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Create a PR](https://github.com/owner/repo/compare/main",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"The branch-name is the current branch: claude/pr-456-20240101_120000",
|
||||
"The branch-name is the current branch: claude/pr-456-20240101-1200",
|
||||
);
|
||||
expect(prompt).toContain("Reference to the original PR");
|
||||
expect(prompt).toContain(
|
||||
@@ -500,12 +499,10 @@ describe("generatePrompt", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain open PR instructions
|
||||
expect(prompt).toContain(
|
||||
"Push directly using mcp__github_file_ops__commit_files to the existing branch",
|
||||
);
|
||||
// Should contain open PR instructions (git commands when not using signing)
|
||||
expect(prompt).toContain("git push");
|
||||
expect(prompt).toContain(
|
||||
"Always push to the existing branch when triggered on a PR",
|
||||
);
|
||||
@@ -528,16 +525,16 @@ describe("generatePrompt", () => {
|
||||
isPR: true,
|
||||
prNumber: "789",
|
||||
commentBody: "@claude please update this",
|
||||
claudeBranch: "claude/pr-789-20240101_123000",
|
||||
claudeBranch: "claude/pr-789-20240101-1230",
|
||||
baseBranch: "develop",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-789-20240101_123000)",
|
||||
"You are already on the correct branch (claude/pr-789-20240101-1230)",
|
||||
);
|
||||
expect(prompt).toContain(
|
||||
"Create a PR](https://github.com/owner/repo/compare/develop",
|
||||
@@ -556,16 +553,16 @@ describe("generatePrompt", () => {
|
||||
prNumber: "999",
|
||||
commentId: "review-comment-123",
|
||||
commentBody: "@claude fix this issue",
|
||||
claudeBranch: "claude/pr-999-20240101_140000",
|
||||
claudeBranch: "claude/pr-999-20240101-1400",
|
||||
baseBranch: "main",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-999-20240101_140000)",
|
||||
"You are already on the correct branch (claude/pr-999-20240101-1400)",
|
||||
);
|
||||
expect(prompt).toContain("Create a PR](https://github.com/");
|
||||
expect(prompt).toContain("Reference to the original PR");
|
||||
@@ -584,20 +581,75 @@ describe("generatePrompt", () => {
|
||||
eventAction: "closed",
|
||||
isPR: true,
|
||||
prNumber: "555",
|
||||
claudeBranch: "claude/pr-555-20240101_150000",
|
||||
claudeBranch: "claude/pr-555-20240101-1500",
|
||||
baseBranch: "main",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData);
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should contain new branch instructions
|
||||
expect(prompt).toContain(
|
||||
"You are already on the correct branch (claude/pr-555-20240101_150000)",
|
||||
"You are already on the correct branch (claude/pr-555-20240101-1500)",
|
||||
);
|
||||
expect(prompt).toContain("Create a PR](https://github.com/");
|
||||
expect(prompt).toContain("Reference to the original PR");
|
||||
});
|
||||
|
||||
test("should include git commands when useCommitSigning is false", () => {
|
||||
const envVars: PreparedContext = {
|
||||
repository: "owner/repo",
|
||||
claudeCommentId: "12345",
|
||||
triggerPhrase: "@claude",
|
||||
eventData: {
|
||||
eventName: "issue_comment",
|
||||
commentId: "67890",
|
||||
isPR: true,
|
||||
prNumber: "123",
|
||||
commentBody: "@claude fix the bug",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, false);
|
||||
|
||||
// Should have git command instructions
|
||||
expect(prompt).toContain("Use git commands via the Bash tool");
|
||||
expect(prompt).toContain("git add");
|
||||
expect(prompt).toContain("git commit");
|
||||
expect(prompt).toContain("git push");
|
||||
|
||||
// Should use the minimal comment tool
|
||||
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Should not have commit signing tool references
|
||||
expect(prompt).not.toContain("mcp__github_file_ops__commit_files");
|
||||
});
|
||||
|
||||
test("should include commit signing tools when useCommitSigning is true", () => {
|
||||
const envVars: PreparedContext = {
|
||||
repository: "owner/repo",
|
||||
claudeCommentId: "12345",
|
||||
triggerPhrase: "@claude",
|
||||
eventData: {
|
||||
eventName: "issue_comment",
|
||||
commentId: "67890",
|
||||
isPR: true,
|
||||
prNumber: "123",
|
||||
commentBody: "@claude fix the bug",
|
||||
},
|
||||
};
|
||||
|
||||
const prompt = generatePrompt(envVars, mockGitHubData, true);
|
||||
|
||||
// Should have commit signing tool instructions
|
||||
expect(prompt).toContain("mcp__github_file_ops__commit_files");
|
||||
expect(prompt).toContain("mcp__github_file_ops__delete_files");
|
||||
// Comment tool should always be from comment server, not file ops
|
||||
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Should not have git command instructions
|
||||
expect(prompt).not.toContain("Use git commands via the Bash tool");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getEventTypeAndContext", () => {
|
||||
@@ -631,7 +683,7 @@ describe("getEventTypeAndContext", () => {
|
||||
isPR: false,
|
||||
issueNumber: "999",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-999-20240101_120000",
|
||||
claudeBranch: "claude/issue-999-20240101-1200",
|
||||
assigneeTrigger: "claude-bot",
|
||||
},
|
||||
};
|
||||
@@ -653,7 +705,7 @@ describe("getEventTypeAndContext", () => {
|
||||
isPR: false,
|
||||
issueNumber: "888",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-888-20240101_120000",
|
||||
claudeBranch: "claude/issue-888-20240101-1200",
|
||||
labelTrigger: "claude-task",
|
||||
},
|
||||
};
|
||||
@@ -676,7 +728,7 @@ describe("getEventTypeAndContext", () => {
|
||||
isPR: false,
|
||||
issueNumber: "999",
|
||||
baseBranch: "main",
|
||||
claudeBranch: "claude/issue-999-20240101_120000",
|
||||
claudeBranch: "claude/issue-999-20240101-1200",
|
||||
// No assigneeTrigger when using directPrompt
|
||||
},
|
||||
};
|
||||
@@ -689,7 +741,7 @@ describe("getEventTypeAndContext", () => {
|
||||
});
|
||||
|
||||
describe("buildAllowedToolsString", () => {
|
||||
test("should return issue comment tool for regular events", () => {
|
||||
test("should return correct tools for regular events (default no signing)", () => {
|
||||
const result = buildAllowedToolsString();
|
||||
|
||||
// The base tools should be in the result
|
||||
@@ -699,15 +751,20 @@ describe("buildAllowedToolsString", () => {
|
||||
expect(result).toContain("LS");
|
||||
expect(result).toContain("Read");
|
||||
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_pull_request_comment");
|
||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||
|
||||
// Default is no commit signing, so should have specific Bash git commands
|
||||
expect(result).toContain("Bash(git add:*)");
|
||||
expect(result).toContain("Bash(git commit:*)");
|
||||
expect(result).toContain("Bash(git push:*)");
|
||||
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Should not have commit signing tools
|
||||
expect(result).not.toContain("mcp__github_file_ops__commit_files");
|
||||
expect(result).not.toContain("mcp__github_file_ops__delete_files");
|
||||
});
|
||||
|
||||
test("should return PR comment tool for inline review comments", () => {
|
||||
const result = buildAllowedToolsString();
|
||||
test("should return correct tools with default parameters", () => {
|
||||
const result = buildAllowedToolsString([], false, false);
|
||||
|
||||
// The base tools should be in the result
|
||||
expect(result).toContain("Edit");
|
||||
@@ -716,11 +773,15 @@ describe("buildAllowedToolsString", () => {
|
||||
expect(result).toContain("LS");
|
||||
expect(result).toContain("Read");
|
||||
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_pull_request_comment");
|
||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||
|
||||
// Should have specific Bash git commands for non-signing mode
|
||||
expect(result).toContain("Bash(git add:*)");
|
||||
expect(result).toContain("Bash(git commit:*)");
|
||||
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Should not have commit signing tools
|
||||
expect(result).not.toContain("mcp__github_file_ops__commit_files");
|
||||
expect(result).not.toContain("mcp__github_file_ops__delete_files");
|
||||
});
|
||||
|
||||
test("should append custom tools when provided", () => {
|
||||
@@ -773,6 +834,79 @@ describe("buildAllowedToolsString", () => {
|
||||
expect(result).toContain("mcp__github_ci__get_workflow_run_details");
|
||||
expect(result).toContain("mcp__github_ci__download_job_log");
|
||||
});
|
||||
|
||||
test("should include commit signing tools when useCommitSigning is true", () => {
|
||||
const result = buildAllowedToolsString([], false, true);
|
||||
|
||||
// Base tools should be present
|
||||
expect(result).toContain("Edit");
|
||||
expect(result).toContain("Glob");
|
||||
expect(result).toContain("Grep");
|
||||
expect(result).toContain("LS");
|
||||
expect(result).toContain("Read");
|
||||
expect(result).toContain("Write");
|
||||
|
||||
// Commit signing tools should be included
|
||||
expect(result).toContain("mcp__github_file_ops__commit_files");
|
||||
expect(result).toContain("mcp__github_file_ops__delete_files");
|
||||
// Comment tool should always be from github_comment server
|
||||
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Bash should NOT be included when using commit signing (except in comment tool name)
|
||||
expect(result).not.toContain("Bash(");
|
||||
});
|
||||
|
||||
test("should include specific Bash git commands when useCommitSigning is false", () => {
|
||||
const result = buildAllowedToolsString([], false, false);
|
||||
|
||||
// Base tools should be present
|
||||
expect(result).toContain("Edit");
|
||||
expect(result).toContain("Glob");
|
||||
expect(result).toContain("Grep");
|
||||
expect(result).toContain("LS");
|
||||
expect(result).toContain("Read");
|
||||
expect(result).toContain("Write");
|
||||
|
||||
// Specific Bash git commands should be included
|
||||
expect(result).toContain("Bash(git add:*)");
|
||||
expect(result).toContain("Bash(git commit:*)");
|
||||
expect(result).toContain("Bash(git push:*)");
|
||||
expect(result).toContain("Bash(git status:*)");
|
||||
expect(result).toContain("Bash(git diff:*)");
|
||||
expect(result).toContain("Bash(git log:*)");
|
||||
expect(result).toContain("Bash(git rm:*)");
|
||||
expect(result).toContain("Bash(git config user.name:*)");
|
||||
expect(result).toContain("Bash(git config user.email:*)");
|
||||
|
||||
// Comment tool from minimal server should be included
|
||||
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Commit signing tools should NOT be included
|
||||
expect(result).not.toContain("mcp__github_file_ops__commit_files");
|
||||
expect(result).not.toContain("mcp__github_file_ops__delete_files");
|
||||
});
|
||||
|
||||
test("should handle all combinations of options", () => {
|
||||
const customTools = ["CustomTool1", "CustomTool2"];
|
||||
const result = buildAllowedToolsString(customTools, true, false);
|
||||
|
||||
// Base tools should be present
|
||||
expect(result).toContain("Edit");
|
||||
expect(result).toContain("Bash(git add:*)");
|
||||
|
||||
// Custom tools should be included
|
||||
expect(result).toContain("CustomTool1");
|
||||
expect(result).toContain("CustomTool2");
|
||||
|
||||
// GitHub Actions tools should be included
|
||||
expect(result).toContain("mcp__github_ci__get_ci_status");
|
||||
|
||||
// Comment tool from minimal server should be included
|
||||
expect(result).toContain("mcp__github_comment__update_claude_comment");
|
||||
|
||||
// Commit signing tools should NOT be included
|
||||
expect(result).not.toContain("mcp__github_file_ops__commit_files");
|
||||
});
|
||||
});
|
||||
|
||||
describe("buildDisallowedToolsString", () => {
|
||||
|
||||
@@ -34,6 +34,7 @@ describe("prepareMcpConfig", () => {
|
||||
branchPrefix: "",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,6 +45,22 @@ describe("prepareMcpConfig", () => {
|
||||
entityNumber: 456,
|
||||
};
|
||||
|
||||
const mockContextWithSigning: ParsedGitHubContext = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const mockPRContextWithSigning: ParsedGitHubContext = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {});
|
||||
consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
||||
@@ -65,12 +82,13 @@ describe("prepareMcpConfig", () => {
|
||||
processExitSpy.mockRestore();
|
||||
});
|
||||
|
||||
test("should return base config when no additional config is provided and no allowed_tools", async () => {
|
||||
test("should return comment server when commit signing is disabled", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
});
|
||||
@@ -78,6 +96,38 @@ describe("prepareMcpConfig", () => {
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
expect(parsed.mcpServers.github_comment.env.REPO_OWNER).toBe("test-owner");
|
||||
expect(parsed.mcpServers.github_comment.env.REPO_NAME).toBe("test-repo");
|
||||
});
|
||||
|
||||
test("should return file ops server when commit signing is enabled", async () => {
|
||||
const contextWithSigning = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
@@ -95,6 +145,7 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
@@ -105,23 +156,33 @@ describe("prepareMcpConfig", () => {
|
||||
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_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
});
|
||||
|
||||
test("should not include github MCP server when only file_ops tools are allowed", async () => {
|
||||
const contextWithSigning = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__update_claude_comment",
|
||||
],
|
||||
context: mockContext,
|
||||
context: contextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -130,12 +191,13 @@ describe("prepareMcpConfig", () => {
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should include file_ops server even when no GitHub tools are allowed", async () => {
|
||||
test("should include comment server when no GitHub tools are allowed and signing disabled", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: ["Edit", "Read", "Write"],
|
||||
context: mockContext,
|
||||
});
|
||||
@@ -143,7 +205,8 @@ describe("prepareMcpConfig", () => {
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
});
|
||||
|
||||
test("should return base config when additional config is empty string", async () => {
|
||||
@@ -152,6 +215,7 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: "",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
@@ -160,7 +224,7 @@ describe("prepareMcpConfig", () => {
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -170,6 +234,7 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: " \n\t ",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
@@ -178,7 +243,7 @@ describe("prepareMcpConfig", () => {
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -200,12 +265,13 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -238,12 +304,13 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -279,9 +346,10 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -299,9 +367,10 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: invalidJson,
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -320,9 +389,10 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: nonObjectJson,
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -344,9 +414,10 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: nullJson,
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -368,9 +439,10 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: arrayJson,
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -415,9 +487,10 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -438,8 +511,9 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -459,8 +533,9 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -478,6 +553,7 @@ describe("prepareMcpConfig", () => {
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
additionalPermissions: new Map([["actions", "read"]]),
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -486,6 +562,7 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
@@ -505,8 +582,9 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -523,8 +601,9 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockPRContext,
|
||||
context: mockPRContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
@@ -554,6 +633,7 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
@@ -582,6 +662,7 @@ describe("prepareMcpConfig", () => {
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ const defaultInputs = {
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map<string, string>(),
|
||||
useCommitSigning: false,
|
||||
};
|
||||
|
||||
const defaultRepository = {
|
||||
|
||||
@@ -70,6 +70,7 @@ describe("checkWritePermissions", () => {
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
process.env = {
|
||||
...BASE_ENV,
|
||||
BASE_BRANCH: "main",
|
||||
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000",
|
||||
CLAUDE_BRANCH: "claude/issue-67890-20240101-1200",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
mockIssueCommentContext,
|
||||
"12345",
|
||||
"main",
|
||||
"claude/issue-67890-20240101_120000",
|
||||
"claude/issue-67890-20240101-1200",
|
||||
);
|
||||
|
||||
expect(result.repository).toBe("test-owner/test-repo");
|
||||
@@ -60,7 +60,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
expect(result.eventData.issueNumber).toBe("55");
|
||||
expect(result.eventData.commentId).toBe("12345678");
|
||||
expect(result.eventData.claudeBranch).toBe(
|
||||
"claude/issue-67890-20240101_120000",
|
||||
"claude/issue-67890-20240101-1200",
|
||||
);
|
||||
expect(result.eventData.baseBranch).toBe("main");
|
||||
expect(result.eventData.commentBody).toBe(
|
||||
@@ -81,7 +81,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
mockIssueCommentContext,
|
||||
"12345",
|
||||
undefined,
|
||||
"claude/issue-67890-20240101_120000",
|
||||
"claude/issue-67890-20240101-1200",
|
||||
),
|
||||
).toThrow("BASE_BRANCH is required for issue_comment event");
|
||||
});
|
||||
@@ -152,7 +152,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
process.env = {
|
||||
...BASE_ENV,
|
||||
BASE_BRANCH: "main",
|
||||
CLAUDE_BRANCH: "claude/issue-42-20240101_120000",
|
||||
CLAUDE_BRANCH: "claude/issue-42-20240101-1200",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
mockIssueOpenedContext,
|
||||
"12345",
|
||||
"main",
|
||||
"claude/issue-42-20240101_120000",
|
||||
"claude/issue-42-20240101-1200",
|
||||
);
|
||||
|
||||
expect(result.eventData.eventName).toBe("issues");
|
||||
@@ -174,7 +174,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
expect(result.eventData.issueNumber).toBe("42");
|
||||
expect(result.eventData.baseBranch).toBe("main");
|
||||
expect(result.eventData.claudeBranch).toBe(
|
||||
"claude/issue-42-20240101_120000",
|
||||
"claude/issue-42-20240101-1200",
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -184,7 +184,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
mockIssueAssignedContext,
|
||||
"12345",
|
||||
"main",
|
||||
"claude/issue-123-20240101_120000",
|
||||
"claude/issue-123-20240101-1200",
|
||||
);
|
||||
|
||||
expect(result.eventData.eventName).toBe("issues");
|
||||
@@ -197,7 +197,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
expect(result.eventData.issueNumber).toBe("123");
|
||||
expect(result.eventData.baseBranch).toBe("main");
|
||||
expect(result.eventData.claudeBranch).toBe(
|
||||
"claude/issue-123-20240101_120000",
|
||||
"claude/issue-123-20240101-1200",
|
||||
);
|
||||
expect(result.eventData.assigneeTrigger).toBe("@claude-bot");
|
||||
}
|
||||
@@ -215,7 +215,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
mockIssueOpenedContext,
|
||||
"12345",
|
||||
undefined,
|
||||
"claude/issue-42-20240101_120000",
|
||||
"claude/issue-42-20240101-1200",
|
||||
),
|
||||
).toThrow("BASE_BRANCH is required for issues event");
|
||||
});
|
||||
@@ -234,7 +234,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
contextWithDirectPrompt,
|
||||
"12345",
|
||||
"main",
|
||||
"claude/issue-123-20240101_120000",
|
||||
"claude/issue-123-20240101-1200",
|
||||
);
|
||||
|
||||
expect(result.eventData.eventName).toBe("issues");
|
||||
@@ -264,7 +264,7 @@ describe("parseEnvVarsWithContext", () => {
|
||||
contextWithoutTriggers,
|
||||
"12345",
|
||||
"main",
|
||||
"claude/issue-123-20240101_120000",
|
||||
"claude/issue-123-20240101-1200",
|
||||
),
|
||||
).toThrow("ASSIGNEE_TRIGGER is required for issue assigned event");
|
||||
});
|
||||
|
||||
@@ -38,6 +38,7 @@ describe("checkContainsTrigger", () => {
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
@@ -68,6 +69,7 @@ describe("checkContainsTrigger", () => {
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
@@ -282,6 +284,7 @@ describe("checkContainsTrigger", () => {
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
@@ -313,6 +316,7 @@ describe("checkContainsTrigger", () => {
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(true);
|
||||
@@ -344,6 +348,7 @@ describe("checkContainsTrigger", () => {
|
||||
branchPrefix: "claude/",
|
||||
useStickyComment: false,
|
||||
additionalPermissions: new Map(),
|
||||
useCommitSigning: false,
|
||||
},
|
||||
});
|
||||
expect(checkContainsTrigger(context)).toBe(false);
|
||||
|
||||
Reference in New Issue
Block a user