Compare commits

..

45 Commits

Author SHA1 Message Date
Kashyap Murali
a630fad41e Test: Trigger CI failure for auto-fix workflow 2025-08-20 12:29:08 -07:00
Kashyap Murali
130874e0b6 Debug: Add logging and always output github_token in prepare step 2025-08-20 11:43:43 -07:00
Kashyap Murali
c72a45a95f feat: Expose GitHub token as action output for external use
This allows workflows to use the Claude App token obtained by the action
for posting comments as claude[bot] instead of github-actions[bot].

Changes:
- Add github_token output to action.yml
- Export token from prepare.ts after authentication
- Allows workflows to use the same token Claude uses internally
2025-08-19 22:53:43 -07:00
km-anthropic
bf04905b4c Remove auto-fix workflows and commands from v1-dev
These files should only exist in km-anthropic fork:
- .github/workflows/auto-fix-ci.yml
- .github/workflows/auto-fix-ci-inline.yml
- slash-commands/fix-ci.md
- .claude/commands/fix-ci.md

The workflow_run event support remains as it's useful for general automation.
2025-08-19 16:22:01 -07:00
km-anthropic
52736c6e60 bun formatting 2025-08-19 16:16:12 -07:00
km-anthropic
91034c2a0e Use proper WorkflowRunEvent type instead of any 2025-08-19 13:56:33 -07:00
km-anthropic
8d32355bcc Add workflow_run event support and auto-fix CI workflows
- Add support for workflow_run event type in GitHub context
- Create /fix-ci slash command for programmatic CI failure fixing
- Add auto-fix-ci.yml workflow using slash command approach
- Add auto-fix-ci-inline.yml workflow with full inline prompt
- Both workflows automatically analyze CI failures and create fix branches
- Fix workflow syntax issues with optional chaining operator
2025-08-19 13:54:17 -07:00
km-anthropic
4ec65ed46e Add auto-fix CI workflows with slash command and inline approaches
- Add /fix-ci slash command for programmatic CI failure fixing
- Create auto-fix-ci.yml workflow using slash command approach
- Create auto-fix-ci-inline.yml workflow with full inline prompt
- Both workflows automatically analyze CI failures and create fix branches
2025-08-19 13:42:25 -07:00
km-anthropic
9613b21ad7 Merge branch 'main' of https://github.com/anthropics/claude-code-action into v1-dev 2025-08-18 17:10:03 -07:00
Ashwin Bhat
68b7ca379c include input bools in claude env (#464) 2025-08-18 17:00:18 -07:00
GitHub Actions
900322ca88 chore: bump Claude Code version to 1.0.84 2025-08-18 23:43:42 +00:00
Ashwin Bhat
8f0a7fe9d3 clarify workflow validation message (#463)
Update the workflow validation message to be more specific about when
Claude Code workflows will start working, providing clearer guidance
to users experiencing this validation error.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-18 15:50:27 -07:00
Chris Burns
db36412854 provides github token for claude code action (#462)
Currently when running the `gh` command in the action, there is an error in the action logs that suggests that the GH_TOKEN isn't being set. We've solved this internally in our company by providing the GH_TOKEN in the action.
2025-08-18 13:13:34 -07:00
Ashwin Bhat
f05d669d5f fix: prevent undefined directory creation when RUNNER_TEMP is not set (#461)
When running tests locally, process.env.RUNNER_TEMP is undefined, causing
the code to literally create "undefined/claude-prompts/" directories in
the working directory. Added fallback to "/tmp" following the pattern
already used in src/mcp/github-actions-server.ts.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-18 10:51:57 -07:00
Ashwin Bhat
e89411bb6f feat: skip action gracefully for workflow validation errors (#460)
* feat: skip action gracefully for workflow validation errors

Handle workflow_not_found_on_default_branch and workflow_content_mismatch
errors by skipping the action with a warning instead of failing. This
improves user experience when adding Claude Code workflows to new
repositories or making workflow changes in PRs.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Update src/github/token.ts

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-18 10:50:15 -07:00
Hironori Yamamoto
02e9ed3181 fix: add Claude Code binary to GitHub Actions PATH (#455) 2025-08-17 21:06:17 -07:00
GitHub Actions
78b07473f5 chore: bump Claude Code version to 1.0.83 2025-08-16 00:11:18 +00:00
Ashwin Bhat
f562ed53e2 fix typo in example (#454) 2025-08-15 13:33:01 -07:00
Ashwin Bhat
a1507aefdc Add GitHub token redaction to comment tools (#453)
* Add GitHub token redaction to update_claude_comment tool

- Add redactGitHubTokens() function to sanitizer.ts that detects and redacts all GitHub token formats (ghp_, gho_, ghs_, ghr_, github_pat_)
- Update sanitizeContent() to include token redaction in the sanitization pipeline
- Apply sanitization to comment body in github-comment-server.ts before updating comments
- Add comprehensive tests covering all token formats, edge cases, and integration scenarios
- Prevents accidental exposure of GitHub tokens in PR/issue comments while preserving existing functionality

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add GitHub token redaction to inline comment server

- Apply sanitizeContent() to comment body in github-inline-comment-server.ts before creating inline PR comments
- Ensures consistency in token redaction across all comment creation tools
- Prevents GitHub tokens from being exposed in inline PR review comments

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-15 13:04:52 -07:00
Ashwin Bhat
ae66eb6a64 Switch to curl-based Claude Code installation (#452)
Replace bun install with official install script for more reliable
installation across different environments.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-15 09:11:02 -07:00
Ashwin Bhat
432c7cc889 update example workflow (#451) 2025-08-14 19:09:58 -07:00
Ashwin Bhat
0b138d9d49 Update token.ts copy (#450) 2025-08-14 16:42:49 -07:00
km-anthropic
30530c9829 formatting 2025-08-14 15:30:29 -07:00
km-anthropic
632f04bbcf feat: Copy project subagents to Claude runtime environment
Enables custom subagents defined in .claude/agents/ to work in GitHub Actions by:
- Checking for project agents in GITHUB_WORKSPACE/.claude/agents/
- Creating ~/.claude/agents/ directory if needed
- Copying all .md agent files to Claude's runtime location
- Following same pattern as slash commands for consistency

Includes comprehensive test coverage for the new functionality.
2025-08-14 15:29:05 -07:00
GitHub Actions
c34e066a3b chore: bump Claude Code version to 1.0.81 2025-08-14 17:00:23 +00:00
GitHub Actions
449c6791bd chore: bump Claude Code version to 1.0.80 2025-08-13 21:17:49 +00:00
GitHub Actions
2b67ac084b chore: bump Claude Code version to 1.0.77 2025-08-13 20:33:11 +00:00
GitHub Actions
76de8a48fc chore: bump Claude Code version to 1.0.79 2025-08-13 20:26:17 +00:00
km-anthropic
d91030de69 Simplify MCP configuration to use multiple --mcp-config flags
- Remove MCP config merging logic from prepareMcpConfig
- Update agent and tag modes to pass multiple --mcp-config flags
- Let Claude handle config merging natively through multiple flags
- Fix TypeScript errors in test file

This approach is cleaner and relies on Claude's built-in support for multiple --mcp-config flags instead of manual JSON merging.
2025-08-13 11:40:41 -07:00
km-anthropic
ab7f1d65d6 minor formatting 2025-08-13 10:19:53 -07:00
km-anthropic
c13c2af69f Better fix: Control environment variables in agent test for predictable behavior 2025-08-13 10:18:17 -07:00
km-anthropic
0ac14b0d37 Fix agent test to handle dynamic branch names from environment 2025-08-13 10:17:25 -07:00
km-anthropic
8c230f7e04 Fix agent mode test to expect branch values 2025-08-13 10:10:51 -07:00
km-anthropic
d24561da51 Format code with prettier 2025-08-13 09:54:57 -07:00
km-anthropic
e3b5697276 Add GitHub MCP support to agent mode
- Parse --allowedTools from claude_args to detect when user wants GitHub MCPs
- Wire up github_inline_comment server in prepareMcpConfig for PR contexts
- Update agent mode to use prepareMcpConfig instead of manual config
- Add comprehensive tests for parseAllowedTools edge cases
- Fix TypeScript types to support both entity and automation contexts
2025-08-13 09:26:04 -07:00
km-anthropic
eb146ef8b8 Merge branch 'main' of https://github.com/anthropics/claude-code-action into v1-dev 2025-08-12 14:42:10 -07:00
km-anthropic
73948c338f Remove GitHub context prefixing and clean up agent mode
- Remove automatic GitHub context fetching and prefixing
- Remove unused imports (fetcher, formatter, context checks)
- Clean up comments
- Agent mode now simply passes through the user's prompt as-is
2025-08-12 13:59:52 -07:00
km-anthropic
30fb4ed5a6 Remove all default MCP servers from agent mode
Agent mode now starts with no default servers - users must explicitly configure any MCP servers they need via mcp_config input
2025-08-12 13:56:13 -07:00
km-anthropic
24433f34e3 Remove github_comment and inline_comment servers from agent mode defaults
- Agent mode now only includes the main GitHub MCP server by default
- Users can add additional servers via mcp_config if needed
- Reduces unnecessary MCP server overhead
2025-08-12 13:06:15 -07:00
km-anthropic
86e2835d40 Delete .github/workflows/claude-auto-review-test.yml 2025-08-12 12:41:56 -07:00
GitHub Actions
a80505bbfb chore: bump Claude Code version to 1.0.77 2025-08-12 19:25:39 +00:00
km-anthropic
55e94369f7 Add GitHub MCP server and context prefix to agent mode
- Include main GitHub MCP server (Docker-based) by default
- Fetch and prefix GitHub context to prompts when in PR/issue context
- Users no longer need to manually configure GitHub tools
2025-08-12 12:10:33 -07:00
GitHub Actions
af23644a50 chore: bump Claude Code version to 1.0.76 2025-08-12 18:10:59 +00:00
GitHub Actions
98e6a902bf chore: bump Claude Code version to 1.0.74 2025-08-12 16:19:34 +00:00
GitHub Actions
8b2bd6d04f chore: bump Claude Code version to 1.0.73 2025-08-11 23:43:47 +00:00
39 changed files with 813 additions and 1236 deletions

View File

@@ -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.

View File

@@ -1,55 +0,0 @@
---
description: Fix CI failures and commit changes (for use when branch already exists)
---
# 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:
```bash
# Stage all changes
git add -A
# Commit with descriptive message
git commit -m "Fix CI failures
- Fixed syntax errors
- Fixed formatting issues
- Fixed test failures
[List actual fixes made]"
```
**IMPORTANT**: You MUST use the Bash tool to run the git add and git commit commands above. 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.

View File

@@ -1,66 +0,0 @@
---
description: Analyze and fix CI failures by examining logs and making targeted fixes
---
# 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, use the Bash tool to:
1. Run: `git add -A` to stage all modified files
2. Run: `git commit -m "Fix CI failures: <description>"` to commit the changes
3. Include details about which CI jobs/tests were fixed in the commit message
4. Important: You MUST use the Bash tool to run these git commands 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.

View File

@@ -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)

View File

@@ -1,175 +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: km-anthropic/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"
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]?.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 }})._`
});
}

View File

@@ -1,117 +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: km-anthropic/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"
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]?.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 }})._`
});
}

View File

@@ -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

View File

@@ -18,9 +18,9 @@ jobs:
fetch-depth: 1 fetch-depth: 1
- name: Auto review PR - name: Auto review PR
uses: anthropics/claude-code-action@v1-dev uses: anthropics/claude-code-action@main
with: with:
prompt: | direct_prompt: |
Please review this PR. Look at the changes and provide thoughtful feedback on: Please review this PR. Look at the changes and provide thoughtful feedback on:
- Code quality and best practices - Code quality and best practices
- Potential bugs or issues - Potential bugs or issues
@@ -30,4 +30,4 @@ jobs:
Be constructive and specific in your feedback. Give inline comments where applicable. Be constructive and specific in your feedback. Give inline comments where applicable.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 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"

View File

@@ -31,13 +31,9 @@ jobs:
- name: Run Claude Code - name: Run Claude Code
id: claude id: claude
uses: janeapp/claude-code-action@main uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)" 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." 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" 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"

View File

@@ -104,3 +104,5 @@ jobs:
mcp_config: /tmp/mcp-config/mcp-servers.json mcp_config: /tmp/mcp-config/mcp-servers.json
timeout_minutes: "5" timeout_minutes: "5"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

175
AUTO_FIX_CHECKPOINT.md Normal file
View 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*

View File

@@ -1,6 +1,6 @@
![Claude Code Action responding to a comment](https://github.com/user-attachments/assets/1d60c2e9-82ed-4ee5-b749-f9e021c85f4d) ![Claude Code Action responding to a comment](https://github.com/user-attachments/assets/1d60c2e9-82ed-4ee5-b749-f9e021c85f4d)
# 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. 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.

View File

@@ -93,6 +93,9 @@ outputs:
branch_name: branch_name:
description: "The branch created by Claude Code for this execution" description: "The branch created by Claude Code for this execution"
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }} 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: runs:
using: "composite" using: "composite"
@@ -130,6 +133,7 @@ runs:
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }} ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }} CLAUDE_ARGS: ${{ inputs.claude_args }}
MCP_CONFIG: ${{ inputs.mcp_config }} MCP_CONFIG: ${{ inputs.mcp_config }}
ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies - name: Install Base Action Dependencies
if: steps.prepare.outputs.contains_trigger == 'true' if: steps.prepare.outputs.contains_trigger == 'true'
@@ -141,7 +145,8 @@ runs:
echo "Base-action dependencies installed" echo "Base-action dependencies installed"
cd - cd -
# Install Claude Code globally # 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 - name: Setup Network Restrictions
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != '' if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
@@ -168,6 +173,7 @@ runs:
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }} INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }} INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
# Model configuration # Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
@@ -241,7 +247,7 @@ runs:
fi fi
- name: Revoke app token - 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 shell: bash
run: | run: |
curl -L \ curl -L \

