Compare commits

..

22 Commits

Author SHA1 Message Date
km-anthropic
0c1cb01197 Add synchronize trigger to auto-review workflow 2025-08-12 11:58:57 -07:00
km-anthropic
f0e69f3979 Fix agent mode imports and GitHub context formatting 2025-08-12 11:58:18 -07:00
km-anthropic
1b2f5c373e Update workflow to use GitHub MCP tools for PR reviews 2025-08-12 11:56:41 -07:00
km-anthropic
49514f528d 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 11:56:00 -07:00
km-anthropic
a32b63580e Update workflow to guide Claude on posting PR comments
- Provide clearer instructions for saving review to file
- Add --repo flag to gh pr comment for explicit repository
- Use heredoc pattern for better handling of multiline content
2025-08-12 09:25:25 -07:00
km-anthropic
b1820e0342 Test: Verify gh pr comment works 2025-08-12 09:18:24 -07:00
km-anthropic
5a9f227619 Simplify PR review: Use gh CLI to post comments
- Removed custom create_comment tool
- Updated workflow to use gh pr comment command
- Allowed Bash(gh pr comment:*) tool
- Simplified prompt to be more user-friendly
2025-08-12 09:17:45 -07:00
km-anthropic
86b376366f Test: Verify MCP servers work correctly 2025-08-12 08:56:09 -07:00
km-anthropic
05d9889165 Fix: Use correct MCP server names and add inline comment server for PRs
- Changed github-comment-server to github_comment (correct registration name)
- Added github_inline_comment server for PR contexts
- Updated workflow to use correct tool names (mcp__github_inline_comment__)
- Simplified prompt to use inline comments instead of full PR reviews
2025-08-12 08:54:10 -07:00
km-anthropic
708a97434b Test: Verify Claude uses GitHub review tools 2025-08-12 08:26:26 -07:00
km-anthropic
23491a8791 Fix: Explicitly instruct Claude to use GitHub review tools 2025-08-12 08:25:56 -07:00
km-anthropic
262c847203 Test: Third attempt with GitHub token 2025-08-12 08:20:55 -07:00
km-anthropic
33463c0075 Fix: Add GitHub token to workflow 2025-08-12 08:19:57 -07:00
km-anthropic
239d95f919 Test: Another attempt to verify token fix 2025-08-12 08:18:39 -07:00
km-anthropic
e740506de0 Test: Use local action to test token fix 2025-08-12 08:18:01 -07:00
km-anthropic
b324e00c16 Test: Verify GitHub token is passed to MCP servers 2025-08-12 08:13:39 -07:00
km-anthropic
0c533aaff5 Fix: Pass GitHub token to MCP servers in agent mode
The token was being obtained successfully but not outputted from the prepare step for use in later steps. Added explicit output of the GitHub token and debug logging.
2025-08-11 16:17:01 -07:00
km-anthropic
8102aebe6d Test: Verify Claude doesn't approve PRs 2025-08-11 15:07:40 -07:00
km-anthropic
57cb0d9828 Match old workflow exactly - prevent approvals
- Use exact same permissions as old workflow
- Use fetch-depth: 1 like old workflow
- Remove timeout and extra features
- Only trigger on opened PRs
- Use claude_args with --allowedTools format
2025-08-11 14:54:48 -07:00
km-anthropic
44dd9dd8a8 Final test: Update README for complete review workflow test 2025-08-11 14:01:41 -07:00
km-anthropic
8052d271ce Enable PR review submission and remove old workflow
- Add back review submission tools to allow Claude to comment
- Keep using v1-dev with the fixed agent mode
- This replaces the old claude-review.yml functionality
2025-08-11 14:00:48 -07:00
km-anthropic
b61185b14c Test: Update README title for workflow testing 2025-08-11 13:55:00 -07:00
29 changed files with 739 additions and 837 deletions

View File

@@ -0,0 +1,38 @@
name: Auto review PRs
on:
pull_request:
types: [opened, synchronize]
jobs:
auto-review:
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Auto review PR
uses: ./
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
prompt: |
Please review this PR and provide comprehensive feedback.
Focus on:
- Code quality and best practices
- Potential bugs or issues
- Suggestions for improvements
- Overall architecture and design decisions
- Documentation consistency
Use the GitHub MCP tools to create a proper PR review with comments.
You can use mcp__github__create_pending_pull_request_review to start a review
and mcp__github__submit_pull_request_review to submit it as a COMMENT (not APPROVE).
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: "--allowedTools mcp__github__create_pending_pull_request_review,mcp__github__submit_pull_request_review,Read,Grep,Glob"

View File

