mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
5 Commits
ashwin-ant
...
fix-500-er
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc172c74aa | ||
|
|
cb9aae1881 | ||
|
|
b02a95b3f3 | ||
|
|
cde954dda0 | ||
|
|
c845eee45f |
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ""
|
|
||||||
labels: bug
|
|
||||||
assignees: ""
|
|
||||||
---
|
|
||||||
|
|
||||||
**Describe the bug**
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
**To Reproduce**
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
|
|
||||||
**Expected behavior**
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
|
|
||||||
**Screenshots**
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
|
|
||||||
**Workflow yml file**
|
|
||||||
If it's not sensitive, consider including a paste of your full Claude workflow.yml file.
|
|
||||||
|
|
||||||
**API Provider**
|
|
||||||
|
|
||||||
[ ] Anthropic First-Party API (default)
|
|
||||||
[ ] AWS Bedrock
|
|
||||||
[ ] GCP Vertex
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context about the problem here.
|
|
||||||
5
.github/workflows/claude.yml
vendored
5
.github/workflows/claude.yml
vendored
@@ -31,9 +31,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
id: claude
|
id: claude
|
||||||
uses: anthropics/claude-code-action@beta
|
uses: ./
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
|
|
||||||
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."
|
|
||||||
model: "claude-opus-4-20250514"
|
|
||||||
|
|||||||
104
.github/workflows/issue-triage.yml
vendored
104
.github/workflows/issue-triage.yml
vendored
@@ -1,104 +0,0 @@
|
|||||||
name: Claude Issue Triage
|
|
||||||
description: Run Claude Code for issue triage in GitHub Actions
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
triage-issue:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 10
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
issues: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup GitHub MCP Server
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/mcp-config
|
|
||||||
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
|
|
||||||
{
|
|
||||||
"github": {
|
|
||||||
"command": "docker",
|
|
||||||
"args": [
|
|
||||||
"run",
|
|
||||||
"-i",
|
|
||||||
"--rm",
|
|
||||||
"-e",
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
|
||||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
|
||||||
],
|
|
||||||
"env": {
|
|
||||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Create triage prompt
|
|
||||||
run: |
|
|
||||||
mkdir -p /tmp/claude-prompts
|
|
||||||
cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF'
|
|
||||||
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
|
|
||||||
|
|
||||||
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
|
|
||||||
|
|
||||||
Issue Information:
|
|
||||||
- REPO: ${{ github.repository }}
|
|
||||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
||||||
|
|
||||||
TASK OVERVIEW:
|
|
||||||
|
|
||||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
|
||||||
|
|
||||||
2. Next, use the GitHub tools to get context about the issue:
|
|
||||||
- You have access to these tools:
|
|
||||||
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
|
|
||||||
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
|
|
||||||
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
|
|
||||||
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
|
|
||||||
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
|
|
||||||
- Start by using mcp__github__get_issue to get the issue details
|
|
||||||
|
|
||||||
3. Analyze the issue content, considering:
|
|
||||||
- The issue title and description
|
|
||||||
- The type of issue (bug report, feature request, question, etc.)
|
|
||||||
- Technical areas mentioned
|
|
||||||
- Severity or priority indicators
|
|
||||||
- User impact
|
|
||||||
- Components affected
|
|
||||||
|
|
||||||
4. Select appropriate labels from the available labels list provided above:
|
|
||||||
- Choose labels that accurately reflect the issue's nature
|
|
||||||
- Be specific but comprehensive
|
|
||||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
|
||||||
- Consider platform labels (android, ios) if applicable
|
|
||||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
|
||||||
|
|
||||||
5. Apply the selected labels:
|
|
||||||
- Use mcp__github__update_issue to apply your selected labels
|
|
||||||
- DO NOT post any comments explaining your decision
|
|
||||||
- DO NOT communicate directly with users
|
|
||||||
- If no labels are clearly applicable, do not apply any labels
|
|
||||||
|
|
||||||
IMPORTANT GUIDELINES:
|
|
||||||
- Be thorough in your analysis
|
|
||||||
- Only select labels from the provided list above
|
|
||||||
- DO NOT post any comments to the issue
|
|
||||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
|
||||||
- It's okay to not add any labels if none are clearly applicable
|
|
||||||
EOF
|
|
||||||
|
|
||||||
- name: Run Claude Code for Issue Triage
|
|
||||||
uses: anthropics/claude-code-base-action@beta
|
|
||||||
with:
|
|
||||||
prompt_file: /tmp/claude-prompts/triage-prompt.txt
|
|
||||||
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
|
||||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
|
||||||
timeout_minutes: "5"
|
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,3 @@
|
|||||||
.DS_Store
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
**/.claude/settings.local.json
|
**/.claude/settings.local.json
|
||||||
|
|||||||
156
FAQ.md
156
FAQ.md
@@ -1,156 +0,0 @@
|
|||||||
# Frequently Asked Questions (FAQ)
|
|
||||||
|
|
||||||
This FAQ addresses common questions and gotchas when using the Claude Code GitHub Action.
|
|
||||||
|
|
||||||
## Triggering and Authentication
|
|
||||||
|
|
||||||
### Why doesn't tagging @claude from my automated workflow work?
|
|
||||||
|
|
||||||
The `github-actions` user cannot trigger subsequent GitHub Actions workflows. This is a GitHub security feature to prevent infinite loops. To make this work, you need to use a Personal Access Token (PAT) instead, which will act as a regular user, or use a separate app token of your own. When posting a comment on an issue or PR from your workflow, use your PAT instead of the `GITHUB_TOKEN` generated in your workflow.
|
|
||||||
|
|
||||||
### Why does Claude say I don't have permission to trigger it?
|
|
||||||
|
|
||||||
Only users with **write permissions** to the repository can trigger Claude. This is a security feature to prevent unauthorized use. Make sure the user commenting has at least write access to the repository.
|
|
||||||
|
|
||||||
### Why am I getting OIDC authentication errors?
|
|
||||||
|
|
||||||
If you're using the default GitHub App authentication, you must add the `id-token: write` permission to your workflow:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
id-token: write # Required for OIDC authentication
|
|
||||||
```
|
|
||||||
|
|
||||||
The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `github_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more.
|
|
||||||
|
|
||||||
## Claude's Capabilities and Limitations
|
|
||||||
|
|
||||||
### Why won't Claude update workflow files when I ask it to?
|
|
||||||
|
|
||||||
The GitHub App for Claude doesn't have workflow write access for security reasons. This prevents Claude from modifying CI/CD configurations that could potentially create unintended consequences. This is something we may reconsider in the future.
|
|
||||||
|
|
||||||
### Why won't Claude rebase my branch?
|
|
||||||
|
|
||||||
By default, Claude only uses commit tools for non-destructive changes to the branch. Claude is configured to:
|
|
||||||
|
|
||||||
- Never push to branches other than where it was invoked (either its own branch or the PR branch)
|
|
||||||
- Never force push or perform destructive operations
|
|
||||||
|
|
||||||
You can grant additional tools via the `allowed_tools` input if needed:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
allowed_tools: "Bash(git rebase:*)" # Use with caution
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why won't Claude create a pull request?
|
|
||||||
|
|
||||||
Claude doesn't create PRs by default. Instead, it pushes commits to a branch and provides a link to a pre-filled PR submission page. This approach ensures your repository's branch protection rules are still adhered to and gives you final control over PR creation.
|
|
||||||
|
|
||||||
### Why can't Claude run my tests or see CI results?
|
|
||||||
|
|
||||||
Claude cannot access GitHub Actions logs, test results, or other CI/CD outputs by default. It only has access to the repository files. If you need Claude to see test results, you can either:
|
|
||||||
|
|
||||||
1. Instruct Claude to run tests before making commits
|
|
||||||
2. Copy and paste CI results into a comment for Claude to analyze
|
|
||||||
|
|
||||||
This limitation exists for security reasons but may be reconsidered in the future based on user feedback.
|
|
||||||
|
|
||||||
### Why does Claude only update one comment instead of creating new ones?
|
|
||||||
|
|
||||||
Claude is configured to update a single comment to avoid cluttering PR/issue discussions. All of Claude's responses, including progress updates and final results, will appear in the same comment with checkboxes showing task progress.
|
|
||||||
|
|
||||||
## Branch and Commit Behavior
|
|
||||||
|
|
||||||
### Why did Claude create a new branch when commenting on a closed PR?
|
|
||||||
|
|
||||||
Claude's branch behavior depends on the context:
|
|
||||||
|
|
||||||
- **Open PRs**: Pushes directly to the existing PR branch
|
|
||||||
- **Closed/Merged PRs**: Creates a new branch (cannot push to closed PR branches)
|
|
||||||
- **Issues**: Always creates a new branch with a timestamp
|
|
||||||
|
|
||||||
### Why are my commits shallow/missing history?
|
|
||||||
|
|
||||||
For performance, Claude uses shallow clones:
|
|
||||||
|
|
||||||
- PRs: `--depth=20` (last 20 commits)
|
|
||||||
- New branches: `--depth=1` (single commit)
|
|
||||||
|
|
||||||
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
|
|
||||||
|
|
||||||
```
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
depth: 0 # will fetch full repo history
|
|
||||||
```
|
|
||||||
|
|
||||||
## Configuration and Tools
|
|
||||||
|
|
||||||
### What's the difference between `direct_prompt` and `custom_instructions`?
|
|
||||||
|
|
||||||
These inputs serve different purposes in how Claude responds:
|
|
||||||
|
|
||||||
- **`direct_prompt`**: Bypasses trigger detection entirely. When provided, Claude executes this exact instruction regardless of comments or mentions. Perfect for automated workflows where you want Claude to perform a specific task on every run (e.g., "Update the API documentation based on changes in this PR").
|
|
||||||
|
|
||||||
- **`custom_instructions`**: Additional context added to Claude's system prompt while still respecting normal triggers. These instructions modify Claude's behavior but don't replace the triggering comment. Use this to give Claude standing instructions like "You have been granted additional tools for ...".
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Using direct_prompt - runs automatically without @claude mention
|
|
||||||
direct_prompt: "Review this PR for security vulnerabilities"
|
|
||||||
|
|
||||||
# Using custom_instructions - still requires @claude trigger
|
|
||||||
custom_instructions: "Focus on performance implications and suggest optimizations"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Why doesn't Claude execute my bash commands?
|
|
||||||
|
|
||||||
The Bash tool is **disabled by default** for security. To enable individual bash commands:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
allowed_tools: "Bash(npm:*),Bash(git:*)" # Allows only npm and git commands
|
|
||||||
```
|
|
||||||
|
|
||||||
### Can Claude work across multiple repositories?
|
|
||||||
|
|
||||||
No, Claude's GitHub app token is sandboxed to the current repository only. It cannot push to any other repositories. It can, however, read public repositories, but to get access to this, you must configure it with tools to do so.
|
|
||||||
|
|
||||||
## MCP Servers and Extended Functionality
|
|
||||||
|
|
||||||
### What MCP servers are available by default?
|
|
||||||
|
|
||||||
Claude Code Action automatically configures two MCP servers:
|
|
||||||
|
|
||||||
1. **GitHub MCP server**: For GitHub API operations
|
|
||||||
2. **File operations server**: For advanced file manipulation
|
|
||||||
|
|
||||||
However, tools from these servers still need to be explicitly allowed via `allowed_tools`.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### How can I debug what Claude is doing?
|
|
||||||
|
|
||||||
Check the GitHub Action log for Claude's run for the full execution trace.
|
|
||||||
|
|
||||||
### Why can't I trigger Claude with `@claude-mention` or `claude!`?
|
|
||||||
|
|
||||||
The trigger uses word boundaries, so `@claude` must be a complete word. Variations like `@claude-bot`, `@claude!`, or `claude@mention` won't work unless you customize the `trigger_phrase`.
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
1. **Always specify permissions explicitly** in your workflow file
|
|
||||||
2. **Use GitHub Secrets** for API keys - never hardcode them
|
|
||||||
3. **Be specific with `allowed_tools`** - only enable what's necessary
|
|
||||||
4. **Test in a separate branch** before using on important PRs
|
|
||||||
5. **Monitor Claude's token usage** to avoid hitting API limits
|
|
||||||
6. **Review Claude's changes** carefully before merging
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
If you encounter issues not covered here:
|
|
||||||
|
|
||||||
1. Check the [GitHub Issues](https://github.com/anthropics/claude-code-action/issues)
|
|
||||||
2. Review the [example workflows](https://github.com/anthropics/claude-code-action#examples)
|
|
||||||
|
|
||||||
[perms]: https://docs.anthropic.com/en/docs/claude-code/settings#permissions
|
|
||||||
@@ -33,10 +33,6 @@ This command will guide you through setting up the GitHub app and required secre
|
|||||||
2. Add `ANTHROPIC_API_KEY` to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions))
|
2. Add `ANTHROPIC_API_KEY` to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions))
|
||||||
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
|
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
|
||||||
|
|
||||||
## 📚 FAQ
|
|
||||||
|
|
||||||
Having issues or questions? Check out our [Frequently Asked Questions](./FAQ.md) for solutions to common problems and detailed explanations of Claude's capabilities and limitations.
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
|
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
|
||||||
@@ -450,7 +446,7 @@ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|||||||
```
|
```
|
||||||
|
|
||||||
This applies to all sensitive values including API keys, access tokens, and credentials.
|
This applies to all sensitive values including API keys, access tokens, and credentials.
|
||||||
We also recommend that you always use short-lived tokens when possible
|
We also reccomend that you always use short-lived tokens when possible
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
15
action.yml
15
action.yml
@@ -12,9 +12,6 @@ inputs:
|
|||||||
assignee_trigger:
|
assignee_trigger:
|
||||||
description: "The assignee username that triggers the action (e.g. @claude)"
|
description: "The assignee username that triggers the action (e.g. @claude)"
|
||||||
required: false
|
required: false
|
||||||
base_branch:
|
|
||||||
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
|
|
||||||
required: false
|
|
||||||
|
|
||||||
# Claude Code configuration
|
# Claude Code configuration
|
||||||
model:
|
model:
|
||||||
@@ -70,7 +67,7 @@ runs:
|
|||||||
using: "composite"
|
using: "composite"
|
||||||
steps:
|
steps:
|
||||||
- name: Install Bun
|
- name: Install Bun
|
||||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
|
uses: oven-sh/setup-bun@v2
|
||||||
with:
|
with:
|
||||||
bun-version: 1.2.11
|
bun-version: 1.2.11
|
||||||
|
|
||||||
@@ -88,7 +85,6 @@ runs:
|
|||||||
env:
|
env:
|
||||||
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||||
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
|
||||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
|
||||||
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
||||||
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
||||||
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
||||||
@@ -98,7 +94,7 @@ runs:
|
|||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
id: claude-code
|
id: claude-code
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||||
uses: anthropics/claude-code-base-action@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8
|
uses: anthropics/claude-code-base-action@beta
|
||||||
with:
|
with:
|
||||||
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
||||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||||
@@ -114,9 +110,6 @@ runs:
|
|||||||
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Provider configuration
|
|
||||||
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
|
||||||
|
|
||||||
# AWS configuration
|
# AWS configuration
|
||||||
AWS_REGION: ${{ env.AWS_REGION }}
|
AWS_REGION: ${{ env.AWS_REGION }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
||||||
@@ -150,12 +143,10 @@ runs:
|
|||||||
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
|
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
|
||||||
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
||||||
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
||||||
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
DEFAULT_BRANCH: ${{ steps.prepare.outputs.DEFAULT_BRANCH }}
|
||||||
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
||||||
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
||||||
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
||||||
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
|
||||||
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
|
||||||
|
|
||||||
- name: Display Claude Code Report
|
- name: Display Claude Code Report
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
||||||
|
|||||||
93
check-pr-status.ts
Normal file
93
check-pr-status.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
// Check the status of PR #105775
|
||||||
|
|
||||||
|
const GITHUB_API_URL = "https://api.github.com";
|
||||||
|
|
||||||
|
async function checkPRStatus(token: string) {
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Check PR details
|
||||||
|
console.log("Checking PR #105775...\n");
|
||||||
|
const prResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/anthropics/anthropic/pulls/105775`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`PR Status: ${prResponse.status}`);
|
||||||
|
|
||||||
|
if (!prResponse.ok) {
|
||||||
|
if (prResponse.status === 404) {
|
||||||
|
console.log("PR not found - it might be in a private repo or deleted");
|
||||||
|
}
|
||||||
|
const error = await prResponse.text();
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prData = await prResponse.json();
|
||||||
|
console.log(`Title: ${prData.title}`);
|
||||||
|
console.log(`State: ${prData.state}`);
|
||||||
|
console.log(`Branch: ${prData.head.ref}`);
|
||||||
|
console.log(`Base: ${prData.base.ref}`);
|
||||||
|
console.log(`Created: ${prData.created_at}`);
|
||||||
|
console.log(`Updated: ${prData.updated_at}`);
|
||||||
|
|
||||||
|
// Check if branch still exists
|
||||||
|
console.log(`\nChecking if branch '${prData.head.ref}' still exists...`);
|
||||||
|
const branchResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/anthropics/anthropic/git/refs/heads/${prData.head.ref}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (branchResponse.ok) {
|
||||||
|
const branchData = await branchResponse.json();
|
||||||
|
console.log(`✓ Branch exists with SHA: ${branchData.object.sha}`);
|
||||||
|
console.log(` PR head SHA: ${prData.head.sha}`);
|
||||||
|
if (branchData.object.sha !== prData.head.sha) {
|
||||||
|
console.log(` ⚠️ Branch has been updated since PR was created`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`✗ Branch does not exist (${branchResponse.status})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get recent comments
|
||||||
|
console.log(`\nFetching recent comments...`);
|
||||||
|
const commentsResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/anthropics/anthropic/issues/105775/comments?per_page=5&sort=created&direction=desc`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (commentsResponse.ok) {
|
||||||
|
const comments = await commentsResponse.json();
|
||||||
|
console.log(`Found ${comments.length} recent comments:`);
|
||||||
|
|
||||||
|
comments.reverse().forEach((comment: any, index: number) => {
|
||||||
|
console.log(`\nComment ${index + 1}:`);
|
||||||
|
console.log(` Author: ${comment.user.login}`);
|
||||||
|
console.log(` Created: ${comment.created_at}`);
|
||||||
|
console.log(` Body preview: ${comment.body.substring(0, 100)}...`);
|
||||||
|
|
||||||
|
// Check if it's a claude-code-action comment
|
||||||
|
if (comment.body.includes("claude") || comment.user.login.includes("bot")) {
|
||||||
|
console.log(` → Appears to be a Claude-related comment`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = process.argv[2];
|
||||||
|
if (!token) {
|
||||||
|
console.log("Usage: bun check-pr-status.ts <github-pat>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPRStatus(token);
|
||||||
183
diagnose-app-permissions.ts
Normal file
183
diagnose-app-permissions.ts
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
// Diagnose why GitHub App permissions are inconsistent
|
||||||
|
|
||||||
|
const GITHUB_API_URL = "https://api.github.com";
|
||||||
|
|
||||||
|
async function diagnosePermissions(token: string, owner: string, repo: string) {
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`\n=== Diagnosing GitHub App Permission Issues ===`);
|
||||||
|
console.log(`Repository: ${owner}/${repo}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Check what type of token we have
|
||||||
|
console.log("1. Token Analysis:");
|
||||||
|
const authHeader = headers.Authorization;
|
||||||
|
if (authHeader.includes('ghs_')) {
|
||||||
|
console.log("✓ GitHub App installation token detected");
|
||||||
|
} else if (authHeader.includes('ghp_')) {
|
||||||
|
console.log("✓ Personal Access Token detected");
|
||||||
|
} else {
|
||||||
|
console.log("? Unknown token type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check rate limit headers (different for apps vs users)
|
||||||
|
console.log("\n2. Rate Limit Analysis:");
|
||||||
|
const rateLimitResponse = await fetch(`${GITHUB_API_URL}/rate_limit`, { headers });
|
||||||
|
if (rateLimitResponse.ok) {
|
||||||
|
const rateData = await rateLimitResponse.json();
|
||||||
|
console.log(` Core limit: ${rateData.rate.remaining}/${rateData.rate.limit}`);
|
||||||
|
if (rateData.rate.limit > 5000) {
|
||||||
|
console.log(" → Higher limit suggests GitHub App token");
|
||||||
|
} else {
|
||||||
|
console.log(" → Standard limit suggests user token");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Test different API endpoints to find permission boundaries
|
||||||
|
console.log("\n3. Testing API Endpoints:");
|
||||||
|
|
||||||
|
// Test regular content API
|
||||||
|
console.log("\n a) Content API (high-level):");
|
||||||
|
const contentResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/contents/README.md`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
console.log(` GET contents: ${contentResponse.status} ${contentResponse.ok ? '✓' : '✗'}`);
|
||||||
|
|
||||||
|
// Test git database read
|
||||||
|
console.log("\n b) Git Database API (read):");
|
||||||
|
const branchResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/branches`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
if (branchResponse.ok) {
|
||||||
|
const branches = await branchResponse.json();
|
||||||
|
const defaultBranch = branches.find((b: any) => b.name === 'main' || b.name === 'master' || b.name === 'staging');
|
||||||
|
if (defaultBranch) {
|
||||||
|
const commitSha = defaultBranch.commit.sha;
|
||||||
|
|
||||||
|
// Try to read commit
|
||||||
|
const commitResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${commitSha}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
console.log(` GET commit: ${commitResponse.status} ${commitResponse.ok ? '✓' : '✗'}`);
|
||||||
|
|
||||||
|
// Try to read tree
|
||||||
|
if (commitResponse.ok) {
|
||||||
|
const commitData = await commitResponse.json();
|
||||||
|
const treeResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees/${commitData.tree.sha}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
console.log(` GET tree: ${treeResponse.status} ${treeResponse.ok ? '✓' : '✗'}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test git database write
|
||||||
|
console.log("\n c) Git Database API (write):");
|
||||||
|
|
||||||
|
// Get a base commit to test with
|
||||||
|
const testBranch = branches[0];
|
||||||
|
if (testBranch) {
|
||||||
|
const baseCommitResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${testBranch.commit.sha}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (baseCommitResponse.ok) {
|
||||||
|
const baseCommit = await baseCommitResponse.json();
|
||||||
|
|
||||||
|
// Try to create a blob
|
||||||
|
const blobResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/blobs`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: "test",
|
||||||
|
encoding: "utf-8",
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(` POST blob: ${blobResponse.status} ${blobResponse.ok ? '✓' : '✗'}`);
|
||||||
|
|
||||||
|
// Try to create a tree
|
||||||
|
const treeResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
base_tree: baseCommit.tree.sha,
|
||||||
|
tree: [{
|
||||||
|
path: "test-permission-check.txt",
|
||||||
|
mode: "100644",
|
||||||
|
type: "blob",
|
||||||
|
content: "test",
|
||||||
|
}],
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
console.log(` POST tree: ${treeResponse.status} ${treeResponse.ok ? '✓' : '✗'}`);
|
||||||
|
|
||||||
|
if (!treeResponse.ok) {
|
||||||
|
const error = await treeResponse.text();
|
||||||
|
console.log(` Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check webhook/app events
|
||||||
|
console.log("\n4. Checking Recent Activity:");
|
||||||
|
const eventsResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/events?per_page=10`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (eventsResponse.ok) {
|
||||||
|
const events = await eventsResponse.json();
|
||||||
|
const appEvents = events.filter((e: any) =>
|
||||||
|
e.actor.login.includes('[bot]') ||
|
||||||
|
e.actor.type === 'Bot'
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(` Found ${appEvents.length} bot/app events in last 10 events`);
|
||||||
|
appEvents.forEach((event: any) => {
|
||||||
|
console.log(` - ${event.actor.login}: ${event.type} at ${event.created_at}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Summary and recommendations
|
||||||
|
console.log("\n=== Analysis Summary ===");
|
||||||
|
console.log("\nPossible causes for inconsistent 500 errors:");
|
||||||
|
console.log("1. Race conditions with other bots (check events above)");
|
||||||
|
console.log("2. Token scope varies based on who triggered the action");
|
||||||
|
console.log("3. GitHub App needs to be reinstalled/reconfigured");
|
||||||
|
console.log("4. Branch-specific protection rules");
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("\nError during diagnosis:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [token, owner, repo] = process.argv.slice(2);
|
||||||
|
if (!token || !owner || !repo) {
|
||||||
|
console.log("Usage: bun diagnose-app-permissions.ts <token> <owner> <repo>");
|
||||||
|
console.log("\nThis script helps diagnose why GitHub App permissions are inconsistent.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnosePermissions(token, owner, repo);
|
||||||
112
revert-test-commit.ts
Normal file
112
revert-test-commit.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
// Revert the test commit we just made
|
||||||
|
|
||||||
|
const GITHUB_API_URL = "https://api.github.com";
|
||||||
|
|
||||||
|
async function revertTestCommit(token: string) {
|
||||||
|
const owner = "anthropics";
|
||||||
|
const repo = "anthropic";
|
||||||
|
const branch = "monty/fixing-pipeline-runner";
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`\n=== Reverting test commit ===`);
|
||||||
|
console.log(`Repository: ${owner}/${repo}`);
|
||||||
|
console.log(`Branch: ${branch}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get current branch state
|
||||||
|
console.log("Getting current branch reference...");
|
||||||
|
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||||
|
const refResponse = await fetch(refUrl, { headers });
|
||||||
|
|
||||||
|
if (!refResponse.ok) {
|
||||||
|
console.error(`Failed to get branch: ${refResponse.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refData = await refResponse.json();
|
||||||
|
const currentSha = refData.object.sha;
|
||||||
|
console.log(`Current branch SHA: ${currentSha}`);
|
||||||
|
|
||||||
|
// Get the current commit to find its parent
|
||||||
|
console.log("\nGetting current commit details...");
|
||||||
|
const commitResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${currentSha}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!commitResponse.ok) {
|
||||||
|
console.error(`Failed to get commit: ${commitResponse.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitData = await commitResponse.json();
|
||||||
|
console.log(`Current commit message: "${commitData.message}"`);
|
||||||
|
|
||||||
|
if (!commitData.message.includes("Debug: Test commit to reproduce 500 error")) {
|
||||||
|
console.log("⚠️ Current commit doesn't look like our test commit");
|
||||||
|
console.log("Are you sure you want to revert this?");
|
||||||
|
console.log("Current message:", commitData.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commitData.parents.length === 0) {
|
||||||
|
console.error("Cannot revert: this appears to be the initial commit");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentSha = commitData.parents[0].sha;
|
||||||
|
console.log(`Parent SHA: ${parentSha}`);
|
||||||
|
|
||||||
|
// Reset the branch to the parent commit
|
||||||
|
console.log("\nReverting branch to parent commit...");
|
||||||
|
const updateRefResponse = await fetch(refUrl, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sha: parentSha,
|
||||||
|
force: true, // Force is needed for this kind of reset
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updateRefResponse.ok) {
|
||||||
|
console.error(`Failed to revert: ${updateRefResponse.status}`);
|
||||||
|
const error = await updateRefResponse.text();
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("✅ Successfully reverted test commit!");
|
||||||
|
console.log(`Branch ${branch} is now back to SHA: ${parentSha}`);
|
||||||
|
|
||||||
|
// Verify the revert
|
||||||
|
console.log("\nVerifying revert...");
|
||||||
|
const verifyResponse = await fetch(refUrl, { headers });
|
||||||
|
const verifyData = await verifyResponse.json();
|
||||||
|
|
||||||
|
if (verifyData.object.sha === parentSha) {
|
||||||
|
console.log("✅ Revert confirmed");
|
||||||
|
} else {
|
||||||
|
console.log("⚠️ Unexpected SHA after revert:", verifyData.object.sha);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = process.argv[2];
|
||||||
|
if (!token) {
|
||||||
|
console.log("Usage: bun revert-test-commit.ts <github-pat>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
revertTestCommit(token);
|
||||||
BIN
src/.DS_Store
vendored
Normal file
BIN
src/.DS_Store
vendored
Normal file
Binary file not shown.
@@ -9,8 +9,8 @@ import {
|
|||||||
formatComments,
|
formatComments,
|
||||||
formatReviewComments,
|
formatReviewComments,
|
||||||
formatChangedFilesWithSHA,
|
formatChangedFilesWithSHA,
|
||||||
|
stripHtmlComments,
|
||||||
} from "../github/data/formatter";
|
} from "../github/data/formatter";
|
||||||
import { sanitizeContent } from "../github/utils/sanitizer";
|
|
||||||
import {
|
import {
|
||||||
isIssuesEvent,
|
isIssuesEvent,
|
||||||
isIssueCommentEvent,
|
isIssueCommentEvent,
|
||||||
@@ -58,27 +58,10 @@ export function buildAllowedToolsString(
|
|||||||
|
|
||||||
export function buildDisallowedToolsString(
|
export function buildDisallowedToolsString(
|
||||||
customDisallowedTools?: string,
|
customDisallowedTools?: string,
|
||||||
allowedTools?: string,
|
|
||||||
): string {
|
): string {
|
||||||
let disallowedTools = [...DISALLOWED_TOOLS];
|
let allDisallowedTools = DISALLOWED_TOOLS.join(",");
|
||||||
|
|
||||||
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
|
|
||||||
if (allowedTools) {
|
|
||||||
const allowedToolsArray = allowedTools
|
|
||||||
.split(",")
|
|
||||||
.map((tool) => tool.trim());
|
|
||||||
disallowedTools = disallowedTools.filter(
|
|
||||||
(tool) => !allowedToolsArray.includes(tool),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let allDisallowedTools = disallowedTools.join(",");
|
|
||||||
if (customDisallowedTools) {
|
if (customDisallowedTools) {
|
||||||
if (allDisallowedTools) {
|
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
|
||||||
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
|
|
||||||
} else {
|
|
||||||
allDisallowedTools = customDisallowedTools;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return allDisallowedTools;
|
return allDisallowedTools;
|
||||||
}
|
}
|
||||||
@@ -86,7 +69,7 @@ export function buildDisallowedToolsString(
|
|||||||
export function prepareContext(
|
export function prepareContext(
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
claudeCommentId: string,
|
claudeCommentId: string,
|
||||||
baseBranch?: string,
|
defaultBranch?: string,
|
||||||
claudeBranch?: string,
|
claudeBranch?: string,
|
||||||
): PreparedContext {
|
): PreparedContext {
|
||||||
const repository = context.repository.full_name;
|
const repository = context.repository.full_name;
|
||||||
@@ -164,7 +147,7 @@ export function prepareContext(
|
|||||||
...(commentId && { commentId }),
|
...(commentId && { commentId }),
|
||||||
commentBody,
|
commentBody,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(baseBranch && { baseBranch }),
|
...(defaultBranch && { defaultBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -186,7 +169,7 @@ export function prepareContext(
|
|||||||
prNumber,
|
prNumber,
|
||||||
commentBody,
|
commentBody,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(baseBranch && { baseBranch }),
|
...(defaultBranch && { defaultBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -211,13 +194,13 @@ export function prepareContext(
|
|||||||
prNumber,
|
prNumber,
|
||||||
commentBody,
|
commentBody,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(baseBranch && { baseBranch }),
|
...(defaultBranch && { defaultBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
} else if (!claudeBranch) {
|
} else if (!claudeBranch) {
|
||||||
throw new Error("CLAUDE_BRANCH is required for issue_comment event");
|
throw new Error("CLAUDE_BRANCH is required for issue_comment event");
|
||||||
} else if (!baseBranch) {
|
} else if (!defaultBranch) {
|
||||||
throw new Error("BASE_BRANCH is required for issue_comment event");
|
throw new Error("DEFAULT_BRANCH is required for issue_comment event");
|
||||||
} else if (!issueNumber) {
|
} else if (!issueNumber) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"ISSUE_NUMBER is required for issue_comment event for issues",
|
"ISSUE_NUMBER is required for issue_comment event for issues",
|
||||||
@@ -229,7 +212,7 @@ export function prepareContext(
|
|||||||
commentId,
|
commentId,
|
||||||
isPR: false,
|
isPR: false,
|
||||||
claudeBranch: claudeBranch,
|
claudeBranch: claudeBranch,
|
||||||
baseBranch,
|
defaultBranch,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
commentBody,
|
commentBody,
|
||||||
};
|
};
|
||||||
@@ -245,8 +228,8 @@ export function prepareContext(
|
|||||||
if (isPR) {
|
if (isPR) {
|
||||||
throw new Error("IS_PR must be false for issues event");
|
throw new Error("IS_PR must be false for issues event");
|
||||||
}
|
}
|
||||||
if (!baseBranch) {
|
if (!defaultBranch) {
|
||||||
throw new Error("BASE_BRANCH is required for issues event");
|
throw new Error("DEFAULT_BRANCH is required for issues event");
|
||||||
}
|
}
|
||||||
if (!claudeBranch) {
|
if (!claudeBranch) {
|
||||||
throw new Error("CLAUDE_BRANCH is required for issues event");
|
throw new Error("CLAUDE_BRANCH is required for issues event");
|
||||||
@@ -263,7 +246,7 @@ export function prepareContext(
|
|||||||
eventAction: "assigned",
|
eventAction: "assigned",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
baseBranch,
|
defaultBranch,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
assigneeTrigger,
|
assigneeTrigger,
|
||||||
};
|
};
|
||||||
@@ -273,7 +256,7 @@ export function prepareContext(
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber,
|
issueNumber,
|
||||||
baseBranch,
|
defaultBranch,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -294,7 +277,7 @@ export function prepareContext(
|
|||||||
isPR: true,
|
isPR: true,
|
||||||
prNumber,
|
prNumber,
|
||||||
...(claudeBranch && { claudeBranch }),
|
...(claudeBranch && { claudeBranch }),
|
||||||
...(baseBranch && { baseBranch }),
|
...(defaultBranch && { defaultBranch }),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -436,14 +419,14 @@ ${
|
|||||||
eventData.eventName === "pull_request_review") &&
|
eventData.eventName === "pull_request_review") &&
|
||||||
eventData.commentBody
|
eventData.commentBody
|
||||||
? `<trigger_comment>
|
? `<trigger_comment>
|
||||||
${sanitizeContent(eventData.commentBody)}
|
${stripHtmlComments(eventData.commentBody)}
|
||||||
</trigger_comment>`
|
</trigger_comment>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
context.directPrompt
|
context.directPrompt
|
||||||
? `<direct_prompt>
|
? `<direct_prompt>
|
||||||
${sanitizeContent(context.directPrompt)}
|
${stripHtmlComments(context.directPrompt)}
|
||||||
</direct_prompt>`
|
</direct_prompt>`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
@@ -541,13 +524,13 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
|
|||||||
${
|
${
|
||||||
eventData.claudeBranch
|
eventData.claudeBranch
|
||||||
? `- Provide a URL to create a PR manually in this format:
|
? `- Provide a URL to create a PR manually in this format:
|
||||||
[Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
[Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.defaultBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
||||||
- IMPORTANT: Use THREE dots (...) between branch names, not two (..)
|
- IMPORTANT: Use THREE dots (...) between branch names, not two (..)
|
||||||
Example: ${GITHUB_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
|
Example: ${GITHUB_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
|
||||||
NOT: ${GITHUB_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect)
|
NOT: ${GITHUB_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect)
|
||||||
- IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces
|
- IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces
|
||||||
Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message"
|
Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message"
|
||||||
- The target-branch should be '${eventData.baseBranch}'.
|
- The target-branch should be '${eventData.defaultBranch}'.
|
||||||
- The branch-name is the current branch: ${eventData.claudeBranch}
|
- The branch-name is the current branch: ${eventData.claudeBranch}
|
||||||
- The body should include:
|
- The body should include:
|
||||||
- A clear description of the changes
|
- A clear description of the changes
|
||||||
@@ -611,11 +594,6 @@ What You CANNOT Do:
|
|||||||
- Execute commands outside the repository context
|
- Execute commands outside the repository context
|
||||||
- Run arbitrary Bash commands (unless explicitly allowed via allowed_tools configuration)
|
- Run arbitrary Bash commands (unless explicitly allowed via allowed_tools configuration)
|
||||||
- Perform branch operations (cannot merge branches, rebase, or perform other git operations beyond pushing commits)
|
- Perform branch operations (cannot merge branches, rebase, or perform other git operations beyond pushing commits)
|
||||||
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
|
|
||||||
- View CI/CD results or workflow run outputs (cannot access GitHub Actions logs or test results)
|
|
||||||
|
|
||||||
When users ask you to perform actions you cannot do, politely explain the limitation and, when applicable, direct them to the FAQ for more information and workarounds:
|
|
||||||
"I'm unable to [specific action] due to [reason]. You can find more information and potential workarounds in the [FAQ](https://github.com/anthropics/claude-code-action/blob/main/FAQ.md)."
|
|
||||||
|
|
||||||
If a user asks for something outside these capabilities (and you have no other tools provided), politely explain that you cannot perform that action and suggest an alternative approach if possible.
|
If a user asks for something outside these capabilities (and you have no other tools provided), politely explain that you cannot perform that action and suggest an alternative approach if possible.
|
||||||
|
|
||||||
@@ -637,7 +615,7 @@ f. If you are unable to complete certain steps, such as running a linter or test
|
|||||||
|
|
||||||
export async function createPrompt(
|
export async function createPrompt(
|
||||||
claudeCommentId: number,
|
claudeCommentId: number,
|
||||||
baseBranch: string | undefined,
|
defaultBranch: string | undefined,
|
||||||
claudeBranch: string | undefined,
|
claudeBranch: string | undefined,
|
||||||
githubData: FetchDataResult,
|
githubData: FetchDataResult,
|
||||||
context: ParsedGitHubContext,
|
context: ParsedGitHubContext,
|
||||||
@@ -646,7 +624,7 @@ export async function createPrompt(
|
|||||||
const preparedContext = prepareContext(
|
const preparedContext = prepareContext(
|
||||||
context,
|
context,
|
||||||
claudeCommentId.toString(),
|
claudeCommentId.toString(),
|
||||||
baseBranch,
|
defaultBranch,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -670,7 +648,6 @@ export async function createPrompt(
|
|||||||
);
|
);
|
||||||
const allDisallowedTools = buildDisallowedToolsString(
|
const allDisallowedTools = buildDisallowedToolsString(
|
||||||
preparedContext.disallowedTools,
|
preparedContext.disallowedTools,
|
||||||
preparedContext.allowedTools,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ type PullRequestReviewCommentEvent = {
|
|||||||
commentId?: string; // May be present for review comments
|
commentId?: string; // May be present for review comments
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
baseBranch?: string;
|
defaultBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PullRequestReviewEvent = {
|
type PullRequestReviewEvent = {
|
||||||
@@ -25,7 +25,7 @@ type PullRequestReviewEvent = {
|
|||||||
prNumber: string;
|
prNumber: string;
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
baseBranch?: string;
|
defaultBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IssueCommentEvent = {
|
type IssueCommentEvent = {
|
||||||
@@ -33,7 +33,7 @@ type IssueCommentEvent = {
|
|||||||
commentId: string;
|
commentId: string;
|
||||||
issueNumber: string;
|
issueNumber: string;
|
||||||
isPR: false;
|
isPR: false;
|
||||||
baseBranch: string;
|
defaultBranch: string;
|
||||||
claudeBranch: string;
|
claudeBranch: string;
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ type PullRequestCommentEvent = {
|
|||||||
isPR: true;
|
isPR: true;
|
||||||
commentBody: string;
|
commentBody: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
baseBranch?: string;
|
defaultBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IssueOpenedEvent = {
|
type IssueOpenedEvent = {
|
||||||
@@ -54,7 +54,7 @@ type IssueOpenedEvent = {
|
|||||||
eventAction: "opened";
|
eventAction: "opened";
|
||||||
isPR: false;
|
isPR: false;
|
||||||
issueNumber: string;
|
issueNumber: string;
|
||||||
baseBranch: string;
|
defaultBranch: string;
|
||||||
claudeBranch: string;
|
claudeBranch: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ type IssueAssignedEvent = {
|
|||||||
eventAction: "assigned";
|
eventAction: "assigned";
|
||||||
isPR: false;
|
isPR: false;
|
||||||
issueNumber: string;
|
issueNumber: string;
|
||||||
baseBranch: string;
|
defaultBranch: string;
|
||||||
claudeBranch: string;
|
claudeBranch: string;
|
||||||
assigneeTrigger: string;
|
assigneeTrigger: string;
|
||||||
};
|
};
|
||||||
@@ -74,7 +74,7 @@ type PullRequestEvent = {
|
|||||||
isPR: true;
|
isPR: true;
|
||||||
prNumber: string;
|
prNumber: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
baseBranch?: string;
|
defaultBranch?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Union type for all possible event types
|
// Union type for all possible event types
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ async function run() {
|
|||||||
// Step 10: Create prompt file
|
// Step 10: Create prompt file
|
||||||
await createPrompt(
|
await createPrompt(
|
||||||
commentId,
|
commentId,
|
||||||
branchInfo.baseBranch,
|
branchInfo.defaultBranch,
|
||||||
branchInfo.claudeBranch,
|
branchInfo.claudeBranch,
|
||||||
githubData,
|
githubData,
|
||||||
context,
|
context,
|
||||||
@@ -92,10 +92,7 @@ async function run() {
|
|||||||
);
|
);
|
||||||
core.setOutput("mcp_config", mcpConfig);
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
core.setFailed(`Prepare step failed with error: ${error}`);
|
||||||
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
|
||||||
// Also output the clean error message for the action to capture
|
|
||||||
core.setOutput("prepare_error", errorMessage);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ async function run() {
|
|||||||
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
|
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
|
||||||
const githubToken = process.env.GITHUB_TOKEN!;
|
const githubToken = process.env.GITHUB_TOKEN!;
|
||||||
const claudeBranch = process.env.CLAUDE_BRANCH;
|
const claudeBranch = process.env.CLAUDE_BRANCH;
|
||||||
const baseBranch = process.env.BASE_BRANCH || "main";
|
const defaultBranch = process.env.DEFAULT_BRANCH || "main";
|
||||||
const triggerUsername = process.env.TRIGGER_USERNAME;
|
const triggerUsername = process.env.TRIGGER_USERNAME;
|
||||||
|
|
||||||
const context = parseGitHubContext();
|
const context = parseGitHubContext();
|
||||||
@@ -92,7 +92,7 @@ async function run() {
|
|||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
claudeBranch,
|
claudeBranch,
|
||||||
baseBranch,
|
defaultBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if we need to add PR URL when we have a new branch
|
// Check if we need to add PR URL when we have a new branch
|
||||||
@@ -102,7 +102,7 @@ async function run() {
|
|||||||
// Check if comment already contains a PR URL
|
// Check if comment already contains a PR URL
|
||||||
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
const prUrlPattern = new RegExp(
|
const prUrlPattern = new RegExp(
|
||||||
`${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
|
`${serverUrlPattern}\\/.+\\/compare\\/${defaultBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
|
||||||
);
|
);
|
||||||
const containsPRUrl = currentBody.match(prUrlPattern);
|
const containsPRUrl = currentBody.match(prUrlPattern);
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ async function run() {
|
|||||||
await octokit.rest.repos.compareCommitsWithBasehead({
|
await octokit.rest.repos.compareCommitsWithBasehead({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
basehead: `${baseBranch}...${claudeBranch}`,
|
basehead: `${defaultBranch}...${claudeBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there are changes (commits or file changes), add the PR URL
|
// If there are changes (commits or file changes), add the PR URL
|
||||||
@@ -128,7 +128,7 @@ async function run() {
|
|||||||
const prBody = encodeURIComponent(
|
const prBody = encodeURIComponent(
|
||||||
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
|
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
|
||||||
);
|
);
|
||||||
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
|
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${defaultBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
|
||||||
prLink = `\n[Create a PR](${prUrl})`;
|
prLink = `\n[Create a PR](${prUrl})`;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -145,48 +145,38 @@ async function run() {
|
|||||||
duration_api_ms?: number;
|
duration_api_ms?: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
let actionFailed = false;
|
let actionFailed = false;
|
||||||
let errorDetails: string | undefined;
|
|
||||||
|
|
||||||
// First check if prepare step failed
|
// Check for existence of output file and parse it if available
|
||||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
try {
|
||||||
const prepareError = process.env.PREPARE_ERROR;
|
const outputFile = process.env.OUTPUT_FILE;
|
||||||
|
if (outputFile) {
|
||||||
|
const fileContent = await fs.readFile(outputFile, "utf8");
|
||||||
|
const outputData = JSON.parse(fileContent);
|
||||||
|
|
||||||
if (!prepareSuccess && prepareError) {
|
// Output file is an array, get the last element which contains execution details
|
||||||
actionFailed = true;
|
if (Array.isArray(outputData) && outputData.length > 0) {
|
||||||
errorDetails = prepareError;
|
const lastElement = outputData[outputData.length - 1];
|
||||||
} else {
|
if (
|
||||||
// Check for existence of output file and parse it if available
|
lastElement.role === "system" &&
|
||||||
try {
|
"cost_usd" in lastElement &&
|
||||||
const outputFile = process.env.OUTPUT_FILE;
|
"duration_ms" in lastElement
|
||||||
if (outputFile) {
|
) {
|
||||||
const fileContent = await fs.readFile(outputFile, "utf8");
|
executionDetails = {
|
||||||
const outputData = JSON.parse(fileContent);
|
cost_usd: lastElement.cost_usd,
|
||||||
|
duration_ms: lastElement.duration_ms,
|
||||||
// Output file is an array, get the last element which contains execution details
|
duration_api_ms: lastElement.duration_api_ms,
|
||||||
if (Array.isArray(outputData) && outputData.length > 0) {
|
};
|
||||||
const lastElement = outputData[outputData.length - 1];
|
|
||||||
if (
|
|
||||||
lastElement.role === "system" &&
|
|
||||||
"cost_usd" in lastElement &&
|
|
||||||
"duration_ms" in lastElement
|
|
||||||
) {
|
|
||||||
executionDetails = {
|
|
||||||
cost_usd: lastElement.cost_usd,
|
|
||||||
duration_ms: lastElement.duration_ms,
|
|
||||||
duration_api_ms: lastElement.duration_api_ms,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the Claude action failed
|
|
||||||
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
|
||||||
actionFailed = !claudeSuccess;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading output file:", error);
|
|
||||||
// If we can't read the file, check for any failure markers
|
|
||||||
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the action failed by looking at the exit code or error marker
|
||||||
|
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
||||||
|
actionFailed = !claudeSuccess;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reading output file:", error);
|
||||||
|
// If we can't read the file, check for any failure markers
|
||||||
|
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare input for updateCommentBody function
|
// Prepare input for updateCommentBody function
|
||||||
@@ -199,7 +189,6 @@ async function run() {
|
|||||||
prLink,
|
prLink,
|
||||||
branchName: shouldDeleteBranch ? undefined : claudeBranch,
|
branchName: shouldDeleteBranch ? undefined : claudeBranch,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
errorDetails,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedBody = updateCommentBody(commentInput);
|
const updatedBody = updateCommentBody(commentInput);
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ export type ParsedGitHubContext = {
|
|||||||
disallowedTools: string;
|
disallowedTools: string;
|
||||||
customInstructions: string;
|
customInstructions: string;
|
||||||
directPrompt: string;
|
directPrompt: string;
|
||||||
baseBranch?: string;
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,7 +55,6 @@ export function parseGitHubContext(): ParsedGitHubContext {
|
|||||||
disallowedTools: process.env.DISALLOWED_TOOLS ?? "",
|
disallowedTools: process.env.DISALLOWED_TOOLS ?? "",
|
||||||
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
|
||||||
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
directPrompt: process.env.DIRECT_PROMPT ?? "",
|
||||||
baseBranch: process.env.BASE_BRANCH,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import type {
|
|||||||
GitHubReview,
|
GitHubReview,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import type { GitHubFileWithSHA } from "./fetcher";
|
import type { GitHubFileWithSHA } from "./fetcher";
|
||||||
import { sanitizeContent } from "../utils/sanitizer";
|
|
||||||
|
export function stripHtmlComments(text: string): string {
|
||||||
|
return text.replace(/<!--[\s\S]*?-->/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
export function formatContext(
|
export function formatContext(
|
||||||
contextData: GitHubPullRequest | GitHubIssue,
|
contextData: GitHubPullRequest | GitHubIssue,
|
||||||
@@ -34,14 +37,13 @@ export function formatBody(
|
|||||||
body: string,
|
body: string,
|
||||||
imageUrlMap: Map<string, string>,
|
imageUrlMap: Map<string, string>,
|
||||||
): string {
|
): string {
|
||||||
let processedBody = body;
|
let processedBody = stripHtmlComments(body);
|
||||||
|
|
||||||
|
// Replace image URLs with local paths
|
||||||
for (const [originalUrl, localPath] of imageUrlMap) {
|
for (const [originalUrl, localPath] of imageUrlMap) {
|
||||||
processedBody = processedBody.replaceAll(originalUrl, localPath);
|
processedBody = processedBody.replaceAll(originalUrl, localPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
processedBody = sanitizeContent(processedBody);
|
|
||||||
|
|
||||||
return processedBody;
|
return processedBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,16 +53,15 @@ export function formatComments(
|
|||||||
): string {
|
): string {
|
||||||
return comments
|
return comments
|
||||||
.map((comment) => {
|
.map((comment) => {
|
||||||
let body = comment.body;
|
let body = stripHtmlComments(comment.body);
|
||||||
|
|
||||||
|
// Replace image URLs with local paths if we have a mapping
|
||||||
if (imageUrlMap && body) {
|
if (imageUrlMap && body) {
|
||||||
for (const [originalUrl, localPath] of imageUrlMap) {
|
for (const [originalUrl, localPath] of imageUrlMap) {
|
||||||
body = body.replaceAll(originalUrl, localPath);
|
body = body.replaceAll(originalUrl, localPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body = sanitizeContent(body);
|
|
||||||
|
|
||||||
return `[${comment.author.login} at ${comment.createdAt}]: ${body}`;
|
return `[${comment.author.login} at ${comment.createdAt}]: ${body}`;
|
||||||
})
|
})
|
||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
@@ -77,19 +78,6 @@ export function formatReviewComments(
|
|||||||
const formattedReviews = reviewData.nodes.map((review) => {
|
const formattedReviews = reviewData.nodes.map((review) => {
|
||||||
let reviewOutput = `[Review by ${review.author.login} at ${review.submittedAt}]: ${review.state}`;
|
let reviewOutput = `[Review by ${review.author.login} at ${review.submittedAt}]: ${review.state}`;
|
||||||
|
|
||||||
if (review.body && review.body.trim()) {
|
|
||||||
let body = review.body;
|
|
||||||
|
|
||||||
if (imageUrlMap) {
|
|
||||||
for (const [originalUrl, localPath] of imageUrlMap) {
|
|
||||||
body = body.replaceAll(originalUrl, localPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sanitizedBody = sanitizeContent(body);
|
|
||||||
reviewOutput += `\n${sanitizedBody}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
review.comments &&
|
review.comments &&
|
||||||
review.comments.nodes &&
|
review.comments.nodes &&
|
||||||
@@ -97,16 +85,15 @@ export function formatReviewComments(
|
|||||||
) {
|
) {
|
||||||
const comments = review.comments.nodes
|
const comments = review.comments.nodes
|
||||||
.map((comment) => {
|
.map((comment) => {
|
||||||
let body = comment.body;
|
let body = stripHtmlComments(comment.body);
|
||||||
|
|
||||||
|
// Replace image URLs with local paths if we have a mapping
|
||||||
if (imageUrlMap) {
|
if (imageUrlMap) {
|
||||||
for (const [originalUrl, localPath] of imageUrlMap) {
|
for (const [originalUrl, localPath] of imageUrlMap) {
|
||||||
body = body.replaceAll(originalUrl, localPath);
|
body = body.replaceAll(originalUrl, localPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body = sanitizeContent(body);
|
|
||||||
|
|
||||||
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
|
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export async function checkAndDeleteEmptyBranch(
|
|||||||
owner: string,
|
owner: string,
|
||||||
repo: string,
|
repo: string,
|
||||||
claudeBranch: string | undefined,
|
claudeBranch: string | undefined,
|
||||||
baseBranch: string,
|
defaultBranch: string,
|
||||||
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
|
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
|
||||||
let branchLink = "";
|
let branchLink = "";
|
||||||
let shouldDeleteBranch = false;
|
let shouldDeleteBranch = false;
|
||||||
@@ -18,7 +18,7 @@ export async function checkAndDeleteEmptyBranch(
|
|||||||
await octokit.rest.repos.compareCommitsWithBasehead({
|
await octokit.rest.repos.compareCommitsWithBasehead({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
basehead: `${baseBranch}...${claudeBranch}`,
|
basehead: `${defaultBranch}...${claudeBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// If there are no commits, mark branch for deletion
|
// If there are no commits, mark branch for deletion
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type { Octokits } from "../api/client";
|
|||||||
import type { FetchDataResult } from "../data/fetcher";
|
import type { FetchDataResult } from "../data/fetcher";
|
||||||
|
|
||||||
export type BranchInfo = {
|
export type BranchInfo = {
|
||||||
baseBranch: string;
|
defaultBranch: string;
|
||||||
claudeBranch?: string;
|
claudeBranch?: string;
|
||||||
currentBranch: string;
|
currentBranch: string;
|
||||||
};
|
};
|
||||||
@@ -26,9 +26,15 @@ export async function setupBranch(
|
|||||||
): Promise<BranchInfo> {
|
): Promise<BranchInfo> {
|
||||||
const { owner, repo } = context.repository;
|
const { owner, repo } = context.repository;
|
||||||
const entityNumber = context.entityNumber;
|
const entityNumber = context.entityNumber;
|
||||||
const { baseBranch } = context.inputs;
|
|
||||||
const isPR = context.isPR;
|
const isPR = context.isPR;
|
||||||
|
|
||||||
|
// Get the default branch first
|
||||||
|
const repoResponse = await octokits.rest.repos.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
});
|
||||||
|
const defaultBranch = repoResponse.data.default_branch;
|
||||||
|
|
||||||
if (isPR) {
|
if (isPR) {
|
||||||
const prData = githubData.contextData as GitHubPullRequest;
|
const prData = githubData.contextData as GitHubPullRequest;
|
||||||
const prState = prData.state;
|
const prState = prData.state;
|
||||||
@@ -36,7 +42,7 @@ export async function setupBranch(
|
|||||||
// Check if PR is closed or merged
|
// Check if PR is closed or merged
|
||||||
if (prState === "CLOSED" || prState === "MERGED") {
|
if (prState === "CLOSED" || prState === "MERGED") {
|
||||||
console.log(
|
console.log(
|
||||||
`PR #${entityNumber} is ${prState}, creating new branch from source...`,
|
`PR #${entityNumber} is ${prState}, creating new branch from default...`,
|
||||||
);
|
);
|
||||||
// Fall through to create a new branch like we do for issues
|
// Fall through to create a new branch like we do for issues
|
||||||
} else {
|
} else {
|
||||||
@@ -45,43 +51,23 @@ export async function setupBranch(
|
|||||||
|
|
||||||
const branchName = prData.headRefName;
|
const branchName = prData.headRefName;
|
||||||
|
|
||||||
// Execute git commands to checkout PR branch (shallow fetch for performance)
|
// Execute git commands to checkout PR branch
|
||||||
// Fetch the branch with a depth of 20 to avoid fetching too much history, while still allowing for some context
|
await $`git fetch origin ${branchName}`;
|
||||||
await $`git fetch origin --depth=20 ${branchName}`;
|
|
||||||
await $`git checkout ${branchName}`;
|
await $`git checkout ${branchName}`;
|
||||||
|
|
||||||
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
||||||
|
|
||||||
// For open PRs, we need to get the base branch of the PR
|
// For open PRs, return branch info
|
||||||
const baseBranch = prData.baseRefName;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
baseBranch,
|
defaultBranch,
|
||||||
currentBranch: branchName,
|
currentBranch: branchName,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine source branch - use baseBranch if provided, otherwise fetch default
|
|
||||||
let sourceBranch: string;
|
|
||||||
|
|
||||||
if (baseBranch) {
|
|
||||||
// Use provided base branch for source
|
|
||||||
sourceBranch = baseBranch;
|
|
||||||
} else {
|
|
||||||
// No base branch provided, fetch the default branch to use as source
|
|
||||||
const repoResponse = await octokits.rest.repos.get({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
});
|
|
||||||
sourceBranch = repoResponse.data.default_branch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Creating a new branch for either an issue or closed/merged PR
|
// Creating a new branch for either an issue or closed/merged PR
|
||||||
const entityType = isPR ? "pr" : "issue";
|
const entityType = isPR ? "pr" : "issue";
|
||||||
console.log(
|
console.log(`Creating new branch for ${entityType} #${entityNumber}...`);
|
||||||
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const timestamp = new Date()
|
const timestamp = new Date()
|
||||||
.toISOString()
|
.toISOString()
|
||||||
@@ -93,14 +79,14 @@ export async function setupBranch(
|
|||||||
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Get the SHA of the source branch
|
// Get the SHA of the default branch
|
||||||
const sourceBranchRef = await octokits.rest.git.getRef({
|
const defaultBranchRef = await octokits.rest.git.getRef({
|
||||||
owner,
|
owner,
|
||||||
repo,
|
repo,
|
||||||
ref: `heads/${sourceBranch}`,
|
ref: `heads/${defaultBranch}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentSHA = sourceBranchRef.data.object.sha;
|
const currentSHA = defaultBranchRef.data.object.sha;
|
||||||
|
|
||||||
console.log(`Current SHA: ${currentSHA}`);
|
console.log(`Current SHA: ${currentSHA}`);
|
||||||
|
|
||||||
@@ -112,8 +98,8 @@ export async function setupBranch(
|
|||||||
sha: currentSHA,
|
sha: currentSHA,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Checkout the new branch (shallow fetch for performance)
|
// Checkout the new branch
|
||||||
await $`git fetch origin --depth=1 ${newBranch}`;
|
await $`git fetch origin ${newBranch}`;
|
||||||
await $`git checkout ${newBranch}`;
|
await $`git checkout ${newBranch}`;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
@@ -122,9 +108,9 @@ export async function setupBranch(
|
|||||||
|
|
||||||
// Set outputs for GitHub Actions
|
// Set outputs for GitHub Actions
|
||||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
core.setOutput("CLAUDE_BRANCH", newBranch);
|
||||||
core.setOutput("BASE_BRANCH", sourceBranch);
|
core.setOutput("DEFAULT_BRANCH", defaultBranch);
|
||||||
return {
|
return {
|
||||||
baseBranch: sourceBranch,
|
defaultBranch,
|
||||||
claudeBranch: newBranch,
|
claudeBranch: newBranch,
|
||||||
currentBranch: newBranch,
|
currentBranch: newBranch,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ export type CommentUpdateInput = {
|
|||||||
prLink?: string;
|
prLink?: string;
|
||||||
branchName?: string;
|
branchName?: string;
|
||||||
triggerUsername?: string;
|
triggerUsername?: string;
|
||||||
errorDetails?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ensureProperlyEncodedUrl(url: string): string | null {
|
export function ensureProperlyEncodedUrl(url: string): string | null {
|
||||||
@@ -76,7 +75,6 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
actionFailed,
|
actionFailed,
|
||||||
branchName,
|
branchName,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
errorDetails,
|
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
// Extract content from the original comment body
|
// Extract content from the original comment body
|
||||||
@@ -179,14 +177,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build the new body with blank line between header and separator
|
// Build the new body with blank line between header and separator
|
||||||
let newBody = `${header}${links}`;
|
let newBody = `${header}${links}\n\n---\n`;
|
||||||
|
|
||||||
// Add error details if available
|
|
||||||
if (actionFailed && errorDetails) {
|
|
||||||
newBody += `\n\n\`\`\`\n${errorDetails}\n\`\`\``;
|
|
||||||
}
|
|
||||||
|
|
||||||
newBody += `\n\n---\n`;
|
|
||||||
|
|
||||||
// Clean up the body content
|
// Clean up the body content
|
||||||
// Remove any existing View job run, branch links from the bottom
|
// Remove any existing View job run, branch links from the bottom
|
||||||
|
|||||||
@@ -39,19 +39,25 @@ async function retryWithBackoff<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error(`Operation failed after ${maxAttempts} attempts`);
|
throw new Error(
|
||||||
throw lastError;
|
`Operation failed after ${maxAttempts} attempts. Last error: ${
|
||||||
|
lastError?.message ?? "Unknown error"
|
||||||
|
}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOidcToken(): Promise<string> {
|
async function getOidcToken(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const oidcToken = await core.getIDToken("claude-code-github-action");
|
const oidcToken = await core.getIDToken("claude-code-github-action");
|
||||||
|
|
||||||
|
if (!oidcToken) {
|
||||||
|
throw new Error("OIDC token not found");
|
||||||
|
}
|
||||||
|
|
||||||
return oidcToken;
|
return oidcToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to get OIDC token:", error);
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Could not fetch an OIDC token. Did you remember to add `id-token: write` to your workflow permissions?",
|
`Failed to get OIDC token: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,15 +74,9 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const responseJson = (await response.json()) as {
|
throw new Error(
|
||||||
error?: {
|
`App token exchange failed: ${response.status} ${response.statusText}`,
|
||||||
message?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
console.error(
|
|
||||||
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
|
||||||
);
|
);
|
||||||
throw new Error(`${responseJson?.error?.message ?? "Unknown error"}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const appTokenData = (await response.json()) as {
|
const appTokenData = (await response.json()) as {
|
||||||
@@ -117,9 +117,7 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
core.setOutput("GITHUB_TOKEN", appToken);
|
core.setOutput("GITHUB_TOKEN", appToken);
|
||||||
return appToken;
|
return appToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(
|
core.setFailed(`Failed to setup GitHub token: ${error}`);
|
||||||
`Failed to setup GitHub token: ${error}.\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
|
|
||||||
);
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
export function stripInvisibleCharacters(content: string): string {
|
|
||||||
content = content.replace(/[\u200B\u200C\u200D\uFEFF]/g, "");
|
|
||||||
content = content.replace(
|
|
||||||
/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g,
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
content = content.replace(/\u00AD/g, "");
|
|
||||||
content = content.replace(/[\u202A-\u202E\u2066-\u2069]/g, "");
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripMarkdownImageAltText(content: string): string {
|
|
||||||
return content.replace(/!\[[^\]]*\]\(/g, ";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripMarkdownLinkTitles(content: string): string {
|
|
||||||
content = content.replace(/(\[[^\]]*\]\([^)]+)\s+"[^"]*"/g, "$1");
|
|
||||||
content = content.replace(/(\[[^\]]*\]\([^)]+)\s+'[^']*'/g, "$1");
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stripHiddenAttributes(content: string): string {
|
|
||||||
content = content.replace(/\salt\s*=\s*["'][^"']*["']/gi, "");
|
|
||||||
content = content.replace(/\salt\s*=\s*[^\s>]+/gi, "");
|
|
||||||
content = content.replace(/\stitle\s*=\s*["'][^"']*["']/gi, "");
|
|
||||||
content = content.replace(/\stitle\s*=\s*[^\s>]+/gi, "");
|
|
||||||
content = content.replace(/\saria-label\s*=\s*["'][^"']*["']/gi, "");
|
|
||||||
content = content.replace(/\saria-label\s*=\s*[^\s>]+/gi, "");
|
|
||||||
content = content.replace(/\sdata-[a-zA-Z0-9-]+\s*=\s*["'][^"']*["']/gi, "");
|
|
||||||
content = content.replace(/\sdata-[a-zA-Z0-9-]+\s*=\s*[^\s>]+/gi, "");
|
|
||||||
content = content.replace(/\splaceholder\s*=\s*["'][^"']*["']/gi, "");
|
|
||||||
content = content.replace(/\splaceholder\s*=\s*[^\s>]+/gi, "");
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeHtmlEntities(content: string): string {
|
|
||||||
content = content.replace(/&#(\d+);/g, (_, dec) => {
|
|
||||||
const num = parseInt(dec, 10);
|
|
||||||
if (num >= 32 && num <= 126) {
|
|
||||||
return String.fromCharCode(num);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
content = content.replace(/&#x([0-9a-fA-F]+);/g, (_, hex) => {
|
|
||||||
const num = parseInt(hex, 16);
|
|
||||||
if (num >= 32 && num <= 126) {
|
|
||||||
return String.fromCharCode(num);
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sanitizeContent(content: string): string {
|
|
||||||
content = stripHtmlComments(content);
|
|
||||||
content = stripInvisibleCharacters(content);
|
|
||||||
content = stripMarkdownImageAltText(content);
|
|
||||||
content = stripMarkdownLinkTitles(content);
|
|
||||||
content = stripHiddenAttributes(content);
|
|
||||||
content = normalizeHtmlEntities(content);
|
|
||||||
return content;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const stripHtmlComments = (content: string) =>
|
|
||||||
content.replace(/<!--[\s\S]*?-->/g, "");
|
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
// GitHub File Operations MCP Server
|
// GitHub File Operations MCP Server - Enhanced with detailed error logging
|
||||||
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { readFile } from "fs/promises";
|
import { readFile } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { GITHUB_API_URL } from "../github/api/config";
|
// Import removed - define inline to ensure subprocess gets the value
|
||||||
|
const GITHUB_API_URL = process.env.GITHUB_API_URL || "https://api.github.com";
|
||||||
|
|
||||||
type GitHubRef = {
|
type GitHubRef = {
|
||||||
object: {
|
object: {
|
||||||
@@ -51,6 +52,18 @@ const server = new McpServer({
|
|||||||
version: "0.0.1",
|
version: "0.0.1",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Enhanced error logging helper
|
||||||
|
function logDetailedError(prefix: string, error: any) {
|
||||||
|
console.error(`[${prefix}] FULL ERROR CAUGHT:`, error);
|
||||||
|
console.error(`[${prefix}] Error type:`, typeof error);
|
||||||
|
console.error(`[${prefix}] Error constructor:`, error?.constructor?.name);
|
||||||
|
console.error(`[${prefix}] Error stack:`, error instanceof Error ? error.stack : 'No stack trace');
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
console.error(`[${prefix}] Error properties:`, Object.keys(error));
|
||||||
|
console.error(`[${prefix}] Error JSON:`, JSON.stringify(error, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Commit files tool
|
// Commit files tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"commit_files",
|
"commit_files",
|
||||||
@@ -67,6 +80,19 @@ server.tool(
|
|||||||
const owner = REPO_OWNER;
|
const owner = REPO_OWNER;
|
||||||
const repo = REPO_NAME;
|
const repo = REPO_NAME;
|
||||||
const branch = BRANCH_NAME;
|
const branch = BRANCH_NAME;
|
||||||
|
console.error(`[commit_files] Starting commit for ${files.length} files to ${owner}/${repo}:${branch}`);
|
||||||
|
console.error(`[commit_files] REPO_DIR: ${REPO_DIR}`);
|
||||||
|
console.error(`[commit_files] Input files:`, files);
|
||||||
|
console.error(`[commit_files] Environment check:`, {
|
||||||
|
GITHUB_TOKEN: process.env.GITHUB_TOKEN ? 'Present' : 'Missing',
|
||||||
|
REPO_OWNER,
|
||||||
|
REPO_NAME,
|
||||||
|
BRANCH_NAME,
|
||||||
|
REPO_DIR,
|
||||||
|
GITHUB_API_URL,
|
||||||
|
CWD: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const githubToken = process.env.GITHUB_TOKEN;
|
const githubToken = process.env.GITHUB_TOKEN;
|
||||||
if (!githubToken) {
|
if (!githubToken) {
|
||||||
@@ -123,7 +149,12 @@ server.tool(
|
|||||||
? filePath
|
? filePath
|
||||||
: join(REPO_DIR, filePath);
|
: join(REPO_DIR, filePath);
|
||||||
|
|
||||||
const content = await readFile(fullPath, "utf-8");
|
console.error(`[commit_files] Reading file: ${fullPath}`);
|
||||||
|
const content = await readFile(fullPath, "utf-8").catch((error) => {
|
||||||
|
console.error(`[commit_files] Failed to read file '${fullPath}':`, error);
|
||||||
|
throw new Error(`Failed to read file '${fullPath}': ${error.message || error}`);
|
||||||
|
});
|
||||||
|
console.error(`[commit_files] Successfully read file: ${fullPath} (${content.length} chars)`);
|
||||||
return {
|
return {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
mode: "100644",
|
mode: "100644",
|
||||||
@@ -186,24 +217,158 @@ server.tool(
|
|||||||
|
|
||||||
// 6. Update the reference to point to the new commit
|
// 6. Update the reference to point to the new commit
|
||||||
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||||
const updateRefResponse = await fetch(updateRefUrl, {
|
console.error(`[commit_files] Updating reference: ${updateRefUrl}`);
|
||||||
method: "PATCH",
|
console.error(`[commit_files] New commit SHA: ${newCommitData.sha}`);
|
||||||
headers: {
|
console.error(`[commit_files] Base SHA was: ${baseSha}`);
|
||||||
Accept: "application/vnd.github+json",
|
|
||||||
Authorization: `Bearer ${githubToken}`,
|
// Log full request context before making the request
|
||||||
"X-GitHub-Api-Version": "2022-11-28",
|
const requestBody = JSON.stringify({
|
||||||
"Content-Type": "application/json",
|
sha: newCommitData.sha,
|
||||||
},
|
force: false,
|
||||||
body: JSON.stringify({
|
|
||||||
sha: newCommitData.sha,
|
|
||||||
force: false,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
const requestHeaders = {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${githubToken}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.error(`[commit_files] Full request details:`, {
|
||||||
|
url: updateRefUrl,
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
...requestHeaders,
|
||||||
|
Authorization: `Bearer [TOKEN_LENGTH:${githubToken?.length || 0}]`,
|
||||||
|
},
|
||||||
|
body: requestBody,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
environment: {
|
||||||
|
NODE_VERSION: process.version,
|
||||||
|
PLATFORM: process.platform,
|
||||||
|
ARCH: process.arch,
|
||||||
|
},
|
||||||
|
previousOperations: {
|
||||||
|
treeCreated: treeData?.sha ? 'YES' : 'NO',
|
||||||
|
commitCreated: newCommitData?.sha ? 'YES' : 'NO',
|
||||||
|
treeSha: treeData?.sha,
|
||||||
|
commitSha: newCommitData?.sha,
|
||||||
|
baseSha: baseSha,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Log memory usage before request
|
||||||
|
const memoryBefore = process.memoryUsage();
|
||||||
|
console.error(`[commit_files] Memory before request:`, {
|
||||||
|
rss: `${(memoryBefore.rss / 1024 / 1024).toFixed(2)} MB`,
|
||||||
|
heapUsed: `${(memoryBefore.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
||||||
|
});
|
||||||
|
|
||||||
|
let updateRefResponse;
|
||||||
|
const requestStartTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
updateRefResponse = await fetch(updateRefUrl, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: requestHeaders,
|
||||||
|
body: requestBody,
|
||||||
|
});
|
||||||
|
} catch (fetchError) {
|
||||||
|
const requestDuration = Date.now() - requestStartTime;
|
||||||
|
console.error(`[commit_files] FETCH ERROR during reference update after ${requestDuration}ms:`, fetchError);
|
||||||
|
logDetailedError('commit_files_fetch', fetchError);
|
||||||
|
throw new Error(`Network error during reference update after ${requestDuration}ms: ${fetchError?.message || 'Unknown fetch error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestDuration = Date.now() - requestStartTime;
|
||||||
|
console.error(`[commit_files] Request completed in ${requestDuration}ms`);
|
||||||
|
console.error(`[commit_files] Response received at: ${new Date().toISOString()}`);
|
||||||
|
|
||||||
|
console.error(`[commit_files] Update reference response status: ${updateRefResponse.status}`);
|
||||||
|
console.error(`[commit_files] Response headers:`, Object.fromEntries(updateRefResponse.headers.entries()));
|
||||||
|
|
||||||
|
// Log specific important headers
|
||||||
|
console.error(`[commit_files] Key response headers:`, {
|
||||||
|
'x-github-request-id': updateRefResponse.headers.get('x-github-request-id'),
|
||||||
|
'x-ratelimit-remaining': updateRefResponse.headers.get('x-ratelimit-remaining'),
|
||||||
|
'x-ratelimit-reset': updateRefResponse.headers.get('x-ratelimit-reset'),
|
||||||
|
'content-type': updateRefResponse.headers.get('content-type'),
|
||||||
|
'content-length': updateRefResponse.headers.get('content-length'),
|
||||||
|
'server': updateRefResponse.headers.get('server'),
|
||||||
|
});
|
||||||
|
|
||||||
if (!updateRefResponse.ok) {
|
if (!updateRefResponse.ok) {
|
||||||
const errorText = await updateRefResponse.text();
|
console.error(`[commit_files] ERROR RESPONSE - Status: ${updateRefResponse.status} ${updateRefResponse.statusText}`);
|
||||||
|
|
||||||
|
// Capture the entire raw response body
|
||||||
|
let responseArrayBuffer;
|
||||||
|
let responseText = '';
|
||||||
|
let responseHex = '';
|
||||||
|
let responseBase64 = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Clone the response so we can read it multiple ways
|
||||||
|
const clonedResponse = updateRefResponse.clone();
|
||||||
|
|
||||||
|
// Get raw bytes
|
||||||
|
responseArrayBuffer = await updateRefResponse.arrayBuffer();
|
||||||
|
const responseBytes = new Uint8Array(responseArrayBuffer);
|
||||||
|
|
||||||
|
// Convert to text (with error handling for non-UTF8)
|
||||||
|
responseText = new TextDecoder('utf-8', { fatal: false }).decode(responseBytes);
|
||||||
|
|
||||||
|
// Convert to hex for debugging binary responses
|
||||||
|
responseHex = Array.from(responseBytes.slice(0, 1000)) // First 1000 bytes to avoid huge logs
|
||||||
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
// Convert to base64
|
||||||
|
responseBase64 = Buffer.from(responseBytes).toString('base64');
|
||||||
|
|
||||||
|
console.error(`[commit_files] COMPLETE ERROR RESPONSE:`);
|
||||||
|
console.error(`[commit_files] ===== RESPONSE BODY (TEXT) =====`);
|
||||||
|
console.error(responseText);
|
||||||
|
console.error(`[commit_files] ===== END RESPONSE BODY =====`);
|
||||||
|
console.error(`[commit_files] Response body length: ${responseBytes.length} bytes`);
|
||||||
|
console.error(`[commit_files] Response body (first 1000 bytes as hex): ${responseHex}${responseBytes.length > 1000 ? '...' : ''}`);
|
||||||
|
console.error(`[commit_files] Response body (base64): ${responseBase64}`);
|
||||||
|
|
||||||
|
// Try to parse as JSON if it looks like JSON
|
||||||
|
if (responseText.trim().startsWith('{') || responseText.trim().startsWith('[')) {
|
||||||
|
try {
|
||||||
|
const parsedError = JSON.parse(responseText);
|
||||||
|
console.error(`[commit_files] Parsed error object:`, JSON.stringify(parsedError, null, 2));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[commit_files] Response looks like JSON but failed to parse:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (readError) {
|
||||||
|
console.error(`[commit_files] CRITICAL: Failed to read error response:`, readError);
|
||||||
|
logDetailedError('commit_files_response_read', readError);
|
||||||
|
responseText = `Failed to read response: ${readError}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log memory state after error
|
||||||
|
const memoryAfter = process.memoryUsage();
|
||||||
|
console.error(`[commit_files] Memory after error:`, {
|
||||||
|
rss: `${(memoryAfter.rss / 1024 / 1024).toFixed(2)} MB`,
|
||||||
|
heapUsed: `${(memoryAfter.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Special handling for 500 errors
|
||||||
|
if (updateRefResponse.status === 500) {
|
||||||
|
const requestId = updateRefResponse.headers.get('x-github-request-id');
|
||||||
|
console.error(`[commit_files] ===== GITHUB 500 ERROR DETAILS =====`);
|
||||||
|
console.error(`[commit_files] GitHub Request ID: ${requestId}`);
|
||||||
|
console.error(`[commit_files] This is an internal GitHub server error`);
|
||||||
|
console.error(`[commit_files] The error may be transient - consider retrying`);
|
||||||
|
console.error(`[commit_files] Note: Tree (${treeData?.sha}) and commit (${newCommitData?.sha}) were created successfully`);
|
||||||
|
console.error(`[commit_files] Only the reference update failed`);
|
||||||
|
console.error(`[commit_files] ===================================`);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
|
`Failed to update reference: ${updateRefResponse.status} - ${responseText || 'No response body'}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,18 +394,21 @@ server.tool(
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logDetailedError('commit_files', error);
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
return {
|
console.error(`[commit_files] Final error message being thrown: "${errorMessage}"`);
|
||||||
content: [
|
|
||||||
{
|
// Ensure we're throwing a proper Error object with a message
|
||||||
type: "text",
|
if (!errorMessage || errorMessage === 'undefined' || errorMessage === '[object Object]') {
|
||||||
text: `Error: ${errorMessage}`,
|
console.error(`[commit_files] WARNING: Error message is undefined or object, using fallback`);
|
||||||
},
|
const fallbackMessage = error instanceof Error && error.stack
|
||||||
],
|
? `Failed to commit files: ${error.stack.split('\n')[0]}`
|
||||||
error: errorMessage,
|
: 'Failed to commit files: Unknown error occurred';
|
||||||
isError: true,
|
throw new Error(fallbackMessage);
|
||||||
};
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -380,22 +548,71 @@ server.tool(
|
|||||||
|
|
||||||
// 6. Update the reference to point to the new commit
|
// 6. Update the reference to point to the new commit
|
||||||
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
|
||||||
const updateRefResponse = await fetch(updateRefUrl, {
|
console.error(`[delete_files] Updating reference: ${updateRefUrl}`);
|
||||||
method: "PATCH",
|
console.error(`[delete_files] New commit SHA: ${newCommitData.sha}`);
|
||||||
headers: {
|
console.error(`[delete_files] Base SHA was: ${baseSha}`);
|
||||||
Accept: "application/vnd.github+json",
|
console.error(`[delete_files] Request body:`, JSON.stringify({
|
||||||
Authorization: `Bearer ${githubToken}`,
|
sha: newCommitData.sha,
|
||||||
"X-GitHub-Api-Version": "2022-11-28",
|
force: false,
|
||||||
"Content-Type": "application/json",
|
}));
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
let updateRefResponse;
|
||||||
sha: newCommitData.sha,
|
try {
|
||||||
force: false,
|
updateRefResponse = await fetch(updateRefUrl, {
|
||||||
}),
|
method: "PATCH",
|
||||||
});
|
headers: {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${githubToken}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
sha: newCommitData.sha,
|
||||||
|
force: false,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.error(`[delete_files] FETCH ERROR during reference update:`, fetchError);
|
||||||
|
logDetailedError('delete_files_fetch', fetchError);
|
||||||
|
throw new Error(`Network error during reference update: ${fetchError?.message || 'Unknown fetch error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[delete_files] Update reference response status: ${updateRefResponse.status}`);
|
||||||
|
console.error(`[delete_files] Response headers:`, Object.fromEntries(updateRefResponse.headers.entries()));
|
||||||
|
|
||||||
if (!updateRefResponse.ok) {
|
if (!updateRefResponse.ok) {
|
||||||
const errorText = await updateRefResponse.text();
|
let errorText;
|
||||||
|
try {
|
||||||
|
errorText = await updateRefResponse.text();
|
||||||
|
} catch (textError) {
|
||||||
|
console.error(`[delete_files] Failed to read error response text:`, textError);
|
||||||
|
errorText = 'Unable to read error response';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[delete_files] Update reference error body: "${errorText}"`);
|
||||||
|
console.error(`[delete_files] Error body length: ${errorText?.length}`);
|
||||||
|
console.error(`[delete_files] Error body type: ${typeof errorText}`);
|
||||||
|
|
||||||
|
// Log additional debugging info for 500 errors
|
||||||
|
if (updateRefResponse.status === 500) {
|
||||||
|
const requestId = updateRefResponse.headers.get('x-github-request-id');
|
||||||
|
console.error(`[delete_files] GitHub Request ID: ${requestId}`);
|
||||||
|
console.error(`[delete_files] This appears to be an internal GitHub error`);
|
||||||
|
console.error(`[delete_files] Token was valid for tree/commit creation but failed for ref update`);
|
||||||
|
console.error(`[delete_files] Branch protection rules or permissions might be an issue`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse error if it's JSON
|
||||||
|
let parsedError;
|
||||||
|
try {
|
||||||
|
if (errorText && errorText.trim().startsWith('{')) {
|
||||||
|
parsedError = JSON.parse(errorText);
|
||||||
|
console.error(`[delete_files] Parsed error:`, parsedError);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[delete_files] Error text is not JSON`);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
|
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
|
||||||
);
|
);
|
||||||
@@ -423,18 +640,21 @@ server.tool(
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
logDetailedError('delete_files', error);
|
||||||
|
|
||||||
const errorMessage =
|
const errorMessage =
|
||||||
error instanceof Error ? error.message : String(error);
|
error instanceof Error ? error.message : String(error);
|
||||||
return {
|
console.error(`[delete_files] Final error message being thrown: "${errorMessage}"`);
|
||||||
content: [
|
|
||||||
{
|
// Ensure we're throwing a proper Error object with a message
|
||||||
type: "text",
|
if (!errorMessage || errorMessage === 'undefined' || errorMessage === '[object Object]') {
|
||||||
text: `Error: ${errorMessage}`,
|
console.error(`[delete_files] WARNING: Error message is undefined or object, using fallback`);
|
||||||
},
|
const fallbackMessage = error instanceof Error && error.stack
|
||||||
],
|
? `Failed to delete files: ${error.stack.split('\n')[0]}`
|
||||||
error: errorMessage,
|
: 'Failed to delete files: Unknown error occurred';
|
||||||
isError: true,
|
throw new Error(fallbackMessage);
|
||||||
};
|
}
|
||||||
|
throw new Error(errorMessage);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -447,4 +667,4 @@ async function runServer() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
runServer().catch(console.error);
|
runServer().catch(console.error);
|
||||||
@@ -35,6 +35,7 @@ export async function prepareMcpConfig(
|
|||||||
REPO_NAME: repo,
|
REPO_NAME: repo,
|
||||||
BRANCH_NAME: branch,
|
BRANCH_NAME: branch,
|
||||||
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
||||||
|
GITHUB_API_URL: process.env.GITHUB_API_URL || "https://api.github.com",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
261
test-with-new-branch.ts
Normal file
261
test-with-new-branch.ts
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
// Test script that creates a new branch to test the commit_files flow
|
||||||
|
// Run with: bun test-with-new-branch.ts <github-pat> <owner> <repo>
|
||||||
|
|
||||||
|
const GITHUB_API_URL = "https://api.github.com";
|
||||||
|
|
||||||
|
async function testCommitFilesWithNewBranch(token: string, owner: string, repo: string) {
|
||||||
|
const headers = {
|
||||||
|
Accept: "application/vnd.github+json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create a unique branch name for testing
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const testBranch = `claude-debug-500-test-${timestamp}`;
|
||||||
|
|
||||||
|
console.log(`\n=== Testing commit_files flow ===`);
|
||||||
|
console.log(`Repository: ${owner}/${repo}`);
|
||||||
|
console.log(`Test branch: ${testBranch}\n`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, get the default branch to branch from
|
||||||
|
console.log("Getting repository info...");
|
||||||
|
const repoResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!repoResponse.ok) {
|
||||||
|
console.error(`Cannot access repository: ${repoResponse.status}`);
|
||||||
|
const error = await repoResponse.text();
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const repoData = await repoResponse.json();
|
||||||
|
const defaultBranch = repoData.default_branch;
|
||||||
|
console.log(`✓ Default branch: ${defaultBranch}`);
|
||||||
|
|
||||||
|
// Get the SHA of the default branch
|
||||||
|
console.log(`\nGetting ${defaultBranch} branch SHA...`);
|
||||||
|
const defaultBranchResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${defaultBranch}`,
|
||||||
|
{ headers }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!defaultBranchResponse.ok) {
|
||||||
|
console.error(`Cannot get default branch: ${defaultBranchResponse.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultBranchData = await defaultBranchResponse.json();
|
||||||
|
const baseSha = defaultBranchData.object.sha;
|
||||||
|
console.log(`✓ Base SHA: ${baseSha}`);
|
||||||
|
|
||||||
|
// Create a new branch
|
||||||
|
console.log(`\nCreating test branch: ${testBranch}...`);
|
||||||
|
const createBranchResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
ref: `refs/heads/${testBranch}`,
|
||||||
|
sha: baseSha,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!createBranchResponse.ok) {
|
||||||
|
console.error(`Failed to create branch: ${createBranchResponse.status}`);
|
||||||
|
const error = await createBranchResponse.text();
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`✓ Created test branch: ${testBranch}`);
|
||||||
|
|
||||||
|
// Now replicate the commit_files flow
|
||||||
|
console.log("\n--- Starting commit_files flow ---");
|
||||||
|
|
||||||
|
// Step 1: Get the branch reference (should be same as baseSha)
|
||||||
|
console.log("\nStep 1: Getting branch reference...");
|
||||||
|
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${testBranch}`;
|
||||||
|
const refResponse = await fetch(refUrl, { headers });
|
||||||
|
|
||||||
|
if (!refResponse.ok) {
|
||||||
|
console.error(`Failed: ${refResponse.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refData = await refResponse.json();
|
||||||
|
console.log(`✓ Branch SHA: ${refData.object.sha}`);
|
||||||
|
|
||||||
|
// Step 2: Get the base commit
|
||||||
|
console.log("\nStep 2: Getting base commit...");
|
||||||
|
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
|
||||||
|
const commitResponse = await fetch(commitUrl, { headers });
|
||||||
|
|
||||||
|
if (!commitResponse.ok) {
|
||||||
|
console.error(`Failed: ${commitResponse.status}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitData = await commitResponse.json();
|
||||||
|
const baseTreeSha = commitData.tree.sha;
|
||||||
|
console.log(`✓ Base tree SHA: ${baseTreeSha}`);
|
||||||
|
|
||||||
|
// Step 3: Create a new tree
|
||||||
|
console.log("\nStep 3: Creating new tree...");
|
||||||
|
const treeUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees`;
|
||||||
|
|
||||||
|
const testFileContent = `# Test file for debugging 500 error
|
||||||
|
# Created at: ${new Date().toISOString()}
|
||||||
|
# This simulates the commit_files operation from claude-code-action
|
||||||
|
|
||||||
|
def test_function():
|
||||||
|
# This simulates fixing a code issue
|
||||||
|
result = "Fixed code"
|
||||||
|
return result
|
||||||
|
`;
|
||||||
|
|
||||||
|
const treeBody = {
|
||||||
|
base_tree: baseTreeSha,
|
||||||
|
tree: [{
|
||||||
|
path: "test-debug-500.py",
|
||||||
|
mode: "100644",
|
||||||
|
type: "blob",
|
||||||
|
content: testFileContent,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
|
||||||
|
const treeResponse = await fetch(treeUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(treeBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!treeResponse.ok) {
|
||||||
|
console.error(`Failed to create tree: ${treeResponse.status}`);
|
||||||
|
const error = await treeResponse.text();
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const treeData = await treeResponse.json();
|
||||||
|
console.log(`✓ Tree SHA: ${treeData.sha}`);
|
||||||
|
|
||||||
|
// Step 4: Create commit
|
||||||
|
console.log("\nStep 4: Creating commit...");
|
||||||
|
const newCommitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits`;
|
||||||
|
const commitBody = {
|
||||||
|
message: "Test: Debugging 500 error in commit_files",
|
||||||
|
tree: treeData.sha,
|
||||||
|
parents: [baseSha],
|
||||||
|
};
|
||||||
|
|
||||||
|
const newCommitResponse = await fetch(newCommitUrl, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(commitBody),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!newCommitResponse.ok) {
|
||||||
|
console.error(`Failed to create commit: ${newCommitResponse.status}`);
|
||||||
|
const error = await newCommitResponse.text();
|
||||||
|
console.error(error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCommitData = await newCommitResponse.json();
|
||||||
|
console.log(`✓ Commit SHA: ${newCommitData.sha}`);
|
||||||
|
|
||||||
|
// Step 5: Update reference (this is where the 500 error happens)
|
||||||
|
console.log("\nStep 5: Updating branch reference (the critical step)...");
|
||||||
|
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${testBranch}`;
|
||||||
|
const updateBody = {
|
||||||
|
sha: newCommitData.sha,
|
||||||
|
force: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`URL: PATCH ${updateRefUrl}`);
|
||||||
|
console.log(`Body: ${JSON.stringify(updateBody)}`);
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
const updateRefResponse = await fetch(updateRefUrl, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updateBody),
|
||||||
|
});
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
console.log(`\nStatus: ${updateRefResponse.status} (took ${duration}ms)`);
|
||||||
|
console.log(`Headers:`, {
|
||||||
|
'x-ratelimit-remaining': updateRefResponse.headers.get('x-ratelimit-remaining'),
|
||||||
|
'x-github-request-id': updateRefResponse.headers.get('x-github-request-id'),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!updateRefResponse.ok) {
|
||||||
|
console.error(`\n✗ FAILED: ${updateRefResponse.status}`);
|
||||||
|
const errorText = await updateRefResponse.text();
|
||||||
|
console.error(`Error body: "${errorText}"`);
|
||||||
|
|
||||||
|
if (updateRefResponse.status === 500) {
|
||||||
|
console.error(`\n🔍 500 ERROR REPRODUCED!`);
|
||||||
|
console.error(`This confirms the issue exists with PAT as well.`);
|
||||||
|
console.error(`GitHub Request ID: ${updateRefResponse.headers.get('x-github-request-id')}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log(`\n✓ SUCCESS: Branch updated!`);
|
||||||
|
console.log(`The 500 error might be specific to certain conditions.`);
|
||||||
|
|
||||||
|
// Cleanup: delete the test branch
|
||||||
|
console.log(`\nCleaning up test branch...`);
|
||||||
|
const deleteResponse = await fetch(
|
||||||
|
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${testBranch}`,
|
||||||
|
{
|
||||||
|
method: "DELETE",
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (deleteResponse.ok) {
|
||||||
|
console.log(`✓ Test branch deleted`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`\nUnexpected error:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main execution
|
||||||
|
const [token, owner, repo] = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (!token || !owner || !repo) {
|
||||||
|
console.log("Usage: bun test-with-new-branch.ts <github-pat> <owner> <repo>");
|
||||||
|
console.log("");
|
||||||
|
console.log("Examples:");
|
||||||
|
console.log(" bun test-with-new-branch.ts ghp_xxx myorg myrepo");
|
||||||
|
console.log(" bun test-with-new-branch.ts ghp_xxx anthropics anthropic");
|
||||||
|
console.log("");
|
||||||
|
console.log("This creates a test branch and replicates the commit_files flow.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Starting test with new branch...");
|
||||||
|
testCommitFilesWithNewBranch(token, owner, repo);
|
||||||
@@ -39,25 +39,6 @@ describe("updateCommentBody", () => {
|
|||||||
expect(result).toContain("**Claude encountered an error after 45s**");
|
expect(result).toContain("**Claude encountered an error after 45s**");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes error details when provided", () => {
|
|
||||||
const input = {
|
|
||||||
...baseInput,
|
|
||||||
currentBody: "Claude Code is working...",
|
|
||||||
actionFailed: true,
|
|
||||||
executionDetails: { duration_ms: 45000 },
|
|
||||||
errorDetails: "Failed to fetch issue data",
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = updateCommentBody(input);
|
|
||||||
expect(result).toContain("**Claude encountered an error after 45s**");
|
|
||||||
expect(result).toContain("[View job]");
|
|
||||||
expect(result).toContain("```\nFailed to fetch issue data\n```");
|
|
||||||
// Ensure error details come after the header/links
|
|
||||||
const errorIndex = result.indexOf("```");
|
|
||||||
const headerIndex = result.indexOf("**Claude encountered an error");
|
|
||||||
expect(errorIndex).toBeGreaterThan(headerIndex);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("handles username extraction from content when not provided", () => {
|
it("handles username extraction from content when not provided", () => {
|
||||||
const input = {
|
const input = {
|
||||||
...baseInput,
|
...baseInput,
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ describe("generatePrompt", () => {
|
|||||||
eventName: "issue_comment",
|
eventName: "issue_comment",
|
||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||||
issueNumber: "67890",
|
issueNumber: "67890",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
@@ -183,7 +183,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "789",
|
issueNumber: "789",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-789-20240101_120000",
|
claudeBranch: "claude/issue-789-20240101_120000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -210,7 +210,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "assigned",
|
eventAction: "assigned",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "999",
|
issueNumber: "999",
|
||||||
baseBranch: "develop",
|
defaultBranch: "develop",
|
||||||
claudeBranch: "claude/issue-999-20240101_120000",
|
claudeBranch: "claude/issue-999-20240101_120000",
|
||||||
assigneeTrigger: "claude-bot",
|
assigneeTrigger: "claude-bot",
|
||||||
},
|
},
|
||||||
@@ -238,7 +238,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "789",
|
issueNumber: "789",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-789-20240101_120000",
|
claudeBranch: "claude/issue-789-20240101_120000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -285,7 +285,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "123",
|
issueNumber: "123",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
},
|
},
|
||||||
@@ -307,7 +307,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "123",
|
issueNumber: "123",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-67890-20240101_120000",
|
claudeBranch: "claude/issue-67890-20240101_120000",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
},
|
},
|
||||||
@@ -362,7 +362,7 @@ describe("generatePrompt", () => {
|
|||||||
eventAction: "opened",
|
eventAction: "opened",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "789",
|
issueNumber: "789",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-789-20240101_120000",
|
claudeBranch: "claude/issue-789-20240101_120000",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -400,7 +400,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "67890",
|
commentId: "67890",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "123",
|
issueNumber: "123",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-123-20240101_120000",
|
claudeBranch: "claude/issue-123-20240101_120000",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
},
|
},
|
||||||
@@ -432,7 +432,7 @@ describe("generatePrompt", () => {
|
|||||||
prNumber: "456",
|
prNumber: "456",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
claudeBranch: "claude/pr-456-20240101_120000",
|
claudeBranch: "claude/pr-456-20240101_120000",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -470,7 +470,7 @@ describe("generatePrompt", () => {
|
|||||||
isPR: true,
|
isPR: true,
|
||||||
prNumber: "456",
|
prNumber: "456",
|
||||||
commentBody: "@claude please fix this",
|
commentBody: "@claude please fix this",
|
||||||
// No claudeBranch or baseBranch for open PRs
|
// No claudeBranch or defaultBranch for open PRs
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -503,7 +503,7 @@ describe("generatePrompt", () => {
|
|||||||
prNumber: "789",
|
prNumber: "789",
|
||||||
commentBody: "@claude please update this",
|
commentBody: "@claude please update this",
|
||||||
claudeBranch: "claude/pr-789-20240101_123000",
|
claudeBranch: "claude/pr-789-20240101_123000",
|
||||||
baseBranch: "develop",
|
defaultBranch: "develop",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -531,7 +531,7 @@ describe("generatePrompt", () => {
|
|||||||
commentId: "review-comment-123",
|
commentId: "review-comment-123",
|
||||||
commentBody: "@claude fix this issue",
|
commentBody: "@claude fix this issue",
|
||||||
claudeBranch: "claude/pr-999-20240101_140000",
|
claudeBranch: "claude/pr-999-20240101_140000",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -559,7 +559,7 @@ describe("generatePrompt", () => {
|
|||||||
isPR: true,
|
isPR: true,
|
||||||
prNumber: "555",
|
prNumber: "555",
|
||||||
claudeBranch: "claude/pr-555-20240101_150000",
|
claudeBranch: "claude/pr-555-20240101_150000",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -604,7 +604,7 @@ describe("getEventTypeAndContext", () => {
|
|||||||
eventAction: "assigned",
|
eventAction: "assigned",
|
||||||
isPR: false,
|
isPR: false,
|
||||||
issueNumber: "999",
|
issueNumber: "999",
|
||||||
baseBranch: "main",
|
defaultBranch: "main",
|
||||||
claudeBranch: "claude/issue-999-20240101_120000",
|
claudeBranch: "claude/issue-999-20240101_120000",
|
||||||
assigneeTrigger: "claude-bot",
|
assigneeTrigger: "claude-bot",
|
||||||
},
|
},
|
||||||
@@ -722,51 +722,4 @@ describe("buildDisallowedToolsString", () => {
|
|||||||
expect(parts).toContain("BadTool1");
|
expect(parts).toContain("BadTool1");
|
||||||
expect(parts).toContain("BadTool2");
|
expect(parts).toContain("BadTool2");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
|
|
||||||
const customDisallowedTools = "BadTool1,BadTool2";
|
|
||||||
const allowedTools = "WebSearch,SomeOtherTool";
|
|
||||||
const result = buildDisallowedToolsString(
|
|
||||||
customDisallowedTools,
|
|
||||||
allowedTools,
|
|
||||||
);
|
|
||||||
|
|
||||||
// WebSearch should be removed from disallowed since it's in allowed
|
|
||||||
expect(result).not.toContain("WebSearch");
|
|
||||||
|
|
||||||
// WebFetch should still be disallowed since it's not in allowed
|
|
||||||
expect(result).toContain("WebFetch");
|
|
||||||
|
|
||||||
// Custom disallowed tools should still be present
|
|
||||||
expect(result).toContain("BadTool1");
|
|
||||||
expect(result).toContain("BadTool2");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
|
|
||||||
const allowedTools = "WebSearch,WebFetch,SomeOtherTool";
|
|
||||||
const result = buildDisallowedToolsString(undefined, allowedTools);
|
|
||||||
|
|
||||||
// Both hardcoded disallowed tools should be removed
|
|
||||||
expect(result).not.toContain("WebSearch");
|
|
||||||
expect(result).not.toContain("WebFetch");
|
|
||||||
|
|
||||||
// Result should be empty since no custom disallowed tools provided
|
|
||||||
expect(result).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
|
|
||||||
const customDisallowedTools = "BadTool1,BadTool2";
|
|
||||||
const allowedTools = "WebSearch,WebFetch";
|
|
||||||
const result = buildDisallowedToolsString(
|
|
||||||
customDisallowedTools,
|
|
||||||
allowedTools,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Hardcoded tools should be removed
|
|
||||||
expect(result).not.toContain("WebSearch");
|
|
||||||
expect(result).not.toContain("WebFetch");
|
|
||||||
|
|
||||||
// Only custom disallowed tools should remain
|
|
||||||
expect(result).toBe("BadTool1,BadTool2");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
formatReviewComments,
|
formatReviewComments,
|
||||||
formatChangedFiles,
|
formatChangedFiles,
|
||||||
formatChangedFilesWithSHA,
|
formatChangedFilesWithSHA,
|
||||||
|
stripHtmlComments,
|
||||||
} from "../src/github/data/formatter";
|
} from "../src/github/data/formatter";
|
||||||
import type {
|
import type {
|
||||||
GitHubPullRequest,
|
GitHubPullRequest,
|
||||||
@@ -98,9 +99,9 @@ Some more text.`;
|
|||||||
|
|
||||||
const result = formatBody(body, imageUrlMap);
|
const result = formatBody(body, imageUrlMap);
|
||||||
expect(result)
|
expect(result)
|
||||||
.toBe(`Here is some text with an image: 
|
.toBe(`Here is some text with an image: 
|
||||||
|
|
||||||
And another one: 
|
And another one: 
|
||||||
|
|
||||||
Some more text.`);
|
Some more text.`);
|
||||||
});
|
});
|
||||||
@@ -123,7 +124,7 @@ Some more text.`);
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const result = formatBody(body, imageUrlMap);
|
const result = formatBody(body, imageUrlMap);
|
||||||
expect(result).toBe("");
|
expect(result).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles multiple occurrences of same image", () => {
|
test("handles multiple occurrences of same image", () => {
|
||||||
@@ -138,8 +139,8 @@ Second: `;
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const result = formatBody(body, imageUrlMap);
|
const result = formatBody(body, imageUrlMap);
|
||||||
expect(result).toBe(`First: 
|
expect(result).toBe(`First: 
|
||||||
Second: `);
|
Second: `);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,7 +205,7 @@ describe("formatComments", () => {
|
|||||||
|
|
||||||
const result = formatComments(comments, imageUrlMap);
|
const result = formatComments(comments, imageUrlMap);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[user1 at 2023-01-01T00:00:00Z]: Check out this screenshot: \n\n[user2 at 2023-01-02T00:00:00Z]: Here's another image: `,
|
`[user1 at 2023-01-01T00:00:00Z]: Check out this screenshot: \n\n[user2 at 2023-01-02T00:00:00Z]: Here's another image: `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -232,7 +233,7 @@ describe("formatComments", () => {
|
|||||||
|
|
||||||
const result = formatComments(comments, imageUrlMap);
|
const result = formatComments(comments, imageUrlMap);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[user1 at 2023-01-01T00:00:00Z]: Two images:  and `,
|
`[user1 at 2023-01-01T00:00:00Z]: Two images:  and `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -249,7 +250,7 @@ describe("formatComments", () => {
|
|||||||
|
|
||||||
const result = formatComments(comments);
|
const result = formatComments(comments);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[user1 at 2023-01-01T00:00:00Z]: Image: `,
|
`[user1 at 2023-01-01T00:00:00Z]: Image: `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -293,7 +294,7 @@ describe("formatReviewComments", () => {
|
|||||||
|
|
||||||
const result = formatReviewComments(reviewData);
|
const result = formatReviewComments(reviewData);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nThis is a great PR! LGTM.\n [Comment on src/index.ts:42]: Nice implementation\n [Comment on src/utils.ts:?]: Consider adding error handling`,
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Nice implementation\n [Comment on src/utils.ts:?]: Consider adding error handling`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -316,7 +317,7 @@ describe("formatReviewComments", () => {
|
|||||||
|
|
||||||
const result = formatReviewComments(reviewData);
|
const result = formatReviewComments(reviewData);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nLooks good to me!`,
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -383,7 +384,7 @@ describe("formatReviewComments", () => {
|
|||||||
|
|
||||||
const result = formatReviewComments(reviewData);
|
const result = formatReviewComments(reviewData);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: CHANGES_REQUESTED\nNeeds changes\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: APPROVED\nLGTM`,
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: CHANGES_REQUESTED\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: APPROVED`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -437,7 +438,7 @@ describe("formatReviewComments", () => {
|
|||||||
|
|
||||||
const result = formatReviewComments(reviewData, imageUrlMap);
|
const result = formatReviewComments(reviewData, imageUrlMap);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview with image: \n [Comment on src/index.ts:42]: Comment with image: `,
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Comment with image: `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -481,7 +482,7 @@ describe("formatReviewComments", () => {
|
|||||||
|
|
||||||
const result = formatReviewComments(reviewData, imageUrlMap);
|
const result = formatReviewComments(reviewData, imageUrlMap);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nGood work\n [Comment on src/main.ts:15]: Two issues:  and `,
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/main.ts:15]: Two issues:  and `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -514,7 +515,7 @@ describe("formatReviewComments", () => {
|
|||||||
|
|
||||||
const result = formatReviewComments(reviewData);
|
const result = formatReviewComments(reviewData);
|
||||||
expect(result).toBe(
|
expect(result).toBe(
|
||||||
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: `,
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Image: `,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -578,3 +579,150 @@ describe("formatChangedFilesWithSHA", () => {
|
|||||||
expect(result).toBe("");
|
expect(result).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("stripHtmlComments", () => {
|
||||||
|
test("strips simple HTML comments", () => {
|
||||||
|
const text = "Hello <!-- hidden comment --> world";
|
||||||
|
expect(stripHtmlComments(text)).toBe("Hello world");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips multiple HTML comments", () => {
|
||||||
|
const text = "Start <!-- first --> middle <!-- second --> end";
|
||||||
|
expect(stripHtmlComments(text)).toBe("Start middle end");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips multi-line HTML comments", () => {
|
||||||
|
const text = `Line 1
|
||||||
|
<!-- This is a
|
||||||
|
multi-line
|
||||||
|
comment -->
|
||||||
|
Line 2`;
|
||||||
|
expect(stripHtmlComments(text)).toBe(`Line 1
|
||||||
|
|
||||||
|
Line 2`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips nested comment-like content", () => {
|
||||||
|
const text = "Text <!-- outer <!-- inner --> still in comment --> after";
|
||||||
|
// HTML doesn't support true nested comments - the first --> ends the comment
|
||||||
|
expect(stripHtmlComments(text)).toBe("Text still in comment --> after");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles empty string", () => {
|
||||||
|
expect(stripHtmlComments("")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles text without comments", () => {
|
||||||
|
const text = "No comments here!";
|
||||||
|
expect(stripHtmlComments(text)).toBe("No comments here!");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips complex hidden content with XML tags", () => {
|
||||||
|
const text = `Normal request
|
||||||
|
<!-- </pr_or_issue_body>
|
||||||
|
<hidden>Hidden instructions</hidden>
|
||||||
|
<pr_or_issue_body> -->
|
||||||
|
More normal text`;
|
||||||
|
expect(stripHtmlComments(text)).toBe(`Normal request
|
||||||
|
|
||||||
|
More normal text`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles malformed comments - no closing", () => {
|
||||||
|
const text = "Text <!-- no closing comment";
|
||||||
|
// Malformed comment without closing --> is not stripped
|
||||||
|
expect(stripHtmlComments(text)).toBe("Text <!-- no closing comment");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles malformed comments - no opening", () => {
|
||||||
|
const text = "Text missing opening --> comment";
|
||||||
|
// Just --> without opening <!-- is not a comment
|
||||||
|
expect(stripHtmlComments(text)).toBe("Text missing opening --> comment");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("preserves legitimate HTML-like content outside comments", () => {
|
||||||
|
const text = "Use <!-- comment --> the <div> tag and </div> closing tag";
|
||||||
|
expect(stripHtmlComments(text)).toBe(
|
||||||
|
"Use the <div> tag and </div> closing tag",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatBody with HTML comment stripping", () => {
|
||||||
|
test("strips HTML comments from body", () => {
|
||||||
|
const body = "Issue description <!-- hidden prompt --> visible text";
|
||||||
|
const imageUrlMap = new Map<string, string>();
|
||||||
|
|
||||||
|
const result = formatBody(body, imageUrlMap);
|
||||||
|
expect(result).toBe("Issue description visible text");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("strips HTML comments and replaces images", () => {
|
||||||
|
const body = `Check this <!-- hidden --> `;
|
||||||
|
const imageUrlMap = new Map([
|
||||||
|
[
|
||||||
|
"https://github.com/user-attachments/assets/test.png",
|
||||||
|
"/tmp/github-images/image-1234-0.png",
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
const result = formatBody(body, imageUrlMap);
|
||||||
|
expect(result).toBe(
|
||||||
|
"Check this ",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatComments with HTML comment stripping", () => {
|
||||||
|
test("strips HTML comments from comment bodies", () => {
|
||||||
|
const comments: GitHubComment[] = [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
databaseId: "100001",
|
||||||
|
body: "Good work <!-- inject prompt --> on this PR",
|
||||||
|
author: { login: "user1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = formatComments(comments);
|
||||||
|
expect(result).toBe(
|
||||||
|
"[user1 at 2023-01-01T00:00:00Z]: Good work on this PR",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("formatReviewComments with HTML comment stripping", () => {
|
||||||
|
test("strips HTML comments from review comment bodies", () => {
|
||||||
|
const reviewData = {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "review1",
|
||||||
|
databaseId: "300001",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
body: "LGTM",
|
||||||
|
state: "APPROVED",
|
||||||
|
submittedAt: "2023-01-01T00:00:00Z",
|
||||||
|
comments: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: "comment1",
|
||||||
|
databaseId: "200001",
|
||||||
|
body: "Nice work <!-- malicious --> here",
|
||||||
|
author: { login: "reviewer1" },
|
||||||
|
createdAt: "2023-01-01T00:00:00Z",
|
||||||
|
path: "src/index.ts",
|
||||||
|
line: 42,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = formatReviewComments(reviewData);
|
||||||
|
expect(result).toBe(
|
||||||
|
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\n [Comment on src/index.ts:42]: Nice work here`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,134 +0,0 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
|
||||||
import { formatBody, formatComments } from "../src/github/data/formatter";
|
|
||||||
import type { GitHubComment } from "../src/github/types";
|
|
||||||
|
|
||||||
describe("Sanitization Integration", () => {
|
|
||||||
it("should sanitize complete issue/PR body with various hidden content patterns", () => {
|
|
||||||
const issueBody = `
|
|
||||||
# Feature Request: Add user dashboard
|
|
||||||
|
|
||||||
## Description
|
|
||||||
We need a new dashboard for users to track their activity.
|
|
||||||
|
|
||||||
<!-- HTML comment that should be removed -->
|
|
||||||
|
|
||||||
## Technical Details
|
|
||||||
The dashboard should display:
|
|
||||||
- User statistics 
|
|
||||||
- Activity graphs <img alt="example graph description" src="graph.jpg">
|
|
||||||
- Recent actions
|
|
||||||
|
|
||||||
## Implementation Notes
|
|
||||||
See [documentation](https://docs.example.com "internal docs title") for API details.
|
|
||||||
|
|
||||||
<div data-instruction="example instruction" aria-label="dashboard label" title="hover text">
|
|
||||||
The implementation should follow our standard patterns.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Additional notes: Textwithsofthyphens and Hidden encoded content.
|
|
||||||
|
|
||||||
<input placeholder="search placeholder" type="text" />
|
|
||||||
|
|
||||||
Direction override test: reversed text should be normalized.`;
|
|
||||||
|
|
||||||
const imageUrlMap = new Map<string, string>();
|
|
||||||
const result = formatBody(issueBody, imageUrlMap);
|
|
||||||
|
|
||||||
// Verify hidden content is removed
|
|
||||||
expect(result).not.toContain("<!-- HTML comment");
|
|
||||||
expect(result).not.toContain("hiddentext");
|
|
||||||
expect(result).not.toContain("example graph description");
|
|
||||||
expect(result).not.toContain("internal docs title");
|
|
||||||
expect(result).not.toContain("example instruction");
|
|
||||||
expect(result).not.toContain("dashboard label");
|
|
||||||
expect(result).not.toContain("hover text");
|
|
||||||
expect(result).not.toContain("search placeholder");
|
|
||||||
expect(result).not.toContain("\u200B");
|
|
||||||
expect(result).not.toContain("\u200C");
|
|
||||||
expect(result).not.toContain("\u200D");
|
|
||||||
expect(result).not.toContain("\u00AD");
|
|
||||||
expect(result).not.toContain("\u202E");
|
|
||||||
expect(result).not.toContain("H");
|
|
||||||
|
|
||||||
// Verify legitimate content is preserved
|
|
||||||
expect(result).toContain("# Feature Request: Add user dashboard");
|
|
||||||
expect(result).toContain("## Description");
|
|
||||||
expect(result).toContain("We need a new dashboard");
|
|
||||||
expect(result).toContain("User statistics");
|
|
||||||
expect(result).toContain("");
|
|
||||||
expect(result).toContain('<img src="graph.jpg">');
|
|
||||||
expect(result).toContain("[documentation](https://docs.example.com)");
|
|
||||||
expect(result).toContain(
|
|
||||||
"The implementation should follow our standard patterns",
|
|
||||||
);
|
|
||||||
expect(result).toContain("Hidden encoded content");
|
|
||||||
expect(result).toContain('<input type="text" />');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should sanitize GitHub comments preserving discussion flow", () => {
|
|
||||||
const comments: GitHubComment[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
databaseId: "100001",
|
|
||||||
body: `Great idea! Here are my thoughts:
|
|
||||||
|
|
||||||
1. We should consider the performance impact
|
|
||||||
2. The UI mockup looks good: 
|
|
||||||
3. Check the [API docs](https://api.example.com "api reference") for rate limits
|
|
||||||
|
|
||||||
<div aria-label="comment metadata" data-comment-type="review">
|
|
||||||
This change would affect multiple systems.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
Note: Implementationshouldfollowbestpractices.`,
|
|
||||||
author: { login: "reviewer1" },
|
|
||||||
createdAt: "2023-01-01T10:00:00Z",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
databaseId: "100002",
|
|
||||||
body: `Thanks for the feedback!
|
|
||||||
|
|
||||||
<!-- Internal note: discussed with team -->
|
|
||||||
|
|
||||||
I've updated the proposal based on your suggestions.
|
|
||||||
|
|
||||||
Test note: All systems checked.
|
|
||||||
|
|
||||||
<span title="status update" data-status="approved">Ready for implementation</span>`,
|
|
||||||
author: { login: "author1" },
|
|
||||||
createdAt: "2023-01-01T12:00:00Z",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = formatComments(comments);
|
|
||||||
|
|
||||||
// Verify hidden content is removed
|
|
||||||
expect(result).not.toContain("<!-- Internal note");
|
|
||||||
expect(result).not.toContain("api reference");
|
|
||||||
expect(result).not.toContain("comment metadata");
|
|
||||||
expect(result).not.toContain('data-comment-type="review"');
|
|
||||||
expect(result).not.toContain("status update");
|
|
||||||
expect(result).not.toContain('data-status="approved"');
|
|
||||||
expect(result).not.toContain("\u200B");
|
|
||||||
expect(result).not.toContain("T");
|
|
||||||
|
|
||||||
// Verify discussion flow is preserved
|
|
||||||
expect(result).toContain("Great idea! Here are my thoughts:");
|
|
||||||
expect(result).toContain("1. We should consider the performance impact");
|
|
||||||
expect(result).toContain("2. The UI mockup looks good: ");
|
|
||||||
expect(result).toContain(
|
|
||||||
"3. Check the [API docs](https://api.example.com)",
|
|
||||||
);
|
|
||||||
expect(result).toContain("This change would affect multiple systems.");
|
|
||||||
expect(result).toContain("Implementationshouldfollowbestpractices");
|
|
||||||
expect(result).toContain("Thanks for the feedback!");
|
|
||||||
expect(result).toContain(
|
|
||||||
"I've updated the proposal based on your suggestions.",
|
|
||||||
);
|
|
||||||
expect(result).toContain("Test note: All systems checked.");
|
|
||||||
expect(result).toContain("Ready for implementation");
|
|
||||||
expect(result).toContain("[reviewer1 at");
|
|
||||||
expect(result).toContain("[author1 at");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -34,7 +34,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env = {
|
process.env = {
|
||||||
...BASE_ENV,
|
...BASE_ENV,
|
||||||
BASE_BRANCH: "main",
|
DEFAULT_BRANCH: "main",
|
||||||
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000",
|
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -62,7 +62,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
expect(result.eventData.claudeBranch).toBe(
|
expect(result.eventData.claudeBranch).toBe(
|
||||||
"claude/issue-67890-20240101_120000",
|
"claude/issue-67890-20240101_120000",
|
||||||
);
|
);
|
||||||
expect(result.eventData.baseBranch).toBe("main");
|
expect(result.eventData.defaultBranch).toBe("main");
|
||||||
expect(result.eventData.commentBody).toBe(
|
expect(result.eventData.commentBody).toBe(
|
||||||
"@claude can you help explain how to configure the logging system?",
|
"@claude can you help explain how to configure the logging system?",
|
||||||
);
|
);
|
||||||
@@ -75,7 +75,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
).toThrow("CLAUDE_BRANCH is required for issue_comment event");
|
).toThrow("CLAUDE_BRANCH is required for issue_comment event");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error when BASE_BRANCH is missing", () => {
|
test("should throw error when DEFAULT_BRANCH is missing", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
prepareContext(
|
prepareContext(
|
||||||
mockIssueCommentContext,
|
mockIssueCommentContext,
|
||||||
@@ -83,7 +83,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
"claude/issue-67890-20240101_120000",
|
"claude/issue-67890-20240101_120000",
|
||||||
),
|
),
|
||||||
).toThrow("BASE_BRANCH is required for issue_comment event");
|
).toThrow("DEFAULT_BRANCH is required for issue_comment event");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
process.env = {
|
process.env = {
|
||||||
...BASE_ENV,
|
...BASE_ENV,
|
||||||
BASE_BRANCH: "main",
|
DEFAULT_BRANCH: "main",
|
||||||
CLAUDE_BRANCH: "claude/issue-42-20240101_120000",
|
CLAUDE_BRANCH: "claude/issue-42-20240101_120000",
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -172,7 +172,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
result.eventData.eventAction === "opened"
|
result.eventData.eventAction === "opened"
|
||||||
) {
|
) {
|
||||||
expect(result.eventData.issueNumber).toBe("42");
|
expect(result.eventData.issueNumber).toBe("42");
|
||||||
expect(result.eventData.baseBranch).toBe("main");
|
expect(result.eventData.defaultBranch).toBe("main");
|
||||||
expect(result.eventData.claudeBranch).toBe(
|
expect(result.eventData.claudeBranch).toBe(
|
||||||
"claude/issue-42-20240101_120000",
|
"claude/issue-42-20240101_120000",
|
||||||
);
|
);
|
||||||
@@ -195,7 +195,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
result.eventData.eventAction === "assigned"
|
result.eventData.eventAction === "assigned"
|
||||||
) {
|
) {
|
||||||
expect(result.eventData.issueNumber).toBe("123");
|
expect(result.eventData.issueNumber).toBe("123");
|
||||||
expect(result.eventData.baseBranch).toBe("main");
|
expect(result.eventData.defaultBranch).toBe("main");
|
||||||
expect(result.eventData.claudeBranch).toBe(
|
expect(result.eventData.claudeBranch).toBe(
|
||||||
"claude/issue-123-20240101_120000",
|
"claude/issue-123-20240101_120000",
|
||||||
);
|
);
|
||||||
@@ -209,7 +209,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
).toThrow("CLAUDE_BRANCH is required for issues event");
|
).toThrow("CLAUDE_BRANCH is required for issues event");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw error when BASE_BRANCH is missing for issues", () => {
|
test("should throw error when DEFAULT_BRANCH is missing for issues", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
prepareContext(
|
prepareContext(
|
||||||
mockIssueOpenedContext,
|
mockIssueOpenedContext,
|
||||||
@@ -217,7 +217,7 @@ describe("parseEnvVarsWithContext", () => {
|
|||||||
undefined,
|
undefined,
|
||||||
"claude/issue-42-20240101_120000",
|
"claude/issue-42-20240101_120000",
|
||||||
),
|
),
|
||||||
).toThrow("BASE_BRANCH is required for issues event");
|
).toThrow("DEFAULT_BRANCH is required for issues event");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,259 +0,0 @@
|
|||||||
import { describe, expect, it } from "bun:test";
|
|
||||||
import {
|
|
||||||
stripInvisibleCharacters,
|
|
||||||
stripMarkdownImageAltText,
|
|
||||||
stripMarkdownLinkTitles,
|
|
||||||
stripHiddenAttributes,
|
|
||||||
normalizeHtmlEntities,
|
|
||||||
sanitizeContent,
|
|
||||||
stripHtmlComments,
|
|
||||||
} from "../src/github/utils/sanitizer";
|
|
||||||
|
|
||||||
describe("stripInvisibleCharacters", () => {
|
|
||||||
it("should remove zero-width characters", () => {
|
|
||||||
expect(stripInvisibleCharacters("Hello\u200BWorld")).toBe("HelloWorld");
|
|
||||||
expect(stripInvisibleCharacters("Text\u200C\u200D")).toBe("Text");
|
|
||||||
expect(stripInvisibleCharacters("\uFEFFStart")).toBe("Start");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove control characters", () => {
|
|
||||||
expect(stripInvisibleCharacters("Hello\u0000World")).toBe("HelloWorld");
|
|
||||||
expect(stripInvisibleCharacters("Text\u001F\u007F")).toBe("Text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve common whitespace", () => {
|
|
||||||
expect(stripInvisibleCharacters("Hello\nWorld")).toBe("Hello\nWorld");
|
|
||||||
expect(stripInvisibleCharacters("Tab\there")).toBe("Tab\there");
|
|
||||||
expect(stripInvisibleCharacters("Carriage\rReturn")).toBe(
|
|
||||||
"Carriage\rReturn",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove soft hyphens", () => {
|
|
||||||
expect(stripInvisibleCharacters("Soft\u00ADHyphen")).toBe("SoftHyphen");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove Unicode direction overrides", () => {
|
|
||||||
expect(stripInvisibleCharacters("Text\u202A\u202BMore")).toBe("TextMore");
|
|
||||||
expect(stripInvisibleCharacters("\u2066Isolated\u2069")).toBe("Isolated");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stripMarkdownImageAltText", () => {
|
|
||||||
it("should remove alt text from markdown images", () => {
|
|
||||||
expect(stripMarkdownImageAltText("")).toBe(
|
|
||||||
"",
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
stripMarkdownImageAltText("Text  more text"),
|
|
||||||
).toBe("Text  more text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiple images", () => {
|
|
||||||
expect(stripMarkdownImageAltText(" ")).toBe(
|
|
||||||
" ",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty alt text", () => {
|
|
||||||
expect(stripMarkdownImageAltText("")).toBe("");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stripMarkdownLinkTitles", () => {
|
|
||||||
it("should remove titles from markdown links", () => {
|
|
||||||
expect(stripMarkdownLinkTitles('[Link](url.com "example title")')).toBe(
|
|
||||||
"[Link](url.com)",
|
|
||||||
);
|
|
||||||
expect(stripMarkdownLinkTitles("[Link](url.com 'example title')")).toBe(
|
|
||||||
"[Link](url.com)",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiple links", () => {
|
|
||||||
expect(
|
|
||||||
stripMarkdownLinkTitles('[One](1.com "first") [Two](2.com "second")'),
|
|
||||||
).toBe("[One](1.com) [Two](2.com)");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve links without titles", () => {
|
|
||||||
expect(stripMarkdownLinkTitles("[Link](url.com)")).toBe("[Link](url.com)");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stripHiddenAttributes", () => {
|
|
||||||
it("should remove alt attributes", () => {
|
|
||||||
expect(
|
|
||||||
stripHiddenAttributes('<img alt="example text" src="pic.jpg">'),
|
|
||||||
).toBe('<img src="pic.jpg">');
|
|
||||||
expect(stripHiddenAttributes("<img alt='example' src=\"pic.jpg\">")).toBe(
|
|
||||||
'<img src="pic.jpg">',
|
|
||||||
);
|
|
||||||
expect(stripHiddenAttributes('<img alt=example src="pic.jpg">')).toBe(
|
|
||||||
'<img src="pic.jpg">',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove title attributes", () => {
|
|
||||||
expect(
|
|
||||||
stripHiddenAttributes('<a title="example text" href="#">Link</a>'),
|
|
||||||
).toBe('<a href="#">Link</a>');
|
|
||||||
expect(stripHiddenAttributes("<div title='example'>Content</div>")).toBe(
|
|
||||||
"<div>Content</div>",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove aria-label attributes", () => {
|
|
||||||
expect(
|
|
||||||
stripHiddenAttributes('<button aria-label="example">Click</button>'),
|
|
||||||
).toBe("<button>Click</button>");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove data-* attributes", () => {
|
|
||||||
expect(
|
|
||||||
stripHiddenAttributes(
|
|
||||||
'<div data-test="example" data-info="more example">Text</div>',
|
|
||||||
),
|
|
||||||
).toBe("<div>Text</div>");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove placeholder attributes", () => {
|
|
||||||
expect(
|
|
||||||
stripHiddenAttributes('<input placeholder="example text" type="text">'),
|
|
||||||
).toBe('<input type="text">');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiple attributes", () => {
|
|
||||||
expect(
|
|
||||||
stripHiddenAttributes(
|
|
||||||
'<img alt="example" title="test" src="pic.jpg" class="image">',
|
|
||||||
),
|
|
||||||
).toBe('<img src="pic.jpg" class="image">');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("normalizeHtmlEntities", () => {
|
|
||||||
it("should decode numeric entities", () => {
|
|
||||||
expect(normalizeHtmlEntities("Hello")).toBe(
|
|
||||||
"Hello",
|
|
||||||
);
|
|
||||||
expect(normalizeHtmlEntities("ABC")).toBe("ABC");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should decode hex entities", () => {
|
|
||||||
expect(normalizeHtmlEntities("Hello")).toBe(
|
|
||||||
"Hello",
|
|
||||||
);
|
|
||||||
expect(normalizeHtmlEntities("ABC")).toBe("ABC");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove non-printable entities", () => {
|
|
||||||
expect(normalizeHtmlEntities("�")).toBe("");
|
|
||||||
expect(normalizeHtmlEntities("�")).toBe("");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve normal text", () => {
|
|
||||||
expect(normalizeHtmlEntities("Normal text")).toBe("Normal text");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("sanitizeContent", () => {
|
|
||||||
it("should apply all sanitization measures", () => {
|
|
||||||
const testContent = `
|
|
||||||
<!-- This is a comment -->
|
|
||||||
<img alt="example alt text" src="image.jpg">
|
|
||||||

|
|
||||||
[click here](https://example.com "example title")
|
|
||||||
<div data-prompt="example data" aria-label="example label">
|
|
||||||
Normal text with hidden\u200Bcharacters
|
|
||||||
</div>
|
|
||||||
Hidden message
|
|
||||||
`;
|
|
||||||
|
|
||||||
const sanitized = sanitizeContent(testContent);
|
|
||||||
|
|
||||||
expect(sanitized).not.toContain("<!-- This is a comment -->");
|
|
||||||
expect(sanitized).not.toContain("example alt text");
|
|
||||||
expect(sanitized).not.toContain("example image description");
|
|
||||||
expect(sanitized).not.toContain("example title");
|
|
||||||
expect(sanitized).not.toContain("example data");
|
|
||||||
expect(sanitized).not.toContain("example label");
|
|
||||||
expect(sanitized).not.toContain("\u200B");
|
|
||||||
expect(sanitized).not.toContain("alt=");
|
|
||||||
expect(sanitized).not.toContain("data-prompt=");
|
|
||||||
expect(sanitized).not.toContain("aria-label=");
|
|
||||||
|
|
||||||
expect(sanitized).toContain("Normal text with hiddencharacters");
|
|
||||||
expect(sanitized).toContain("Hidden message");
|
|
||||||
expect(sanitized).toContain('<img src="image.jpg">');
|
|
||||||
expect(sanitized).toContain("");
|
|
||||||
expect(sanitized).toContain("[click here](https://example.com)");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle complex nested patterns", () => {
|
|
||||||
const complexContent = `
|
|
||||||
Text with  and more.
|
|
||||||
<a href="#" title="example\u00ADtitle">Link</a>
|
|
||||||
<div data-x="Hi">Content</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const sanitized = sanitizeContent(complexContent);
|
|
||||||
|
|
||||||
expect(sanitized).not.toContain("\u200B");
|
|
||||||
expect(sanitized).not.toContain("\u00AD");
|
|
||||||
expect(sanitized).not.toContain("alt ");
|
|
||||||
expect(sanitized).not.toContain('title="');
|
|
||||||
expect(sanitized).not.toContain('data-x="');
|
|
||||||
expect(sanitized).toContain("");
|
|
||||||
expect(sanitized).toContain('<a href="#">Link</a>');
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should preserve legitimate markdown and HTML", () => {
|
|
||||||
const legitimateContent = `
|
|
||||||
# Heading
|
|
||||||
|
|
||||||
This is **bold** and *italic* text.
|
|
||||||
|
|
||||||
Here's a normal image: 
|
|
||||||
And a normal link: [Click here](https://example.com)
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<p id="para">Normal paragraph</p>
|
|
||||||
<input type="text" name="field">
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const sanitized = sanitizeContent(legitimateContent);
|
|
||||||
|
|
||||||
expect(sanitized).toBe(legitimateContent);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle entity-encoded text", () => {
|
|
||||||
const encodedText = `
|
|
||||||
Hidden message
|
|
||||||
<div title="example">Test</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const sanitized = sanitizeContent(encodedText);
|
|
||||||
|
|
||||||
expect(sanitized).toContain("Hidden message");
|
|
||||||
expect(sanitized).not.toContain('title="');
|
|
||||||
expect(sanitized).toContain("<div>Test</div>");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("stripHtmlComments (legacy)", () => {
|
|
||||||
it("should remove HTML comments", () => {
|
|
||||||
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(
|
|
||||||
"Hello World",
|
|
||||||
);
|
|
||||||
expect(stripHtmlComments("<!-- comment -->Text")).toBe("Text");
|
|
||||||
expect(stripHtmlComments("Text<!-- comment -->")).toBe("Text");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiline comments", () => {
|
|
||||||
expect(stripHtmlComments("Hello <!-- \nexample\n -->World")).toBe(
|
|
||||||
"Hello World",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user