View File

@@ -85,7 +85,7 @@ runs:
- name: Install Claude Code - name: Install Claude Code
shell: bash 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 - name: Run Claude Code Action
shell: bash shell: bash

View File

@@ -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 { return {
claudeArgs, claudeArgs,
promptPath, promptPath,
env: {}, env: customEnv,
}; };
} }
@@ -88,9 +94,11 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
console.log(`Prompt file size: ${promptSize} bytes`); console.log(`Prompt file size: ${promptSize} bytes`);
// Log custom environment variables if any // Log custom environment variables if any
if (Object.keys(config.env).length > 0) { const customEnvKeys = Object.keys(config.env).filter(
const envKeys = Object.keys(config.env).join(", "); (key) => key !== "CLAUDE_ACTION_INPUTS_PRESENT",
console.log(`Custom environment variables: ${envKeys}`); );
if (customEnvKeys.length > 0) {
console.log(`Custom environment variables: ${customEnvKeys.join(", ")}`);
} }
// Log custom arguments if any // Log custom arguments if any

View File

@@ -80,36 +80,26 @@ export async function setupClaudeCodeSettings(
} }
} }
// Copy subagent files from repository to Claude's agents directory // Copy project subagents to Claude's agents directory
// CLAUDE_WORKING_DIR is set by the action to point to the repo being processed // Use GITHUB_WORKSPACE if available (set by GitHub Actions), otherwise use current directory
const workingDir = process.env.CLAUDE_WORKING_DIR || process.cwd(); const workspaceDir = process.env.GITHUB_WORKSPACE || process.cwd();
const repoAgentsDir = `${workingDir}/.claude/agents`; const projectAgentsDir = `${workspaceDir}/.claude/agents`;
const targetAgentsDir = `${home}/.claude/agents`; const claudeAgentsDir = `${home}/.claude/agents`;
try { try {
const agentsDirExists = await $`test -d ${repoAgentsDir}`.quiet().nothrow(); await $`test -d ${projectAgentsDir}`.quiet();
if (agentsDirExists.exitCode === 0) { console.log(`Found project agents directory at ${projectAgentsDir}`);
console.log(`Found subagents directory at ${repoAgentsDir}`);
// Create target agents directory if it doesn't exist await $`mkdir -p ${claudeAgentsDir}`.quiet();
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 await $`cp ${projectAgentsDir}/*.md ${claudeAgentsDir}/ 2>/dev/null || true`.quiet();
const copyResult = await $`cp -r ${repoAgentsDir}/*.md ${targetAgentsDir}/ 2>/dev/null`.quiet().nothrow();
if (copyResult.exitCode === 0) { const agentFiles = await $`ls ${claudeAgentsDir}/*.md 2>/dev/null | wc -l`
// List copied agents for logging .quiet()
const agents = await $`ls -la ${targetAgentsDir}/*.md 2>/dev/null | wc -l`.quiet().text(); .text();
const agentCount = parseInt(agents.trim()) || 0; const agentCount = parseInt(agentFiles.trim()) || 0;
console.log(`Successfully copied ${agentCount} subagent(s) to ${targetAgentsDir}`); console.log(`Copied ${agentCount} agent(s) to ${claudeAgentsDir}`);
} else {
console.log(`No subagent files found in ${repoAgentsDir}`);
}
} else {
console.log(`No subagents directory found at ${repoAgentsDir}`);
}
} catch (e) { } catch (e) {
console.log(`Error handling subagents: ${e}`); console.log(`No project agents directory found at ${projectAgentsDir}`);
} }
} }