@@ -1,33 +0,0 @@
name: Auto review PRs
on:
pull_request:
types: [opened]
jobs:
auto-review:
permissions:
contents: read
id-token: write
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Auto review PR
uses: anthropics/claude-code-action@main
with:
direct_prompt: |
Please review this PR. Look at the changes and provide thoughtful feedback on:
- Code quality and best practices
- Potential bugs or issues
- Suggestions for improvements
- Overall architecture and design decisions
- Documentation consistency: Verify that README.md and other documentation files are updated to reflect any code changes (especially new inputs, features, or configuration options)
Be constructive and specific in your feedback. Give inline comments where applicable.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
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

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

View File

@@ -1,175 +0,0 @@
# 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
# Claude Code Action - No Approval Test
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.
@@ -57,3 +57,21 @@ Having issues or questions? Check out our [Frequently Asked Questions](./docs/fa
## License
This project is licensed under the MIT License—see the LICENSE file for details.
## Testing token fix
## Testing token fix v2
## Testing token fix v3
## Testing review tools
## Testing MCP server fix
## Testing gh comment command

View File

@@ -93,9 +93,6 @@ outputs:
branch_name:
description: "The branch created by Claude Code for this execution"
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
github_token:
description: "The GitHub token used by the action (Claude App token if available)"
value: ${{ steps.prepare.outputs.github_token }}
runs:
using: "composite"
@@ -133,7 +130,6 @@ runs:
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }}
MCP_CONFIG: ${{ inputs.mcp_config }}
ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies
if: steps.prepare.outputs.contains_trigger == 'true'
@@ -145,8 +141,7 @@ runs:
echo "Base-action dependencies installed"
cd -
# Install Claude Code globally
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.84
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
bun install -g @anthropic-ai/claude-code@1.0.72
- name: Setup Network Restrictions
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
@@ -173,7 +168,6 @@ runs:
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
# Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
@@ -247,7 +241,7 @@ runs:
fi
- name: Revoke app token
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
if: always() && inputs.github_token == ''
shell: bash
run: |
curl -L \

View File

@@ -85,7 +85,7 @@ runs:
- name: Install Claude Code
shell: bash
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.84
run: bun install -g @anthropic-ai/claude-code@1.0.72
- name: Run Claude Code Action
shell: bash

View File

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

View File

@@ -79,27 +79,4 @@ export async function setupClaudeCodeSettings(
console.log(`Slash commands directory not found or error copying: ${e}`);
}
}
// Copy project subagents to Claude's agents directory
// Use GITHUB_WORKSPACE if available (set by GitHub Actions), otherwise use current directory
const workspaceDir = process.env.GITHUB_WORKSPACE || process.cwd();
const projectAgentsDir = `${workspaceDir}/.claude/agents`;
const claudeAgentsDir = `${home}/.claude/agents`;
try {
await $`test -d ${projectAgentsDir}`.quiet();
console.log(`Found project agents directory at ${projectAgentsDir}`);
await $`mkdir -p ${claudeAgentsDir}`.quiet();
await $`cp ${projectAgentsDir}/*.md ${claudeAgentsDir}/ 2>/dev/null || true`.quiet();
const agentFiles = await $`ls ${claudeAgentsDir}/*.md 2>/dev/null | wc -l`
.quiet()
.text();
const agentCount = parseInt(agentFiles.trim()) || 0;
console.log(`Copied ${agentCount} agent(s) to ${claudeAgentsDir}`);
} catch (e) {
console.log(`No project agents directory found at ${projectAgentsDir}`);
}
}

View File

@@ -215,70 +215,4 @@ describe("setupClaudeCodeSettings", () => {
const settingsContent = await readFile(settingsPath, "utf-8");
expect(JSON.parse(settingsContent).enableAllProjectMcpServers).toBe(true);
});
test("should copy project agents when .claude/agents directory exists", async () => {
// Create a mock project structure with agents
const projectDir = join(testHomeDir, "test-project");
const projectAgentsDir = join(projectDir, ".claude", "agents");
await mkdir(projectAgentsDir, { recursive: true });
// Create test agent files
await writeFile(
join(projectAgentsDir, "test-agent.md"),
"---\nname: test-agent\ndescription: Test agent\n---\nTest agent content",
);
await writeFile(
join(projectAgentsDir, "another-agent.md"),
"---\nname: another-agent\n---\nAnother agent",
);
// Set GITHUB_WORKSPACE to the test project directory
const originalWorkspace = process.env.GITHUB_WORKSPACE;
process.env.GITHUB_WORKSPACE = projectDir;
try {
await setupClaudeCodeSettings(undefined, testHomeDir);
// Check that agents were copied
const agentsDir = join(testHomeDir, ".claude", "agents");
const files = await readdir(agentsDir);
expect(files).toContain("test-agent.md");
expect(files).toContain("another-agent.md");
// Verify content was copied correctly
const content = await readFile(join(agentsDir, "test-agent.md"), "utf-8");
expect(content).toContain("Test agent content");
} finally {
// Restore original GITHUB_WORKSPACE
if (originalWorkspace !== undefined) {
process.env.GITHUB_WORKSPACE = originalWorkspace;
} else {
delete process.env.GITHUB_WORKSPACE;
}
}
});
test("should handle missing project agents directory gracefully", async () => {
// Set GITHUB_WORKSPACE to a directory without .claude/agents
const projectDir = join(testHomeDir, "project-without-agents");
await mkdir(projectDir, { recursive: true });
const originalWorkspace = process.env.GITHUB_WORKSPACE;
process.env.GITHUB_WORKSPACE = projectDir;
try {
await setupClaudeCodeSettings(undefined, testHomeDir);
// Should complete without errors
const settingsContent = await readFile(settingsPath, "utf-8");
const settings = JSON.parse(settingsContent);
expect(settings.enableAllProjectMcpServers).toBe(true);
} finally {
if (originalWorkspace !== undefined) {
process.env.GITHUB_WORKSPACE = originalWorkspace;
} else {
delete process.env.GITHUB_WORKSPACE;
}
}
});
});

