mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
45 Commits
test-autof
...
test-auto-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a630fad41e | ||
|
|
130874e0b6 | ||
|
|
c72a45a95f | ||
|
|
bf04905b4c | ||
|
|
52736c6e60 | ||
|
|
91034c2a0e | ||
|
|
8d32355bcc | ||
|
|
4ec65ed46e | ||
|
|
9613b21ad7 | ||
|
|
68b7ca379c | ||
|
|
900322ca88 | ||
|
|
8f0a7fe9d3 | ||
|
|
db36412854 | ||
|
|
f05d669d5f | ||
|
|
e89411bb6f | ||
|
|
02e9ed3181 | ||
|
|
78b07473f5 | ||
|
|
f562ed53e2 | ||
|
|
a1507aefdc | ||
|
|
ae66eb6a64 | ||
|
|
432c7cc889 | ||
|
|
0b138d9d49 | ||
|
|
30530c9829 | ||
|
|
632f04bbcf | ||
|
|
c34e066a3b | ||
|
|
449c6791bd | ||
|
|
2b67ac084b | ||
|
|
76de8a48fc | ||
|
|
d91030de69 | ||
|
|
ab7f1d65d6 | ||
|
|
c13c2af69f | ||
|
|
0ac14b0d37 | ||
|
|
8c230f7e04 | ||
|
|
d24561da51 | ||
|
|
e3b5697276 | ||
|
|
eb146ef8b8 | ||
|
|
73948c338f | ||
|
|
30fb4ed5a6 | ||
|
|
24433f34e3 | ||
|
|
86e2835d40 | ||
|
|
a80505bbfb | ||
|
|
55e94369f7 | ||
|
|
af23644a50 | ||
|
|
98e6a902bf | ||
|
|
8b2bd6d04f |
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: deep-thinker
|
||||
description: A subagent that performs deep analysis with extended thinking
|
||||
tools:
|
||||
- "*"
|
||||
proactive: false
|
||||
---
|
||||
|
||||
# Deep Thinker Subagent
|
||||
|
||||
You are a specialized subagent designed to perform deep, thorough analysis of complex problems using extended thinking capabilities.
|
||||
|
||||
## Your Purpose
|
||||
|
||||
You excel at:
|
||||
- Breaking down complex problems into smaller components
|
||||
- Analyzing trade-offs and implications
|
||||
- Providing comprehensive, well-reasoned solutions
|
||||
- Exploring edge cases and potential issues
|
||||
|
||||
## Instructions
|
||||
|
||||
When given a task:
|
||||
1. Use extended thinking to thoroughly analyze the problem
|
||||
2. Consider multiple approaches and their trade-offs
|
||||
3. Identify potential issues or edge cases
|
||||
4. Provide a detailed, well-structured response
|
||||
|
||||
## Important
|
||||
|
||||
Always think deeply before responding. Take your time to ensure thoroughness and accuracy in your analysis.
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
description: Fix CI failures and commit changes (for use when branch already exists)
|
||||
allowed_tools: "*"
|
||||
---
|
||||
|
||||
# Fix CI Failures and Commit
|
||||
|
||||
You are on a branch that was created to fix CI failures. Your task is to fix the issues and commit the changes.
|
||||
|
||||
## CI Failure Information
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Your Tasks
|
||||
|
||||
1. **Analyze the failures** - Understand what went wrong from the logs
|
||||
2. **Fix the issues** - Make the necessary code changes
|
||||
3. **Commit your fixes** - Use git to commit all changes
|
||||
|
||||
## Step-by-Step Instructions
|
||||
|
||||
### 1. Fix the Issues
|
||||
|
||||
Based on the error logs:
|
||||
- Fix syntax errors
|
||||
- Fix formatting issues
|
||||
- Fix test failures
|
||||
- Fix any other CI problems
|
||||
|
||||
### 2. Commit Your Changes (REQUIRED)
|
||||
|
||||
After fixing ALL issues, you MUST:
|
||||
|
||||
Use the `mcp__github_file_ops__commit_files` tool to commit all your changes with a descriptive message like:
|
||||
|
||||
```
|
||||
Fix CI failures
|
||||
|
||||
- Fixed syntax errors
|
||||
- Fixed formatting issues
|
||||
- Fixed test failures
|
||||
[List actual fixes made]
|
||||
```
|
||||
|
||||
**IMPORTANT**: You MUST use the MCP file ops tool to commit your changes. The workflow expects you to commit your changes.
|
||||
|
||||
### 3. Verify (Optional)
|
||||
|
||||
If possible, run verification commands:
|
||||
- `bun run format:check` for formatting
|
||||
- `bun test` for tests
|
||||
- `bun run typecheck` for TypeScript
|
||||
|
||||
Begin by analyzing the failure logs and then fix the issues.
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
description: Analyze and fix CI failures by examining logs and making targeted fixes
|
||||
allowed_tools: "*"
|
||||
---
|
||||
|
||||
# Fix CI Failures
|
||||
|
||||
You are tasked with analyzing CI failure logs and fixing the issues. Follow these steps:
|
||||
|
||||
## Context Provided
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Step 1: Analyze the Failure
|
||||
|
||||
Parse the provided CI failure information to understand:
|
||||
- Which jobs failed and why
|
||||
- The specific error messages and stack traces
|
||||
- Whether failures are test-related, build-related, or linting issues
|
||||
|
||||
## Step 2: Search and Understand the Codebase
|
||||
|
||||
Use search tools to locate the failing code:
|
||||
- Search for the failing test names or functions
|
||||
- Find the source files mentioned in error messages
|
||||
- Review related configuration files (package.json, tsconfig.json, etc.)
|
||||
|
||||
## Step 3: Apply Targeted Fixes
|
||||
|
||||
Make minimal, focused changes:
|
||||
- **For test failures**: Determine if the test or implementation needs fixing
|
||||
- **For type errors**: Fix type definitions or correct the code logic
|
||||
- **For linting issues**: Apply formatting using the project's tools
|
||||
- **For build errors**: Resolve dependency or configuration issues
|
||||
- **For missing imports**: Add the necessary imports or install packages
|
||||
|
||||
Requirements:
|
||||
- Only fix the actual CI failures, avoid unrelated changes
|
||||
- Follow existing code patterns and conventions
|
||||
- Ensure changes are production-ready, not temporary hacks
|
||||
- Preserve existing functionality while fixing issues
|
||||
|
||||
## Step 4: Commit Changes
|
||||
|
||||
After applying ALL fixes:
|
||||
1. Use the `mcp__github_file_ops__commit_files` tool to commit your changes
|
||||
2. Include a descriptive commit message explaining what was fixed
|
||||
3. Document which CI jobs/tests were addressed in the commit message
|
||||
4. Important: Use the MCP file ops tool, not git commands directly
|
||||
|
||||
## Step 5: Verify Fixes Locally
|
||||
|
||||
Run available verification commands:
|
||||
- Execute the failing tests locally to confirm they pass
|
||||
- Run the project's lint command (check package.json for scripts)
|
||||
- Run type checking if available
|
||||
- Execute any build commands to ensure compilation succeeds
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- Focus exclusively on fixing the reported CI failures
|
||||
- Maintain code quality and follow the project's established patterns
|
||||
- If a fix requires significant refactoring, document why it's necessary
|
||||
- When multiple solutions exist, choose the simplest one that maintains code quality
|
||||
- Add clear comments only if the fix is non-obvious
|
||||
|
||||
Begin by analyzing the failure details provided above.
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
allowed-tools: Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*), Read, Glob, Grep
|
||||
description: Code review a pull request
|
||||
---
|
||||
|
||||
Review the current pull request and provide feedback.
|
||||
|
||||
1. Use `gh pr view` to get the PR details and `gh pr diff` to see the changes
|
||||
2. Look for potential bugs, issues, or improvements
|
||||
3. Always post a comment with your findings using `gh pr comment`
|
||||
|
||||
Format your comment like this:
|
||||
|
||||
## Code Review
|
||||
|
||||
[Your feedback here - be specific and constructive]
|
||||
|
||||
- If you find issues, describe them clearly
|
||||
- If everything looks good, say so
|
||||
- Link to specific lines when relevant
|
||||
|
||||
🤖 Generated with [Claude Code](https://claude.ai/code)
|
||||
177
.github/workflows/auto-fix-ci-inline.yml
vendored
177
.github/workflows/auto-fix-ci-inline.yml
vendored
@@ -1,177 +0,0 @@
|
||||
name: Auto Fix CI Failures (Inline)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
auto-fix:
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'failure' &&
|
||||
github.event.workflow_run.name != 'Auto Fix CI Failures' &&
|
||||
github.event.workflow_run.name != 'Auto Fix CI Failures (Inline)'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name "claude[bot]"
|
||||
git config --global user.email "198276+claude[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Create fix branch
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get CI failure details
|
||||
id: failure_details
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = await github.rest.actions.getWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure');
|
||||
|
||||
let errorLogs = [];
|
||||
for (const job of failedJobs) {
|
||||
const logs = await github.rest.actions.downloadJobLogsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
job_id: job.id
|
||||
});
|
||||
errorLogs.push({
|
||||
jobName: job.name,
|
||||
logs: logs.data
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
runUrl: run.data.html_url,
|
||||
failedJobs: failedJobs.map(j => j.name),
|
||||
errorLogs: errorLogs
|
||||
};
|
||||
|
||||
- name: Fix CI failures with Claude
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
with:
|
||||
prompt: |
|
||||
You are tasked with analyzing CI failure logs and fixing the issues. Follow these steps:
|
||||
|
||||
## Context Provided
|
||||
|
||||
Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }}
|
||||
Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }}
|
||||
|
||||
Error logs:
|
||||
${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }}
|
||||
|
||||
## Step 1: Analyze the Failure
|
||||
|
||||
Parse the provided CI failure information to understand:
|
||||
- Which jobs failed and why
|
||||
- The specific error messages and stack traces
|
||||
- Whether failures are test-related, build-related, or linting issues
|
||||
|
||||
## Step 2: Search and Understand the Codebase
|
||||
|
||||
Use search tools to locate the failing code:
|
||||
- Search for the failing test names or functions
|
||||
- Find the source files mentioned in error messages
|
||||
- Review related configuration files (package.json, tsconfig.json, etc.)
|
||||
|
||||
## Step 3: Apply Targeted Fixes
|
||||
|
||||
Make minimal, focused changes:
|
||||
- **For test failures**: Determine if the test or implementation needs fixing
|
||||
- **For type errors**: Fix type definitions or correct the code logic
|
||||
- **For linting issues**: Apply formatting using the project's tools
|
||||
- **For build errors**: Resolve dependency or configuration issues
|
||||
- **For missing imports**: Add the necessary imports or install packages
|
||||
|
||||
Requirements:
|
||||
- Only fix the actual CI failures, avoid unrelated changes
|
||||
- Follow existing code patterns and conventions
|
||||
- Ensure changes are production-ready, not temporary hacks
|
||||
- Preserve existing functionality while fixing issues
|
||||
|
||||
## Step 4: Commit Changes
|
||||
|
||||
After applying ALL fixes:
|
||||
1. Stage all modified files with `git add -A`
|
||||
2. Commit with: `git commit -m "Fix CI failures: prettier formatting and syntax errors"`
|
||||
3. Important: You MUST commit your changes - the branch already exists
|
||||
|
||||
## Step 5: Verify Fixes Locally
|
||||
|
||||
Run available verification commands:
|
||||
- Execute the failing tests locally to confirm they pass
|
||||
- Run the project's lint command (check package.json for scripts)
|
||||
- Run type checking if available
|
||||
- Execute any build commands to ensure compilation succeeds
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- Focus exclusively on fixing the reported CI failures
|
||||
- Maintain code quality and follow the project's established patterns
|
||||
- If a fix requires significant refactoring, document why it's necessary
|
||||
- When multiple solutions exist, choose the simplest one that maintains code quality
|
||||
|
||||
Begin by analyzing the failure details provided above.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: "30"
|
||||
use_sticky_comment: "true"
|
||||
use_commit_signing: "true"
|
||||
allowed_tools: "Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash,mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files"
|
||||
claude_args: "--max-turns 15"
|
||||
|
||||
- name: Push fix branch
|
||||
if: success()
|
||||
run: |
|
||||
git push origin ${{ steps.branch.outputs.branch_name }}
|
||||
|
||||
- name: Create pull request comment
|
||||
if: success()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const branchName = '${{ steps.branch.outputs.branch_name }}';
|
||||
const baseBranch = '${{ github.event.workflow_run.head_branch }}';
|
||||
const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/compare/${baseBranch}...${branchName}?quick_pull=1`;
|
||||
|
||||
const issueNumber = ${{ github.event.workflow_run.pull_requests[0] && github.event.workflow_run.pull_requests[0].number || 'null' }};
|
||||
|
||||
if (issueNumber) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `## 🤖 CI Auto-Fix Available\n\nClaude has analyzed the CI failures and prepared fixes.\n\n[**→ Create pull request to fix CI**](${prUrl})\n\n_This fix was generated automatically based on the [failed CI run](${{ fromJSON(steps.failure_details.outputs.result).runUrl }})._`
|
||||
});
|
||||
}
|
||||
119
.github/workflows/auto-fix-ci.yml
vendored
119
.github/workflows/auto-fix-ci.yml
vendored
@@ -1,119 +0,0 @@
|
||||
name: Auto Fix CI Failures
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
auto-fix:
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'failure' &&
|
||||
github.event.workflow_run.name != 'Auto Fix CI Failures'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Setup git
|
||||
run: |
|
||||
git config --global user.name "claude[bot]"
|
||||
git config --global user.email "198276+claude[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Create fix branch
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Get CI failure details
|
||||
id: failure_details
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = await github.rest.actions.getWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure');
|
||||
|
||||
let errorLogs = [];
|
||||
for (const job of failedJobs) {
|
||||
const logs = await github.rest.actions.downloadJobLogsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
job_id: job.id
|
||||
});
|
||||
errorLogs.push({
|
||||
jobName: job.name,
|
||||
logs: logs.data
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
runUrl: run.data.html_url,
|
||||
failedJobs: failedJobs.map(j => j.name),
|
||||
errorLogs: errorLogs
|
||||
};
|
||||
|
||||
- name: Fix CI failures with Claude
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
with:
|
||||
prompt: |
|
||||
/fix-ci-commit Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }}
|
||||
Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }}
|
||||
|
||||
Error logs:
|
||||
${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
timeout_minutes: "30"
|
||||
use_sticky_comment: "true"
|
||||
use_commit_signing: "true"
|
||||
allowed_tools: "Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash,mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files"
|
||||
claude_args: "--max-turns 15"
|
||||
|
||||
- name: Push fix branch
|
||||
if: success()
|
||||
run: |
|
||||
git push origin ${{ steps.branch.outputs.branch_name }}
|
||||
|
||||
- name: Create pull request comment
|
||||
if: success()
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const branchName = '${{ steps.branch.outputs.branch_name }}';
|
||||
const baseBranch = '${{ github.event.workflow_run.head_branch }}';
|
||||
const prUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/compare/${baseBranch}...${branchName}?quick_pull=1`;
|
||||
|
||||
const issueNumber = ${{ github.event.workflow_run.pull_requests[0] && github.event.workflow_run.pull_requests[0].number || 'null' }};
|
||||
|
||||
if (issueNumber) {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: `## 🤖 CI Auto-Fix Available\n\nClaude has analyzed the CI failures and prepared fixes.\n\n[**→ Create pull request to fix CI**](${prUrl})\n\n_This fix was generated automatically based on the [failed CI run](${{ fromJSON(steps.failure_details.outputs.result).runUrl }})._`
|
||||
});
|
||||
}
|
||||
26
.github/workflows/claude-auto-review.yml
vendored
26
.github/workflows/claude-auto-review.yml
vendored
@@ -1,26 +0,0 @@
|
||||
name: Auto Review PRs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
auto-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Auto review PR
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: /review
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Auto review PR
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
uses: anthropics/claude-code-action@main
|
||||
with:
|
||||
prompt: |
|
||||
direct_prompt: |
|
||||
Please review this PR. Look at the changes and provide thoughtful feedback on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
@@ -30,4 +30,4 @@ jobs:
|
||||
|
||||
Be constructive and specific in your feedback. Give inline comments where applicable.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: "--allowedTools mcp__github-comment-server__create_pending_pull_request_review,mcp__github-comment-server__add_comment_to_pending_review,mcp__github-comment-server__submit_pending_pull_request_review,mcp__github-comment-server__get_pull_request_diff"
|
||||
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
||||
6
.github/workflows/claude.yml
vendored
6
.github/workflows/claude.yml
vendored
@@ -31,13 +31,9 @@ jobs:
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: janeapp/claude-code-action@main
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
|
||||
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."
|
||||
model: "claude-opus-4-1-20250805"
|
||||
# Testing PR 411 - sticky comment customization
|
||||
use_sticky_comment: true
|
||||
sticky_comment_app_bot_id: "209825114"
|
||||
sticky_comment_app_bot_name: "claude"
|
||||
|
||||
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -104,3 +104,5 @@ jobs:
|
||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||
timeout_minutes: "5"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
175
AUTO_FIX_CHECKPOINT.md
Normal file
175
AUTO_FIX_CHECKPOINT.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Auto-Fix CI Workflow Implementation Checkpoint
|
||||
|
||||
## Overview
|
||||
This document captures the learnings from implementing auto-fix CI workflows that allow Claude to automatically fix CI failures and post as claude[bot].
|
||||
|
||||
## Journey Summary
|
||||
|
||||
### Initial Goal
|
||||
Create an auto-fix CI workflow similar to Cursor's implementation that:
|
||||
1. Detects CI failures on PRs
|
||||
2. Automatically triggers Claude to fix the issues
|
||||
3. Creates branches with fixes
|
||||
4. Posts PR comments as claude[bot] (not github-actions[bot])
|
||||
|
||||
### Key Implementation Files
|
||||
|
||||
#### 1. Auto-Fix Workflow
|
||||
**File**: `.github/workflows/auto-fix-ci-inline.yml`
|
||||
- Triggers on `workflow_run` event when CI fails
|
||||
- Creates fix branch
|
||||
- Collects failure logs
|
||||
- Calls Claude Code Action with `/fix-ci` slash command
|
||||
- Posts PR comment with fix branch link
|
||||
|
||||
#### 2. Fix-CI Slash Command
|
||||
**File**: `.claude/commands/fix-ci.md`
|
||||
- Contains all instructions for analyzing and fixing CI failures
|
||||
- Handles test failures, type errors, linting issues
|
||||
- Commits and pushes fixes
|
||||
|
||||
#### 3. Claude Code Action Changes (v1-dev branch)
|
||||
**Modified Files**:
|
||||
- `src/entrypoints/prepare.ts` - Exposes GitHub token as output
|
||||
- `action.yml` - Adds github_token output definition
|
||||
|
||||
## Critical Discoveries
|
||||
|
||||
### 1. Authentication Architecture
|
||||
|
||||
#### How Tag Mode Works (Success Case)
|
||||
1. User comments "@claude" on PR → `issue_comment` event
|
||||
2. Action requests OIDC token with audience "claude-code-github-action"
|
||||
3. Token exchange at `api.anthropic.com/api/github/github-app-token-exchange`
|
||||
4. Backend validates event type is in allowed list
|
||||
5. Returns Claude App token → posts as claude[bot]
|
||||
|
||||
#### Why Workflow_Run Failed
|
||||
1. Auto-fix workflow triggers on `workflow_run` event
|
||||
2. OIDC token has `event_name: "workflow_run"` claim
|
||||
3. Backend's `allowed_events` list didn't include "workflow_run"
|
||||
4. Token exchange fails with "401 Unauthorized - Invalid OIDC token"
|
||||
5. Can't get Claude App token → falls back to github-actions[bot]
|
||||
|
||||
### 2. OIDC Token Claims
|
||||
GitHub Actions OIDC tokens include:
|
||||
- `event_name`: The triggering event (pull_request, issue_comment, workflow_run, etc.)
|
||||
- `repository`: The repo where action runs
|
||||
- `actor`: Who triggered the action
|
||||
- `job_workflow_ref`: Reference to the workflow file
|
||||
- And many other claims for verification
|
||||
|
||||
### 3. Backend Validation
|
||||
**File**: `anthropic/api/api/private_api/routes/github/github_app_token_exchange.py`
|
||||
|
||||
The backend validates:
|
||||
```python
|
||||
allowed_events = [
|
||||
"pull_request",
|
||||
"issue_comment",
|
||||
"pull_request_comment",
|
||||
"issues",
|
||||
"pull_request_review",
|
||||
"pull_request_review_comment",
|
||||
"repository_dispatch",
|
||||
"workflow_dispatch",
|
||||
"schedule",
|
||||
# "workflow_run" was missing!
|
||||
]
|
||||
```
|
||||
|
||||
### 4. Agent Mode vs Tag Mode
|
||||
- **Tag Mode**: Triggers on PR/issue events, creates tracking comments
|
||||
- **Agent Mode**: Triggers on automation events (workflow_dispatch, schedule, and now workflow_run)
|
||||
- Both modes can use Claude App token if event is in allowed list
|
||||
|
||||
## Solution Implemented
|
||||
|
||||
### Backend Change (PR Created)
|
||||
Add `"workflow_run"` to the `allowed_events` list in the Claude backend to enable OIDC token exchange for workflow_run events.
|
||||
|
||||
### Why This Works
|
||||
- No special handling needed for different event types
|
||||
- Backend treats all allowed events the same way
|
||||
- Just validates token, checks permissions, returns Claude App token
|
||||
- Event name only used for validation and logging/metrics
|
||||
|
||||
## Current Status
|
||||
|
||||
### Completed
|
||||
- ✅ Created auto-fix workflow and slash command
|
||||
- ✅ Modified Claude Code Action to expose GitHub token as output
|
||||
- ✅ Identified root cause of authentication failure
|
||||
- ✅ Created PR to add workflow_run to backend allowed events
|
||||
|
||||
### Waiting On
|
||||
- ⏳ Backend PR approval and deployment
|
||||
- ⏳ Testing with updated backend
|
||||
|
||||
## Next Steps
|
||||
|
||||
Once the backend PR is merged and deployed:
|
||||
|
||||
### 1. Test Auto-Fix Workflow
|
||||
- Create a test PR with intentional CI failures
|
||||
- Verify auto-fix workflow triggers
|
||||
- Confirm Claude can authenticate via OIDC
|
||||
- Verify comments come from claude[bot]
|
||||
|
||||
### 2. Potential Improvements
|
||||
- Add more sophisticated CI failure detection
|
||||
- Handle different types of failures (tests, linting, types, build)
|
||||
- Add progress indicators in PR comments
|
||||
- Consider batching multiple fixes
|
||||
- Add retry logic for transient failures
|
||||
|
||||
### 3. Documentation
|
||||
- Document the auto-fix workflow setup
|
||||
- Create examples for different CI systems
|
||||
- Add troubleshooting guide
|
||||
|
||||
### 4. Extended Features
|
||||
- Support for multiple CI workflows
|
||||
- Customizable fix strategies per project
|
||||
- Integration with other GitHub Actions events
|
||||
- Support for monorepo structures
|
||||
|
||||
## Alternative Approaches (If Backend Change Blocked)
|
||||
|
||||
### Option 1: Repository Dispatch
|
||||
Instead of `workflow_run`, use `repository_dispatch`:
|
||||
- Original workflow triggers dispatch event on failure
|
||||
- Auto-fix workflow responds to dispatch event
|
||||
- Works today without backend changes
|
||||
|
||||
### Option 2: Direct PR Event
|
||||
Trigger on `pull_request` with conditional logic:
|
||||
- Check CI status in the workflow
|
||||
- Only run if CI failed
|
||||
- Keeps PR context for OIDC exchange
|
||||
|
||||
### Option 3: Custom GitHub App
|
||||
Create separate GitHub App for auto-fix:
|
||||
- Has its own authentication
|
||||
- Posts as custom bot (not claude[bot])
|
||||
- More complex but fully independent
|
||||
|
||||
## Key Learnings
|
||||
|
||||
1. **OIDC Context Matters**: The event context in OIDC tokens determines authentication success
|
||||
2. **Backend Validation is Simple**: Just a list check, no complex event-specific logic
|
||||
3. **Agent Mode is Powerful**: Designed for automation, just needed backend support
|
||||
4. **Token Flow is Critical**: Understanding the full auth flow helped identify the issue
|
||||
5. **Incremental Solutions Work**: Start simple, identify blockers, fix systematically
|
||||
|
||||
## Resources
|
||||
|
||||
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
||||
- [Claude Code Action Repository](https://github.com/anthropics/claude-code-action)
|
||||
- [Backend PR for workflow_run support](#) (Add link when available)
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-08-20*
|
||||
*Session Duration: ~6 hours*
|
||||
*Key Achievement: Identified and resolved Claude App authentication for workflow_run events*
|
||||
@@ -1,6 +1,6 @@
|
||||

|
||||
|
||||
# Claude Code Action (Final Test)
|
||||
# Claude Code Action
|
||||
|
||||
A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs and issues that can answer questions and implement code changes. This action listens for a trigger phrase in comments and activates Claude act on the request. It supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, and Google Vertex AI.
|
||||
|
||||
|
||||
15
action.yml
15
action.yml
@@ -81,10 +81,6 @@ inputs:
|
||||
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
||||
required: false
|
||||
default: "false"
|
||||
allowed_tools:
|
||||
description: "Comma-separated list of tools to allow Claude to use (e.g., 'Edit,MultiEdit,Write,Read'). If not set, mode defaults apply."
|
||||
required: false
|
||||
default: ""
|
||||
experimental_allowed_domains:
|
||||
description: "Restrict network access to these domains only (newline-separated). If not set, no restrictions are applied. Provider domains are auto-detected."
|
||||
required: false
|
||||
@@ -97,6 +93,9 @@ outputs:
|
||||
branch_name:
|
||||
description: "The branch created by Claude Code for this execution"
|
||||
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
||||
github_token:
|
||||
description: "The GitHub token used by the action (Claude App token if available)"
|
||||
value: ${{ steps.prepare.outputs.github_token }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@@ -134,7 +133,7 @@ runs:
|
||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||
MCP_CONFIG: ${{ inputs.mcp_config }}
|
||||
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
|
||||
ALL_INPUTS: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Install Base Action Dependencies
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
@@ -146,7 +145,8 @@ runs:
|
||||
echo "Base-action dependencies installed"
|
||||
cd -
|
||||
# Install Claude Code globally
|
||||
bun install -g @anthropic-ai/claude-code@1.0.72
|
||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.84
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Setup Network Restrictions
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||
@@ -173,6 +173,7 @@ runs:
|
||||
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
|
||||
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
|
||||
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
||||
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
||||
|
||||
# Model configuration
|
||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||
@@ -246,7 +247,7 @@ runs:
|
||||
fi
|
||||
|
||||
- name: Revoke app token
|
||||
if: always() && inputs.github_token == ''
|
||||
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L \
|
||||
|
||||
@@ -85,7 +85,7 @@ runs:
|
||||
|
||||
- name: Install Claude Code
|
||||
shell: bash
|
||||
run: bun install -g @anthropic-ai/claude-code@1.0.72
|
||||
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.84
|
||||
|
||||
- name: Run Claude Code Action
|
||||
shell: bash
|
||||
|
||||
@@ -56,10 +56,16 @@ export function prepareRunConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const customEnv: Record<string, string> = {};
|
||||
|
||||
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
||||
customEnv.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
||||
}
|
||||
|
||||
return {
|
||||
claudeArgs,
|
||||
promptPath,
|
||||
env: {},
|
||||
env: customEnv,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,9 +94,11 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
||||
console.log(`Prompt file size: ${promptSize} bytes`);
|
||||
|
||||
// Log custom environment variables if any
|
||||
if (Object.keys(config.env).length > 0) {
|
||||
const envKeys = Object.keys(config.env).join(", ");
|
||||
console.log(`Custom environment variables: ${envKeys}`);
|
||||
const customEnvKeys = Object.keys(config.env).filter(
|
||||
(key) => key !== "CLAUDE_ACTION_INPUTS_PRESENT",
|
||||
);
|
||||
if (customEnvKeys.length > 0) {
|
||||
console.log(`Custom environment variables: ${customEnvKeys.join(", ")}`);
|
||||
}
|
||||
|
||||
// Log custom arguments if any
|
||||
|
||||
@@ -80,36 +80,26 @@ export async function setupClaudeCodeSettings(
|
||||
}
|
||||
}
|
||||
|
||||
// Copy subagent files from repository to Claude's agents directory
|
||||
// CLAUDE_WORKING_DIR is set by the action to point to the repo being processed
|
||||
const workingDir = process.env.CLAUDE_WORKING_DIR || process.cwd();
|
||||
const repoAgentsDir = `${workingDir}/.claude/agents`;
|
||||
const targetAgentsDir = `${home}/.claude/agents`;
|
||||
|
||||
// Copy project subagents to Claude's agents directory
|
||||
// Use GITHUB_WORKSPACE if available (set by GitHub Actions), otherwise use current directory
|
||||
const workspaceDir = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||
const projectAgentsDir = `${workspaceDir}/.claude/agents`;
|
||||
const claudeAgentsDir = `${home}/.claude/agents`;
|
||||
|
||||
try {
|
||||
const agentsDirExists = await $`test -d ${repoAgentsDir}`.quiet().nothrow();
|
||||
if (agentsDirExists.exitCode === 0) {
|
||||
console.log(`Found subagents directory at ${repoAgentsDir}`);
|
||||
|
||||
// Create target agents directory if it doesn't exist
|
||||
await $`mkdir -p ${targetAgentsDir}`.quiet();
|
||||
console.log(`Created target agents directory at ${targetAgentsDir}`);
|
||||
|
||||
// Copy all .md files from repo agents to Claude's agents directory
|
||||
const copyResult = await $`cp -r ${repoAgentsDir}/*.md ${targetAgentsDir}/ 2>/dev/null`.quiet().nothrow();
|
||||
|
||||
if (copyResult.exitCode === 0) {
|
||||
// List copied agents for logging
|
||||
const agents = await $`ls -la ${targetAgentsDir}/*.md 2>/dev/null | wc -l`.quiet().text();
|
||||
const agentCount = parseInt(agents.trim()) || 0;
|
||||
console.log(`Successfully copied ${agentCount} subagent(s) to ${targetAgentsDir}`);
|
||||
} else {
|
||||
console.log(`No subagent files found in ${repoAgentsDir}`);
|
||||
}
|
||||
} else {
|
||||
console.log(`No subagents directory found at ${repoAgentsDir}`);
|
||||
}
|
||||
await $`test -d ${projectAgentsDir}`.quiet();
|
||||
console.log(`Found project agents directory at ${projectAgentsDir}`);
|
||||
|
||||
await $`mkdir -p ${claudeAgentsDir}`.quiet();
|
||||
|
||||
await $`cp ${projectAgentsDir}/*.md ${claudeAgentsDir}/ 2>/dev/null || true`.quiet();
|
||||
|
||||
const agentFiles = await $`ls ${claudeAgentsDir}/*.md 2>/dev/null | wc -l`
|
||||
.quiet()
|
||||
.text();
|
||||
const agentCount = parseInt(agentFiles.trim()) || 0;
|
||||
console.log(`Copied ${agentCount} agent(s) to ${claudeAgentsDir}`);
|
||||
} catch (e) {
|
||||
console.log(`Error handling subagents: ${e}`);
|
||||
console.log(`No project agents directory found at ${projectAgentsDir}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,4 +215,70 @@ describe("setupClaudeCodeSettings", () => {
|
||||
const settingsContent = await readFile(settingsPath, "utf-8");
|
||||
expect(JSON.parse(settingsContent).enableAllProjectMcpServers).toBe(true);
|
||||
});
|
||||
|
||||
test("should copy project agents when .claude/agents directory exists", async () => {
|
||||
// Create a mock project structure with agents
|
||||
const projectDir = join(testHomeDir, "test-project");
|
||||
const projectAgentsDir = join(projectDir, ".claude", "agents");
|
||||
await mkdir(projectAgentsDir, { recursive: true });
|
||||
|
||||
// Create test agent files
|
||||
await writeFile(
|
||||
join(projectAgentsDir, "test-agent.md"),
|
||||
"---\nname: test-agent\ndescription: Test agent\n---\nTest agent content",
|
||||
);
|
||||
await writeFile(
|
||||
join(projectAgentsDir, "another-agent.md"),
|
||||
"---\nname: another-agent\n---\nAnother agent",
|
||||
);
|
||||
|
||||
// Set GITHUB_WORKSPACE to the test project directory
|
||||
const originalWorkspace = process.env.GITHUB_WORKSPACE;
|
||||
process.env.GITHUB_WORKSPACE = projectDir;
|
||||
|
||||
try {
|
||||
await setupClaudeCodeSettings(undefined, testHomeDir);
|
||||
|
||||
// Check that agents were copied
|
||||
const agentsDir = join(testHomeDir, ".claude", "agents");
|
||||
const files = await readdir(agentsDir);
|
||||
expect(files).toContain("test-agent.md");
|
||||
expect(files).toContain("another-agent.md");
|
||||
|
||||
// Verify content was copied correctly
|
||||
const content = await readFile(join(agentsDir, "test-agent.md"), "utf-8");
|
||||
expect(content).toContain("Test agent content");
|
||||
} finally {
|
||||
// Restore original GITHUB_WORKSPACE
|
||||
if (originalWorkspace !== undefined) {
|
||||
process.env.GITHUB_WORKSPACE = originalWorkspace;
|
||||
} else {
|
||||
delete process.env.GITHUB_WORKSPACE;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test("should handle missing project agents directory gracefully", async () => {
|
||||
// Set GITHUB_WORKSPACE to a directory without .claude/agents
|
||||
const projectDir = join(testHomeDir, "project-without-agents");
|
||||
await mkdir(projectDir, { recursive: true });
|
||||
|
||||
const originalWorkspace = process.env.GITHUB_WORKSPACE;
|
||||
process.env.GITHUB_WORKSPACE = projectDir;
|
||||
|
||||
try {
|
||||
await setupClaudeCodeSettings(undefined, testHomeDir);
|
||||
|
||||
// Should complete without errors
|
||||
const settingsContent = await readFile(settingsPath, "utf-8");
|
||||
const settings = JSON.parse(settingsContent);
|
||||
expect(settings.enableAllProjectMcpServers).toBe(true);
|
||||
} finally {
|
||||
if (originalWorkspace !== undefined) {
|
||||
process.env.GITHUB_WORKSPACE = originalWorkspace;
|
||||
} else {
|
||||
delete process.env.GITHUB_WORKSPACE;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Claude PR Assistant
|
||||
name: Claude Code
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
@@ -11,38 +11,53 @@ on:
|
||||
types: [submitted]
|
||||
|
||||
jobs:
|
||||
claude-code-action:
|
||||
claude:
|
||||
if: |
|
||||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
|
||||
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude PR Action
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Or use OAuth token instead:
|
||||
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
timeout_minutes: "60"
|
||||
# mode: tag # Default: responds to @claude mentions
|
||||
# Optional: Restrict network access to specific domains only
|
||||
# experimental_allowed_domains: |
|
||||
# .anthropic.com
|
||||
# .github.com
|
||||
# api.github.com
|
||||
# .githubusercontent.com
|
||||
# bun.sh
|
||||
# registry.npmjs.org
|
||||
# .blob.core.windows.net
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
|
||||
# model: "claude-opus-4-1-20250805"
|
||||
|
||||
# Optional: Customize the trigger phrase (default: @claude)
|
||||
# trigger_phrase: "/claude"
|
||||
|
||||
# Optional: Trigger when specific user is assigned to an issue
|
||||
# assignee_trigger: "claude-bot"
|
||||
|
||||
# Optional: Allow Claude to run specific commands
|
||||
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
|
||||
|
||||
# Optional: Add custom instructions for Claude to customize its behavior for your project
|
||||
# custom_instructions: |
|
||||
# Follow our coding standards
|
||||
# Ensure all new code has tests
|
||||
# Use TypeScript for new files
|
||||
|
||||
# Optional: Custom environment variables for Claude
|
||||
# claude_env: |
|
||||
# NODE_ENV: test
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
---
|
||||
description: Fix CI failures and commit changes (for use when branch already exists)
|
||||
allowed_tools: "*"
|
||||
---
|
||||
|
||||
# Fix CI Failures and Commit
|
||||
|
||||
You are on a branch that was created to fix CI failures. Your task is to fix the issues and commit the changes.
|
||||
|
||||
## CI Failure Information
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Your Tasks
|
||||
|
||||
1. **Analyze the failures** - Understand what went wrong from the logs
|
||||
2. **Fix the issues** - Make the necessary code changes
|
||||
3. **Commit your fixes** - Use git to commit all changes
|
||||
|
||||
## Step-by-Step Instructions
|
||||
|
||||
### 1. Fix the Issues
|
||||
|
||||
Based on the error logs:
|
||||
- Fix syntax errors
|
||||
- Fix formatting issues
|
||||
- Fix test failures
|
||||
- Fix any other CI problems
|
||||
|
||||
### 2. Commit Your Changes (REQUIRED)
|
||||
|
||||
After fixing ALL issues, you MUST:
|
||||
|
||||
Use the `mcp__github_file_ops__commit_files` tool to commit all your changes with a descriptive message like:
|
||||
|
||||
```
|
||||
Fix CI failures
|
||||
|
||||
- Fixed syntax errors
|
||||
- Fixed formatting issues
|
||||
- Fixed test failures
|
||||
[List actual fixes made]
|
||||
```
|
||||
|
||||
**IMPORTANT**: You MUST use the MCP file ops tool to commit your changes. The workflow expects you to commit your changes.
|
||||
|
||||
### 3. Verify (Optional)
|
||||
|
||||
If possible, run verification commands:
|
||||
- `bun run format:check` for formatting
|
||||
- `bun test` for tests
|
||||
- `bun run typecheck` for TypeScript
|
||||
|
||||
Begin by analyzing the failure logs and then fix the issues.
|
||||
@@ -1,67 +0,0 @@
|
||||
---
|
||||
description: Analyze and fix CI failures by examining logs and making targeted fixes
|
||||
allowed_tools: "*"
|
||||
---
|
||||
|
||||
# Fix CI Failures
|
||||
|
||||
You are tasked with analyzing CI failure logs and fixing the issues. Follow these steps:
|
||||
|
||||
## Context Provided
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Step 1: Analyze the Failure
|
||||
|
||||
Parse the provided CI failure information to understand:
|
||||
- Which jobs failed and why
|
||||
- The specific error messages and stack traces
|
||||
- Whether failures are test-related, build-related, or linting issues
|
||||
|
||||
## Step 2: Search and Understand the Codebase
|
||||
|
||||
Use search tools to locate the failing code:
|
||||
- Search for the failing test names or functions
|
||||
- Find the source files mentioned in error messages
|
||||
- Review related configuration files (package.json, tsconfig.json, etc.)
|
||||
|
||||
## Step 3: Apply Targeted Fixes
|
||||
|
||||
Make minimal, focused changes:
|
||||
- **For test failures**: Determine if the test or implementation needs fixing
|
||||
- **For type errors**: Fix type definitions or correct the code logic
|
||||
- **For linting issues**: Apply formatting using the project's tools
|
||||
- **For build errors**: Resolve dependency or configuration issues
|
||||
- **For missing imports**: Add the necessary imports or install packages
|
||||
|
||||
Requirements:
|
||||
- Only fix the actual CI failures, avoid unrelated changes
|
||||
- Follow existing code patterns and conventions
|
||||
- Ensure changes are production-ready, not temporary hacks
|
||||
- Preserve existing functionality while fixing issues
|
||||
|
||||
## Step 4: Commit Changes
|
||||
|
||||
After applying fixes:
|
||||
1. Use the `mcp__github_file_ops__commit_files` tool to commit your changes
|
||||
2. Include a descriptive commit message explaining what was fixed
|
||||
3. Document which CI jobs/tests were addressed in the commit message
|
||||
4. Important: Use the MCP file ops tool to commit your changes
|
||||
|
||||
## Step 5: Verify Fixes Locally
|
||||
|
||||
Run available verification commands:
|
||||
- Execute the failing tests locally to confirm they pass
|
||||
- Run the project's lint command (check package.json for scripts)
|
||||
- Run type checking if available
|
||||
- Execute any build commands to ensure compilation succeeds
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- Focus exclusively on fixing the reported CI failures
|
||||
- Maintain code quality and follow the project's established patterns
|
||||
- If a fix requires significant refactoring, document why it's necessary
|
||||
- When multiple solutions exist, choose the simplest one that maintains code quality
|
||||
- Add clear comments only if the fix is non-obvious
|
||||
|
||||
Begin by analyzing the failure details provided above.
|
||||
@@ -750,7 +750,7 @@ export async function createPrompt(
|
||||
modeContext.claudeBranch,
|
||||
);
|
||||
|
||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
||||
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||
recursive: true,
|
||||
});
|
||||
|
||||
@@ -769,7 +769,7 @@ export async function createPrompt(
|
||||
|
||||
// Write the prompt file
|
||||
await writeFile(
|
||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
||||
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||
promptContent,
|
||||
);
|
||||
|
||||
|
||||
59
src/entrypoints/collect-inputs.ts
Normal file
59
src/entrypoints/collect-inputs.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
export function collectActionInputsPresence(): void {
|
||||
const inputDefaults: Record<string, string> = {
|
||||
trigger_phrase: "@claude",
|
||||
assignee_trigger: "",
|
||||
label_trigger: "claude",
|
||||
base_branch: "",
|
||||
branch_prefix: "claude/",
|
||||
allowed_bots: "",
|
||||
mode: "tag",
|
||||
model: "",
|
||||
anthropic_model: "",
|
||||
fallback_model: "",
|
||||
allowed_tools: "",
|
||||
disallowed_tools: "",
|
||||
custom_instructions: "",
|
||||
direct_prompt: "",
|
||||
override_prompt: "",
|
||||
mcp_config: "",
|
||||
additional_permissions: "",
|
||||
claude_env: "",
|
||||
settings: "",
|
||||
anthropic_api_key: "",
|
||||
claude_code_oauth_token: "",
|
||||
github_token: "",
|
||||
max_turns: "",
|
||||
use_sticky_comment: "false",
|
||||
use_commit_signing: "false",
|
||||
experimental_allowed_domains: "",
|
||||
};
|
||||
|
||||
const allInputsJson = process.env.ALL_INPUTS;
|
||||
if (!allInputsJson) {
|
||||
console.log("ALL_INPUTS environment variable not found");
|
||||
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||
return;
|
||||
}
|
||||
|
||||
let allInputs: Record<string, string>;
|
||||
try {
|
||||
allInputs = JSON.parse(allInputsJson);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ALL_INPUTS JSON:", e);
|
||||
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||
return;
|
||||
}
|
||||
|
||||
const presentInputs: Record<string, boolean> = {};
|
||||
|
||||
for (const [name, defaultValue] of Object.entries(inputDefaults)) {
|
||||
const actualValue = allInputs[name] || "";
|
||||
|
||||
const isSet = actualValue !== defaultValue;
|
||||
presentInputs[name] = isSet;
|
||||
}
|
||||
|
||||
core.setOutput("action_inputs_present", JSON.stringify(presentInputs));
|
||||
}
|
||||
@@ -12,9 +12,12 @@ import { createOctokit } from "../github/api/client";
|
||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||
import { getMode } from "../modes/registry";
|
||||
import { prepare } from "../prepare";
|
||||
import { collectActionInputsPresence } from "./collect-inputs";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
collectActionInputsPresence();
|
||||
|
||||
// Parse GitHub context first to enable mode detection
|
||||
const context = parseGitHubContext();
|
||||
|
||||
@@ -41,11 +44,18 @@ async function run() {
|
||||
// Check trigger conditions
|
||||
const containsTrigger = mode.shouldTrigger(context);
|
||||
|
||||
// Debug logging
|
||||
console.log(`Mode: ${mode.name}`);
|
||||
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
||||
console.log(`Trigger result: ${containsTrigger}`);
|
||||
|
||||
// Set output for action.yml to check
|
||||
core.setOutput("contains_trigger", containsTrigger.toString());
|
||||
|
||||
if (!containsTrigger) {
|
||||
console.log("No trigger found, skipping remaining steps");
|
||||
// Still set github_token output even when skipping
|
||||
core.setOutput("github_token", githubToken);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,6 +68,9 @@ async function run() {
|
||||
});
|
||||
|
||||
// MCP config is handled by individual modes (tag/agent) and included in their claude_args output
|
||||
|
||||
// Expose the GitHub token (Claude App token) as an output
|
||||
core.setOutput("github_token", githubToken);
|
||||
|
||||
// Step 6: Get system prompt from mode if available
|
||||
if (mode.getSystemPrompt) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
PullRequestEvent,
|
||||
PullRequestReviewEvent,
|
||||
PullRequestReviewCommentEvent,
|
||||
WorkflowRunEvent,
|
||||
} from "@octokit/webhooks-types";
|
||||
// Custom types for GitHub Actions events that aren't webhooks
|
||||
export type WorkflowDispatchEvent = {
|
||||
@@ -44,7 +45,11 @@ const ENTITY_EVENT_NAMES = [
|
||||
"pull_request_review_comment",
|
||||
] as const;
|
||||
|
||||
const AUTOMATION_EVENT_NAMES = ["workflow_dispatch", "schedule"] as const;
|
||||
const AUTOMATION_EVENT_NAMES = [
|
||||
"workflow_dispatch",
|
||||
"schedule",
|
||||
"workflow_run",
|
||||
] as const;
|
||||
|
||||
// Derive types from constants for better maintainability
|
||||
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
|
||||
@@ -86,10 +91,10 @@ export type ParsedGitHubContext = BaseContext & {
|
||||
isPR: boolean;
|
||||
};
|
||||
|
||||
// Context for automation events (workflow_dispatch, schedule)
|
||||
// Context for automation events (workflow_dispatch, schedule, workflow_run)
|
||||
export type AutomationContext = BaseContext & {
|
||||
eventName: AutomationEventName;
|
||||
payload: WorkflowDispatchEvent | ScheduleEvent;
|
||||
payload: WorkflowDispatchEvent | ScheduleEvent | WorkflowRunEvent;
|
||||
};
|
||||
|
||||
// Union type for all contexts
|
||||
@@ -185,6 +190,13 @@ export function parseGitHubContext(): GitHubContext {
|
||||
payload: context.payload as unknown as ScheduleEvent,
|
||||
};
|
||||
}
|
||||
case "workflow_run": {
|
||||
return {
|
||||
...commonFields,
|
||||
eventName: "workflow_run",
|
||||
payload: context.payload as unknown as WorkflowRunEvent,
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported event type: ${context.eventName}`);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,30 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
||||
const responseJson = (await response.json()) as {
|
||||
error?: {
|
||||
message?: string;
|
||||
details?: {
|
||||
error_code?: string;
|
||||
};
|
||||
};
|
||||
type?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
// Check for specific workflow validation error codes that should skip the action
|
||||
const errorCode = responseJson.error?.details?.error_code;
|
||||
|
||||
if (errorCode === "workflow_not_found_on_default_branch") {
|
||||
const message =
|
||||
responseJson.message ??
|
||||
responseJson.error?.message ??
|
||||
"Workflow validation failed";
|
||||
core.warning(`Skipping action due to workflow validation: ${message}`);
|
||||
console.log(
|
||||
"Action skipped due to workflow validation error. This is expected when adding Claude Code workflows to new repositories or on PRs with workflow changes. If you're seeing this, your workflow will begin working once you merge your PR.",
|
||||
);
|
||||
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.error(
|
||||
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
||||
);
|
||||
@@ -77,8 +99,9 @@ export async function setupGitHubToken(): Promise<string> {
|
||||
core.setOutput("GITHUB_TOKEN", appToken);
|
||||
return appToken;
|
||||
} catch (error) {
|
||||
// Only set failed if we get here - workflow validation errors will exit(0) before this
|
||||
core.setFailed(
|
||||
`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.`,
|
||||
`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);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,41 @@ export function sanitizeContent(content: string): string {
|
||||
content = stripMarkdownLinkTitles(content);
|
||||
content = stripHiddenAttributes(content);
|
||||
content = normalizeHtmlEntities(content);
|
||||
content = redactGitHubTokens(content);
|
||||
return content;
|
||||
}
|
||||
|
||||
export function redactGitHubTokens(content: string): string {
|
||||
// GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||
content = content.replace(
|
||||
/\bghp_[A-Za-z0-9]{36}\b/g,
|
||||
"[REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
|
||||
// GitHub OAuth tokens: gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||
content = content.replace(
|
||||
/\bgho_[A-Za-z0-9]{36}\b/g,
|
||||
"[REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
|
||||
// GitHub installation tokens: ghs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||
content = content.replace(
|
||||
/\bghs_[A-Za-z0-9]{36}\b/g,
|
||||
"[REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
|
||||
// GitHub refresh tokens: ghr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
|
||||
content = content.replace(
|
||||
/\bghr_[A-Za-z0-9]{36}\b/g,
|
||||
"[REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
|
||||
// GitHub fine-grained personal access tokens: github_pat_XXXXXXXXXX (up to 255 chars)
|
||||
content = content.replace(
|
||||
/\bgithub_pat_[A-Za-z0-9_]{11,221}\b/g,
|
||||
"[REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { z } from "zod";
|
||||
import { GITHUB_API_URL } from "../github/api/config";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||
|
||||
// Get repository information from environment variables
|
||||
const REPO_OWNER = process.env.REPO_OWNER;
|
||||
@@ -54,11 +55,13 @@ server.tool(
|
||||
const isPullRequestReviewComment =
|
||||
eventName === "pull_request_review_comment";
|
||||
|
||||
const sanitizedBody = sanitizeContent(body);
|
||||
|
||||
const result = await updateClaudeComment(octokit, {
|
||||
owner,
|
||||
repo,
|
||||
commentId,
|
||||
body,
|
||||
body: sanitizedBody,
|
||||
isPullRequestReviewComment,
|
||||
});
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import { z } from "zod";
|
||||
import { createOctokit } from "../github/api/client";
|
||||
import { sanitizeContent } from "../github/utils/sanitizer";
|
||||
|
||||
// Get repository and PR information from environment variables
|
||||
const REPO_OWNER = process.env.REPO_OWNER;
|
||||
@@ -81,6 +82,9 @@ server.tool(
|
||||
|
||||
const octokit = createOctokit(githubToken).rest;
|
||||
|
||||
// Sanitize the comment body to remove any potential GitHub tokens
|
||||
const sanitizedBody = sanitizeContent(body);
|
||||
|
||||
// Validate that either line or both startLine and line are provided
|
||||
if (!line && !startLine) {
|
||||
throw new Error(
|
||||
@@ -104,7 +108,7 @@ server.tool(
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
body,
|
||||
body: sanitizedBody,
|
||||
path,
|
||||
side: side || "RIGHT",
|
||||
commit_id: commit_id || pr.data.head.sha,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as core from "@actions/core";
|
||||
import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import { isEntityContext } from "../github/context";
|
||||
import { Octokit } from "@octokit/rest";
|
||||
|
||||
type PrepareConfigParams = {
|
||||
@@ -9,10 +10,9 @@ type PrepareConfigParams = {
|
||||
repo: string;
|
||||
branch: string;
|
||||
baseBranch: string;
|
||||
additionalMcpConfig?: string;
|
||||
claudeCommentId?: string;
|
||||
allowedTools: string[];
|
||||
context: ParsedGitHubContext;
|
||||
context: GitHubContext;
|
||||
};
|
||||
|
||||
async function checkActionsReadPermission(
|
||||
@@ -56,7 +56,6 @@ export async function prepareMcpConfig(
|
||||
repo,
|
||||
branch,
|
||||
baseBranch,
|
||||
additionalMcpConfig,
|
||||
claudeCommentId,
|
||||
allowedTools,
|
||||
context,
|
||||
@@ -68,6 +67,10 @@ export async function prepareMcpConfig(
|
||||
tool.startsWith("mcp__github__"),
|
||||
);
|
||||
|
||||
const hasInlineCommentTools = allowedToolsList.some((tool) =>
|
||||
tool.startsWith("mcp__github_inline_comment__"),
|
||||
);
|
||||
|
||||
const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
|
||||
mcpServers: {},
|
||||
};
|
||||
@@ -111,10 +114,32 @@ export async function prepareMcpConfig(
|
||||
};
|
||||
}
|
||||
|
||||
// Include inline comment server for PRs when requested via allowed tools
|
||||
if (
|
||||
isEntityContext(context) &&
|
||||
context.isPR &&
|
||||
(hasGitHubMcpTools || hasInlineCommentTools)
|
||||
) {
|
||||
baseMcpConfig.mcpServers.github_inline_comment = {
|
||||
command: "bun",
|
||||
args: [
|
||||
"run",
|
||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/github-inline-comment-server.ts`,
|
||||
],
|
||||
env: {
|
||||
GITHUB_TOKEN: githubToken,
|
||||
REPO_OWNER: owner,
|
||||
REPO_NAME: repo,
|
||||
PR_NUMBER: context.entityNumber?.toString() || "",
|
||||
GITHUB_API_URL: GITHUB_API_URL,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// CI server is included when we have a workflow token and context is a PR
|
||||
const hasWorkflowToken = !!process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||
|
||||
if (context.isPR && hasWorkflowToken) {
|
||||
if (isEntityContext(context) && context.isPR && hasWorkflowToken) {
|
||||
// Verify the token actually has actions:read permission
|
||||
const actuallyHasPermission = await checkActionsReadPermission(
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN || "",
|
||||
@@ -166,38 +191,8 @@ export async function prepareMcpConfig(
|
||||
};
|
||||
}
|
||||
|
||||
// Merge with additional MCP config if provided
|
||||
if (additionalMcpConfig && additionalMcpConfig.trim()) {
|
||||
try {
|
||||
const additionalConfig = JSON.parse(additionalMcpConfig);
|
||||
|
||||
// Validate that parsed JSON is an object
|
||||
if (typeof additionalConfig !== "object" || additionalConfig === null) {
|
||||
throw new Error("MCP config must be a valid JSON object");
|
||||
}
|
||||
|
||||
core.info(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
);
|
||||
|
||||
// Merge configurations with user config overriding built-in servers
|
||||
const mergedConfig = {
|
||||
...baseMcpConfig,
|
||||
...additionalConfig,
|
||||
mcpServers: {
|
||||
...baseMcpConfig.mcpServers,
|
||||
...additionalConfig.mcpServers,
|
||||
},
|
||||
};
|
||||
|
||||
return JSON.stringify(mergedConfig, null, 2);
|
||||
} catch (parseError) {
|
||||
core.warning(
|
||||
`Failed to parse additional MCP config: ${parseError}. Using base config only.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Return only our GitHub servers config
|
||||
// User's config will be passed as separate --mcp-config flags
|
||||
return JSON.stringify(baseMcpConfig, null, 2);
|
||||
} catch (error) {
|
||||
core.setFailed(`Install MCP server failed with error: ${error}`);
|
||||
|
||||
@@ -2,6 +2,8 @@ import * as core from "@actions/core";
|
||||
import { mkdir, writeFile } from "fs/promises";
|
||||
import type { Mode, ModeOptions, ModeResult } from "../types";
|
||||
import type { PreparedContext } from "../../create-prompt/types";
|
||||
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
|
||||
import { parseAllowedTools } from "./parse-tools";
|
||||
|
||||
/**
|
||||
* Agent mode implementation.
|
||||
@@ -40,116 +42,71 @@ export const agentMode: Mode = {
|
||||
},
|
||||
|
||||
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
|
||||
// Agent mode handles automation events and any event with explicit prompts
|
||||
|
||||
// TODO: handle by createPrompt (similar to tag and review modes)
|
||||
// Create prompt directory
|
||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
||||
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||
recursive: true,
|
||||
});
|
||||
// Write the prompt file - the base action requires a prompt_file parameter.
|
||||
// Use the unified prompt field from v1.0.
|
||||
|
||||
// Write the prompt file - use the user's prompt directly
|
||||
const promptContent =
|
||||
context.inputs.prompt ||
|
||||
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||
|
||||
await writeFile(
|
||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
||||
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||
promptContent,
|
||||
);
|
||||
|
||||
// Agent mode: User has full control via claudeArgs
|
||||
// No default tools are enforced - Claude Code's defaults will apply
|
||||
|
||||
// Always include the GitHub comment server in agent mode
|
||||
// This ensures GitHub tools (PR reviews, comments, etc.) work out of the box
|
||||
// without requiring users to manually configure the MCP server
|
||||
const mcpConfig: any = {
|
||||
mcpServers: {
|
||||
"github-comment-server": {
|
||||
command: "bun",
|
||||
args: [
|
||||
"run",
|
||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/github-comment-server.ts`,
|
||||
],
|
||||
env: {
|
||||
GITHUB_TOKEN: githubToken || "",
|
||||
REPO_OWNER: context.repository.owner,
|
||||
REPO_NAME: context.repository.repo,
|
||||
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
|
||||
GITHUB_API_URL:
|
||||
process.env.GITHUB_API_URL || "https://api.github.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Add GitHub file ops server when using commit signing
|
||||
if (context.inputs?.useCommitSigning) {
|
||||
mcpConfig.mcpServers["github-file-ops-server"] = {
|
||||
command: "bun",
|
||||
args: [
|
||||
"run",
|
||||
`${process.env.GITHUB_ACTION_PATH}/src/mcp/github-file-ops-server.ts`,
|
||||
],
|
||||
env: {
|
||||
GITHUB_TOKEN: githubToken || "",
|
||||
REPO_OWNER: context.repository.owner,
|
||||
REPO_NAME: context.repository.repo,
|
||||
BRANCH_NAME: "", // Agent mode doesn't pre-create branches
|
||||
BASE_BRANCH: "",
|
||||
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
|
||||
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
|
||||
IS_PR: "false", // Agent mode doesn't create PRs by default
|
||||
GITHUB_API_URL:
|
||||
process.env.GITHUB_API_URL || "https://api.github.com",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Add user-provided additional MCP config if any
|
||||
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||
if (additionalMcpConfig.trim()) {
|
||||
try {
|
||||
const additional = JSON.parse(additionalMcpConfig);
|
||||
if (additional && typeof additional === "object") {
|
||||
// Merge mcpServers if both have them
|
||||
if (additional.mcpServers && mcpConfig.mcpServers) {
|
||||
Object.assign(mcpConfig.mcpServers, additional.mcpServers);
|
||||
} else {
|
||||
Object.assign(mcpConfig, additional);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
core.warning(`Failed to parse additional MCP config: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Agent mode: pass through user's claude_args with MCP config and allowed_tools
|
||||
// Parse allowed tools from user's claude_args
|
||||
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
||||
const userAllowedTools = process.env.ALLOWED_TOOLS || "";
|
||||
const escapedMcpConfig = JSON.stringify(mcpConfig).replace(/'/g, "'\\''");
|
||||
let claudeArgs = `--mcp-config '${escapedMcpConfig}'`;
|
||||
|
||||
// Add allowed_tools if specified
|
||||
if (userAllowedTools) {
|
||||
claudeArgs += ` --allowedTools "${userAllowedTools}"`;
|
||||
const allowedTools = parseAllowedTools(userClaudeArgs);
|
||||
|
||||
// Detect current branch from GitHub environment
|
||||
const currentBranch =
|
||||
process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || "main";
|
||||
|
||||
// Get our GitHub MCP servers config
|
||||
const ourMcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: currentBranch,
|
||||
baseBranch: context.inputs.baseBranch || "main",
|
||||
claudeCommentId: undefined, // No tracking comment in agent mode
|
||||
allowedTools,
|
||||
context,
|
||||
});
|
||||
|
||||
// Build final claude_args with multiple --mcp-config flags
|
||||
let claudeArgs = "";
|
||||
|
||||
// Add our GitHub servers config if we have any
|
||||
const ourConfig = JSON.parse(ourMcpConfig);
|
||||
if (ourConfig.mcpServers && Object.keys(ourConfig.mcpServers).length > 0) {
|
||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||
}
|
||||
|
||||
// Add user's additional claude_args
|
||||
if (userClaudeArgs) {
|
||||
claudeArgs += ` ${userClaudeArgs}`;
|
||||
|
||||
// Add user's MCP_CONFIG env var as separate --mcp-config
|
||||
const userMcpConfig = process.env.MCP_CONFIG;
|
||||
if (userMcpConfig?.trim()) {
|
||||
const escapedUserConfig = userMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `${claudeArgs} --mcp-config '${escapedUserConfig}'`.trim();
|
||||
}
|
||||
|
||||
core.setOutput("claude_args", claudeArgs.trim());
|
||||
|
||||
// Append user's claude_args (which may have more --mcp-config flags)
|
||||
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
|
||||
|
||||
core.setOutput("claude_args", claudeArgs);
|
||||
|
||||
return {
|
||||
commentId: undefined,
|
||||
branchInfo: {
|
||||
baseBranch: "",
|
||||
currentBranch: "",
|
||||
baseBranch: context.inputs.baseBranch || "main",
|
||||
currentBranch,
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: JSON.stringify(mcpConfig),
|
||||
mcpConfig: ourMcpConfig,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
22
src/modes/agent/parse-tools.ts
Normal file
22
src/modes/agent/parse-tools.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export function parseAllowedTools(claudeArgs: string): string[] {
|
||||
// Match --allowedTools followed by the value
|
||||
// Handle both quoted and unquoted values
|
||||
const patterns = [
|
||||
/--allowedTools\s+"([^"]+)"/, // Double quoted
|
||||
/--allowedTools\s+'([^']+)'/, // Single quoted
|
||||
/--allowedTools\s+([^\s]+)/, // Unquoted
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
const match = claudeArgs.match(pattern);
|
||||
if (match && match[1]) {
|
||||
// Don't return if the value starts with -- (another flag)
|
||||
if (match[1].startsWith("--")) {
|
||||
return [];
|
||||
}
|
||||
return match[1].split(",").map((t) => t.trim());
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
@@ -100,15 +100,13 @@ export const tagMode: Mode = {
|
||||
|
||||
await createPrompt(tagMode, modeContext, githubData, context);
|
||||
|
||||
// Get MCP configuration
|
||||
const additionalMcpConfig = process.env.MCP_CONFIG || "";
|
||||
const mcpConfig = await prepareMcpConfig({
|
||||
// Get our GitHub MCP servers configuration
|
||||
const ourMcpConfig = await prepareMcpConfig({
|
||||
githubToken,
|
||||
owner: context.repository.owner,
|
||||
repo: context.repository.repo,
|
||||
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
|
||||
baseBranch: branchInfo.baseBranch,
|
||||
additionalMcpConfig,
|
||||
claudeCommentId: commentId.toString(),
|
||||
allowedTools: [],
|
||||
context,
|
||||
@@ -150,14 +148,26 @@ export const tagMode: Mode = {
|
||||
|
||||
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
|
||||
|
||||
// Build complete claude_args with MCP config (as JSON string), tools, and user args
|
||||
// Note: Once Claude supports multiple --mcp-config flags, we can pass as file path
|
||||
// Escape single quotes in JSON to prevent shell injection
|
||||
const escapedMcpConfig = mcpConfig.replace(/'/g, "'\\''");
|
||||
let claudeArgs = `--mcp-config '${escapedMcpConfig}' `;
|
||||
claudeArgs += `--allowedTools "${tagModeTools.join(",")}" `;
|
||||
// Build complete claude_args with multiple --mcp-config flags
|
||||
let claudeArgs = "";
|
||||
|
||||
// Add our GitHub servers config
|
||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||
|
||||
// Add user's MCP_CONFIG env var as separate --mcp-config
|
||||
const userMcpConfig = process.env.MCP_CONFIG;
|
||||
if (userMcpConfig?.trim()) {
|
||||
const escapedUserConfig = userMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `${claudeArgs} --mcp-config '${escapedUserConfig}'`;
|
||||
}
|
||||
|
||||
// Add required tools for tag mode
|
||||
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
|
||||
|
||||
// Append user's claude_args (which may have more --mcp-config flags)
|
||||
if (userClaudeArgs) {
|
||||
claudeArgs += userClaudeArgs;
|
||||
claudeArgs += ` ${userClaudeArgs}`;
|
||||
}
|
||||
|
||||
core.setOutput("claude_args", claudeArgs.trim());
|
||||
@@ -165,7 +175,7 @@ export const tagMode: Mode = {
|
||||
return {
|
||||
commentId,
|
||||
branchInfo,
|
||||
mcpConfig,
|
||||
mcpConfig: ourMcpConfig,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@@ -38,3 +38,4 @@ export async function retryWithBackoff<T>(
|
||||
console.error(`Operation failed after ${maxAttempts} attempts`);
|
||||
throw lastError;
|
||||
}
|
||||
// Test change to trigger CI
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
// This file intentionally has TypeScript errors to trigger CI failure
|
||||
// Testing auto-fix with MCP file ops enabled
|
||||
const testFunction = (param: string): number => {
|
||||
// Type error: returning string instead of number
|
||||
return "this should be a number";
|
||||
}
|
||||
|
||||
// Syntax error: missing closing brace
|
||||
function brokenFunction() {
|
||||
console.log("missing closing brace"
|
||||
}
|
||||
|
||||
export { testFunction, brokenFunction };
|
||||
@@ -50,14 +50,6 @@ describe("prepareMcpConfig", () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockPRContextWithSigning: ParsedGitHubContext = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {});
|
||||
consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
||||
@@ -98,19 +90,9 @@ describe("prepareMcpConfig", () => {
|
||||
expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
expect(parsed.mcpServers.github_comment.env.REPO_OWNER).toBe("test-owner");
|
||||
expect(parsed.mcpServers.github_comment.env.REPO_NAME).toBe("test-repo");
|
||||
});
|
||||
|
||||
test("should return file ops server when commit signing is enabled", async () => {
|
||||
const contextWithSigning = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
test("should include file ops server when commit signing is enabled", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
@@ -118,19 +100,16 @@ describe("prepareMcpConfig", () => {
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithSigning,
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
expect(parsed.mcpServers.github_file_ops.env.REPO_OWNER).toBe("test-owner");
|
||||
expect(parsed.mcpServers.github_file_ops.env.REPO_NAME).toBe("test-repo");
|
||||
expect(parsed.mcpServers.github_file_ops.env.BRANCH_NAME).toBe(
|
||||
"test-branch",
|
||||
);
|
||||
@@ -143,49 +122,37 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
allowedTools: ["mcp__github__create_issue", "mcp__github__create_pr"],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github.command).toBe("docker");
|
||||
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
});
|
||||
|
||||
test("should not include github MCP server when only file_ops tools are allowed", async () => {
|
||||
const contextWithSigning = {
|
||||
...mockContext,
|
||||
inputs: {
|
||||
...mockContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
test("should include inline comment server for PRs when tools are allowed", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [
|
||||
"mcp__github_file_ops__commit_files",
|
||||
"mcp__github_file_ops__update_claude_comment",
|
||||
],
|
||||
context: contextWithSigning,
|
||||
allowedTools: ["mcp__github_inline_comment__create_inline_comment"],
|
||||
context: mockPRContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.github_inline_comment).toBeDefined();
|
||||
expect(parsed.mcpServers.github_inline_comment.env.GITHUB_TOKEN).toBe(
|
||||
"test-token",
|
||||
);
|
||||
expect(parsed.mcpServers.github_inline_comment.env.PR_NUMBER).toBe("456");
|
||||
});
|
||||
|
||||
test("should include comment server when no GitHub tools are allowed and signing disabled", async () => {
|
||||
@@ -195,7 +162,7 @@ describe("prepareMcpConfig", () => {
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: ["Edit", "Read", "Write"],
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
@@ -206,301 +173,7 @@ describe("prepareMcpConfig", () => {
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
});
|
||||
|
||||
test("should return base config when additional config is empty string", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: "",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should return base config when additional config is whitespace only", async () => {
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: " \n\t ",
|
||||
allowedTools: [],
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_comment).toBeDefined();
|
||||
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("should merge valid additional config with base config", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
custom_server: {
|
||||
command: "custom-command",
|
||||
args: ["arg1", "arg2"],
|
||||
env: {
|
||||
CUSTOM_ENV: "custom-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
);
|
||||
expect(parsed.mcpServers.github).toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||
expect(parsed.mcpServers.custom_server.command).toBe("custom-command");
|
||||
expect(parsed.mcpServers.custom_server.args).toEqual(["arg1", "arg2"]);
|
||||
expect(parsed.mcpServers.custom_server.env.CUSTOM_ENV).toBe("custom-value");
|
||||
});
|
||||
|
||||
test("should override built-in servers when additional config has same server names", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
github: {
|
||||
command: "overridden-command",
|
||||
args: ["overridden-arg"],
|
||||
env: {
|
||||
OVERRIDDEN_ENV: "overridden-value",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_file_ops__commit_files",
|
||||
],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
);
|
||||
expect(parsed.mcpServers.github.command).toBe("overridden-command");
|
||||
expect(parsed.mcpServers.github.args).toEqual(["overridden-arg"]);
|
||||
expect(parsed.mcpServers.github.env.OVERRIDDEN_ENV).toBe(
|
||||
"overridden-value",
|
||||
);
|
||||
expect(
|
||||
parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN,
|
||||
).toBeUndefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should merge additional root-level properties", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
customProperty: "custom-value",
|
||||
anotherProperty: {
|
||||
nested: "value",
|
||||
},
|
||||
mcpServers: {
|
||||
custom_server: {
|
||||
command: "custom",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.customProperty).toBe("custom-value");
|
||||
expect(parsed.anotherProperty).toEqual({ nested: "value" });
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.custom_server).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle invalid JSON gracefully", async () => {
|
||||
const invalidJson = "{ invalid json }";
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: invalidJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle non-object JSON values", async () => {
|
||||
const nonObjectJson = JSON.stringify("string value");
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: nonObjectJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||
);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle null JSON value", async () => {
|
||||
const nullJson = JSON.stringify(null);
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: nullJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to parse additional MCP config:"),
|
||||
);
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("MCP config must be a valid JSON object"),
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should handle array JSON value", async () => {
|
||||
const arrayJson = JSON.stringify([1, 2, 3]);
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: arrayJson,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
// Arrays are objects in JavaScript, so they pass the object check
|
||||
// But they'll fail when trying to spread or access mcpServers property
|
||||
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
||||
"Merging additional MCP server configuration with built-in servers",
|
||||
);
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
|
||||
expect(parsed[0]).toBe(1);
|
||||
expect(parsed[1]).toBe(2);
|
||||
expect(parsed[2]).toBe(3);
|
||||
});
|
||||
|
||||
test("should merge complex nested configurations", async () => {
|
||||
const additionalConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
server1: {
|
||||
command: "cmd1",
|
||||
env: { KEY1: "value1" },
|
||||
},
|
||||
server2: {
|
||||
command: "cmd2",
|
||||
env: { KEY2: "value2" },
|
||||
},
|
||||
github_file_ops: {
|
||||
command: "overridden",
|
||||
env: { CUSTOM: "value" },
|
||||
},
|
||||
},
|
||||
otherConfig: {
|
||||
nested: {
|
||||
deeply: "value",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
additionalMcpConfig: additionalConfig,
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.server1).toBeDefined();
|
||||
expect(parsed.mcpServers.server2).toBeDefined();
|
||||
expect(parsed.mcpServers.github).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
|
||||
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
|
||||
expect(parsed.otherConfig.nested.deeply).toBe("value");
|
||||
});
|
||||
|
||||
test("should preserve GITHUB_ACTION_PATH in file_ops server args", async () => {
|
||||
const oldEnv = process.env.GITHUB_ACTION_PATH;
|
||||
test("should set GITHUB_ACTION_PATH correctly", async () => {
|
||||
process.env.GITHUB_ACTION_PATH = "/test/action/path";
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
@@ -514,15 +187,12 @@ describe("prepareMcpConfig", () => {
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_file_ops.args[1]).toBe(
|
||||
expect(parsed.mcpServers.github_file_ops.args).toContain(
|
||||
"/test/action/path/src/mcp/github-file-ops-server.ts",
|
||||
);
|
||||
|
||||
process.env.GITHUB_ACTION_PATH = oldEnv;
|
||||
});
|
||||
|
||||
test("should use process.cwd() when GITHUB_WORKSPACE is not set", async () => {
|
||||
const oldEnv = process.env.GITHUB_WORKSPACE;
|
||||
test("should use current working directory when GITHUB_WORKSPACE is not set", async () => {
|
||||
delete process.env.GITHUB_WORKSPACE;
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
@@ -537,22 +207,11 @@ describe("prepareMcpConfig", () => {
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_file_ops.env.REPO_DIR).toBe(process.cwd());
|
||||
|
||||
process.env.GITHUB_WORKSPACE = oldEnv;
|
||||
});
|
||||
|
||||
test("should include github_ci server when context.isPR is true and workflow token is present", async () => {
|
||||
const oldEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||
test("should include CI server when context.isPR is true and DEFAULT_WORKFLOW_TOKEN exists", async () => {
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
|
||||
|
||||
const contextWithPermissions = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
useCommitSigning: true,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
@@ -560,16 +219,15 @@ describe("prepareMcpConfig", () => {
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
context: mockPRContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
|
||||
expect(parsed.mcpServers.github_ci.env.PR_NUMBER).toBe("456");
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN = oldEnv;
|
||||
delete process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||
});
|
||||
|
||||
test("should not include github_ci server when context.isPR is false", async () => {
|
||||
@@ -580,16 +238,14 @@ describe("prepareMcpConfig", () => {
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockContextWithSigning,
|
||||
context: mockContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
});
|
||||
|
||||
test("should not include github_ci server when workflow token is not present", async () => {
|
||||
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||
test("should not include github_ci server when DEFAULT_WORKFLOW_TOKEN is missing", async () => {
|
||||
delete process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
@@ -599,73 +255,10 @@ describe("prepareMcpConfig", () => {
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: mockPRContextWithSigning,
|
||||
context: mockPRContext,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
||||
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
||||
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
|
||||
});
|
||||
|
||||
test("should include github_ci server when workflow token is present for PR context", async () => {
|
||||
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
|
||||
|
||||
const contextWithPermissions = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
|
||||
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
|
||||
});
|
||||
|
||||
test("should warn when workflow token lacks actions:read permission", async () => {
|
||||
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN = "invalid-token";
|
||||
|
||||
const contextWithPermissions = {
|
||||
...mockPRContext,
|
||||
inputs: {
|
||||
...mockPRContext.inputs,
|
||||
},
|
||||
};
|
||||
|
||||
const result = await prepareMcpConfig({
|
||||
githubToken: "test-token",
|
||||
owner: "test-owner",
|
||||
repo: "test-repo",
|
||||
branch: "test-branch",
|
||||
baseBranch: "main",
|
||||
allowedTools: [],
|
||||
context: contextWithPermissions,
|
||||
});
|
||||
|
||||
const parsed = JSON.parse(result);
|
||||
expect(parsed.mcpServers.github_ci).toBeDefined();
|
||||
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining(
|
||||
"The github_ci MCP server requires 'actions: read' permission",
|
||||
),
|
||||
);
|
||||
|
||||
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -104,6 +104,12 @@ describe("Agent Mode", () => {
|
||||
eventName: "workflow_dispatch",
|
||||
});
|
||||
|
||||
// Save original env vars and set test values
|
||||
const originalHeadRef = process.env.GITHUB_HEAD_REF;
|
||||
const originalRefName = process.env.GITHUB_REF_NAME;
|
||||
delete process.env.GITHUB_HEAD_REF;
|
||||
delete process.env.GITHUB_REF_NAME;
|
||||
|
||||
// Set CLAUDE_ARGS environment variable
|
||||
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
|
||||
|
||||
@@ -120,12 +126,12 @@ describe("Agent Mode", () => {
|
||||
expect(callArgs[1]).toContain("--mcp-config");
|
||||
expect(callArgs[1]).toContain("--model claude-sonnet-4 --max-turns 10");
|
||||
|
||||
// Verify return structure
|
||||
// Verify return structure - should use "main" as fallback when no env vars set
|
||||
expect(result).toEqual({
|
||||
commentId: undefined,
|
||||
branchInfo: {
|
||||
baseBranch: "",
|
||||
currentBranch: "",
|
||||
baseBranch: "main",
|
||||
currentBranch: "main",
|
||||
claudeBranch: undefined,
|
||||
},
|
||||
mcpConfig: expect.any(String),
|
||||
@@ -133,6 +139,10 @@ describe("Agent Mode", () => {
|
||||
|
||||
// Clean up
|
||||
delete process.env.CLAUDE_ARGS;
|
||||
if (originalHeadRef !== undefined)
|
||||
process.env.GITHUB_HEAD_REF = originalHeadRef;
|
||||
if (originalRefName !== undefined)
|
||||
process.env.GITHUB_REF_NAME = originalRefName;
|
||||
});
|
||||
|
||||
test("prepare method creates prompt file with correct content", async () => {
|
||||
|
||||
71
test/modes/parse-tools.test.ts
Normal file
71
test/modes/parse-tools.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { parseAllowedTools } from "../../src/modes/agent/parse-tools";
|
||||
|
||||
describe("parseAllowedTools", () => {
|
||||
test("parses unquoted tools", () => {
|
||||
const args = "--allowedTools mcp__github__*,mcp__github_comment__*";
|
||||
expect(parseAllowedTools(args)).toEqual([
|
||||
"mcp__github__*",
|
||||
"mcp__github_comment__*",
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses double-quoted tools", () => {
|
||||
const args = '--allowedTools "mcp__github__*,mcp__github_comment__*"';
|
||||
expect(parseAllowedTools(args)).toEqual([
|
||||
"mcp__github__*",
|
||||
"mcp__github_comment__*",
|
||||
]);
|
||||
});
|
||||
|
||||
test("parses single-quoted tools", () => {
|
||||
const args = "--allowedTools 'mcp__github__*,mcp__github_comment__*'";
|
||||
expect(parseAllowedTools(args)).toEqual([
|
||||
"mcp__github__*",
|
||||
"mcp__github_comment__*",
|
||||
]);
|
||||
});
|
||||
|
||||
test("returns empty array when no allowedTools", () => {
|
||||
const args = "--someOtherFlag value";
|
||||
expect(parseAllowedTools(args)).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles empty string", () => {
|
||||
expect(parseAllowedTools("")).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles duplicate --allowedTools flags", () => {
|
||||
const args = "--allowedTools --allowedTools mcp__github__*";
|
||||
// Should not match the first one since the value is another flag
|
||||
expect(parseAllowedTools(args)).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles typo --alloedTools", () => {
|
||||
const args = "--alloedTools mcp__github__*";
|
||||
expect(parseAllowedTools(args)).toEqual([]);
|
||||
});
|
||||
|
||||
test("handles multiple flags with allowedTools in middle", () => {
|
||||
const args =
|
||||
'--flag1 value1 --allowedTools "mcp__github__*" --flag2 value2';
|
||||
expect(parseAllowedTools(args)).toEqual(["mcp__github__*"]);
|
||||
});
|
||||
|
||||
test("trims whitespace from tool names", () => {
|
||||
const args = "--allowedTools 'mcp__github__* , mcp__github_comment__* '";
|
||||
expect(parseAllowedTools(args)).toEqual([
|
||||
"mcp__github__*",
|
||||
"mcp__github_comment__*",
|
||||
]);
|
||||
});
|
||||
|
||||
test("handles tools with special characters", () => {
|
||||
const args =
|
||||
'--allowedTools "mcp__github__create_issue,mcp__github_comment__update"';
|
||||
expect(parseAllowedTools(args)).toEqual([
|
||||
"mcp__github__create_issue",
|
||||
"mcp__github_comment__update",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
normalizeHtmlEntities,
|
||||
sanitizeContent,
|
||||
stripHtmlComments,
|
||||
redactGitHubTokens,
|
||||
} from "../src/github/utils/sanitizer";
|
||||
|
||||
describe("stripInvisibleCharacters", () => {
|
||||
@@ -242,6 +243,109 @@ describe("sanitizeContent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("redactGitHubTokens", () => {
|
||||
it("should redact personal access tokens (ghp_)", () => {
|
||||
const token = "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";
|
||||
expect(redactGitHubTokens(`Token: ${token}`)).toBe(
|
||||
"Token: [REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
expect(redactGitHubTokens(`Here's a token: ${token} in text`)).toBe(
|
||||
"Here's a token: [REDACTED_GITHUB_TOKEN] in text",
|
||||
);
|
||||
});
|
||||
|
||||
it("should redact OAuth tokens (gho_)", () => {
|
||||
const token = "gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
||||
expect(redactGitHubTokens(`OAuth: ${token}`)).toBe(
|
||||
"OAuth: [REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
});
|
||||
|
||||
it("should redact installation tokens (ghs_)", () => {
|
||||
const token = "ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW";
|
||||
expect(redactGitHubTokens(`Install token: ${token}`)).toBe(
|
||||
"Install token: [REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
});
|
||||
|
||||
it("should redact refresh tokens (ghr_)", () => {
|
||||
const token = "ghr_1B4a2e77838347a253e56d7b5253e7d11667";
|
||||
expect(redactGitHubTokens(`Refresh: ${token}`)).toBe(
|
||||
"Refresh: [REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
});
|
||||
|
||||
it("should redact fine-grained tokens (github_pat_)", () => {
|
||||
const token =
|
||||
"github_pat_11ABCDEFG0example5of9_2nVwvsylpmOLboQwTPTLewDcE621dQ0AAaBBCCDDEEFFHH";
|
||||
expect(redactGitHubTokens(`Fine-grained: ${token}`)).toBe(
|
||||
"Fine-grained: [REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle tokens in code blocks", () => {
|
||||
const content = `\`\`\`bash
|
||||
export GITHUB_TOKEN=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
|
||||
\`\`\``;
|
||||
const expected = `\`\`\`bash
|
||||
export GITHUB_TOKEN=[REDACTED_GITHUB_TOKEN]
|
||||
\`\`\``;
|
||||
expect(redactGitHubTokens(content)).toBe(expected);
|
||||
});
|
||||
|
||||
it("should handle multiple tokens in one text", () => {
|
||||
const content =
|
||||
"Token 1: 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 and token 2: gho_16C7e42F292c6912E7710c838347Ae178B4a";
|
||||
expect(redactGitHubTokens(content)).toBe(
|
||||
"Token 1: [REDACTED_GITHUB_TOKEN] and token 2: [REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle tokens in URLs", () => {
|
||||
const content =
|
||||
"https://api.github.com/user?access_token=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";
|
||||
expect(redactGitHubTokens(content)).toBe(
|
||||
"https://api.github.com/user?access_token=[REDACTED_GITHUB_TOKEN]",
|
||||
);
|
||||
});
|
||||
|
||||
it("should not redact partial matches or invalid tokens", () => {
|
||||
const content =
|
||||
"This is not a token: ghp_short or gho_toolong1234567890123456789012345678901234567890";
|
||||
expect(redactGitHubTokens(content)).toBe(content);
|
||||
});
|
||||
|
||||
it("should preserve normal text", () => {
|
||||
const content = "Normal text with no tokens";
|
||||
expect(redactGitHubTokens(content)).toBe(content);
|
||||
});
|
||||
|
||||
it("should handle edge cases", () => {
|
||||
expect(redactGitHubTokens("")).toBe("");
|
||||
expect(redactGitHubTokens("ghp_")).toBe("ghp_");
|
||||
expect(redactGitHubTokens("github_pat_short")).toBe("github_pat_short");
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeContent with token redaction", () => {
|
||||
it("should redact tokens as part of full sanitization", () => {
|
||||
const content = `
|
||||
<!-- Hidden comment with token: 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 -->
|
||||
Here's some text with a token: gho_16C7e42F292c6912E7710c838347Ae178B4a
|
||||
And invisible chars: test\u200Btoken
|
||||
`;
|
||||
|
||||
const sanitized = sanitizeContent(content);
|
||||
|
||||
expect(sanitized).not.toContain("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");
|
||||
expect(sanitized).not.toContain("gho_16C7e42F292c6912E7710c838347Ae178B4a");
|
||||
expect(sanitized).not.toContain("<!-- Hidden comment");
|
||||
expect(sanitized).not.toContain("\u200B");
|
||||
expect(sanitized).toContain("[REDACTED_GITHUB_TOKEN]");
|
||||
expect(sanitized).toContain("Here's some text with a token:");
|
||||
});
|
||||
});
|
||||
|
||||
describe("stripHtmlComments (legacy)", () => {
|
||||
it("should remove HTML comments", () => {
|
||||
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Custom prompt content
|
||||
Reference in New Issue
Block a user