View File

@@ -215,4 +215,70 @@ describe("setupClaudeCodeSettings", () => {
const settingsContent = await readFile(settingsPath, "utf-8"); const settingsContent = await readFile(settingsPath, "utf-8");
expect(JSON.parse(settingsContent).enableAllProjectMcpServers).toBe(true); 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;
}
}
});
}); });

View File

@@ -1,4 +1,4 @@
name: Claude PR Assistant name: Claude Code
on: on:
issue_comment: issue_comment:
@@ -11,38 +11,53 @@ on:
types: [submitted] types: [submitted]
jobs: jobs:
claude-code-action: claude:
if: | if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || (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_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.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 runs-on: ubuntu-latest
permissions: permissions:
contents: read contents: write
pull-requests: read pull-requests: write
issues: read issues: write
id-token: write id-token: write
actions: read # Required for Claude to read CI results on PRs
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Run Claude PR Action - name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or use OAuth token instead:
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} # This is an optional setting that allows Claude to read CI results on PRs
timeout_minutes: "60" additional_permissions: |
# mode: tag # Default: responds to @claude mentions actions: read
# Optional: Restrict network access to specific domains only
# experimental_allowed_domains: | # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
# .anthropic.com # model: "claude-opus-4-1-20250805"
# .github.com
# api.github.com # Optional: Customize the trigger phrase (default: @claude)
# .githubusercontent.com # trigger_phrase: "/claude"
# bun.sh
# registry.npmjs.org # Optional: Trigger when specific user is assigned to an issue
# .blob.core.windows.net # 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