View File

@@ -1,4 +1,4 @@
name: Claude Code
name: Claude PR Assistant
on:
issue_comment:
@@ -11,53 +11,38 @@ on:
types: [submitted]
jobs:
claude:
claude-code-action:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
contents: read
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code
id: claude
- name: Run Claude PR Action
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
# model: "claude-opus-4-1-20250805"
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
# Optional: Allow Claude to run specific commands
# allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
# Optional: Add custom instructions for Claude to customize its behavior for your project
# custom_instructions: |
# Follow our coding standards
# Ensure all new code has tests
# Use TypeScript for new files
# Optional: Custom environment variables for Claude
# claude_env: |
# NODE_ENV: test
# Or use OAuth token instead:
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
timeout_minutes: "60"
# mode: tag # Default: responds to @claude mentions
# Optional: Restrict network access to specific domains only
# experimental_allowed_domains: |
# .anthropic.com
# .github.com
# api.github.com
# .githubusercontent.com
# bun.sh
# registry.npmjs.org
# .blob.core.windows.net

View File

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

View File

@@ -1,59 +0,0 @@
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,12 +12,9 @@ import { createOctokit } from "../github/api/client";
import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry";
import { prepare } from "../prepare";
import { collectActionInputsPresence } from "./collect-inputs";
async function run() {
try {
collectActionInputsPresence();
// Parse GitHub context first to enable mode detection
const context = parseGitHubContext();
@@ -44,18 +41,11 @@ async function run() {
// Check trigger conditions
const containsTrigger = mode.shouldTrigger(context);
// Debug logging
console.log(`Mode: ${mode.name}`);
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
console.log(`Trigger result: ${containsTrigger}`);
// Set output for action.yml to check
core.setOutput("contains_trigger", containsTrigger.toString());
if (!containsTrigger) {
console.log("No trigger found, skipping remaining steps");
// Still set github_token output even when skipping
core.setOutput("github_token", githubToken);
return;
}
@@ -66,11 +56,9 @@ async function run() {
mode,
githubToken,
});
core.setOutput("GITHUB_TOKEN", githubToken);
// MCP config is handled by individual modes (tag/agent) and included in their claude_args output
// Expose the GitHub token (Claude App token) as an output
core.setOutput("github_token", githubToken);
// Step 6: Get system prompt from mode if available
if (mode.getSystemPrompt) {

View File

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

View File

@@ -31,30 +31,8 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
const responseJson = (await response.json()) as {
error?: {
message?: string;
details?: {
error_code?: string;
};
};
type?: string;
message?: string;
};
// Check for specific workflow validation error codes that should skip the action
const errorCode = responseJson.error?.details?.error_code;
if (errorCode === "workflow_not_found_on_default_branch") {
const message =
responseJson.message ??
responseJson.error?.message ??
"Workflow validation failed";
core.warning(`Skipping action due to workflow validation: ${message}`);
console.log(
"Action skipped due to workflow validation error. This is expected when adding Claude Code workflows to new repositories or on PRs with workflow changes. If you're seeing this, your workflow will begin working once you merge your PR.",
);
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
process.exit(0);
}
console.error(
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
);
@@ -99,9 +77,8 @@ export async function setupGitHubToken(): Promise<string> {
core.setOutput("GITHUB_TOKEN", appToken);
return appToken;
} catch (error) {
// Only set failed if we get here - workflow validation errors will exit(0) before this
core.setFailed(
`Failed to setup GitHub token: ${error}\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
`Failed to setup GitHub token: ${error}.\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
);
process.exit(1);
}

View File

@@ -58,41 +58,6 @@ export function sanitizeContent(content: string): string {
content = stripMarkdownLinkTitles(content);
content = stripHiddenAttributes(content);
content = normalizeHtmlEntities(content);
content = redactGitHubTokens(content);
return content;
}
export function redactGitHubTokens(content: string): string {
// GitHub Personal Access Tokens (classic): ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
content = content.replace(
/\bghp_[A-Za-z0-9]{36}\b/g,
"[REDACTED_GITHUB_TOKEN]",
);
// GitHub OAuth tokens: gho_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
content = content.replace(
/\bgho_[A-Za-z0-9]{36}\b/g,
"[REDACTED_GITHUB_TOKEN]",
);
// GitHub installation tokens: ghs_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
content = content.replace(
/\bghs_[A-Za-z0-9]{36}\b/g,
"[REDACTED_GITHUB_TOKEN]",
);
// GitHub refresh tokens: ghr_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX (40 chars)
content = content.replace(
/\bghr_[A-Za-z0-9]{36}\b/g,
"[REDACTED_GITHUB_TOKEN]",
);
// GitHub fine-grained personal access tokens: github_pat_XXXXXXXXXX (up to 255 chars)
content = content.replace(
/\bgithub_pat_[A-Za-z0-9_]{11,221}\b/g,
"[REDACTED_GITHUB_TOKEN]",
);
return content;
}

View File

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

View File

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

View File

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

View File

@@ -2,8 +2,16 @@ import * as core from "@actions/core";
import { mkdir, writeFile } from "fs/promises";
import type { Mode, ModeOptions, ModeResult } from "../types";
import type { PreparedContext } from "../../create-prompt/types";
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { parseAllowedTools } from "./parse-tools";
import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../../github/api/config";
import { fetchGitHubData } from "../../github/data/fetcher";
import {
formatContext,
formatBody,
formatComments,
formatReviewComments,
formatChangedFilesWithSHA
} from "../../github/data/formatter";
import { isEntityContext } from "../../github/context";
/**
* Agent mode implementation.
@@ -41,72 +49,172 @@ export const agentMode: Mode = {
return false;
},
async prepare({ context, githubToken }: ModeOptions): Promise<ModeResult> {
async prepare({ context, githubToken, octokit }: ModeOptions): Promise<ModeResult> {
// Agent mode handles automation events and any event with explicit prompts
console.log(`Agent mode: githubToken provided: ${!!githubToken}, length: ${githubToken?.length || 0}`);
// Create prompt directory
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
recursive: true,
});
// Fetch GitHub context data if we're in an entity context (PR/issue)
let githubContextPrefix = '';
if (isEntityContext(context)) {
try {
const githubData = await fetchGitHubData({
octokits: octokit,
repository: `${context.repository.owner}/${context.repository.repo}`,
prNumber: context.entityNumber.toString(),
isPR: context.isPR,
triggerUsername: context.actor,
});
// Format the GitHub data into a readable context
const formattedContext = formatContext(githubData.contextData, context.isPR);
const formattedBody = githubData.contextData?.body
? formatBody(githubData.contextData.body, githubData.imageUrlMap)
: "No description provided";
const formattedComments = formatComments(githubData.comments, githubData.imageUrlMap);
// Build the context prefix
githubContextPrefix = `## GitHub Context
// Write the prompt file - use the user's prompt directly
const promptContent =
context.inputs.prompt ||
${formattedContext}
### Description
${formattedBody}`;
if (formattedComments && formattedComments.trim()) {
githubContextPrefix += `\n\n### Comments\n${formattedComments}`;
}
if (context.isPR && githubData.changedFilesWithSHA) {
const formattedFiles = formatChangedFilesWithSHA(githubData.changedFilesWithSHA);
githubContextPrefix += `\n\n### Changed Files\n${formattedFiles}`;
}
githubContextPrefix += '\n\n## Your Task\n\n';
} catch (error) {
console.warn('Failed to fetch GitHub context:', error);
// Continue without GitHub context if fetching fails
}
}
// Write the prompt file with GitHub context prefix
const userPrompt = context.inputs.prompt ||
`Repository: ${context.repository.owner}/${context.repository.repo}`;
const promptContent = githubContextPrefix + userPrompt;
await writeFile(
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
promptContent,
);
// Parse allowed tools from user's claude_args
// Agent mode: User has full control via claudeArgs
// No default tools are enforced - Claude Code's defaults will apply
// Include both GitHub comment server and main GitHub MCP server by default
// This ensures comprehensive GitHub tools work out of the box
const mcpConfig: any = {
mcpServers: {
// GitHub comment server for updating Claude comments
github_comment: {
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,
CLAUDE_COMMENT_ID: process.env.CLAUDE_COMMENT_ID || "",
PR_NUMBER: (context as any).entityNumber?.toString() || process.env.GITHUB_EVENT_PULL_REQUEST_NUMBER || "",
ISSUE_NUMBER: (context as any).entityNumber?.toString() || "",
GITHUB_EVENT_NAME: process.env.GITHUB_EVENT_NAME || "",
GITHUB_API_URL:
process.env.GITHUB_API_URL || "https://api.github.com",
},
},
// Main GitHub MCP server for comprehensive GitHub operations
github: {
command: "docker",
args: [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"-e",
"GITHUB_HOST",
"ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
],
env: {
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken || "",
GITHUB_HOST: GITHUB_SERVER_URL,
},
},
},
};
// Include inline comment server for PR contexts
if (context.eventName === "pull_request" || context.eventName === "pull_request_review") {
// Get PR number from the context payload
const prNumber = (context as any).payload?.pull_request?.number ||
(context as any).entityNumber ||
"";
mcpConfig.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: context.repository.owner,
REPO_NAME: context.repository.repo,
PR_NUMBER: prNumber.toString(),
GITHUB_API_URL: process.env.GITHUB_API_URL || "https://api.github.com",
},
};
}
// Add user-provided additional MCP config if any
const additionalMcpConfig = process.env.MCP_CONFIG || "";
if (additionalMcpConfig.trim()) {
try {
const additional = JSON.parse(additionalMcpConfig);
if (additional && typeof additional === "object") {
// Merge mcpServers if both have them
if (additional.mcpServers && mcpConfig.mcpServers) {
Object.assign(mcpConfig.mcpServers, additional.mcpServers);
} else {
Object.assign(mcpConfig, additional);
}
}
} catch (error) {
core.warning(`Failed to parse additional MCP config: ${error}`);
}
}
// Agent mode: pass through user's claude_args with MCP config
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
const allowedTools = parseAllowedTools(userClaudeArgs);
// Detect current branch from GitHub environment
const currentBranch =
process.env.GITHUB_HEAD_REF || process.env.GITHUB_REF_NAME || "main";
// Get our GitHub MCP servers config
const ourMcpConfig = await prepareMcpConfig({
githubToken,
owner: context.repository.owner,
repo: context.repository.repo,
branch: currentBranch,
baseBranch: context.inputs.baseBranch || "main",
claudeCommentId: undefined, // No tracking comment in agent mode
allowedTools,
context,
});
// Build final claude_args with multiple --mcp-config flags
let claudeArgs = "";
// Add our GitHub servers config if we have any
const ourConfig = JSON.parse(ourMcpConfig);
if (ourConfig.mcpServers && Object.keys(ourConfig.mcpServers).length > 0) {
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
}
// Add user's MCP_CONFIG env var as separate --mcp-config
const userMcpConfig = process.env.MCP_CONFIG;
if (userMcpConfig?.trim()) {
const escapedUserConfig = userMcpConfig.replace(/'/g, "'\\''");
claudeArgs = `${claudeArgs} --mcp-config '${escapedUserConfig}'`.trim();
}
// Append user's claude_args (which may have more --mcp-config flags)
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
const escapedMcpConfig = JSON.stringify(mcpConfig).replace(/'/g, "'\\''");
const claudeArgs =
`--mcp-config '${escapedMcpConfig}' ${userClaudeArgs}`.trim();
core.setOutput("claude_args", claudeArgs);
return {
commentId: undefined,
branchInfo: {
baseBranch: context.inputs.baseBranch || "main",
currentBranch,
baseBranch: "",
currentBranch: "",
claudeBranch: undefined,
},
mcpConfig: ourMcpConfig,
mcpConfig: JSON.stringify(mcpConfig),
};
},

View File

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

View File

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

View File

@@ -50,6 +50,14 @@ describe("prepareMcpConfig", () => {
},
};
const mockPRContextWithSigning: ParsedGitHubContext = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
useCommitSigning: true,
},
};
beforeEach(() => {
consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {});
consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
@@ -90,9 +98,19 @@ describe("prepareMcpConfig", () => {
expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe(
"test-token",
);
expect(parsed.mcpServers.github_comment.env.REPO_OWNER).toBe("test-owner");
expect(parsed.mcpServers.github_comment.env.REPO_NAME).toBe("test-repo");
});
test("should include file ops server when commit signing is enabled", async () => {
test("should return file ops server when commit signing is enabled", async () => {
const contextWithSigning = {
...mockContext,
inputs: {
...mockContext.inputs,
useCommitSigning: true,
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
@@ -100,16 +118,19 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockContextWithSigning,
context: contextWithSigning,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_comment).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
"test-token",
);
expect(parsed.mcpServers.github_file_ops.env.REPO_OWNER).toBe("test-owner");
expect(parsed.mcpServers.github_file_ops.env.REPO_NAME).toBe("test-repo");
expect(parsed.mcpServers.github_file_ops.env.BRANCH_NAME).toBe(
"test-branch",
);
@@ -122,37 +143,49 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: ["mcp__github__create_issue", "mcp__github__create_pr"],
allowedTools: [
"mcp__github__create_issue",
"mcp__github_file_ops__commit_files",
],
context: mockContext,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github.command).toBe("docker");
expect(parsed.mcpServers.github_comment).toBeDefined();
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
"test-token",
);
});
test("should include inline comment server for PRs when tools are allowed", async () => {
test("should not include github MCP server when only file_ops tools are allowed", async () => {
const contextWithSigning = {
...mockContext,
inputs: {
...mockContext.inputs,
useCommitSigning: true,
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: ["mcp__github_inline_comment__create_inline_comment"],
context: mockPRContext,
allowedTools: [
"mcp__github_file_ops__commit_files",
"mcp__github_file_ops__update_claude_comment",
],
context: contextWithSigning,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github_inline_comment).toBeDefined();
expect(parsed.mcpServers.github_inline_comment.env.GITHUB_TOKEN).toBe(
"test-token",
);
expect(parsed.mcpServers.github_inline_comment.env.PR_NUMBER).toBe("456");
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should include comment server when no GitHub tools are allowed and signing disabled", async () => {
@@ -162,7 +195,7 @@ describe("prepareMcpConfig", () => {
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
allowedTools: ["Edit", "Read", "Write"],
context: mockContext,
});
@@ -173,7 +206,301 @@ describe("prepareMcpConfig", () => {
expect(parsed.mcpServers.github_comment).toBeDefined();
});
test("should set GITHUB_ACTION_PATH correctly", async () => {
test("should return base config when additional config is empty string", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: "",
allowedTools: [],
context: mockContext,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_comment).toBeDefined();
expect(consoleWarningSpy).not.toHaveBeenCalled();
});
test("should return base config when additional config is whitespace only", async () => {
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: " \n\t ",
allowedTools: [],
context: mockContext,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_comment).toBeDefined();
expect(consoleWarningSpy).not.toHaveBeenCalled();
});
test("should merge valid additional config with base config", async () => {
const additionalConfig = JSON.stringify({
mcpServers: {
custom_server: {
command: "custom-command",
args: ["arg1", "arg2"],
env: {
CUSTOM_ENV: "custom-value",
},
},
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: additionalConfig,
allowedTools: [
"mcp__github__create_issue",
"mcp__github_file_ops__commit_files",
],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(consoleInfoSpy).toHaveBeenCalledWith(
"Merging additional MCP server configuration with built-in servers",
);
expect(parsed.mcpServers.github).toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
expect(parsed.mcpServers.custom_server).toBeDefined();
expect(parsed.mcpServers.custom_server.command).toBe("custom-command");
expect(parsed.mcpServers.custom_server.args).toEqual(["arg1", "arg2"]);
expect(parsed.mcpServers.custom_server.env.CUSTOM_ENV).toBe("custom-value");
});
test("should override built-in servers when additional config has same server names", async () => {
const additionalConfig = JSON.stringify({
mcpServers: {
github: {
command: "overridden-command",
args: ["overridden-arg"],
env: {
OVERRIDDEN_ENV: "overridden-value",
},
},
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: additionalConfig,
allowedTools: [
"mcp__github__create_issue",
"mcp__github_file_ops__commit_files",
],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(consoleInfoSpy).toHaveBeenCalledWith(
"Merging additional MCP server configuration with built-in servers",
);
expect(parsed.mcpServers.github.command).toBe("overridden-command");
expect(parsed.mcpServers.github.args).toEqual(["overridden-arg"]);
expect(parsed.mcpServers.github.env.OVERRIDDEN_ENV).toBe(
"overridden-value",
);
expect(
parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN,
).toBeUndefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should merge additional root-level properties", async () => {
const additionalConfig = JSON.stringify({
customProperty: "custom-value",
anotherProperty: {
nested: "value",
},
mcpServers: {
custom_server: {
command: "custom",
},
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: additionalConfig,
allowedTools: [],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(parsed.customProperty).toBe("custom-value");
expect(parsed.anotherProperty).toEqual({ nested: "value" });
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.custom_server).toBeDefined();
});
test("should handle invalid JSON gracefully", async () => {
const invalidJson = "{ invalid json }";
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: invalidJson,
allowedTools: [],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("Failed to parse additional MCP config:"),
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should handle non-object JSON values", async () => {
const nonObjectJson = JSON.stringify("string value");
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: nonObjectJson,
allowedTools: [],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("Failed to parse additional MCP config:"),
);
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("MCP config must be a valid JSON object"),
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should handle null JSON value", async () => {
const nullJson = JSON.stringify(null);
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: nullJson,
allowedTools: [],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("Failed to parse additional MCP config:"),
);
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining("MCP config must be a valid JSON object"),
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should handle array JSON value", async () => {
const arrayJson = JSON.stringify([1, 2, 3]);
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: arrayJson,
allowedTools: [],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
// Arrays are objects in JavaScript, so they pass the object check
// But they'll fail when trying to spread or access mcpServers property
expect(consoleInfoSpy).toHaveBeenCalledWith(
"Merging additional MCP server configuration with built-in servers",
);
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
expect(parsed[0]).toBe(1);
expect(parsed[1]).toBe(2);
expect(parsed[2]).toBe(3);
});
test("should merge complex nested configurations", async () => {
const additionalConfig = JSON.stringify({
mcpServers: {
server1: {
command: "cmd1",
env: { KEY1: "value1" },
},
server2: {
command: "cmd2",
env: { KEY2: "value2" },
},
github_file_ops: {
command: "overridden",
env: { CUSTOM: "value" },
},
},
otherConfig: {
nested: {
deeply: "value",
},
},
});
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
additionalMcpConfig: additionalConfig,
allowedTools: [],
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.server1).toBeDefined();
expect(parsed.mcpServers.server2).toBeDefined();
expect(parsed.mcpServers.github).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
expect(parsed.otherConfig.nested.deeply).toBe("value");
});
test("should preserve GITHUB_ACTION_PATH in file_ops server args", async () => {
const oldEnv = process.env.GITHUB_ACTION_PATH;
process.env.GITHUB_ACTION_PATH = "/test/action/path";
const result = await prepareMcpConfig({
@@ -187,12 +514,15 @@ describe("prepareMcpConfig", () => {
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_file_ops.args).toContain(
expect(parsed.mcpServers.github_file_ops.args[1]).toBe(
"/test/action/path/src/mcp/github-file-ops-server.ts",
);
process.env.GITHUB_ACTION_PATH = oldEnv;
});
test("should use current working directory when GITHUB_WORKSPACE is not set", async () => {
test("should use process.cwd() when GITHUB_WORKSPACE is not set", async () => {
const oldEnv = process.env.GITHUB_WORKSPACE;
delete process.env.GITHUB_WORKSPACE;
const result = await prepareMcpConfig({
@@ -207,11 +537,22 @@ describe("prepareMcpConfig", () => {
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_file_ops.env.REPO_DIR).toBe(process.cwd());
process.env.GITHUB_WORKSPACE = oldEnv;
});
test("should include CI server when context.isPR is true and DEFAULT_WORKFLOW_TOKEN exists", async () => {
test("should include github_ci server when context.isPR is true and workflow token is present", async () => {
const oldEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
const contextWithPermissions = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
useCommitSigning: true,
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
@@ -219,15 +560,16 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockPRContext,
context: contextWithPermissions,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).toBeDefined();
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
expect(parsed.mcpServers.github_ci.env.PR_NUMBER).toBe("456");
expect(parsed.mcpServers.github_file_ops).toBeDefined();
delete process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = oldEnv;
});
test("should not include github_ci server when context.isPR is false", async () => {
@@ -238,14 +580,16 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockContext,
context: mockContextWithSigning,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
});
test("should not include github_ci server when DEFAULT_WORKFLOW_TOKEN is missing", async () => {
test("should not include github_ci server when workflow token is not present", async () => {
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
delete process.env.DEFAULT_WORKFLOW_TOKEN;
const result = await prepareMcpConfig({
@@ -255,10 +599,73 @@ describe("prepareMcpConfig", () => {
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: mockPRContext,
context: mockPRContextWithSigning,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
});
test("should include github_ci server when workflow token is present for PR context", async () => {
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
const contextWithPermissions = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: contextWithPermissions,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).toBeDefined();
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
});
test("should warn when workflow token lacks actions:read permission", async () => {
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "invalid-token";
const contextWithPermissions = {
...mockPRContext,
inputs: {
...mockPRContext.inputs,
},
};
const result = await prepareMcpConfig({
githubToken: "test-token",
owner: "test-owner",
repo: "test-repo",
branch: "test-branch",
baseBranch: "main",
allowedTools: [],
context: contextWithPermissions,
});
const parsed = JSON.parse(result);
expect(parsed.mcpServers.github_ci).toBeDefined();
expect(consoleWarningSpy).toHaveBeenCalledWith(
expect.stringContaining(
"The github_ci MCP server requires 'actions: read' permission",
),
);
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
});
});

View File

@@ -104,12 +104,6 @@ describe("Agent Mode", () => {
eventName: "workflow_dispatch",
});
// Save original env vars and set test values
const originalHeadRef = process.env.GITHUB_HEAD_REF;
const originalRefName = process.env.GITHUB_REF_NAME;
delete process.env.GITHUB_HEAD_REF;
delete process.env.GITHUB_REF_NAME;
// Set CLAUDE_ARGS environment variable
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
@@ -126,12 +120,12 @@ describe("Agent Mode", () => {
expect(callArgs[1]).toContain("--mcp-config");
expect(callArgs[1]).toContain("--model claude-sonnet-4 --max-turns 10");
// Verify return structure - should use "main" as fallback when no env vars set
// Verify return structure
expect(result).toEqual({
commentId: undefined,
branchInfo: {
baseBranch: "main",
currentBranch: "main",
baseBranch: "",
currentBranch: "",
claudeBranch: undefined,
},
mcpConfig: expect.any(String),
@@ -139,10 +133,6 @@ describe("Agent Mode", () => {
// Clean up
delete process.env.CLAUDE_ARGS;
if (originalHeadRef !== undefined)
process.env.GITHUB_HEAD_REF = originalHeadRef;
if (originalRefName !== undefined)
process.env.GITHUB_REF_NAME = originalRefName;
});
test("prepare method creates prompt file with correct content", async () => {

View File

@@ -1,71 +0,0 @@
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,7 +7,6 @@ import {
normalizeHtmlEntities,
sanitizeContent,
stripHtmlComments,
redactGitHubTokens,
} from "../src/github/utils/sanitizer";
describe("stripInvisibleCharacters", () => {
@@ -243,109 +242,6 @@ describe("sanitizeContent", () => {
});
});
describe("redactGitHubTokens", () => {
it("should redact personal access tokens (ghp_)", () => {
const token = "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";
expect(redactGitHubTokens(`Token: ${token}`)).toBe(
"Token: [REDACTED_GITHUB_TOKEN]",
);
expect(redactGitHubTokens(`Here's a token: ${token} in text`)).toBe(
"Here's a token: [REDACTED_GITHUB_TOKEN] in text",
);
});
it("should redact OAuth tokens (gho_)", () => {
const token = "gho_16C7e42F292c6912E7710c838347Ae178B4a";
expect(redactGitHubTokens(`OAuth: ${token}`)).toBe(
"OAuth: [REDACTED_GITHUB_TOKEN]",
);
});
it("should redact installation tokens (ghs_)", () => {
const token = "ghs_xz7yzju2SZjGPa0dUNMAx0SH4xDOCS31LXQW";
expect(redactGitHubTokens(`Install token: ${token}`)).toBe(
"Install token: [REDACTED_GITHUB_TOKEN]",
);
});
it("should redact refresh tokens (ghr_)", () => {
const token = "ghr_1B4a2e77838347a253e56d7b5253e7d11667";
expect(redactGitHubTokens(`Refresh: ${token}`)).toBe(
"Refresh: [REDACTED_GITHUB_TOKEN]",
);
});
it("should redact fine-grained tokens (github_pat_)", () => {
const token =
"github_pat_11ABCDEFG0example5of9_2nVwvsylpmOLboQwTPTLewDcE621dQ0AAaBBCCDDEEFFHH";
expect(redactGitHubTokens(`Fine-grained: ${token}`)).toBe(
"Fine-grained: [REDACTED_GITHUB_TOKEN]",
);
});
it("should handle tokens in code blocks", () => {
const content = `\`\`\`bash
export GITHUB_TOKEN=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
\`\`\``;
const expected = `\`\`\`bash
export GITHUB_TOKEN=[REDACTED_GITHUB_TOKEN]
\`\`\``;
expect(redactGitHubTokens(content)).toBe(expected);
});
it("should handle multiple tokens in one text", () => {
const content =
"Token 1: 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 and token 2: gho_16C7e42F292c6912E7710c838347Ae178B4a";
expect(redactGitHubTokens(content)).toBe(
"Token 1: [REDACTED_GITHUB_TOKEN] and token 2: [REDACTED_GITHUB_TOKEN]",
);
});
it("should handle tokens in URLs", () => {
const content =
"https://api.github.com/user?access_token=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";
expect(redactGitHubTokens(content)).toBe(
"https://api.github.com/user?access_token=[REDACTED_GITHUB_TOKEN]",
);
});
it("should not redact partial matches or invalid tokens", () => {
const content =
"This is not a token: ghp_short or gho_toolong1234567890123456789012345678901234567890";
expect(redactGitHubTokens(content)).toBe(content);
});
it("should preserve normal text", () => {
const content = "Normal text with no tokens";
expect(redactGitHubTokens(content)).toBe(content);
});
it("should handle edge cases", () => {
expect(redactGitHubTokens("")).toBe("");
expect(redactGitHubTokens("ghp_")).toBe("ghp_");
expect(redactGitHubTokens("github_pat_short")).toBe("github_pat_short");
});
});
describe("sanitizeContent with token redaction", () => {
it("should redact tokens as part of full sanitization", () => {
const content = `
<!-- Hidden comment with token: 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 -->
Here's some text with a token: gho_16C7e42F292c6912E7710c838347Ae178B4a
And invisible chars: test\u200Btoken
`;
const sanitized = sanitizeContent(content);
expect(sanitized).not.toContain("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");
expect(sanitized).not.toContain("gho_16C7e42F292c6912E7710c838347Ae178B4a");
expect(sanitized).not.toContain("<!-- Hidden comment");
expect(sanitized).not.toContain("\u200B");
expect(sanitized).toContain("[REDACTED_GITHUB_TOKEN]");
expect(sanitized).toContain("Here's some text with a token:");
});
});
describe("stripHtmlComments (legacy)", () => {
it("should remove HTML comments", () => {
expect(stripHtmlComments("Hello <!-- example -->World")).toBe(

View File

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