View File

@@ -1,55 +0,0 @@
---
description: Fix CI failures and commit changes (for use when branch already exists)
---
# 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:
```bash
# Stage all changes
git add -A
# Commit with descriptive message
git commit -m "Fix CI failures
- Fixed syntax errors
- Fixed formatting issues
- Fixed test failures
[List actual fixes made]"
```
**IMPORTANT**: You MUST use the Bash tool to run the git add and git commit commands above. 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.

View File

@@ -1,66 +0,0 @@
---
description: Analyze and fix CI failures by examining logs and making targeted fixes
---
# 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. Stage all modified files with `git add -A` or `git add .`
2. Commit with a descriptive message using `git commit -m "Fix CI failures: <description>"`
3. Include details about which CI jobs/tests were fixed in the commit message
4. Important: Always commit your changes even if you're already on a branch
## 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.

View File

@@ -750,7 +750,7 @@ export async function createPrompt(
modeContext.claudeBranch, modeContext.claudeBranch,
); );
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, { await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true, recursive: true,
}); });
@@ -769,7 +769,7 @@ export async function createPrompt(
// Write the prompt file // Write the prompt file
await writeFile( await writeFile(
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`, `${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
promptContent, promptContent,
); );

View 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));
}

View File

@@ -12,9 +12,12 @@ import { createOctokit } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context"; import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry"; import { getMode } from "../modes/registry";
import { prepare } from "../prepare"; import { prepare } from "../prepare";
import { collectActionInputsPresence } from "./collect-inputs";
async function run() { async function run() {
try { try {
collectActionInputsPresence();
// Parse GitHub context first to enable mode detection // Parse GitHub context first to enable mode detection
const context = parseGitHubContext(); const context = parseGitHubContext();
@@ -41,11 +44,18 @@ async function run() {
// Check trigger conditions // Check trigger conditions
const containsTrigger = mode.shouldTrigger(context); 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 // Set output for action.yml to check
core.setOutput("contains_trigger", containsTrigger.toString()); core.setOutput("contains_trigger", containsTrigger.toString());
if (!containsTrigger) { if (!containsTrigger) {
console.log("No trigger found, skipping remaining steps"); console.log("No trigger found, skipping remaining steps");
// Still set github_token output even when skipping
core.setOutput("github_token", githubToken);
return; return;
} }
@@ -59,6 +69,9 @@ async function run() {
// MCP config is handled by individual modes (tag/agent) and included in their claude_args output // 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 // Step 6: Get system prompt from mode if available
if (mode.getSystemPrompt) { if (mode.getSystemPrompt) {
const modeContext = mode.prepareContext(context, { const modeContext = mode.prepareContext(context, {

View File

@@ -6,6 +6,7 @@ import type {
PullRequestEvent, PullRequestEvent,
PullRequestReviewEvent, PullRequestReviewEvent,
PullRequestReviewCommentEvent, PullRequestReviewCommentEvent,
WorkflowRunEvent,
} from "@octokit/webhooks-types"; } from "@octokit/webhooks-types";
// Custom types for GitHub Actions events that aren't webhooks // Custom types for GitHub Actions events that aren't webhooks
export type WorkflowDispatchEvent = { export type WorkflowDispatchEvent = {
@@ -44,7 +45,11 @@ const ENTITY_EVENT_NAMES = [
"pull_request_review_comment", "pull_request_review_comment",
] as const; ] 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 // Derive types from constants for better maintainability
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number]; type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
@@ -86,10 +91,10 @@ export type ParsedGitHubContext = BaseContext & {
isPR: boolean; isPR: boolean;
}; };
// Context for automation events (workflow_dispatch, schedule) // Context for automation events (workflow_dispatch, schedule, workflow_run)
export type AutomationContext = BaseContext & { export type AutomationContext = BaseContext & {
eventName: AutomationEventName; eventName: AutomationEventName;
payload: WorkflowDispatchEvent | ScheduleEvent; payload: WorkflowDispatchEvent | ScheduleEvent | WorkflowRunEvent;
}; };
// Union type for all contexts // Union type for all contexts
@@ -185,6 +190,13 @@ export function parseGitHubContext(): GitHubContext {
payload: context.payload as unknown as ScheduleEvent, payload: context.payload as unknown as ScheduleEvent,
}; };
} }
case "workflow_run": {
return {
...commonFields,
eventName: "workflow_run",
payload: context.payload as unknown as WorkflowRunEvent,
};
}
default: default:
throw new Error(`Unsupported event type: ${context.eventName}`); throw new Error(`Unsupported event type: ${context.eventName}`);
} }

View File

@@ -31,8 +31,30 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
const responseJson = (await response.json()) as { const responseJson = (await response.json()) as {
error?: { error?: {
message?: string; 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( console.error(
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown 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); core.setOutput("GITHUB_TOKEN", appToken);
return appToken; return appToken;
} catch (error) { } catch (error) {
// Only set failed if we get here - workflow validation errors will exit(0) before this
core.setFailed( 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); process.exit(1);
} }

View File

@@ -58,6 +58,41 @@ export function sanitizeContent(content: string): string {
content = stripMarkdownLinkTitles(content); content = stripMarkdownLinkTitles(content);
content = stripHiddenAttributes(content); content = stripHiddenAttributes(content);
content = normalizeHtmlEntities(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; return content;
} }

View File

@@ -6,6 +6,7 @@ import { z } from "zod";
import { GITHUB_API_URL } from "../github/api/config"; import { GITHUB_API_URL } from "../github/api/config";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment"; import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
import { sanitizeContent } from "../github/utils/sanitizer";
// Get repository information from environment variables // Get repository information from environment variables
const REPO_OWNER = process.env.REPO_OWNER; const REPO_OWNER = process.env.REPO_OWNER;
@@ -54,11 +55,13 @@ server.tool(
const isPullRequestReviewComment = const isPullRequestReviewComment =
eventName === "pull_request_review_comment"; eventName === "pull_request_review_comment";
const sanitizedBody = sanitizeContent(body);
const result = await updateClaudeComment(octokit, { const result = await updateClaudeComment(octokit, {
owner, owner,
repo, repo,
commentId, commentId,
body, body: sanitizedBody,
isPullRequestReviewComment, isPullRequestReviewComment,
}); });

View File

@@ -3,6 +3,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; import { z } from "zod";
import { createOctokit } from "../github/api/client"; import { createOctokit } from "../github/api/client";
import { sanitizeContent } from "../github/utils/sanitizer";
// Get repository and PR information from environment variables // Get repository and PR information from environment variables
const REPO_OWNER = process.env.REPO_OWNER; const REPO_OWNER = process.env.REPO_OWNER;
@@ -81,6 +82,9 @@ server.tool(
const octokit = createOctokit(githubToken).rest; 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 // Validate that either line or both startLine and line are provided
if (!line && !startLine) { if (!line && !startLine) {
throw new Error( throw new Error(
@@ -104,7 +108,7 @@ server.tool(
owner, owner,
repo, repo,
pull_number, pull_number,
body, body: sanitizedBody,
path, path,
side: side || "RIGHT", side: side || "RIGHT",
commit_id: commit_id || pr.data.head.sha, commit_id: commit_id || pr.data.head.sha,

View File

@@ -1,6 +1,7 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config"; 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"; import { Octokit } from "@octokit/rest";
type PrepareConfigParams = { type PrepareConfigParams = {
@@ -9,10 +10,9 @@ type PrepareConfigParams = {
repo: string; repo: string;
branch: string; branch: string;
baseBranch: string; baseBranch: string;
additionalMcpConfig?: string;
claudeCommentId?: string; claudeCommentId?: string;
allowedTools: string[]; allowedTools: string[];
context: ParsedGitHubContext; context: GitHubContext;
}; };
async function checkActionsReadPermission( async function checkActionsReadPermission(
@@ -56,7 +56,6 @@ export async function prepareMcpConfig(
repo, repo,
branch, branch,
baseBranch, baseBranch,
additionalMcpConfig,
claudeCommentId, claudeCommentId,
allowedTools, allowedTools,
context, context,
@@ -68,6 +67,10 @@ export async function prepareMcpConfig(
tool.startsWith("mcp__github__"), tool.startsWith("mcp__github__"),
); );
const hasInlineCommentTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github_inline_comment__"),
);
const baseMcpConfig: { mcpServers: Record<string, unknown> } = { const baseMcpConfig: { mcpServers: Record<string, unknown> } = {
mcpServers: {}, 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 // CI server is included when we have a workflow token and context is a PR
const hasWorkflowToken = !!process.env.DEFAULT_WORKFLOW_TOKEN; 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 // Verify the token actually has actions:read permission
const actuallyHasPermission = await checkActionsReadPermission( const actuallyHasPermission = await checkActionsReadPermission(
process.env.DEFAULT_WORKFLOW_TOKEN || "", process.env.DEFAULT_WORKFLOW_TOKEN || "",
@@ -166,38 +191,8 @@ export async function prepareMcpConfig(
}; };
} }
// Merge with additional MCP config if provided // Return only our GitHub servers config
if (additionalMcpConfig && additionalMcpConfig.trim()) { // User's config will be passed as separate --mcp-config flags
try {
const additionalConfig = JSON.parse(additionalMcpConfig);
// Validate that parsed JSON is an object
if (typeof additionalConfig !== "object" || additionalConfig === null) {
throw new Error("MCP config must be a valid JSON object");
}
core.info(
"Merging additional MCP server configuration with built-in servers",
);
// Merge configurations with user config overriding built-in servers
const mergedConfig = {
...baseMcpConfig,
...additionalConfig,
mcpServers: {
...baseMcpConfig.mcpServers,
...additionalConfig.mcpServers,
},
};
return JSON.stringify(mergedConfig, null, 2);
} catch (parseError) {
core.warning(
`Failed to parse additional MCP config: ${parseError}. Using base config only.`,
);
}
}
return JSON.stringify(baseMcpConfig, null, 2); return JSON.stringify(baseMcpConfig, null, 2);
} catch (error) { } catch (error) {
core.setFailed(`Install MCP server failed with error: ${error}`); core.setFailed(`Install MCP server failed with error: ${error}`);

View File

@@ -2,6 +2,8 @@ import * as core from "@actions/core";
import { mkdir, writeFile } from "fs/promises"; import { mkdir, writeFile } from "fs/promises";
import type { Mode, ModeOptions, ModeResult } from "../types"; import type { Mode, ModeOptions, ModeResult } from "../types";
import type { PreparedContext } from "../../create-prompt/types"; import type { PreparedContext } from "../../create-prompt/types";
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { parseAllowedTools } from "./parse-tools";
/** /**
* Agent mode implementation. * Agent mode implementation.
@@ -40,82 +42,71 @@ export const agentMode: Mode = {
}, },
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> { 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 // Create prompt directory
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, { await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true, 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 = const promptContent =
context.inputs.prompt || context.inputs.prompt ||
`Repository: ${context.repository.owner}/${context.repository.repo}`; `Repository: ${context.repository.owner}/${context.repository.repo}`;
await writeFile( await writeFile(
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`, `${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
promptContent, promptContent,
); );
// Agent mode: User has full control via claudeArgs // Parse allowed tools from user's claude_args
// No default tools are enforced - Claude Code's defaults will apply const userClaudeArgs = process.env.CLAUDE_ARGS || "";
const allowedTools = parseAllowedTools(userClaudeArgs);
// Always include the GitHub comment server in agent mode // Detect current branch from GitHub environment
// This ensures GitHub tools (PR reviews, comments, etc.) work out of the box const currentBranch =
// without requiring users to manually configure the MCP server process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || "main";
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 user-provided additional MCP config if any // Get our GitHub MCP servers config
const additionalMcpConfig = process.env.MCP_CONFIG || ""; const ourMcpConfig = await prepareMcpConfig({
if (additionalMcpConfig.trim()) { githubToken,
try { owner: context.repository.owner,
const additional = JSON.parse(additionalMcpConfig); repo: context.repository.repo,
if (additional && typeof additional === "object") { branch: currentBranch,
// Merge mcpServers if both have them baseBranch: context.inputs.baseBranch || "main",
if (additional.mcpServers && mcpConfig.mcpServers) { claudeCommentId: undefined, // No tracking comment in agent mode
Object.assign(mcpConfig.mcpServers, additional.mcpServers); allowedTools,
} else { context,
Object.assign(mcpConfig, additional); });
}
} // Build final claude_args with multiple --mcp-config flags
} catch (error) { let claudeArgs = "";
core.warning(`Failed to parse additional MCP config: ${error}`);
} // 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}'`;
} }
// Agent mode: pass through user's claude_args with MCP config // Add user's MCP_CONFIG env var as separate --mcp-config
const userClaudeArgs = process.env.CLAUDE_ARGS || ""; const userMcpConfig = process.env.MCP_CONFIG;
const escapedMcpConfig = JSON.stringify(mcpConfig).replace(/'/g, "'\\''"); if (userMcpConfig?.trim()) {
const claudeArgs = const escapedUserConfig = userMcpConfig.replace(/'/g, "'\\''");
`--mcp-config '${escapedMcpConfig}' ${userClaudeArgs}`.trim(); claudeArgs = `${claudeArgs} --mcp-config '${escapedUserConfig}'`.trim();
}
// Append user's claude_args (which may have more --mcp-config flags)
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
core.setOutput("claude_args", claudeArgs); core.setOutput("claude_args", claudeArgs);
return { return {
commentId: undefined, commentId: undefined,
branchInfo: { branchInfo: {
baseBranch: "", baseBranch: context.inputs.baseBranch || "main",
currentBranch: "", currentBranch,
claudeBranch: undefined, claudeBranch: undefined,
}, },
mcpConfig: JSON.stringify(mcpConfig), mcpConfig: ourMcpConfig,
}; };
}, },

View 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 [];
}

View File

@@ -100,15 +100,13 @@ export const tagMode: Mode = {
await createPrompt(tagMode, modeContext, githubData, context); await createPrompt(tagMode, modeContext, githubData, context);
// Get MCP configuration // Get our GitHub MCP servers configuration
const additionalMcpConfig = process.env.MCP_CONFIG || ""; const ourMcpConfig = await prepareMcpConfig({
const mcpConfig = await prepareMcpConfig({
githubToken, githubToken,
owner: context.repository.owner, owner: context.repository.owner,
repo: context.repository.repo, repo: context.repository.repo,
branch: branchInfo.claudeBranch || branchInfo.currentBranch, branch: branchInfo.claudeBranch || branchInfo.currentBranch,
baseBranch: branchInfo.baseBranch, baseBranch: branchInfo.baseBranch,
additionalMcpConfig,
claudeCommentId: commentId.toString(), claudeCommentId: commentId.toString(),
allowedTools: [], allowedTools: [],
context, context,
@@ -150,14 +148,26 @@ export const tagMode: Mode = {
const userClaudeArgs = process.env.CLAUDE_ARGS || ""; const userClaudeArgs = process.env.CLAUDE_ARGS || "";
// Build complete claude_args with MCP config (as JSON string), tools, and user args // Build complete claude_args with multiple --mcp-config flags
// Note: Once Claude supports multiple --mcp-config flags, we can pass as file path let claudeArgs = "";
// Escape single quotes in JSON to prevent shell injection
const escapedMcpConfig = mcpConfig.replace(/'/g, "'\\''"); // Add our GitHub servers config
let claudeArgs = `--mcp-config '${escapedMcpConfig}' `; const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
claudeArgs += `--allowedTools "${tagModeTools.join(",")}" `; 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) { if (userClaudeArgs) {
claudeArgs += userClaudeArgs; claudeArgs += ` ${userClaudeArgs}`;
} }
core.setOutput("claude_args", claudeArgs.trim()); core.setOutput("claude_args", claudeArgs.trim());
@@ -165,7 +175,7 @@ export const tagMode: Mode = {
return { return {
commentId, commentId,
branchInfo, branchInfo,
mcpConfig, mcpConfig: ourMcpConfig,
}; };
}, },

View File

@@ -38,3 +38,4 @@ export async function retryWithBackoff<T>(
console.error(`Operation failed after ${maxAttempts} attempts`); console.error(`Operation failed after ${maxAttempts} attempts`);
throw lastError; throw lastError;
} }
// Test change to trigger CI

View File

@@ -1,13 +0,0 @@
// This file intentionally has TypeScript errors to trigger CI failure
// Updated to trigger new workflow run
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 };

View File

@@ -50,14 +50,6 @@ describe("prepareMcpConfig", () => {
}, },
}; };
const mockPRContextWithSigning: ParsedGitHubContext = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
useCommitSigning: true,
},
};
beforeEach(() => { beforeEach(() => {
consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {}); consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {});
consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {}); consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
@@ -98,19 +90,9 @@ describe("prepareMcpConfig", () => {
expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe( expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe(
"test-token", "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 () => { test("should include file ops server when commit signing is enabled", async () => {
const contextWithSigning = {
...mockContext,
inputs: {
...mockContext.inputs,
useCommitSigning: true,
},
};
const result = await prepareMcpConfig({ const result = await prepareMcpConfig({
githubToken: "test-token", githubToken: "test-token",
owner: "test-owner", owner: "test-owner",
@@ -118,19 +100,16 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
context: contextWithSigning, context: mockContextWithSigning,
}); });
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined(); expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.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).toBeDefined();
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe( expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
"test-token", "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( expect(parsed.mcpServers.github_file_ops.env.BRANCH_NAME).toBe(
"test-branch", "test-branch",
); );
@@ -143,49 +122,37 @@ describe("prepareMcpConfig", () => {
repo: "test-repo", repo: "test-repo",
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [ allowedTools: ["mcp__github__create_issue", "mcp__github__create_pr"],
"mcp__github__create_issue",
"mcp__github_file_ops__commit_files",
],
context: mockContext, context: mockContext,
}); });
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined(); expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).toBeDefined(); expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_comment).toBeDefined(); expect(parsed.mcpServers.github.command).toBe("docker");
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe( expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
"test-token", "test-token",
); );
}); });
test("should not include github MCP server when only file_ops tools are allowed", async () => { test("should include inline comment server for PRs when tools are allowed", async () => {
const contextWithSigning = {
...mockContext,
inputs: {
...mockContext.inputs,
useCommitSigning: true,
},
};
const result = await prepareMcpConfig({ const result = await prepareMcpConfig({
githubToken: "test-token", githubToken: "test-token",
owner: "test-owner", owner: "test-owner",
repo: "test-repo", repo: "test-repo",
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [ allowedTools: ["mcp__github_inline_comment__create_inline_comment"],
"mcp__github_file_ops__commit_files", context: mockPRContext,
"mcp__github_file_ops__update_claude_comment",
],
context: contextWithSigning,
}); });
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined(); expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined(); expect(parsed.mcpServers.github_inline_comment).toBeDefined();
expect(parsed.mcpServers.github_file_ops).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 () => { test("should include comment server when no GitHub tools are allowed and signing disabled", async () => {
@@ -195,7 +162,7 @@ describe("prepareMcpConfig", () => {
repo: "test-repo", repo: "test-repo",
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: ["Edit", "Read", "Write"], allowedTools: [],
context: mockContext, context: mockContext,
}); });
@@ -206,301 +173,7 @@ describe("prepareMcpConfig", () => {
expect(parsed.mcpServers.github_comment).toBeDefined(); expect(parsed.mcpServers.github_comment).toBeDefined();
}); });
test("should return base config when additional config is empty string", async () => { test("should set GITHUB_ACTION_PATH correctly", 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;
process.env.GITHUB_ACTION_PATH = "/test/action/path"; process.env.GITHUB_ACTION_PATH = "/test/action/path";
const result = await prepareMcpConfig({ const result = await prepareMcpConfig({
@@ -514,15 +187,12 @@ describe("prepareMcpConfig", () => {
}); });
const parsed = JSON.parse(result); 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", "/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 () => { test("should use current working directory when GITHUB_WORKSPACE is not set", async () => {
const oldEnv = process.env.GITHUB_WORKSPACE;
delete process.env.GITHUB_WORKSPACE; delete process.env.GITHUB_WORKSPACE;
const result = await prepareMcpConfig({ const result = await prepareMcpConfig({
@@ -537,22 +207,11 @@ describe("prepareMcpConfig", () => {
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_file_ops.env.REPO_DIR).toBe(process.cwd()); 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 () => { test("should include CI server when context.isPR is true and DEFAULT_WORKFLOW_TOKEN exists", async () => {
const oldEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token"; process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
const contextWithPermissions = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
useCommitSigning: true,
},
};
const result = await prepareMcpConfig({ const result = await prepareMcpConfig({
githubToken: "test-token", githubToken: "test-token",
owner: "test-owner", owner: "test-owner",
@@ -560,16 +219,15 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
context: contextWithPermissions, context: mockPRContext,
}); });
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).toBeDefined(); expect(parsed.mcpServers.github_ci).toBeDefined();
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token"); 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_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 () => { test("should not include github_ci server when context.isPR is false", async () => {
@@ -580,16 +238,14 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
context: mockContextWithSigning, context: mockContext,
}); });
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).not.toBeDefined(); 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 () => { test("should not include github_ci server when DEFAULT_WORKFLOW_TOKEN is missing", async () => {
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
delete process.env.DEFAULT_WORKFLOW_TOKEN; delete process.env.DEFAULT_WORKFLOW_TOKEN;
const result = await prepareMcpConfig({ const result = await prepareMcpConfig({
@@ -599,73 +255,10 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
context: mockPRContextWithSigning, context: mockPRContext,
}); });
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).not.toBeDefined(); 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;
}); });
}); });

View File

@@ -104,6 +104,12 @@ describe("Agent Mode", () => {
eventName: "workflow_dispatch", 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 // Set CLAUDE_ARGS environment variable
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10"; 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("--mcp-config");
expect(callArgs[1]).toContain("--model claude-sonnet-4 --max-turns 10"); 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({ expect(result).toEqual({
commentId: undefined, commentId: undefined,
branchInfo: { branchInfo: {
baseBranch: "", baseBranch: "main",
currentBranch: "", currentBranch: "main",
claudeBranch: undefined, claudeBranch: undefined,
}, },
mcpConfig: expect.any(String), mcpConfig: expect.any(String),
@@ -133,6 +139,10 @@ describe("Agent Mode", () => {
// Clean up // Clean up
delete process.env.CLAUDE_ARGS; 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 () => { test("prepare method creates prompt file with correct content", async () => {

View 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",
]);
});
});

View File

@@ -7,6 +7,7 @@ import {
normalizeHtmlEntities, normalizeHtmlEntities,
sanitizeContent, sanitizeContent,
stripHtmlComments, stripHtmlComments,
redactGitHubTokens,
} from "../src/github/utils/sanitizer"; } from "../src/github/utils/sanitizer";
describe("stripInvisibleCharacters", () => { 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)", () => { describe("stripHtmlComments (legacy)", () => {
it("should remove HTML comments", () => { it("should remove HTML comments", () => {
expect(stripHtmlComments("Hello <!-- example -->World")).toBe( expect(stripHtmlComments("Hello <!-- example -->World")).toBe(

View File

@@ -1 +0,0 @@
Custom prompt content