Compare commits

...

75 Commits

Author SHA1 Message Date
Ashwin Bhat
8c83c5c17b uncommit npmrc temporarily 2025-08-27 09:04:08 -07:00
Ashwin Bhat
c05ccc5ce4 temporarily remove mcp outer action tests (#487) 2025-08-26 09:47:06 -07:00
Ashwin Bhat
41e5ba9012 chore: migrate GitHub workflows from @beta to @v1 (#486)
* tmp

* chore: migrate GitHub workflows from @beta to @v1

- Update claude.yml and issue-triage.yml to use claude-code-action@v1
- Migrate deprecated inputs to new claude_args format
- Move mcp_config to --mcp-config in claude_args
- Follow v1 migration guide for simplified configuration

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-26 09:46:56 -07:00
Ashwin Bhat
e6f32c8321 Remove mcp_config input in favor of --mcp-config in claude_args (#485)
* Remove mcp_config input in favor of --mcp-config in claude_args

BREAKING CHANGE: The mcp_config input has been removed. Users should now use --mcp-config flag in claude_args instead.

This simplifies the action's input surface area and aligns better with the Claude Code CLI interface. Users can still add multiple MCP configurations by using multiple --mcp-config flags.

Migration:
- Before: mcp_config: '{"mcpServers": {...}}'
- After: claude_args: '--mcp-config {"mcpServers": {...}}'

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

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

* Add outer action MCP tests to workflow

- Add test-outer-action-inline-mcp job to test inline MCP config via claude_args
- Add test-outer-action-file-mcp job to test file-based MCP config via claude_args
- Keep base-action tests unchanged (they still use mcp_config parameter)
- Test that MCP tools are properly discovered and can be executed through the outer action

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

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

* Fix: Add Bun setup to outer action MCP test jobs

The test jobs for the outer action were failing because Bun wasn't installed.
Added Setup Bun step to both test-outer-action-inline-mcp and test-outer-action-file-mcp jobs.

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

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

* Add id-token permission to outer action MCP test jobs

The outer action needs id-token: write permission for OIDC authentication
when using the GitHub App. Added full permissions block to both
test-outer-action-inline-mcp and test-outer-action-file-mcp jobs.

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

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

* Use github_token parameter instead of id-token permission

Replace id-token: write permission with explicit github_token parameter
for both outer action MCP test jobs. This simplifies authentication by
using the provided GitHub token directly.

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

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

* Use RUNNER_TEMP environment variable consistently

Changed from GitHub Actions expression syntax to environment variable
for consistency with the rest of the workflow file.

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

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

* Use execution_file output from action instead of hardcoded path

Updated outer action test jobs to:
- Add step IDs (claude-inline-test, claude-file-test)
- Use the execution_file output from the action steps
- This is more reliable than hardcoding the output file path

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

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

* tmp

* Fix MCP test assertions to match actual output format

Updated the test assertions to match the actual JSON structure:
- Tool calls are in assistant messages with type='tool_use'
- Tool results are in user messages with type='tool_result'
- The test tool returns 'Test tool response' not 'Hello from test tool'

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

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

* Make inline MCP test actually use the tool instead of just listing

Changed the inline MCP test to:
- Request that Claude uses the test tool (not just list it)
- Add --allowedTools to ensure the tool can be used
- Check that the tool was actually called and returned expected result
- Output the full JSON for debugging

This makes both tests (inline and file-based) consistent in their approach.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-26 09:43:18 -07:00
GitHub Actions
ada5bc42eb chore: bump Claude Code version to 1.0.92 2025-08-26 00:56:19 +00:00
Ashwin Bhat
d6d3ddd4a7 chore: remove beta tag job from release workflow (#479)
Remove the update-beta-tag job since the update-major-tag job already handles major version tagging (v0, v1, v2). Keep release marked as non-latest to allow v1 to remain the latest release.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-25 12:57:50 -07:00
km-anthropic
0630ef383a feat: implement Claude Code GitHub Action v1.0 with auto-detection and slash commands (#421)
* feat: implement Claude Code GitHub Action v1.0 with auto-detection and slash commands

Major features:
- Mode auto-detection based on GitHub event type
- Unified prompt field replacing override_prompt and direct_prompt
- Slash command system with pre-built commands
- Full backward compatibility with v0.x

Key changes:
- Add mode detector for automatic mode selection
- Implement slash command loader with YAML frontmatter support
- Update action.yml with new prompt input
- Create pre-built slash commands for common tasks
- Update all tests for v1.0 compatibility

Breaking changes (with compatibility):
- Mode input now optional (auto-detected)
- override_prompt deprecated (use prompt)
- direct_prompt deprecated (use prompt)

* test + formatting fixes

* feat: simplify to two modes (tag and agent) for v1.0

BREAKING CHANGES:
- Remove review mode entirely - now handled via slash commands in agent mode
- Remove all deprecated backward compatibility fields (mode, anthropic_model, override_prompt, direct_prompt)
- Simplify mode detection: prompt overrides everything, then @claude mentions trigger tag mode, default is agent mode
- Remove slash command resolution from GitHub Action - Claude Code handles natively
- Remove variable substitution - prompts passed through as-is

Architecture changes:
- Only two modes now: tag (for @claude mentions) and agent (everything else)
- Agent mode is the default for all events including PRs
- Users configure behavior via prompts/slash commands (e.g. /review)
- GitHub Action is now a thin wrapper that passes prompts to Claude Code
- Mode names changed: 'experimental-review' → removed entirely

This aligns with the philosophy that the GitHub Action should do minimal work and delegate to Claude Code for all intelligent behavior.

* fix: address PR review comments for v1.0 simplification

- Remove duplicate prompt field spread (line 160)
- Remove async from generatePrompt since slash commands are handled by Claude Code
- Add detailed comment explaining why prompt → agent mode logic
- Remove entire slash-commands loader and directories as Claude Code handles natively
- Simplify prompt generation to just pass through to Claude Code

These changes align with v1.0 philosophy: GitHub Action is a thin wrapper
that delegates everything to Claude Code for native handling.

* chore: remove unused js-yaml dependencies

These were added for slash-command YAML parsing but are no longer
needed since we removed slash-command preprocessing entirely

* fix: remove experimental-review mode reference from MCP config

The inline comment server configuration was checking for deprecated
'mode' field. Since review mode is removed in v1.0, this conditional
block is no longer needed.

* prettify

* feat: add claudeArgs input for direct CLI argument passing

- Add claude_args input to both action.yml files
- Implement shell-style argument parsing with quote handling
- Pass arguments directly to Claude CLI for maximum flexibility
- Add comprehensive tests for argument parsing
- Log custom arguments for debugging

Users can now pass any Claude CLI arguments directly:
  claude_args: '--max-turns 3 --mcp-config /path/to/config.json'

This provides power users full control over Claude's behavior without
waiting for specific inputs to be added to the action.

* refactor: use industry-standard shell-quote for argument parsing

- Replace custom parseShellArgs with battle-tested shell-quote package
- Simplify code by removing unnecessary -p filtering (Claude handles it)
- Update tests to use shell-quote directly
- Add example workflow showing claude_args usage

This provides more robust argument parsing while reducing code complexity.

* bun format

* feat: add claudeArgs input for direct CLI argument passing

- Add claude_args input to action.yml for flexible CLI control
- Parse arguments with industry-standard shell-quote library
- Maintain proper argument order: -p [claudeArgs] [legacy] [BASE_ARGS]
- Keep tag mode defaults (needed for functionality)
- Agent mode has no defaults (full user control)
- Add comprehensive tests for new functionality
- Add example workflow showing usage

* format

* refactor: complete v1.0 simplification by removing all legacy inputs

- Remove all backward compatibility for v1.0 simplification
- Remove 10 legacy inputs from base-action/action.yml
- Remove 9 legacy inputs from main action.yml
- Simplify ClaudeOptions type to just timeoutMinutes and claudeArgs
- Remove all legacy option handling from prepareRunConfig
- Update tests to remove references to deleted fields
- Remove obsolete test file github/context.test.ts
- Clean up types to remove customInstructions, allowedTools, disallowedTools

Users now use claudeArgs exclusively for CLI control.

* fix: update MCP server tests after removing additionalPermissions

- Change github_ci server logic to check for workflow token presence
- Update test names to reflect new behavior
- Fix test that was incorrectly setting workflow token

* model version update

* Update package json

* remove deprecated workflow file (tests features we no longer support)

* Simplify agent mode and re-add additional_permissions input

- Agent mode now only triggers when explicit prompt is provided
- Removed automatic triggering for workflow_dispatch/schedule without prompt
- Re-added additional_permissions input for requesting GitHub permissions
- Fixed TypeScript types for mock context helpers to properly handle partial inputs
- Updated documentation to reflect simplified mode behavior

* Fix MCP config not being passed to Claude CLI

The MCP servers (including github_comment server) were configured but not passed to Claude. This caused the "update_claude_comment" tool to be unavailable.

Changes:
- Write MCP config to a file at $RUNNER_TEMP/claude-mcp-config.json
- Add mcp_config_file output from prepare.ts
- Pass MCP config file via --mcp-config flag in claude_args
- Use fs/promises writeFile to match codebase conventions

* Fix MCP tool availability and shell escaping in tag mode

Pass MCP config and allowed tools through claude_args to ensure tools like
mcp__github_comment__update_claude_comment are properly available to Claude CLI.

Key changes:
- Tag mode outputs claude_args with MCP config (as JSON string) and allowed tools
- Fixed shell escaping vulnerability when JSON contains single quotes
- Agent mode passes through user-provided claude_args unchanged
- Re-added mcp_config input for users to provide custom MCP servers
- Cleaned up misleading comments and unused file operations
- Clarified test workflow is for fork testing

Security fix: Properly escape single quotes in MCP config JSON to prevent
shell injection vulnerabilities.

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

* bun format

* tests, typecheck, format

* registry test update

* Update agent mode to have github server as a default

* Fix agent mode to include GitHub MCP server with proper token

* Simplify review workflow - prevent multiple submissions

- Rename workflow to avoid conflicts
- Remove review submission tools
- Keep only essential tools for reading and analyzing PR

* 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

* Delete .github/workflows/claude-auto-review-test.yml

* 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

* 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

* 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

* 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

* Format code with prettier

* Fix agent mode test to expect branch values

* Fix agent test to handle dynamic branch names from environment

* Better fix: Control environment variables in agent test for predictable behavior

* minor formatting

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

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

* formatting

* 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

* 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

* Use proper WorkflowRunEvent type instead of any

* bun formatting

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

* 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

* Debug: Add logging and always output github_token in prepare step

* Fix: Add git authentication to agent mode

Agent mode now fetches the authenticated user (claude[bot] when using Claude App token)
and configures git identity properly, matching the behavior of tag mode.

This fixes the issue where commits in agent mode were failing due to missing git identity.

* minor bun format

* remove unnecessary file

* fix: Add branch environment variable support to agent mode for signed commits

- Read CLAUDE_BRANCH and BASE_BRANCH env vars in agent mode
- Pass correct branch info to MCP file ops server
- Enables signed auto-fix workflows to create branches via API

* feat: Add auto-fix CI workflow examples

- Add auto-fix-ci example with inline git commits
- Add auto-fix-ci-signed example with signed commits via MCP
- Include corresponding slash commands for both workflows
- Examples demonstrate automated CI failure detection and fixing

* fix: Fix TypeScript error in agent mode git config

- Remove dependency on configureGitAuth which expects ParsedGitHubContext
- Implement git configuration directly for automation contexts
- Properly handle git authentication for agent mode

* fix: Align agent mode git config with existing patterns

- Use GITHUB_SERVER_URL from config module consistently
- Remove existing headers before setting new ones
- Use remote URL with embedded token like git-config.ts does
- Match the existing git authentication pattern in the codebase

* refactor: Use shared configureGitAuth function in agent mode

- Update configureGitAuth to accept GitHubContext instead of ParsedGitHubContext
- This allows both tag mode and agent mode to use the same function
- Removes code duplication and ensures consistent git configuration

* feat: Improve error message for 403 permission errors when committing

When the github_file_ops MCP server gets a 403 error, it now shows a cleaner
message suggesting to rebase from main/master branch to fix the issue.

* docs: Update documentation for v1.0 release (#476)

* docs: Update documentation for v1.0 release

- Integrate breaking changes naturally without alarming users
- Replace deprecated inputs (direct_prompt, custom_instructions, mode) with new unified approach
- Update all examples to use prompt and claude_args instead of deprecated inputs
- Add migration guides to help users transition from v0.x to v1.0
- Emphasize automatic mode detection as a key feature
- Update all workflow examples to @v1 from @beta
- Document how claude_args provides direct CLI control
- Update FAQ with automatic mode detection explanation
- Convert all tool configuration to use claude_args format

* fix: Apply prettier formatting to documentation files

* fix: Update all Claude model versions to latest and improve documentation accuracy

- Update all model references to claude-4-0-sonnet-20250805 (latest Sonnet 4)
- Update Bedrock models to anthropic.claude-4-0-sonnet-20250805-v1:0
- Update Vertex models to claude-4-0-sonnet@20250805
- Fix cloud-providers.md to use claude_args instead of deprecated model input
- Ensure all examples use @v1 instead of @beta
- Keep claude-opus-4-1-20250805 in examples where Opus is demonstrated
- Align all documentation with v1.0 patterns consistently

* feat: Add dedicated migration guide as requested in PR feedback

- Create comprehensive migration-guide.md with step-by-step instructions
- Add prominent links to migration guide in README.md
- Update usage.md to reference the separate migration guide
- Include before/after examples for all common scenarios
- Add checklist for systematic migration
- Address Ashwin's feedback about having a separate, clearly linked migration guide

* feat: Add comprehensive examples for hero use cases

- Add dedicated issue deduplication workflow example
- Add issue triage example (moved from .github/workflows)
- Update all examples to use v1-dev branch consistently
- Enable MCP tools in claude-auto-review.yml
- Consolidate PR review examples into single comprehensive example

Hero use cases now covered:
1. Code reviews (claude-auto-review.yml)
2. Issue triaging (issue-triage.yml)
3. Issue deduplication (issue-deduplication.yml)
4. Auto-fix CI failures (auto-fix-ci/auto-fix-ci.yml)

All examples updated to follow v1-dev paradigm with proper prompt and claude_args configuration.

* refactor: Remove timeout_minutes parameter from action (#482)

This change removes the custom timeout_minutes parameter from the action in favor of using GitHub Actions' native timeout-minutes feature.

Changes:
- Removed timeout_minutes input from action.yml and base-action/action.yml
- Removed all timeout handling logic from base-action/src/run-claude.ts
- Updated base-action/src/index.ts to remove timeoutMinutes parameter
- Removed timeout-related tests from base-action/test/run-claude.test.ts
- Removed timeout_minutes from all example workflow files (19 files)

Rationale:
- Simplifies the codebase by removing custom timeout logic
- Users can use GitHub Actions' native timeout-minutes at the job/step level
- Reduces complexity and maintenance burden
- Follows GitHub Actions best practices

BREAKING CHANGE: The timeout_minutes parameter is no longer supported. Users should use GitHub Actions' native timeout-minutes instead.

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

Co-authored-by: Claude <noreply@anthropic.com>

* refactor: Remove unused slash commands and agents copying logic

Removes experimental file copying features that had no default content:
- Removed experimental_slash_commands_dir parameter and related logic
- Removed automatic project agents copying from .claude/agents/
- Eliminated flaky error-prone cp operations with stderr suppression
- Removed 175 lines of unused code and associated tests

These features were infrastructure without default content that used
problematic error handling patterns (2>/dev/null || true) which could
hide real filesystem errors.

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

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

* docs: Remove references to timeout_minutes parameter

The timeout_minutes parameter was removed in commit 986e40a but
documentation still referenced it. This updates:
- docs/usage.md: Removed timeout_minutes from inputs table
- base-action/README.md: Removed from inputs table and example

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

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

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Kashyap Murali <13315300+katchu11@users.noreply.github.com>
2025-08-25 12:51:37 -07:00
Ashwin Bhat
9c7e1bac94 feat: add path_to_bun_executable input for custom Bun installations (#481)
* feat: add path_to_bun_executable input for custom Bun installations

Adds optional input to specify a custom Bun executable path, bypassing automatic installation. This enables:
- Using pre-installed Bun binaries for faster workflow runs
- Testing with specific Bun versions for debugging
- Custom installation paths in unique environments

Follows the same pattern as path_to_claude_code_executable input.

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

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

* refactor: consolidate custom executable tests into single workflow

- Remove separate test-custom-executable.yml workflow
- Rename test-custom-bun.yml to test-custom-executables.yml
- Add comprehensive tests for custom Claude, custom Bun, and both together
- Improve verification steps with better error handling

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

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

* simplify: test workflow to single job testing both custom executables

- Remove individual test jobs for Claude and Bun
- Keep only the combined test that validates both custom executables work together
- Simplifies CI workflow and reduces redundant testing

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-25 11:08:12 -07:00
Ashwin Bhat
dc65f4ac98 feat: add path_to_claude_code_executable input for custom Claude Code… (#480)
* feat: add path_to_claude_code_executable input for custom Claude Code installations

Adds optional input to specify a custom Claude Code executable path, bypassing automatic installation. This enables:
- Using pre-installed Claude Code binaries
- Testing with specific versions for debugging
- Custom installation paths in unique environments

Includes warning that older versions may cause compatibility issues with new features.

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

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

* test: add workflow to test custom Claude Code executable path

Adds test workflow that:
- Manually installs Claude Code via install script
- Uses the new path_to_claude_code_executable input
- Verifies the custom executable path works correctly
- Validates output and execution success

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-25 08:11:15 -07:00
GitHub Actions
88be3fe6f5 chore: bump Claude Code version to 1.0.90 2025-08-24 23:05:18 +00:00
GitHub Actions
a47fdbe49f chore: bump Claude Code version to 1.0.89 2025-08-22 23:01:22 +00:00
GitHub Actions
28f8362010 chore: bump Claude Code version to 1.0.88 2025-08-22 01:22:25 +00:00
Ashwin Bhat
9f02f6f6d4 fix: Increase maxBuffer for jq processing to handle large Claude outputs (#473)
Fixes "stdout maxBuffer length exceeded" error by increasing the buffer
from Node.js default of 1MB to 10MB when processing Claude output with jq.
This prevents failures when Claude produces large execution logs.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-20 20:02:00 -07:00
GitHub Actions
79cee96324 chore: bump Claude Code version to 1.0.86 2025-08-20 23:27:37 +00:00
Chris Lloyd
194fca8b05 feat: preserve file permissions when committing via GitHub API (#469)
Add file permission detection to github-file-ops-server.ts to properly preserve file modes (regular, executable, symlink) when committing files through the GitHub API. This ensures executable scripts and other special files maintain their correct permissions.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-19 17:18:05 -07:00
GitHub Actions
0f913a6e0e chore: bump Claude Code version to 1.0.85 2025-08-19 23:59:52 +00: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
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
GitHub Actions
a80505bbfb chore: bump Claude Code version to 1.0.77 2025-08-12 19:25:39 +00: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
Ashwin Bhat
4f4f43f044 docs: add prominent notice about upcoming v1.0 breaking changes (#437)
- Add GitHub alert box highlighting the v1.0 roadmap
- Link to discussion #428 for community feedback
- Briefly summarize key changes (automatic mode selection, unified prompt interface)
- Position prominently at top of README for maximum visibility
2025-08-10 16:19:08 -07:00
Matthew Burke
8a5d751740 fix - allowed and disallowed tools ignored in agent mode (#424) 2025-08-08 14:34:55 -07:00
GitHub Actions
bc423b47f5 chore: bump Claude Code version to 1.0.72 2025-08-08 18:16:40 +00:00
Steve
6d5c92076b non negative line validation for comment server (#429)
* enforce non-negative validation for line in GH comment server

* include  .nonnegative() for startLine too
2025-08-08 08:36:20 -07:00
Yuku Kotani
fec554fc7c feat: add flexible bot access control with allowed_bots option (#117)
* feat: skip permission check for GitHub App bot users

GitHub Apps (users ending with [bot]) now bypass permission checks
as they have their own authorization mechanism.

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

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

* feat: add allow_bot_users option to control bot user access

- Add allow_bot_users input parameter (default: false)
- Modify checkHumanActor to optionally allow bot users
- Add comprehensive tests for bot user handling
- Improve security by blocking bot users by default

This change prevents potential prompt injection attacks from bot users
while providing flexibility for trusted bot integrations.

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

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

* docs: mark bot user support feature as completed in roadmap

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

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

* refactor: move allowedBots parameter to context object

Move allowedBots from function parameter to context.inputs to maintain
consistency with other input handling throughout the codebase.

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

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

* docs: update README for bot user support feature

Add documentation for the new allowed_bots parameter that enables
bot users to trigger Claude actions with granular control.

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

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

* fix: add missing allowedBots property in permissions test

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

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

* fix: update bot name format to include [bot] suffix in tests and docs

- Update test cases to use correct bot actor names with [bot] suffix
- Update documentation example to show correct bot name format
- Align with GitHub's actual bot naming convention

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

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

* feat: normalize bot names for allowed_bots validation

- Strip [bot] suffix from both actor names and allowed bot list for comparison
- Allow both "dependabot" and "dependabot[bot]" formats in allowed_bots input
- Display normalized bot names in error messages for consistency
- Add comprehensive test coverage for both naming formats

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-07 18:03:20 -07:00
GitHub Actions
59ca6e42d9 chore: bump Claude Code version to 1.0.71 2025-08-07 22:57:57 +00:00
Aner Cohen
7afc848186 fix: improve GitHub suggestion guidelines in review mode to prevent code duplication (#422)
* fix: prevent duplicate function signatures in review mode suggestions

This fixes a critical bug in the experimental review mode where GitHub
suggestions could create duplicate function signatures when applied.

The issue occurred because:
- GitHub suggestions REPLACE the entire selected line range
- Claude wasn't aware of this behavior and would include the function
  signature in multi-line suggestions, causing duplication

Changes:
- Added detailed instructions about GitHub's line replacement behavior
- Provided clear examples for single-line vs multi-line suggestions
- Added explicit warnings about common mistakes (duplicate signatures)
- Improved code readability by using a codeBlock variable instead of
  escaped backticks in template strings

This ensures Claude creates syntactically correct suggestions that
won't break code when applied through GitHub's suggestion feature.

* chore: format
2025-08-07 08:56:30 -07:00
Graham Campbell
6debac392b Go with Opus 4.1 (#420) 2025-08-06 21:22:15 -07:00
GitHub Actions
55fb6a96d0 chore: bump Claude Code version to 1.0.70 2025-08-06 19:59:40 +00:00
Ashwin Bhat
15db2b3c79 feat: add inline comment MCP server for experimental review mode (#414)
* feat: add inline comment MCP server for experimental review mode

- Create standalone inline PR comments without review workflow
- Support single-line and multi-line comments
- Auto-install server when in experimental review mode
- Uses octokit.rest.pulls.createReviewComment() directly

* docs: clarify GitHub code suggestion syntax in inline comment server

Add clear documentation that suggestion blocks replace the entire selected
line range and must be syntactically complete drop-in replacements.

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

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-06 08:21:29 -07:00
Ashwin Bhat
188d526721 refactor: change git hook from pre-push to pre-commit (#401)
- Renamed scripts/pre-push to scripts/pre-commit
- Updated install-hooks.sh to install pre-commit hook
- Hook now runs formatting, type checking, and tests before commit
2025-08-05 17:02:34 -07:00
Ashwin Bhat
a519840051 fix: remove git config user.name and user.email from allowed tools (#410)
These git config commands are no longer needed as allowed tools since
Claude should not be modifying git configuration settings. Updated
the corresponding test to reflect this intentional change.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-08-05 11:32:46 -07:00
yoshikouki
85287e957d fix: restore prompt file creation in agent mode (#405)
- Restore prompt file creation logic that was accidentally removed in PR #374
- Agent mode now creates the prompt file directly in prepare() method
- Uses override_prompt or direct_prompt if available, falls back to minimal prompt
- Fixes 'Prompt file does not exist' error for workflow_dispatch and schedule events
- Add TODO comment to refactor this to use createPrompt in the future

Fixes #403
2025-08-05 11:14:28 -07:00
GitHub Actions
c6a07895d7 chore: bump Claude Code version to 1.0.69 2025-08-05 16:50:23 +00:00
atsushi-ishibashi
0c5d54472f feat: Add HTML img tag support to GitHub image downloader (#402)
* feat: support html img tag

* rm files

* refactor
2025-08-04 19:37:50 -07:00
GitHub Actions
2845685880 chore: bump Claude Code version to 1.0.68 2025-08-04 23:29:44 +00:00
Ashwin Bhat
b39377f9bc feat: add getSystemPrompt method to mode interface (#400)
Allows modes to provide custom system prompts that are appended to Claude's base system prompt. This enables mode-specific instructions without modifying the core action logic.

- Add optional getSystemPrompt method to Mode interface
- Implement method in all existing modes (tag, agent, review)
- Update prepare.ts to call getSystemPrompt and export as env var
- Wire up APPEND_SYSTEM_PROMPT in action.yml to pass to base-action

All modes currently return undefined (no additional prompts), but the infrastructure is now in place for future modes to provide custom instructions.
2025-08-04 10:51:30 -07:00
Matthew Burke
618565bc0e Update documentation incorrectly reverted after refactor (#399) 2025-08-04 09:00:22 -07:00
Ashwin Bhat
0d9513b3b3 refactor: restructure documentation into organized docs directory (#383)
- Move FAQ.md to docs/faq.md
- Create structured documentation files:
  - setup.md: Manual setup and custom GitHub app instructions
  - usage.md: Basic usage and workflow configuration
  - custom-automations.md: Automation examples
  - configuration.md: MCP servers and advanced settings
  - experimental.md: Execution modes and network restrictions
  - cloud-providers.md: AWS Bedrock and Google Vertex setup
  - capabilities-and-limitations.md: Features and constraints
  - security.md: Security information
- Condense README.md to overview with links to detailed docs
- Keep CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md at top level
2025-08-03 21:16:50 -07:00
km-anthropic
458e4b9e7f feat: ship slash commands with GitHub Action (#381)
* feat: add slash command shipping infrastructure

- Created /slash-commands/ directory to store bundled slash commands
- Added code-review.md slash command for automated PR reviews
- Modified setup-claude-code-settings.ts to copy slash commands to ~/.claude/
- Added test coverage for slash command installation
- Commands are automatically installed when the GitHub Action runs

* fix: simplify slash command implementation to match codebase patterns

- Reverted to using Bun's $ shell syntax consistently with the rest of the codebase
- Simplified slash command copying to basic shell commands
- Removed unnecessary fs/promises complexity
- Maintained all functionality and test coverage
- More appropriate for GitHub Action context where inputs are trusted

* remove test slash command

* fix: rename slash_commands_dir to experimental_slash_commands_dir

- Added 'experimental' prefix as suggested by Ashwin
- Updated all references in action.yml and base-action
- Restored accidentally removed code-review.md file

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
2025-08-03 21:05:33 -07:00
Ashwin Bhat
d66adfb7fa refactor: rename ACTIONS_TOKEN to DEFAULT_WORKFLOW_TOKEN (#385)
Updated all references from ACTIONS_TOKEN to DEFAULT_WORKFLOW_TOKEN to match
the naming convention used in action.yml where the GitHub token is passed as
DEFAULT_WORKFLOW_TOKEN environment variable.
2025-08-02 21:26:52 -07:00
GitHub Actions
d829b4d14b chore: bump Claude Code version to 1.0.67 2025-08-01 22:56:22 +00:00
km-anthropic
0a78530f89 docs: clarify agent mode only works with workflow_dispatch and schedule events (#378)
* docs: clarify agent mode only works with workflow_dispatch and schedule events

Updates documentation to match the current implementation where agent mode
is restricted to workflow_dispatch and schedule events only. This addresses
the confusion reported in issues #364 and #376.

Changes:
- Updated README to clearly state agent mode limitations
- Added explicit note that agent mode does NOT work with PR/issue events
- Updated example workflows to only show supported event types
- Updated CLAUDE.md internal documentation

Fixes #364
Fixes #376

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

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

* minor formatting update

* update agent mode docs

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-08-01 15:47:53 -07:00
GitHub Actions
20e09ef881 chore: bump Claude Code version to 1.0.65 2025-08-01 22:28:48 +00:00
km-anthropic
56179f5fc9 feat: add review mode for automated PR code reviews (#374)
* feat: add review mode for PR code reviews

- Add 'review' as a new execution mode in action.yml
- Use default GitHub Action token (ACTIONS_TOKEN) for review mode
- Create review mode implementation with GitHub MCP tools included by default
- Move review-specific prompt to review mode's generatePrompt method
- Add comprehensive review workflow instructions for inline comments
- Fix type safety with proper mode validation
- Keep agent mode's simple inline prompt handling

* docs: add review mode example workflow

* update sample workflow

* fix: update review mode example to use @beta tag

* fix: enable automatic triggering for review mode on PR events

* fix: export allowed tools environment variables in review mode

The GitHub MCP tools were not being properly allowed because review mode
wasn't exporting the ALLOWED_TOOLS environment variable like agent mode does.
This caused all GitHub MCP tool calls to be blocked with permission errors.

* feat: add review mode workflow for testing

* fix: use INPUT_ prefix for allowed/disallowed tools environment variables

The base action expects INPUT_ALLOWED_TOOLS and INPUT_DISALLOWED_TOOLS
(following GitHub Actions input naming convention) but we were exporting
them without the INPUT_ prefix. This was causing the tools to not be
properly allowed in the base action.

* fix: add explicit review tool names and additional workflow permissions

- Add explicit tool names in case wildcards aren't working properly
- Add statuses and checks write permissions to workflow
- Include both github and github_comment MCP server tools

* refactor: consolidate review workflows and use review mode

- Update claude-review.yml to use review mode instead of direct_prompt
- Use km-anthropic fork action
- Remove duplicate claude-review-mode.yml workflow
- Add synchronize event to review PR updates
- Update permissions for review mode (remove id-token, add pull-requests/issues write)

* feat: enhance review mode to provide detailed tracking comment summary

- Update review mode prompt to explicitly request detailed summaries
- Include issue counts, key findings, and recommendations in tracking comment
- Ensure users can see complete review overview without checking each inline comment

* Revert "refactor: consolidate review workflows and use review mode"

This reverts commit 54ca948599.

* fix: address PR review feedback for review mode

- Make generatePrompt required in Mode interface
- Implement generatePrompt in all modes (tag, agent, review)
- Remove unnecessary git/branch operations from review mode
- Restrict review mode triggers to specific PR actions
- Fix type safety issues by removing any types
- Update tests to support new Mode interface

* test: update mode registry tests to include review mode

* chore: run prettier formatting

* fix: make mode parameter required in generatePrompt function

Remove optional mode parameter since the function throws an error when mode is not provided. This makes the type signature consistent with the actual behavior.

* fix: remove last any type and update README with review mode

- Remove any type cast in review mode by using isPullRequestEvent type guard
- Add review mode documentation to README execution modes section
- Update mode parameter description in README configuration table

* mandatory bun format

* fix: improve review mode GitHub suggestion format instructions

- Add clear guidance on GitHub's suggestion block format
- Emphasize that suggestions must only replace the specific commented lines
- Add examples of correct vs incorrect suggestion formatting
- Clarify when to use multi-line comments with startLine and line parameters
- Guide on handling complex changes that require multiple modifications

This should resolve issues where suggestions aren't directly committable.

* Add missing MCP tools for experimental-review mode based on test requirements

* chore: format code

* docs: add experimental-review mode documentation with clear warnings

* docs: remove emojis from experimental-review mode documentation

* docs: clarify experimental-review mode triggers - depends on workflow configuration

* minor format update

* test: fix registry tests for experimental-review mode name change

* refactor: clean up review mode implementation based on feedback

- Remove unused parameters from generatePrompt in agent and review modes
- Keep Claude comment requirement for review mode (tracking comment)
- Add overridePrompt support to review mode
- Remove non-existent MCP tools from review mode allowed list
- Fix unused import in agent mode

These changes address all review feedback while maintaining clean code
and proper functionality.

* fix: remove redundant update_claude_comment from review mode allowed tools

The github_comment server is always included automatically, so we don't
need to explicitly list mcp__github_comment__update_claude_comment in
the allowed tools.

* feat: review mode now uses review body instead of tracking comment

- Remove tracking comment creation from review mode
- Update prompt to instruct Claude to write comprehensive review in body
- Remove comment ID requirement for review mode
- The review submission body now serves as the main review content

This makes review mode cleaner with one less comment on the PR. The
review body contains all the information that would have been in the
tracking comment.

* add back id-token: write for example

* Add PR number for context + make it mandatory to have a PR associated

* add `mcp__github__add_issue_comment` tool

* rename token

* bun format

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
2025-08-01 15:04:23 -07:00
GitHub Actions
0e5fbc0d44 chore: bump Claude Code version to 1.0.66 2025-08-01 21:53:55 +00:00
atsushi-ishibashi
b4cc5cd6c5 fix: include cache tokens in token usage display (#367)
* fix: Include cache tokens in the stats

* Update format-turns.ts

* fix test

* format
2025-08-01 07:40:07 -07:00
GitHub Actions
1b4ac7d7e0 chore: bump Claude Code version to 1.0.65 2025-07-31 22:02:40 +00:00
GitHub Actions
1f6e3225b0 chore: bump Claude Code version to 1.0.64 2025-07-30 21:49:34 +00:00
atsushi-ishibashi
6672e9b357 Remove empty XML tags in Issue context to reduce token usage (#369)
* chore: remove empty xml tags

* format
2025-07-30 07:30:32 -07:00
aki77
950bdc01df fix: update GitHub MCP server tool name for PR review comments (#363)
Update add_pull_request_review_comment_to_pending_review to add_comment_to_pending_review
  following upstream change in github/github-mcp-server#697

  - Update .github/workflows/claude-review.yml
  - Update examples/claude-auto-review.yml
2025-07-30 07:20:20 -07:00
atsushi-ishibashi
15dd796e97 use total_cost_usd (#366) 2025-07-30 07:19:29 -07:00
atsushi-ishibashi
fd012347a2 feat: exclude hidden (minimized) comments from GitHub Issues and PRs (#368)
* feat: ignore minimized comments

* fix tests
2025-07-30 07:18:34 -07:00
Ashwin Bhat
5bdc533a52 docs: enhance CLAUDE.md with comprehensive architecture overview (#362)
* docs: enhance CLAUDE.md with comprehensive architecture overview

- Add detailed two-phase execution architecture documentation
- Document mode system (tag/agent) and extensible registry pattern
- Include comprehensive GitHub integration layer breakdown
- Add MCP server architecture and authentication flow details
- Document branch strategy, comment threading, and code conventions
- Provide complete project structure with component descriptions

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

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

* docs: clarify base-action dual purpose and remove branch strategy

- Explain base-action serves as both standalone published action and internal logic
- Remove branch strategy section as requested
- Improve architecture documentation clarity

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-29 18:03:45 -07:00
YutaSaito
d45539c118 fix: move env var before image name in docker run for github-mcp-server (#361)
In the previous commit (e07ea013bd), the GITHUB_HOST variable was placed after the image name in the Docker run command, which caused a runtime error. This commit moves the -e option before the image name so it is correctly passed into the container.
2025-07-29 16:22:39 -07:00
km-anthropic
daac7e353f refactor: implement discriminated unions for GitHub contexts (#360)
* feat: add agent mode for automation scenarios

- Add agent mode that always triggers without checking for mentions
- Implement Mode interface with support for mode-specific tool configuration
- Add getAllowedTools() and getDisallowedTools() methods to Mode interface
- Simplify tests by combining related test cases
- Update documentation and examples to include agent mode
- Fix TypeScript imports to prevent circular dependencies

Agent mode is designed for automation and workflow_dispatch scenarios
where Claude should always run without requiring trigger phrases.

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

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

* Minor update to readme (from @main to @beta)

* Since workflow_dispatch isn't in the base action, update the examples accordingly

* minor formatting issue

* Update to say beta instead of main

* Fix missed tracking comment to be false

* add schedule & workflow dispatch paths. Also make prepare logic conditional

* tests

* Add test workflow for workflow_dispatch functionality

* Update workflow to use correct branch reference

* remove test workflow dispatch file

* minor lint update

* update workflow dispatch agent example

* minor lint update

* refactor: simplify prepare logic with mode-specific implementations

* ensure tag mode can't work with workflow dispatch and schedule tasks

* simplify: remove workflow_dispatch/schedule from create-prompt

- Remove workflow_dispatch and schedule event handling from create-prompt
  since agent mode doesn't use the standard prompt generation flow
- Enforce mode compatibility at selection time in the registry instead
  of runtime validation in tag mode
- Add explanatory comment in agent mode about why prompt file is needed
- Update tests to reflect simplified event handling

This reduces code duplication and makes the separation between tag mode
(entity-based events) and agent mode (automation events) clearer.

* simplify PR by making agent mode only work with workflow dispatch and schedule events

* remove unnecessary changes

* remove unnecessary changes from PR

- Revert update-comment-link.ts changes (agent mode doesn't use this)
- Revert create-initial.ts changes (agent mode doesn't create comments)
- Remove unused default-branch.ts file
- Revert install-mcp-server.ts changes (agent mode uses minimal MCP)

These files are only used by tag mode for entity-based events, not needed
for workflow_dispatch/schedule support via agent mode.

* fix: handle optional entityNumber for TypeScript

- Add runtime checks in files that require entityNumber
- These files are only used by tag mode which always has entityNumber
- Agent mode (workflow_dispatch/schedule) doesn't use these files

* linting update

* refactor: implement discriminated unions for GitHub contexts

Split ParsedGitHubContext into entity-specific and automation contexts:
- ParsedGitHubContext: For entity events (issues/PRs) with required entityNumber and isPR
- AutomationContext: For workflow_dispatch/schedule events without entity fields
- GitHubContext: Union type for all contexts

This eliminates ~20 null checks throughout the codebase and provides better type safety.
Entity-specific code paths are now guaranteed to have the required fields.

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

* update comment

* More robust type checking

* refactor: improve discriminated union implementation based on review feedback

- Use eventName checks instead of 'in' operator for more robust type guards
- Remove unnecessary type assertions - TypeScript's control flow analysis works correctly
- Remove redundant runtime checks for entityNumber and isPR
- Simplify code by using context directly after type guard

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

* some structural simplification

* refactor: further simplify discriminated union implementation

- Add event name constants to reduce duplication
- Derive EntityEventName and AutomationEventName types from constants
- Use isAutomationContext consistently in agent mode and registry
- Simplify parseGitHubContext by removing redundant type assertions
- Extract payload casts to variables for cleaner code

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

* bun format

* specify the type

* minor linting update again

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-29 14:58:59 -07:00
GitHub Actions
bdfdd1f788 chore: bump Claude Code version to 1.0.63 2025-07-29 21:43:48 +00:00
km-anthropic
ec0e9b4f87 add schedule & workflow dispatch paths. Also make prepare logic conditional (#353)
* feat: add agent mode for automation scenarios

- Add agent mode that always triggers without checking for mentions
- Implement Mode interface with support for mode-specific tool configuration
- Add getAllowedTools() and getDisallowedTools() methods to Mode interface
- Simplify tests by combining related test cases
- Update documentation and examples to include agent mode
- Fix TypeScript imports to prevent circular dependencies

Agent mode is designed for automation and workflow_dispatch scenarios
where Claude should always run without requiring trigger phrases.

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

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

* Minor update to readme (from @main to @beta)

* Since workflow_dispatch isn't in the base action, update the examples accordingly

* minor formatting issue

* Update to say beta instead of main

* Fix missed tracking comment to be false

* add schedule & workflow dispatch paths. Also make prepare logic conditional

* tests

* Add test workflow for workflow_dispatch functionality

* Update workflow to use correct branch reference

* remove test workflow dispatch file

* minor lint update

* update workflow dispatch agent example

* minor lint update

* refactor: simplify prepare logic with mode-specific implementations

* ensure tag mode can't work with workflow dispatch and schedule tasks

* simplify: remove workflow_dispatch/schedule from create-prompt

- Remove workflow_dispatch and schedule event handling from create-prompt
  since agent mode doesn't use the standard prompt generation flow
- Enforce mode compatibility at selection time in the registry instead
  of runtime validation in tag mode
- Add explanatory comment in agent mode about why prompt file is needed
- Update tests to reflect simplified event handling

This reduces code duplication and makes the separation between tag mode
(entity-based events) and agent mode (automation events) clearer.

* simplify PR by making agent mode only work with workflow dispatch and schedule events

* remove unnecessary changes

* remove unnecessary changes from PR

- Revert update-comment-link.ts changes (agent mode doesn't use this)
- Revert create-initial.ts changes (agent mode doesn't create comments)
- Remove unused default-branch.ts file
- Revert install-mcp-server.ts changes (agent mode uses minimal MCP)

These files are only used by tag mode for entity-based events, not needed
for workflow_dispatch/schedule support via agent mode.

* fix: handle optional entityNumber for TypeScript

- Add runtime checks in files that require entityNumber
- These files are only used by tag mode which always has entityNumber
- Agent mode (workflow_dispatch/schedule) doesn't use these files

* linting update

---------

Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-07-29 11:52:45 -07:00
98 changed files with 5285 additions and 2914 deletions

View File

@@ -30,4 +30,4 @@ jobs:
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_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__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"

38
.github/workflows/claude-test.yml vendored Normal file
View File

@@ -0,0 +1,38 @@
# Test workflow for km-anthropic fork (v1-dev branch)
# This tests the fork implementation, not the main repo
name: Claude Code (Fork Test)
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (
contains(github.event.issue.body, '@claude') ||
contains(github.event.issue.title, '@claude')
))
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write # Required for OIDC token exchange
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Run Claude Code
uses: km-anthropic/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

View File

@@ -31,9 +31,9 @@ jobs:
- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@beta
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."
model: "claude-opus-4-20250514"
claude_args: |
--allowedTools "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
--model "claude-opus-4-1-20250805"

View File

@@ -97,10 +97,12 @@ jobs:
EOF
- name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-base-action@beta
uses: anthropics/claude-code-base-action@v1
with:
prompt_file: /tmp/claude-prompts/triage-prompt.txt
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
mcp_config: /tmp/mcp-config/mcp-servers.json
timeout_minutes: "5"
prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues
--mcp-config /tmp/mcp-config/mcp-servers.json
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -80,38 +80,7 @@ jobs:
gh release create "$next_version" \
--title "$next_version" \
--generate-notes \
--latest=false # We want to keep beta as the latest
update-beta-tag:
needs: create-release
if: ${{ !inputs.dry_run }}
runs-on: ubuntu-latest
environment: production
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Update beta tag
run: |
# Get the latest version tag
VERSION=$(git tag -l 'v[0-9]*' | sort -V | tail -1)
# Update the beta tag to point to this release
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -fa beta -m "Update beta tag to ${VERSION}"
git push origin beta --force
- name: Update beta release to be latest
env:
GH_TOKEN: ${{ github.token }}
run: |
# Update beta release to be marked as latest
gh release edit beta --latest
--latest=false # keep v1 as latest
update-major-tag:
needs: create-release

View File

@@ -25,7 +25,6 @@ jobs:
prompt: ${{ github.event.inputs.test_prompt || 'List the files in the current directory starting with "package"' }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "LS,Read"
timeout_minutes: "3"
- name: Verify inline prompt output
run: |
@@ -83,7 +82,6 @@ jobs:
prompt_file: "test-prompt.txt"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "LS,Read"
timeout_minutes: "3"
- name: Verify prompt file output
run: |

View File

@@ -1,47 +0,0 @@
name: Test Claude Env Feature
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
test-claude-env-with-comments:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test with comments in env
id: comment-test
uses: ./base-action
with:
prompt: |
Use the Bash tool to run: echo "VAR1: $VAR1" && echo "VAR2: $VAR2"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_env: |
# This is a comment
VAR1: value1
# Another comment
VAR2: value2
# Empty lines above should be ignored
allowed_tools: "Bash(echo:*)"
timeout_minutes: "2"
- name: Verify comment handling
run: |
OUTPUT_FILE="${{ steps.comment-test.outputs.execution_file }}"
if [ "${{ steps.comment-test.outputs.conclusion }}" = "success" ]; then
echo "✅ Comments in claude_env handled correctly"
if grep -q "value1" "$OUTPUT_FILE" && grep -q "value2" "$OUTPUT_FILE"; then
echo "✅ Environment variables set correctly despite comments"
else
echo "❌ Environment variables not found"
exit 1
fi
else
echo "❌ Failed with comments in claude_env"
exit 1
fi

View File

@@ -0,0 +1,89 @@
name: Test Custom Executables
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
jobs:
test-custom-executables:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Install Bun manually
run: |
echo "Installing Bun..."
curl -fsSL https://bun.sh/install | bash
echo "Bun installed at: $HOME/.bun/bin/bun"
# Verify Bun installation
if [ -f "$HOME/.bun/bin/bun" ]; then
echo "✅ Bun executable found"
$HOME/.bun/bin/bun --version
else
echo "❌ Bun executable not found"
exit 1
fi
- name: Install Claude Code manually
run: |
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s latest
echo "Claude Code installed at: $HOME/.local/bin/claude"
# Verify Claude installation
if [ -f "$HOME/.local/bin/claude" ]; then
echo "✅ Claude executable found"
ls -la "$HOME/.local/bin/claude"
else
echo "❌ Claude executable not found"
exit 1
fi
- name: Test with both custom executables
id: custom-test
uses: ./base-action
with:
prompt: |
List the files in the current directory starting with "package"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
path_to_claude_code_executable: /home/runner/.local/bin/claude
path_to_bun_executable: /home/runner/.bun/bin/bun
allowed_tools: "LS,Read"
- name: Verify custom executables worked
run: |
OUTPUT_FILE="${{ steps.custom-test.outputs.execution_file }}"
CONCLUSION="${{ steps.custom-test.outputs.conclusion }}"
echo "Conclusion: $CONCLUSION"
echo "Output file: $OUTPUT_FILE"
if [ "$CONCLUSION" = "success" ]; then
echo "✅ Action completed successfully with both custom executables"
else
echo "❌ Action failed with custom executables"
exit 1
fi
if [ -f "$OUTPUT_FILE" ] && [ -s "$OUTPUT_FILE" ]; then
echo "✅ Execution log file created successfully"
if jq . "$OUTPUT_FILE" > /dev/null 2>&1; then
echo "✅ Output is valid JSON"
# Verify the task was completed
if grep -q "package" "$OUTPUT_FILE"; then
echo "✅ Claude successfully listed package files"
else
echo "⚠️ Could not verify if package files were listed"
fi
else
echo "❌ Output is not valid JSON"
exit 1
fi
else
echo "❌ Execution log file not found or empty"
exit 1
fi

View File

@@ -26,7 +26,6 @@ jobs:
"allow": ["Bash(echo:*)"]
}
}
timeout_minutes: "2"
- name: Verify echo worked
run: |
@@ -76,7 +75,6 @@ jobs:
"deny": ["Bash(echo:*)"]
}
}
timeout_minutes: "2"
- name: Verify echo was denied
run: |
@@ -114,7 +112,6 @@ jobs:
Use Bash to echo "Hello from settings file test"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
settings: "test-settings.json"
timeout_minutes: "2"
- name: Verify echo worked
run: |
@@ -169,7 +166,6 @@ jobs:
Use Bash to echo "This should not work from file"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
settings: "test-settings.json"
timeout_minutes: "2"
- name: Verify echo was denied
run: |

2
.npmrc
View File

@@ -1,2 +0,0 @@
engine-strict=true
registry=https://registry.npmjs.org/

128
CLAUDE.md
View File

@@ -1,10 +1,11 @@
# CLAUDE.md
This file provides guidance to Claude Code when working with code in this repository.
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Tools
- Runtime: Bun 1.2.11
- TypeScript with strict configuration
## Common Development Tasks
@@ -17,42 +18,119 @@ bun test
# Formatting
bun run format # Format code with prettier
bun run format:check # Check code formatting
# Type checking
bun run typecheck # Run TypeScript type checker
```
## Architecture Overview
This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action:
This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action operates in two main phases:
1. **Trigger Detection**: Uses `check-trigger.ts` to determine if Claude should respond based on comment/issue content
2. **Context Gathering**: Fetches GitHub data (PRs, issues, comments) via `github-data-fetcher.ts` and formats it using `github-data-formatter.ts`
3. **AI Integration**: Supports multiple Claude providers (Anthropic API, AWS Bedrock, Google Vertex AI)
4. **Prompt Creation**: Generates context-rich prompts using `create-prompt.ts`
5. **MCP Server Integration**: Installs and configures GitHub MCP server for extended functionality
### Phase 1: Preparation (`src/entrypoints/prepare.ts`)
### Key Components
1. **Authentication Setup**: Establishes GitHub token via OIDC or GitHub App
2. **Permission Validation**: Verifies actor has write permissions
3. **Trigger Detection**: Uses mode-specific logic to determine if Claude should respond
4. **Context Creation**: Prepares GitHub context and initial tracking comment
- **Trigger System**: Responds to `/claude` comments or issue assignments
- **Authentication**: OIDC-based token exchange for secure GitHub interactions
- **Cloud Integration**: Supports direct Anthropic API, AWS Bedrock, and Google Vertex AI
- **GitHub Operations**: Creates branches, posts comments, and manages PRs/issues
### Phase 2: Execution (`base-action/`)
The `base-action/` directory contains the core Claude Code execution logic, which serves a dual purpose:
- **Standalone Action**: Published separately as `@anthropic-ai/claude-code-base-action` for direct use
- **Inner Logic**: Used internally by this GitHub Action after preparation phase completes
Execution steps:
1. **MCP Server Setup**: Installs and configures GitHub MCP server for tool access
2. **Prompt Generation**: Creates context-rich prompts from GitHub data
3. **Claude Integration**: Executes via multiple providers (Anthropic API, AWS Bedrock, Google Vertex AI)
4. **Result Processing**: Updates comments and creates branches/PRs as needed
### Key Architectural Components
#### Mode System (`src/modes/`)
- **Tag Mode** (`tag/`): Responds to `@claude` mentions and issue assignments
- **Agent Mode** (`agent/`): Direct execution when explicit prompt is provided
- Extensible registry pattern in `modes/registry.ts`
#### GitHub Integration (`src/github/`)
- **Context Parsing** (`context.ts`): Unified GitHub event handling
- **Data Fetching** (`data/fetcher.ts`): Retrieves PR/issue data via GraphQL/REST
- **Data Formatting** (`data/formatter.ts`): Converts GitHub data to Claude-readable format
- **Branch Operations** (`operations/branch.ts`): Handles branch creation and cleanup
- **Comment Management** (`operations/comments/`): Creates and updates tracking comments
#### MCP Server Integration (`src/mcp/`)
- **GitHub Actions Server** (`github-actions-server.ts`): Workflow and CI access
- **GitHub Comment Server** (`github-comment-server.ts`): Comment operations
- **GitHub File Operations** (`github-file-ops-server.ts`): File system access
- Auto-installation and configuration in `install-mcp-server.ts`
#### Authentication & Security (`src/github/`)
- **Token Management** (`token.ts`): OIDC token exchange and GitHub App authentication
- **Permission Validation** (`validation/permissions.ts`): Write access verification
- **Actor Validation** (`validation/actor.ts`): Human vs bot detection
### Project Structure
```
src/
├── check-trigger.ts # Determines if Claude should respond
├── create-prompt.ts # Generates contextual prompts
├── github-data-fetcher.ts # Retrieves GitHub data
├── github-data-formatter.ts # Formats GitHub data for prompts
├── install-mcp-server.ts # Sets up GitHub MCP server
├── update-comment-with-link.ts # Updates comments with job links
└── types/
── github.ts # TypeScript types for GitHub data
├── entrypoints/ # Action entry points
├── prepare.ts # Main preparation logic
│ ├── update-comment-link.ts # Post-execution comment updates
│ └── format-turns.ts # Claude conversation formatting
├── github/ # GitHub integration layer
│ ├── api/ # REST/GraphQL clients
│ ├── data/ # Data fetching and formatting
── operations/ # Branch, comment, git operations
│ ├── validation/ # Permission and trigger validation
│ └── utils/ # Image downloading, sanitization
├── modes/ # Execution modes
│ ├── tag/ # @claude mention mode
│ ├── agent/ # Automation mode
│ └── registry.ts # Mode selection logic
├── mcp/ # MCP server implementations
├── prepare/ # Preparation orchestration
└── utils/ # Shared utilities
```
## Important Notes
## Important Implementation Notes
- Actions are triggered by `@claude` comments or issue assignment unless a different trigger_phrase is specified
- The action creates branches for issues and pushes to PR branches directly
- All actions create OIDC tokens for secure authentication
- Progress is tracked through dynamic comment updates with checkboxes
### Authentication Flow
- Uses GitHub OIDC token exchange for secure authentication
- Supports custom GitHub Apps via `APP_ID` and `APP_PRIVATE_KEY`
- Falls back to official Claude GitHub App if no custom app provided
### MCP Server Architecture
- Each MCP server has specific GitHub API access patterns
- Servers are auto-installed in `~/.claude/mcp/github-{type}-server/`
- Configuration merged with user-provided MCP config via `mcp_config` input
### Mode System Design
- Modes implement `Mode` interface with `shouldTrigger()` and `prepare()` methods
- Registry validates mode compatibility with GitHub event types
- Agent mode triggers when explicit prompt is provided
### Comment Threading
- Single tracking comment updated throughout execution
- Progress indicated via dynamic checkboxes
- Links to job runs and created branches/PRs
- Sticky comment option for consolidated PR comments
## Code Conventions
- Use Bun-specific TypeScript configuration with `moduleResolution: "bundler"`
- Strict TypeScript with `noUnusedLocals` and `noUnusedParameters` enabled
- Prefer explicit error handling with detailed error messages
- Use discriminated unions for GitHub context types
- Implement retry logic for GitHub API operations via `utils/retry.ts`

952
README.md
View File

@@ -2,10 +2,11 @@
# 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 intelligently detects when to activate based on your workflow context—whether responding to @claude mentions, issue assignments, or executing automation tasks with explicit prompts. It supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, and Google Vertex AI.
## Features
- 🎯 **Intelligent Mode Detection**: Automatically selects the appropriate execution mode based on your workflow context—no configuration needed
- 🤖 **Interactive Code Assistant**: Claude can answer questions about code, architecture, and programming
- 🔍 **Code Review**: Analyzes PR changes and suggests improvements
-**Code Implementation**: Can implement simple fixes, refactoring, and even new features
@@ -13,6 +14,11 @@ A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs an
- 🛠️ **Flexible Tool Access**: Access to GitHub APIs and file operations (additional tools can be enabled via configuration)
- 📋 **Progress Tracking**: Visual progress indicators with checkboxes that dynamically update as Claude completes tasks
- 🏃 **Runs on Your Infrastructure**: The action executes entirely on your own GitHub runner (Anthropic API calls go to your chosen provider)
- ⚙️ **Simplified Configuration**: Unified `prompt` and `claude_args` inputs provide clean, powerful configuration aligned with Claude Code SDK
## 📦 Upgrading from v0.x?
**See our [Migration Guide](./docs/migration-guide.md)** for step-by-step instructions on updating your workflows to v1.0. The new version simplifies configuration while maintaining compatibility with most existing setups.
## Quickstart
@@ -23,942 +29,24 @@ This command will guide you through setting up the GitHub app and required secre
**Note**:
- You must be a repository admin to install the GitHub app and add secrets
- This quickstart method is only available for direct Anthropic API users. If you're using AWS Bedrock, please see the instructions below.
- This quickstart method is only available for direct Anthropic API users. For AWS Bedrock or Google Vertex AI setup, see [docs/cloud-providers.md](./docs/cloud-providers.md).
### Manual Setup (Direct API)
## Documentation
**Requirements**: You must be a repository admin to complete these steps.
1. Install the Claude GitHub app to your repository: https://github.com/apps/claude
2. Add authentication to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions)):
- Either `ANTHROPIC_API_KEY` for API key authentication
- Or `CLAUDE_CODE_OAUTH_TOKEN` for OAuth token authentication (Pro and Max users can generate this by running `claude setup-token` locally)
3. Copy the workflow file from [`examples/claude.yml`](./examples/claude.yml) into your repository's `.github/workflows/`
### Using a Custom GitHub App
If you prefer not to install the official Claude app, you can create your own GitHub App to use with this action. This gives you complete control over permissions and access.
**When you may want to use a custom GitHub App:**
- You need more restrictive permissions than the official app
- Organization policies prevent installing third-party apps
- You're using AWS Bedrock or Google Vertex AI
**Steps to create and use a custom GitHub App:**
1. **Create a new GitHub App:**
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
- Click "New GitHub App"
- Configure the app with these minimum permissions:
- **Repository permissions:**
- Contents: Read & Write
- Issues: Read & Write
- Pull requests: Read & Write
- **Account permissions:** None required
- Set "Where can this GitHub App be installed?" to your preference
- Create the app
2. **Generate and download a private key:**
- After creating the app, scroll down to "Private keys"
- Click "Generate a private key"
- Download the `.pem` file (keep this secure!)
3. **Install the app on your repository:**
- Go to the app's settings page
- Click "Install App"
- Select the repositories where you want to use Claude
4. **Add the app credentials to your repository secrets:**
- Go to your repository's Settings → Secrets and variables → Actions
- Add these secrets:
- `APP_ID`: Your GitHub App's ID (found in the app settings)
- `APP_PRIVATE_KEY`: The contents of the downloaded `.pem` file
5. **Update your workflow to use the custom app:**
```yaml
name: Claude with Custom App
on:
issue_comment:
types: [created]
# ... other triggers
jobs:
claude-response:
runs-on: ubuntu-latest
steps:
# Generate a token from your custom app
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Use Claude with your custom app's token
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.app-token.outputs.token }}
# ... other configuration
```
**Important notes:**
- The custom app must have read/write permissions for Issues, Pull Requests, and Contents
- Your app's token will have the exact permissions you configured, nothing more
For more information on creating GitHub Apps, see the [GitHub documentation](https://docs.github.com/en/apps/creating-github-apps).
- **[Migration Guide](./docs/migration-guide.md)** - **⭐ Upgrading from v0.x to v1.0**
- [Setup Guide](./docs/setup.md) - Manual setup, custom GitHub apps, and security best practices
- [Usage Guide](./docs/usage.md) - Basic usage, workflow configuration, and input parameters
- [Custom Automations](./docs/custom-automations.md) - Examples of automated workflows and custom prompts
- [Configuration](./docs/configuration.md) - MCP servers, permissions, environment variables, and advanced settings
- [Experimental Features](./docs/experimental.md) - Execution modes and network restrictions
- [Cloud Providers](./docs/cloud-providers.md) - AWS Bedrock and Google Vertex AI setup
- [Capabilities & Limitations](./docs/capabilities-and-limitations.md) - What Claude can and cannot do
- [Security](./docs/security.md) - Access control, permissions, and commit signing
- [FAQ](./docs/faq.md) - Common questions and troubleshooting
## 📚 FAQ
Having issues or questions? Check out our [Frequently Asked Questions](./FAQ.md) for solutions to common problems and detailed explanations of Claude's capabilities and limitations.
## Usage
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
```yaml
name: Claude Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned, labeled]
pull_request_review:
types: [submitted]
jobs:
claude-response:
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or use OAuth token instead:
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
# Optional: set execution mode (default: tag)
# mode: "tag"
# Optional: add custom trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: add assignee trigger for issues
# assignee_trigger: "claude"
# Optional: add label trigger for issues
# label_trigger: "claude"
# Optional: add custom environment variables (YAML format)
# claude_env: |
# NODE_ENV: test
# DEBUG: true
# API_URL: https://api.example.com
# Optional: limit the number of conversation turns
# max_turns: "5"
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
# additional_permissions: |
# actions: read
```
## Inputs
| Input | Description | Required | Default |
| ------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
| `mode` | Execution mode: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking) | No | `tag` |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
| `override_prompt` | Complete replacement of Claude's prompt with custom template (supports variable substitution) | No | - |
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `max_turns` | Maximum number of conversation turns Claude can take (limits back-and-forth exchanges) | No | - |
| `timeout_minutes` | Timeout in minutes for execution | No | `30` |
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
| `fallback_model` | Enable automatic fallback to specified model when primary model is unavailable | No | - |
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
| `disallowed_tools` | Tools that Claude should never use | No | "" |
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML format) | No | "" |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
## Execution Modes
The action supports two execution modes, each optimized for different use cases:
### Tag Mode (Default)
The traditional implementation mode that responds to @claude mentions, issue assignments, or labels.
- **Triggers**: `@claude` mentions, issue assignment, label application
- **Features**: Creates tracking comments with progress checkboxes, full implementation capabilities
- **Use case**: General-purpose code implementation and Q&A
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# mode: tag is the default
```
### Agent Mode
For automation and scheduled tasks without trigger checking.
- **Triggers**: Always runs (no trigger checking)
- **Features**: Perfect for scheduled tasks, works with `override_prompt`
- **Use case**: Maintenance tasks, automated reporting, scheduled checks
```yaml
- uses: anthropics/claude-code-action@beta
with:
mode: agent
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
override_prompt: |
Check for outdated dependencies and create an issue if any are found.
```
See [`examples/claude-modes.yml`](./examples/claude-modes.yml) for complete examples of each mode.
### Using Custom MCP Configuration
The `mcp_config` input allows you to add custom MCP (Model Context Protocol) servers to extend Claude's capabilities. These servers merge with the built-in GitHub MCP servers.
#### Basic Example: Adding a Sequential Thinking Server
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
mcp_config: |
{
"mcpServers": {
"sequential-thinking": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-sequential-thinking"
]
}
}
}
allowed_tools: "mcp__sequential-thinking__sequentialthinking" # Important: Each MCP tool from your server must be listed here, comma-separated
# ... other inputs
```
#### Passing Secrets to MCP Servers
For MCP servers that require sensitive information like API keys or tokens, use GitHub Secrets in the environment variables:
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
mcp_config: |
{
"mcpServers": {
"custom-api-server": {
"command": "npx",
"args": ["-y", "@example/api-server"],
"env": {
"API_KEY": "${{ secrets.CUSTOM_API_KEY }}",
"BASE_URL": "https://api.example.com"
}
}
}
}
# ... other inputs
```
#### Using Python MCP Servers with uv
For Python-based MCP servers managed with `uv`, you need to specify the directory containing your server:
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
mcp_config: |
{
"mcpServers": {
"my-python-server": {
"type": "stdio",
"command": "uv",
"args": [
"--directory",
"${{ github.workspace }}/path/to/server/",
"run",
"server_file.py"
]
}
}
}
allowed_tools: "my-python-server__<tool_name>" # Replace <tool_name> with your server's tool names
# ... other inputs
```
For example, if your Python MCP server is at `mcp_servers/weather.py`, you would use:
```yaml
"args":
["--directory", "${{ github.workspace }}/mcp_servers/", "run", "weather.py"]
```
**Important**:
- Always use GitHub Secrets (`${{ secrets.SECRET_NAME }}`) for sensitive values like API keys, tokens, or passwords. Never hardcode secrets directly in the workflow file.
- Your custom servers will override any built-in servers with the same name.
## Examples
### Ways to Tag @claude
These examples show how to interact with Claude using comments in PRs and issues. By default, Claude will be triggered anytime you mention `@claude`, but you can customize the exact trigger phrase using the `trigger_phrase` input in the workflow.
Claude will see the full PR context, including any comments.
#### Ask Questions
Add a comment to a PR or issue:
```
@claude What does this function do and how could we improve it?
```
Claude will analyze the code and provide a detailed explanation with suggestions.
#### Request Fixes
Ask Claude to implement specific changes:
```
@claude Can you add error handling to this function?
```
#### Code Review
Get a thorough review:
```
@claude Please review this PR and suggest improvements
```
Claude will analyze the changes and provide feedback.
#### Fix Bugs from Screenshots
Upload a screenshot of a bug and ask Claude to fix it:
```
@claude Here's a screenshot of a bug I'm seeing [upload screenshot]. Can you fix it?
```
Claude can see and analyze images, making it easy to fix visual bugs or UI issues.
### Custom Automations
These examples show how to configure Claude to act automatically based on GitHub events, without requiring manual @mentions.
#### Supported GitHub Events
This action supports the following GitHub events ([learn more GitHub event triggers](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)):
- `pull_request` - When PRs are opened or synchronized
- `issue_comment` - When comments are created on issues or PRs
- `pull_request_comment` - When comments are made on PR diffs
- `issues` - When issues are opened or assigned
- `pull_request_review` - When PR reviews are submitted
- `pull_request_review_comment` - When comments are made on PR reviews
- `repository_dispatch` - Custom events triggered via API (coming soon)
- `workflow_dispatch` - Manual workflow triggers (coming soon)
#### Automated Documentation Updates
Automatically update documentation when specific files change (see [`examples/claude-pr-path-specific.yml`](./examples/claude-pr-path-specific.yml)):
```yaml
on:
pull_request:
paths:
- "src/api/**/*.ts"
steps:
- uses: anthropics/claude-code-action@beta
with:
direct_prompt: |
Update the API documentation in README.md to reflect
the changes made to the API endpoints in this PR.
```
When API files are modified, Claude automatically updates your README with the latest endpoint documentation and pushes the changes back to the PR, keeping your docs in sync with your code.
#### Author-Specific Code Reviews
Automatically review PRs from specific authors or external contributors (see [`examples/claude-review-from-author.yml`](./examples/claude-review-from-author.yml)):
```yaml
on:
pull_request:
types: [opened, synchronize]
jobs:
review-by-author:
if: |
github.event.pull_request.user.login == 'developer1' ||
github.event.pull_request.user.login == 'external-contributor'
steps:
- uses: anthropics/claude-code-action@beta
with:
direct_prompt: |
Please provide a thorough review of this pull request.
Pay extra attention to coding standards, security practices,
and test coverage since this is from an external contributor.
```
Perfect for automatically reviewing PRs from new team members, external contributors, or specific developers who need extra guidance.
#### Custom Prompt Templates
Use `override_prompt` for complete control over Claude's behavior with variable substitution:
```yaml
- uses: anthropics/claude-code-action@beta
with:
override_prompt: |
Analyze PR #$PR_NUMBER in $REPOSITORY for security vulnerabilities.
Changed files:
$CHANGED_FILES
Focus on:
- SQL injection risks
- XSS vulnerabilities
- Authentication bypasses
- Exposed secrets or credentials
Provide severity ratings (Critical/High/Medium/Low) for any issues found.
```
The `override_prompt` feature supports these variables:
- `$REPOSITORY`, `$PR_NUMBER`, `$ISSUE_NUMBER`
- `$PR_TITLE`, `$ISSUE_TITLE`, `$PR_BODY`, `$ISSUE_BODY`
- `$PR_COMMENTS`, `$ISSUE_COMMENTS`, `$REVIEW_COMMENTS`
- `$CHANGED_FILES`, `$TRIGGER_COMMENT`, `$TRIGGER_USERNAME`
- `$BRANCH_NAME`, `$BASE_BRANCH`, `$EVENT_TYPE`, `$IS_PR`
## How It Works
1. **Trigger Detection**: Listens for comments containing the trigger phrase (default: `@claude`) or issue assignment to a specific user
2. **Context Gathering**: Analyzes the PR/issue, comments, code changes
3. **Smart Responses**: Either answers questions or implements changes
4. **Branch Management**: Creates new PRs for human authors, pushes directly for Claude's own PRs
5. **Communication**: Posts updates at every step to keep you informed
This action is built on top of [`anthropics/claude-code-base-action`](https://github.com/anthropics/claude-code-base-action).
## Capabilities and Limitations
### What Claude Can Do
- **Respond in a Single Comment**: Claude operates by updating a single initial comment with progress and results
- **Answer Questions**: Analyze code and provide explanations
- **Implement Code Changes**: Make simple to moderate code changes based on requests
- **Prepare Pull Requests**: Creates commits on a branch and links back to a prefilled PR creation page
- **Perform Code Reviews**: Analyze PR changes and provide detailed feedback
- **Smart Branch Handling**:
- When triggered on an **issue**: Always creates a new branch for the work
- When triggered on an **open PR**: Always pushes directly to the existing PR branch
- When triggered on a **closed PR**: Creates a new branch since the original is no longer active
- **View GitHub Actions Results**: Can access workflow runs, job logs, and test results on the PR where it's tagged when `actions: read` permission is configured (see [Additional Permissions for CI/CD Integration](#additional-permissions-for-cicd-integration))
### What Claude Cannot Do
- **Submit PR Reviews**: Claude cannot submit formal GitHub PR reviews
- **Approve PRs**: For security reasons, Claude cannot approve pull requests
- **Post Multiple Comments**: Claude only acts by updating its initial comment
- **Execute Commands Outside Its Context**: Claude only has access to the repository and PR/issue context it's triggered in
- **Run Arbitrary Bash Commands**: By default, Claude cannot execute Bash commands unless explicitly allowed using the `allowed_tools` configuration
- **Perform Branch Operations**: Cannot merge branches, rebase, or perform other git operations beyond pushing commits
## Advanced Configuration
### Additional Permissions for CI/CD Integration
The `additional_permissions` input allows Claude to access GitHub Actions workflow information when you grant the necessary permissions. This is particularly useful for analyzing CI/CD failures and debugging workflow issues.
#### Enabling GitHub Actions Access
To allow Claude to view workflow run results, job logs, and CI status:
1. **Grant the necessary permission to your GitHub token**:
- When using the default `GITHUB_TOKEN`, add the `actions: read` permission to your workflow:
```yaml
permissions:
contents: write
pull-requests: write
issues: write
actions: read # Add this line
```
2. **Configure the action with additional permissions**:
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
# ... other inputs
```
3. **Claude will automatically get access to CI/CD tools**:
When you enable `actions: read`, Claude can use the following MCP tools:
- `mcp__github_ci__get_ci_status` - View workflow run statuses
- `mcp__github_ci__get_workflow_run_details` - Get detailed workflow information
- `mcp__github_ci__download_job_log` - Download and analyze job logs
#### Example: Debugging Failed CI Runs
```yaml
name: Claude CI Helper
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
issues: write
actions: read # Required for CI access
jobs:
claude-ci-helper:
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
# Now Claude can respond to "@claude why did the CI fail?"
```
**Important Notes**:
- The GitHub token must have the `actions: read` permission in your workflow
- If the permission is missing, Claude will warn you and suggest adding it
- Currently, only `actions: read` is supported, but the format allows for future extensions
### Custom Environment Variables
You can pass custom environment variables to Claude Code execution using the `claude_env` input. This is useful for CI/test setups that require specific environment variables:
```yaml
- uses: anthropics/claude-code-action@beta
with:
claude_env: |
NODE_ENV: test
CI: true
DATABASE_URL: postgres://test:test@localhost:5432/test_db
# ... other inputs
```
The `claude_env` input accepts YAML format where each line defines a key-value pair. These environment variables will be available to Claude Code during execution, allowing it to run tests, build processes, or other commands that depend on specific environment configurations.
### Limiting Conversation Turns
You can use the `max_turns` parameter to limit the number of back-and-forth exchanges Claude can have during task execution. This is useful for:
- Controlling costs by preventing runaway conversations
- Setting time boundaries for automated workflows
- Ensuring predictable behavior in CI/CD pipelines
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
max_turns: "5" # Limit to 5 conversation turns
# ... other inputs
```
When the turn limit is reached, Claude will stop execution gracefully. Choose a value that gives Claude enough turns to complete typical tasks while preventing excessive usage.
### Custom Tools
By default, Claude only has access to:
- File operations (reading, committing, editing files, read-only git commands)
- Comment management (creating/updating comments)
- Basic GitHub operations
Claude does **not** have access to execute arbitrary Bash commands by default. If you want Claude to run specific commands (e.g., npm install, npm test), you must explicitly allow them using the `allowed_tools` configuration:
**Note**: If your repository has a `.mcp.json` file in the root directory, Claude will automatically detect and use the MCP server tools defined there. However, these tools still need to be explicitly allowed via the `allowed_tools` configuration.
```yaml
- uses: anthropics/claude-code-action@beta
with:
allowed_tools: |
Bash(npm install)
Bash(npm run test)
Edit
Replace
NotebookEditCell
disallowed_tools: |
TaskOutput
KillTask
# ... other inputs
```
**Note**: The base GitHub tools are always included. Use `allowed_tools` to add additional tools (including specific Bash commands), and `disallowed_tools` to prevent specific tools from being used.
### Custom Model
Use a specific Claude model:
```yaml
- uses: anthropics/claude-code-action@beta
with:
# model: "claude-3-5-sonnet-20241022" # Optional: specify a different model
# ... other inputs
```
### Network Restrictions
For enhanced security, you can restrict Claude's network access to specific domains only. This feature is particularly useful for:
- Enterprise environments with strict security policies
- Preventing access to external services
- Limiting Claude to only your internal APIs and services
When `experimental_allowed_domains` is set, Claude can only access the domains you explicitly list. You'll need to include the appropriate provider domains based on your authentication method.
#### Provider-Specific Examples
##### If using Anthropic API or subscription
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
experimental_allowed_domains: |
.anthropic.com
```
##### If using AWS Bedrock
```yaml
- uses: anthropics/claude-code-action@beta
with:
use_bedrock: "true"
experimental_allowed_domains: |
bedrock.*.amazonaws.com
bedrock-runtime.*.amazonaws.com
```
##### If using Google Vertex AI
```yaml
- uses: anthropics/claude-code-action@beta
with:
use_vertex: "true"
experimental_allowed_domains: |
*.googleapis.com
vertexai.googleapis.com
```
#### Common GitHub Domains
In addition to your provider domains, you may need to include GitHub-related domains. For GitHub.com users, common domains include:
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
experimental_allowed_domains: |
.anthropic.com # For Anthropic API
.github.com
.githubusercontent.com
ghcr.io
.blob.core.windows.net
```
For GitHub Enterprise users, replace the GitHub.com domains above with your enterprise domains (e.g., `.github.company.com`, `packages.company.com`, etc.).
To determine which domains your workflow needs, you can temporarily run without restrictions and monitor the network requests, or check your GitHub Enterprise configuration for the specific services you use.
### Claude Code Settings
You can provide Claude Code settings to customize behavior such as model selection, environment variables, permissions, and hooks. Settings can be provided either as a JSON string or a path to a settings file.
#### Option 1: Settings File
```yaml
- uses: anthropics/claude-code-action@beta
with:
settings: "path/to/settings.json"
# ... other inputs
```
#### Option 2: Inline Settings
```yaml
- uses: anthropics/claude-code-action@beta
with:
settings: |
{
"model": "claude-opus-4-20250514",
"env": {
"DEBUG": "true",
"API_URL": "https://api.example.com"
},
"permissions": {
"allow": ["Bash", "Read"],
"deny": ["WebFetch"]
},
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "echo Running bash command..."
}]
}]
}
}
# ... other inputs
```
The settings support all Claude Code settings options including:
- `model`: Override the default model
- `env`: Environment variables for the session
- `permissions`: Tool usage permissions
- `hooks`: Pre/post tool execution hooks
- And more...
For a complete list of available settings and their descriptions, see the [Claude Code settings documentation](https://docs.anthropic.com/en/docs/claude-code/settings).
**Notes**:
- The `enableAllProjectMcpServers` setting is always set to `true` by this action to ensure MCP servers work correctly.
- If both the `model` input parameter and a `model` in settings are provided, the `model` input parameter takes precedence.
- The `allowed_tools` and `disallowed_tools` input parameters take precedence over `permissions` in settings.
- In a future version, we may deprecate individual input parameters in favor of using the settings file for all configuration.
## Cloud Providers
You can authenticate with Claude using any of these three methods:
1. Direct Anthropic API (default)
2. Amazon Bedrock with OIDC authentication
3. Google Vertex AI with OIDC authentication
For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions#using-with-aws-bedrock-%26-google-vertex-ai).
**Note**:
- Bedrock and Vertex use OIDC authentication exclusively
- AWS Bedrock automatically uses cross-region inference profiles for certain models
- For cross-region inference profile models, you need to request and be granted access to the Claude models in all regions that the inference profile uses
### Model Configuration
Use provider-specific model names based on your chosen provider:
```yaml
# For direct Anthropic API (default)
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# ... other inputs
# For Amazon Bedrock with OIDC
- uses: anthropics/claude-code-action@beta
with:
model: "anthropic.claude-3-7-sonnet-20250219-beta:0" # Cross-region inference
use_bedrock: "true"
# ... other inputs
# For Google Vertex AI with OIDC
- uses: anthropics/claude-code-action@beta
with:
model: "claude-3-7-sonnet@20250219"
use_vertex: "true"
# ... other inputs
```
### OIDC Authentication for Bedrock and Vertex
Both AWS Bedrock and GCP Vertex AI require OIDC authentication.
```yaml
# For AWS Bedrock with OIDC
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: us-west-2
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: anthropics/claude-code-action@beta
with:
model: "anthropic.claude-3-7-sonnet-20250219-beta:0"
use_bedrock: "true"
# ... other inputs
permissions:
id-token: write # Required for OIDC
```
```yaml
# For GCP Vertex AI with OIDC
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: anthropics/claude-code-action@beta
with:
model: "claude-3-7-sonnet@20250219"
use_vertex: "true"
# ... other inputs
permissions:
id-token: write # Required for OIDC
```
## Security
### Access Control
- **Repository Access**: The action can only be triggered by users with write access to the repository
- **No Bot Triggers**: GitHub Apps and bots cannot trigger this action
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
### GitHub App Permissions
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
- **Pull Requests**: Read and write to create PRs and push changes
- **Issues**: Read and write to respond to issues
- **Contents**: Read and write to modify repository files
### Commit Signing
All commits made by Claude through this action are automatically signed with commit signatures. This ensures the authenticity and integrity of commits, providing a verifiable trail of changes made by the action.
### ⚠️ Authentication Protection
**CRITICAL: Never hardcode your Anthropic API key or OAuth token in workflow files!**
Your authentication credentials must always be stored in GitHub secrets to prevent unauthorized access:
```yaml
# CORRECT ✅
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# OR
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# NEVER DO THIS ❌
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable!
```
### Setting Up GitHub Secrets
1. Go to your repository's Settings
2. Click on "Secrets and variables" → "Actions"
3. Click "New repository secret"
4. For authentication, choose one:
- API Key: Name: `ANTHROPIC_API_KEY`, Value: Your Anthropic API key (starting with `sk-ant-`)
- OAuth Token: Name: `CLAUDE_CODE_OAUTH_TOKEN`, Value: Your Claude Code OAuth token (Pro and Max users can generate this by running `claude setup-token` locally)
5. Click "Add secret"
### Best Practices for Authentication
1. ✅ Always use `${{ secrets.ANTHROPIC_API_KEY }}` or `${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}` in workflows
2. ✅ Never commit API keys or tokens to version control
3. ✅ Regularly rotate your API keys and tokens
4. ✅ Use environment secrets for organization-wide access
5. ❌ Never share API keys or tokens in pull requests or issues
6. ❌ Avoid logging workflow variables that might contain keys
## Security Best Practices
**⚠️ IMPORTANT: Never commit API keys directly to your repository! Always use GitHub Actions secrets.**
To securely use your Anthropic API key:
1. Add your API key as a repository secret:
- Go to your repository's Settings
- Navigate to "Secrets and variables" → "Actions"
- Click "New repository secret"
- Name it `ANTHROPIC_API_KEY`
- Paste your API key as the value
2. Reference the secret in your workflow:
```yaml
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
**Never do this:**
```yaml
# ❌ WRONG - Exposes your API key
anthropic_api_key: "sk-ant-..."
```
**Always do this:**
```yaml
# ✅ CORRECT - Uses GitHub secrets
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
This applies to all sensitive values including API keys, access tokens, and credentials.
We also recommend that you always use short-lived tokens when possible
Having issues or questions? Check out our [Frequently Asked Questions](./docs/faq.md) for solutions to common problems and detailed explanations of Claude's capabilities and limitations.
## License

View File

@@ -10,7 +10,7 @@ Thank you for trying out the beta of our GitHub Action! This document outlines o
- **Support for workflow_dispatch and repository_dispatch events** - Dispatch Claude on events triggered via API from other workflows or from other services
- **Ability to disable commit signing** - Option to turn off GPG signing for environments where it's not required. This will enable Claude to use normal `git` bash commands for committing. This will likely become the default behavior once added.
- **Better code review behavior** - Support inline comments on specific lines, provide higher quality reviews with more actionable feedback
- **Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude
- ~**Support triggering @claude from bot users** - Allow automation and bot accounts to invoke Claude~
- **Customizable base prompts** - Full control over Claude's initial context with template variables like `$PR_COMMENTS`, `$PR_FILES`, etc. Users can replace our default prompt entirely while still accessing key contextual data
---

View File

@@ -1,5 +1,5 @@
name: "Claude Code Action Official"
description: "General-purpose Claude agent for GitHub PRs and issues. Can answer questions and implement code changes."
name: "Claude Code Action v1.0"
description: "Flexible GitHub automation platform with Claude. Auto-detects mode based on event type: PR reviews, @claude mentions, or custom automation."
branding:
icon: "at-sign"
color: "orange"
@@ -23,51 +23,14 @@ inputs:
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
required: false
default: "claude/"
# Mode configuration
mode:
description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking)"
allowed_bots:
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
required: false
default: "tag"
default: ""
# Claude Code configuration
model:
description: "Model to use (provider-specific format required for Bedrock/Vertex)"
required: false
anthropic_model:
description: "DEPRECATED: Use 'model' instead. Model to use (provider-specific format required for Bedrock/Vertex)"
required: false
fallback_model:
description: "Enable automatic fallback to specified model when primary model is unavailable"
required: false
allowed_tools:
description: "Additional tools for Claude to use (the base GitHub tools will always be included)"
required: false
default: ""
disallowed_tools:
description: "Tools that Claude should never use"
required: false
default: ""
custom_instructions:
description: "Additional custom instructions to include in the prompt for Claude"
required: false
default: ""
direct_prompt:
description: "Direct instruction for Claude (bypasses normal trigger detection)"
required: false
default: ""
override_prompt:
description: "Complete replacement of Claude's prompt with custom template (supports variable substitution)"
required: false
default: ""
mcp_config:
description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers"
additional_permissions:
description: "Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results"
required: false
default: ""
claude_env:
description: "Custom environment variables to pass to Claude Code execution (YAML format)"
prompt:
description: "Instructions for Claude. Can be a direct prompt or custom template."
required: false
default: ""
settings:
@@ -94,14 +57,14 @@ inputs:
required: false
default: "false"
max_turns:
description: "Maximum number of conversation turns"
claude_args:
description: "Additional arguments to pass directly to Claude CLI"
required: false
default: ""
timeout_minutes:
description: "Timeout in minutes for execution"
additional_permissions:
description: "Additional GitHub permissions to request (e.g., 'actions: read')"
required: false
default: "30"
default: ""
use_sticky_comment:
description: "Use just one comment to deliver issue/PR comments"
required: false
@@ -114,6 +77,14 @@ inputs:
description: "Restrict network access to these domains only (newline-separated). If not set, no restrictions are applied. Provider domains are auto-detected."
required: false
default: ""
path_to_claude_code_executable:
description: "Optional path to a custom Claude Code executable. If provided, skips automatic installation and uses this executable instead. WARNING: Using an older version may cause problems if the action begins taking advantage of new Claude Code features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
path_to_bun_executable:
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
outputs:
execution_file:
@@ -122,15 +93,28 @@ 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"
steps:
- name: Install Bun
if: inputs.path_to_bun_executable == ''
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
with:
bun-version: 1.2.11
- name: Setup Custom Bun Path
if: inputs.path_to_bun_executable != ''
shell: bash
run: |
echo "Using custom Bun executable: ${{ inputs.path_to_bun_executable }}"
# Add the directory containing the custom executable to PATH
BUN_DIR=$(dirname "${{ inputs.path_to_bun_executable }}")
echo "$BUN_DIR" >> "$GITHUB_PATH"
- name: Install Dependencies
shell: bash
run: |
@@ -144,23 +128,21 @@ runs:
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
env:
MODE: ${{ inputs.mode }}
PROMPT: ${{ inputs.prompt }}
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
LABEL_TRIGGER: ${{ inputs.label_trigger }}
BASE_BRANCH: ${{ inputs.base_branch }}
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
OVERRIDE_PROMPT: ${{ inputs.override_prompt }}
MCP_CONFIG: ${{ inputs.mcp_config }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
GITHUB_RUN_ID: ${{ github.run_id }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
ACTIONS_TOKEN: ${{ github.token }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }}
ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies
if: steps.prepare.outputs.contains_trigger == 'true'
@@ -171,8 +153,18 @@ runs:
bun install
echo "Base-action dependencies installed"
cd -
# Install Claude Code globally
bun install -g @anthropic-ai/claude-code@1.0.62
# Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.92
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH
CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}")
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
fi
- name: Setup Network Restrictions
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
@@ -195,19 +187,14 @@ runs:
# Base-action inputs
CLAUDE_CODE_ACTION: "1"
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
INPUT_ALLOWED_TOOLS: ${{ env.ALLOWED_TOOLS }}
INPUT_DISALLOWED_TOOLS: ${{ env.DISALLOWED_TOOLS }}
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
INPUT_MCP_CONFIG: ${{ steps.prepare.outputs.mcp_config }}
INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_SYSTEM_PROMPT: ""
INPUT_APPEND_SYSTEM_PROMPT: ""
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
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 }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
# Model configuration
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
NODE_VERSION: ${{ env.NODE_VERSION }}
DETAILED_PERMISSION_MESSAGES: "1"
@@ -279,7 +266,7 @@ runs:
fi
- name: Revoke app token
if: always() && inputs.github_token == ''
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
shell: bash
run: |
curl -L \

View File

@@ -1,2 +0,0 @@
engine-strict=true
registry=https://registry.npmjs.org/

View File

@@ -69,7 +69,7 @@ Add the following to your workflow file:
uses: anthropics/claude-code-base-action@beta
with:
prompt: "Review and fix TypeScript errors"
model: "claude-opus-4-20250514"
model: "claude-opus-4-1-20250805"
fallback_model: "claude-sonnet-4-20250514"
allowed_tools: "Bash(git:*),View,GlobTool,GrepTool,BatchTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -100,7 +100,6 @@ Add the following to your workflow file:
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
| `timeout_minutes` | Timeout in minutes for Claude Code execution | No | '10' |
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |
@@ -217,7 +216,7 @@ Provide the settings configuration directly as a JSON string:
prompt: "Your prompt here"
settings: |
{
"model": "claude-opus-4-20250514",
"model": "claude-opus-4-1-20250805",
"env": {
"DEBUG": "true",
"API_URL": "https://api.example.com"
@@ -320,7 +319,6 @@ You can combine MCP config with other inputs like allowed tools:
prompt: "Access the custom MCP server and use its tools"
mcp_config: "mcp-config.json"
allowed_tools: "Bash(git:*),View,mcp__server-name__custom_tool"
timeout_minutes: "15"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```

View File

@@ -14,53 +14,16 @@ inputs:
description: "Path to a file containing the prompt to send to Claude Code (mutually exclusive with prompt)"
required: false
default: ""
allowed_tools:
description: "Comma-separated list of allowed tools for Claude Code to use"
required: false
default: ""
disallowed_tools:
description: "Comma-separated list of disallowed tools that Claude Code cannot use"
required: false
default: ""
max_turns:
description: "Maximum number of conversation turns (default: no limit)"
required: false
default: ""
mcp_config:
description: "MCP configuration as JSON string or path to MCP configuration JSON file"
required: false
default: ""
settings:
description: "Claude Code settings as JSON string or path to settings JSON file"
required: false
default: ""
system_prompt:
description: "Override system prompt"
required: false
default: ""
append_system_prompt:
description: "Append to system prompt"
required: false
default: ""
model:
description: "Model to use (provider-specific format required for Bedrock/Vertex)"
required: false
anthropic_model:
description: "DEPRECATED: Use 'model' instead. Model to use (provider-specific format required for Bedrock/Vertex)"
required: false
fallback_model:
description: "Enable automatic fallback to specified model when default model is unavailable"
required: false
claude_env:
description: "Custom environment variables to pass to Claude Code execution (YAML multiline format)"
required: false
default: ""
# Action settings
timeout_minutes:
description: "Timeout in minutes for Claude Code execution"
claude_args:
description: "Additional arguments to pass directly to Claude CLI (e.g., '--max-turns 3 --mcp-config /path/to/config.json')"
required: false
default: "10"
default: ""
# Authentication settings
anthropic_api_key:
@@ -84,6 +47,14 @@ inputs:
description: "Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files)"
required: false
default: "false"
path_to_claude_code_executable:
description: "Optional path to a custom Claude Code executable. If provided, skips automatic installation and uses this executable instead. WARNING: Using an older version may cause problems if the action begins taking advantage of new Claude Code features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
path_to_bun_executable:
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
required: false
default: ""
outputs:
conclusion:
@@ -103,10 +74,20 @@ runs:
cache: ${{ inputs.use_node_cache == 'true' && 'npm' || '' }}
- name: Install Bun
if: inputs.path_to_bun_executable == ''
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
with:
bun-version: 1.2.11
- name: Setup Custom Bun Path
if: inputs.path_to_bun_executable != ''
shell: bash
run: |
echo "Using custom Bun executable: ${{ inputs.path_to_bun_executable }}"
# Add the directory containing the custom executable to PATH
BUN_DIR=$(dirname "${{ inputs.path_to_bun_executable }}")
echo "$BUN_DIR" >> "$GITHUB_PATH"
- name: Install Dependencies
shell: bash
run: |
@@ -115,7 +96,16 @@ runs:
- name: Install Claude Code
shell: bash
run: bun install -g @anthropic-ai/claude-code@1.0.62
run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.92
else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH
CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}")
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
fi
- name: Run Claude Code Action
shell: bash
@@ -130,19 +120,12 @@ runs:
env:
# Model configuration
CLAUDE_CODE_ACTION: "1"
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
INPUT_PROMPT: ${{ inputs.prompt }}
INPUT_PROMPT_FILE: ${{ inputs.prompt_file }}
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
INPUT_DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }}
INPUT_MAX_TURNS: ${{ inputs.max_turns }}
INPUT_MCP_CONFIG: ${{ inputs.mcp_config }}
INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_SYSTEM_PROMPT: ${{ inputs.system_prompt }}
INPUT_APPEND_SYSTEM_PROMPT: ${{ inputs.append_system_prompt }}
INPUT_TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
# Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}

View File

@@ -5,10 +5,12 @@
"name": "@anthropic-ai/claude-code-base-action",
"dependencies": {
"@actions/core": "^1.10.1",
"shell-quote": "^1.8.3",
},
"devDependencies": {
"@types/bun": "^1.2.12",
"@types/node": "^20.0.0",
"@types/shell-quote": "^1.7.5",
"prettier": "3.5.3",
"typescript": "^5.8.3",
},
@@ -31,12 +33,16 @@
"@types/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/shell-quote": ["@types/shell-quote@1.7.5", "", {}, "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw=="],
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],

View File

@@ -103,6 +103,6 @@ jobs:
with:
prompt_file: /tmp/claude-prompts/triage-prompt.txt
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
mcp_config: /tmp/mcp-config/mcp-servers.json
timeout_minutes: "5"
claude_args: |
--mcp-config /tmp/mcp-config/mcp-servers.json
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

View File

@@ -10,11 +10,13 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@actions/core": "^1.10.1"
"@actions/core": "^1.10.1",
"shell-quote": "^1.8.3"
},
"devDependencies": {
"@types/bun": "^1.2.12",
"@types/node": "^20.0.0",
"@types/shell-quote": "^1.7.5",
"prettier": "3.5.3",
"typescript": "^5.8.3"
}

View File

@@ -10,7 +10,10 @@ async function run() {
try {
validateEnvironmentVariables();
await setupClaudeCodeSettings(process.env.INPUT_SETTINGS);
await setupClaudeCodeSettings(
process.env.INPUT_SETTINGS,
undefined, // homeDir
);
const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "",
@@ -18,6 +21,7 @@ async function run() {
});
await runClaude(promptConfig.path, {
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
allowedTools: process.env.INPUT_ALLOWED_TOOLS,
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
maxTurns: process.env.INPUT_MAX_TURNS,
@@ -27,6 +31,8 @@ async function run() {
claudeEnv: process.env.INPUT_CLAUDE_ENV,
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable:
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
});
} catch (error) {
core.setFailed(`Action failed with error: ${error}`);

View File

@@ -4,14 +4,18 @@ import { promisify } from "util";
import { unlink, writeFile, stat } from "fs/promises";
import { createWriteStream } from "fs";
import { spawn } from "child_process";
import { parse as parseShellArgs } from "shell-quote";
const execAsync = promisify(exec);
const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`;
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
const BASE_ARGS = ["-p", "--verbose", "--output-format", "stream-json"];
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
export type ClaudeOptions = {
claudeArgs?: string;
model?: string;
pathToClaudeCodeExecutable?: string;
allowedTools?: string;
disallowedTools?: string;
maxTurns?: string;
@@ -20,8 +24,6 @@ export type ClaudeOptions = {
appendSystemPrompt?: string;
claudeEnv?: string;
fallbackModel?: string;
timeoutMinutes?: string;
model?: string;
};
type PreparedConfig = {
@@ -30,85 +32,34 @@ type PreparedConfig = {
env: Record<string, string>;
};
function parseCustomEnvVars(claudeEnv?: string): Record<string, string> {
if (!claudeEnv || claudeEnv.trim() === "") {
return {};
}
const customEnv: Record<string, string> = {};
// Split by lines and parse each line as KEY: VALUE
const lines = claudeEnv.split("\n");
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === "" || trimmedLine.startsWith("#")) {
continue; // Skip empty lines and comments
}
const colonIndex = trimmedLine.indexOf(":");
if (colonIndex === -1) {
continue; // Skip lines without colons
}
const key = trimmedLine.substring(0, colonIndex).trim();
const value = trimmedLine.substring(colonIndex + 1).trim();
if (key) {
customEnv[key] = value;
}
}
return customEnv;
}
export function prepareRunConfig(
promptPath: string,
options: ClaudeOptions,
): PreparedConfig {
const claudeArgs = [...BASE_ARGS];
// Build Claude CLI arguments:
// 1. Prompt flag (always first)
// 2. User's claudeArgs (full control)
// 3. BASE_ARGS (always last, cannot be overridden)
if (options.allowedTools) {
claudeArgs.push("--allowedTools", options.allowedTools);
}
if (options.disallowedTools) {
claudeArgs.push("--disallowedTools", options.disallowedTools);
}
if (options.maxTurns) {
const maxTurnsNum = parseInt(options.maxTurns, 10);
if (isNaN(maxTurnsNum) || maxTurnsNum <= 0) {
throw new Error(
`maxTurns must be a positive number, got: ${options.maxTurns}`,
);
}
claudeArgs.push("--max-turns", options.maxTurns);
}
if (options.mcpConfig) {
claudeArgs.push("--mcp-config", options.mcpConfig);
}
if (options.systemPrompt) {
claudeArgs.push("--system-prompt", options.systemPrompt);
}
if (options.appendSystemPrompt) {
claudeArgs.push("--append-system-prompt", options.appendSystemPrompt);
}
if (options.fallbackModel) {
claudeArgs.push("--fallback-model", options.fallbackModel);
}
if (options.model) {
claudeArgs.push("--model", options.model);
}
if (options.timeoutMinutes) {
const timeoutMinutesNum = parseInt(options.timeoutMinutes, 10);
if (isNaN(timeoutMinutesNum) || timeoutMinutesNum <= 0) {
throw new Error(
`timeoutMinutes must be a positive number, got: ${options.timeoutMinutes}`,
);
}
const claudeArgs = ["-p"];
// Parse and add user's custom Claude arguments
if (options.claudeArgs?.trim()) {
const parsed = parseShellArgs(options.claudeArgs);
const customArgs = parsed.filter(
(arg): arg is string => typeof arg === "string",
);
claudeArgs.push(...customArgs);
}
// Parse custom environment variables
const customEnv = parseCustomEnvVars(options.claudeEnv);
// BASE_ARGS are always appended last (cannot be overridden)
claudeArgs.push(...BASE_ARGS);
const customEnv: Record<string, string> = {};
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
customEnv.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
}
return {
claudeArgs,
@@ -142,13 +93,21 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
console.log(`Prompt file size: ${promptSize} bytes`);
// Log custom environment variables if any
if (Object.keys(config.env).length > 0) {
const envKeys = Object.keys(config.env).join(", ");
console.log(`Custom environment variables: ${envKeys}`);
const customEnvKeys = Object.keys(config.env).filter(
(key) => key !== "CLAUDE_ACTION_INPUTS_PRESENT",
);
if (customEnvKeys.length > 0) {
console.log(`Custom environment variables: ${customEnvKeys.join(", ")}`);
}
// Log custom arguments if any
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
console.log(`Custom Claude arguments: ${options.claudeArgs}`);
}
// Output to console
console.log(`Running Claude with prompt from file: ${config.promptPath}`);
console.log(`Full command: claude ${config.claudeArgs.join(" ")}`);
// Start sending prompt to pipe in background
const catProcess = spawn("cat", [config.promptPath], {
@@ -162,7 +121,10 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
pipeStream.destroy();
});
const claudeProcess = spawn("claude", config.claudeArgs, {
// Use custom executable path if provided, otherwise default to "claude"
const claudeExecutable = options.pathToClaudeCodeExecutable || "claude";
const claudeProcess = spawn(claudeExecutable, config.claudeArgs, {
stdio: ["pipe", "pipe", "inherit"],
env: {
...process.env,
@@ -221,57 +183,15 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
claudeProcess.kill("SIGTERM");
});
// Wait for Claude to finish with timeout
let timeoutMs = 10 * 60 * 1000; // Default 10 minutes
if (options.timeoutMinutes) {
timeoutMs = parseInt(options.timeoutMinutes, 10) * 60 * 1000;
} else if (process.env.INPUT_TIMEOUT_MINUTES) {
const envTimeout = parseInt(process.env.INPUT_TIMEOUT_MINUTES, 10);
if (isNaN(envTimeout) || envTimeout <= 0) {
throw new Error(
`INPUT_TIMEOUT_MINUTES must be a positive number, got: ${process.env.INPUT_TIMEOUT_MINUTES}`,
);
}
timeoutMs = envTimeout * 60 * 1000;
}
// Wait for Claude to finish
const exitCode = await new Promise<number>((resolve) => {
let resolved = false;
// Set a timeout for the process
const timeoutId = setTimeout(() => {
if (!resolved) {
console.error(
`Claude process timed out after ${timeoutMs / 1000} seconds`,
);
claudeProcess.kill("SIGTERM");
// Give it 5 seconds to terminate gracefully, then force kill
setTimeout(() => {
try {
claudeProcess.kill("SIGKILL");
} catch (e) {
// Process may already be dead
}
}, 5000);
resolved = true;
resolve(124); // Standard timeout exit code
}
}, timeoutMs);
claudeProcess.on("close", (code) => {
if (!resolved) {
clearTimeout(timeoutId);
resolved = true;
resolve(code || 0);
}
resolve(code || 0);
});
claudeProcess.on("error", (error) => {
if (!resolved) {
console.error("Claude process error:", error);
clearTimeout(timeoutId);
resolved = true;
resolve(1);
}
console.error("Claude process error:", error);
resolve(1);
});
});
@@ -301,7 +221,10 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
await writeFile("output.txt", output);
// Process output.txt into JSON and save to execution file
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt");
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
maxBuffer: 10 * 1024 * 1024,
});
await writeFile(EXECUTION_FILE, jsonOutput);
console.log(`Log saved to ${EXECUTION_FILE}`);
@@ -318,7 +241,10 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
if (output) {
try {
await writeFile("output.txt", output);
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt");
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
maxBuffer: 10 * 1024 * 1024,
});
await writeFile(EXECUTION_FILE, jsonOutput);
core.setOutput("execution_file", EXECUTION_FILE);
} catch (e) {

View File

@@ -0,0 +1,67 @@
import { describe, expect, test } from "bun:test";
import { parse as parseShellArgs } from "shell-quote";
describe("shell-quote parseShellArgs", () => {
test("should handle empty input", () => {
expect(parseShellArgs("")).toEqual([]);
expect(parseShellArgs(" ")).toEqual([]);
});
test("should parse simple arguments", () => {
expect(parseShellArgs("--max-turns 3")).toEqual(["--max-turns", "3"]);
expect(parseShellArgs("-a -b -c")).toEqual(["-a", "-b", "-c"]);
});
test("should handle double quotes", () => {
expect(parseShellArgs('--config "/path/to/config.json"')).toEqual([
"--config",
"/path/to/config.json",
]);
expect(parseShellArgs('"arg with spaces"')).toEqual(["arg with spaces"]);
});
test("should handle single quotes", () => {
expect(parseShellArgs("--config '/path/to/config.json'")).toEqual([
"--config",
"/path/to/config.json",
]);
expect(parseShellArgs("'arg with spaces'")).toEqual(["arg with spaces"]);
});
test("should handle escaped characters", () => {
expect(parseShellArgs("arg\\ with\\ spaces")).toEqual(["arg with spaces"]);
expect(parseShellArgs('arg\\"with\\"quotes')).toEqual(['arg"with"quotes']);
});
test("should handle mixed quotes", () => {
expect(parseShellArgs(`--msg "It's a test"`)).toEqual([
"--msg",
"It's a test",
]);
expect(parseShellArgs(`--msg 'He said "hello"'`)).toEqual([
"--msg",
'He said "hello"',
]);
});
test("should handle complex real-world example", () => {
const input = `--max-turns 3 --mcp-config "/Users/john/config.json" --model claude-3-5-sonnet-latest --system-prompt 'You are helpful'`;
expect(parseShellArgs(input)).toEqual([
"--max-turns",
"3",
"--mcp-config",
"/Users/john/config.json",
"--model",
"claude-3-5-sonnet-latest",
"--system-prompt",
"You are helpful",
]);
});
test("should filter out non-string results", () => {
// shell-quote can return objects for operators like | > < etc
const result = parseShellArgs("echo hello");
const filtered = result.filter((arg) => typeof arg === "string");
expect(filtered).toEqual(["echo", "hello"]);
});
});

View File

@@ -8,7 +8,7 @@ describe("prepareRunConfig", () => {
const options: ClaudeOptions = {};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs.slice(0, 4)).toEqual([
expect(prepared.claudeArgs).toEqual([
"-p",
"--verbose",
"--output-format",
@@ -23,79 +23,6 @@ describe("prepareRunConfig", () => {
expect(prepared.promptPath).toBe("/tmp/test-prompt.txt");
});
test("should include allowed tools in command arguments", () => {
const options: ClaudeOptions = {
allowedTools: "Bash,Read",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--allowedTools");
expect(prepared.claudeArgs).toContain("Bash,Read");
});
test("should include disallowed tools in command arguments", () => {
const options: ClaudeOptions = {
disallowedTools: "Bash,Read",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--disallowedTools");
expect(prepared.claudeArgs).toContain("Bash,Read");
});
test("should include max turns in command arguments", () => {
const options: ClaudeOptions = {
maxTurns: "5",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--max-turns");
expect(prepared.claudeArgs).toContain("5");
});
test("should include mcp config in command arguments", () => {
const options: ClaudeOptions = {
mcpConfig: "/path/to/mcp-config.json",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--mcp-config");
expect(prepared.claudeArgs).toContain("/path/to/mcp-config.json");
});
test("should include system prompt in command arguments", () => {
const options: ClaudeOptions = {
systemPrompt: "You are a senior backend engineer.",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--system-prompt");
expect(prepared.claudeArgs).toContain("You are a senior backend engineer.");
});
test("should include append system prompt in command arguments", () => {
const options: ClaudeOptions = {
appendSystemPrompt:
"After writing code, be sure to code review yourself.",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--append-system-prompt");
expect(prepared.claudeArgs).toContain(
"After writing code, be sure to code review yourself.",
);
});
test("should include fallback model in command arguments", () => {
const options: ClaudeOptions = {
fallbackModel: "claude-sonnet-4-20250514",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--fallback-model");
expect(prepared.claudeArgs).toContain("claude-sonnet-4-20250514");
});
test("should use provided prompt path", () => {
const options: ClaudeOptions = {};
const prepared = prepareRunConfig("/custom/prompt/path.txt", options);
@@ -103,195 +30,53 @@ describe("prepareRunConfig", () => {
expect(prepared.promptPath).toBe("/custom/prompt/path.txt");
});
test("should not include optional arguments when not set", () => {
const options: ClaudeOptions = {};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).not.toContain("--allowedTools");
expect(prepared.claudeArgs).not.toContain("--disallowedTools");
expect(prepared.claudeArgs).not.toContain("--max-turns");
expect(prepared.claudeArgs).not.toContain("--mcp-config");
expect(prepared.claudeArgs).not.toContain("--system-prompt");
expect(prepared.claudeArgs).not.toContain("--append-system-prompt");
expect(prepared.claudeArgs).not.toContain("--fallback-model");
});
test("should preserve order of claude arguments", () => {
const options: ClaudeOptions = {
allowedTools: "Bash,Read",
maxTurns: "3",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toEqual([
"-p",
"--verbose",
"--output-format",
"stream-json",
"--allowedTools",
"Bash,Read",
"--max-turns",
"3",
]);
});
test("should preserve order with all options including fallback model", () => {
const options: ClaudeOptions = {
allowedTools: "Bash,Read",
disallowedTools: "Write",
maxTurns: "3",
mcpConfig: "/path/to/config.json",
systemPrompt: "You are a helpful assistant",
appendSystemPrompt: "Be concise",
fallbackModel: "claude-sonnet-4-20250514",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toEqual([
"-p",
"--verbose",
"--output-format",
"stream-json",
"--allowedTools",
"Bash,Read",
"--disallowedTools",
"Write",
"--max-turns",
"3",
"--mcp-config",
"/path/to/config.json",
"--system-prompt",
"You are a helpful assistant",
"--append-system-prompt",
"Be concise",
"--fallback-model",
"claude-sonnet-4-20250514",
]);
});
describe("maxTurns validation", () => {
test("should accept valid maxTurns value", () => {
const options: ClaudeOptions = { maxTurns: "5" };
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--max-turns");
expect(prepared.claudeArgs).toContain("5");
});
test("should throw error for non-numeric maxTurns", () => {
const options: ClaudeOptions = { maxTurns: "abc" };
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
"maxTurns must be a positive number, got: abc",
);
});
test("should throw error for negative maxTurns", () => {
const options: ClaudeOptions = { maxTurns: "-1" };
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
"maxTurns must be a positive number, got: -1",
);
});
test("should throw error for zero maxTurns", () => {
const options: ClaudeOptions = { maxTurns: "0" };
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
"maxTurns must be a positive number, got: 0",
);
});
});
describe("timeoutMinutes validation", () => {
test("should accept valid timeoutMinutes value", () => {
const options: ClaudeOptions = { timeoutMinutes: "15" };
expect(() =>
prepareRunConfig("/tmp/test-prompt.txt", options),
).not.toThrow();
});
test("should throw error for non-numeric timeoutMinutes", () => {
const options: ClaudeOptions = { timeoutMinutes: "abc" };
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
"timeoutMinutes must be a positive number, got: abc",
);
});
test("should throw error for negative timeoutMinutes", () => {
const options: ClaudeOptions = { timeoutMinutes: "-5" };
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
"timeoutMinutes must be a positive number, got: -5",
);
});
test("should throw error for zero timeoutMinutes", () => {
const options: ClaudeOptions = { timeoutMinutes: "0" };
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
"timeoutMinutes must be a positive number, got: 0",
);
});
});
describe("custom environment variables", () => {
test("should parse empty claudeEnv correctly", () => {
const options: ClaudeOptions = { claudeEnv: "" };
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.env).toEqual({});
});
test("should parse single environment variable", () => {
const options: ClaudeOptions = { claudeEnv: "API_KEY: secret123" };
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.env).toEqual({ API_KEY: "secret123" });
});
test("should parse multiple environment variables", () => {
describe("claudeArgs handling", () => {
test("should parse and include custom claude arguments", () => {
const options: ClaudeOptions = {
claudeEnv: "API_KEY: secret123\nDEBUG: true\nUSER: testuser",
claudeArgs: "--max-turns 10 --model claude-3-opus-20240229",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.env).toEqual({
API_KEY: "secret123",
DEBUG: "true",
USER: "testuser",
});
expect(prepared.claudeArgs).toEqual([
"-p",
"--max-turns",
"10",
"--model",
"claude-3-opus-20240229",
"--verbose",
"--output-format",
"stream-json",
]);
});
test("should handle environment variables with spaces around values", () => {
test("should handle empty claudeArgs", () => {
const options: ClaudeOptions = {
claudeEnv: "API_KEY: secret123 \n DEBUG : true ",
claudeArgs: "",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.env).toEqual({
API_KEY: "secret123",
DEBUG: "true",
});
expect(prepared.claudeArgs).toEqual([
"-p",
"--verbose",
"--output-format",
"stream-json",
]);
});
test("should skip empty lines and comments", () => {
test("should handle claudeArgs with quoted strings", () => {
const options: ClaudeOptions = {
claudeEnv:
"API_KEY: secret123\n\n# This is a comment\nDEBUG: true\n# Another comment",
claudeArgs: '--system-prompt "You are a helpful assistant"',
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.env).toEqual({
API_KEY: "secret123",
DEBUG: "true",
});
});
test("should skip lines without colons", () => {
const options: ClaudeOptions = {
claudeEnv: "API_KEY: secret123\nINVALID_LINE\nDEBUG: true",
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.env).toEqual({
API_KEY: "secret123",
DEBUG: "true",
});
});
test("should handle undefined claudeEnv", () => {
const options: ClaudeOptions = {};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.env).toEqual({});
expect(prepared.claudeArgs).toEqual([
"-p",
"--system-prompt",
"You are a helpful assistant",
"--verbose",
"--output-format",
"stream-json",
]);
});
});
});

View File

@@ -134,7 +134,7 @@ describe("setupClaudeCodeSettings", () => {
// Then, add new settings
const newSettings = JSON.stringify({
newKey: "newValue",
model: "claude-opus-4-20250514",
model: "claude-opus-4-1-20250805",
});
await setupClaudeCodeSettings(newSettings, testHomeDir);
@@ -145,6 +145,6 @@ describe("setupClaudeCodeSettings", () => {
expect(settings.enableAllProjectMcpServers).toBe(true);
expect(settings.existingKey).toBe("existingValue");
expect(settings.newKey).toBe("newValue");
expect(settings.model).toBe("claude-opus-4-20250514");
expect(settings.model).toBe("claude-opus-4-1-20250805");
});
});

View File

@@ -11,12 +11,14 @@
"@octokit/rest": "^21.1.1",
"@octokit/webhooks-types": "^7.6.1",
"node-fetch": "^3.3.2",
"shell-quote": "^1.8.3",
"zod": "^3.24.4",
},
"devDependencies": {
"@types/bun": "1.2.11",
"@types/node": "^20.0.0",
"@types/node-fetch": "^2.6.12",
"@types/shell-quote": "^1.7.5",
"prettier": "3.5.3",
"typescript": "^5.8.3",
},
@@ -69,6 +71,8 @@
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
"@types/shell-quote": ["@types/shell-quote@1.7.5", "", {}, "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
@@ -245,6 +249,8 @@
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],

View File

@@ -0,0 +1,33 @@
# Capabilities and Limitations
## What Claude Can Do
- **Respond in a Single Comment**: Claude operates by updating a single initial comment with progress and results
- **Answer Questions**: Analyze code and provide explanations
- **Implement Code Changes**: Make simple to moderate code changes based on requests
- **Prepare Pull Requests**: Creates commits on a branch and links back to a prefilled PR creation page
- **Perform Code Reviews**: Analyze PR changes and provide detailed feedback
- **Smart Branch Handling**:
- When triggered on an **issue**: Always creates a new branch for the work
- When triggered on an **open PR**: Always pushes directly to the existing PR branch
- When triggered on a **closed PR**: Creates a new branch since the original is no longer active
- **View GitHub Actions Results**: Can access workflow runs, job logs, and test results on the PR where it's tagged when `actions: read` permission is configured (see [Additional Permissions for CI/CD Integration](./configuration.md#additional-permissions-for-cicd-integration))
## What Claude Cannot Do
- **Submit PR Reviews**: Claude cannot submit formal GitHub PR reviews
- **Approve PRs**: For security reasons, Claude cannot approve pull requests
- **Post Multiple Comments**: Claude only acts by updating its initial comment
- **Execute Commands Outside Its Context**: Claude only has access to the repository and PR/issue context it's triggered in
- **Run Arbitrary Bash Commands**: By default, Claude cannot execute Bash commands unless explicitly allowed using the `allowed_tools` configuration
- **Perform Branch Operations**: Cannot merge branches, rebase, or perform other git operations beyond pushing commits
## How It Works
1. **Trigger Detection**: Listens for comments containing the trigger phrase (default: `@claude`) or issue assignment to a specific user
2. **Context Gathering**: Analyzes the PR/issue, comments, code changes
3. **Smart Responses**: Either answers questions or implements changes
4. **Branch Management**: Creates new PRs for human authors, pushes directly for Claude's own PRs
5. **Communication**: Posts updates at every step to keep you informed
This action is built on top of [`anthropics/claude-code-base-action`](https://github.com/anthropics/claude-code-base-action).

99
docs/cloud-providers.md Normal file
View File

@@ -0,0 +1,99 @@
# Cloud Providers
You can authenticate with Claude using any of these three methods:
1. Direct Anthropic API (default)
2. Amazon Bedrock with OIDC authentication
3. Google Vertex AI with OIDC authentication
For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions#using-with-aws-bedrock-%26-google-vertex-ai).
**Note**:
- Bedrock and Vertex use OIDC authentication exclusively
- AWS Bedrock automatically uses cross-region inference profiles for certain models
- For cross-region inference profile models, you need to request and be granted access to the Claude models in all regions that the inference profile uses
## Model Configuration
Use provider-specific model names based on your chosen provider:
```yaml
# For direct Anthropic API (default)
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# ... other inputs
# For Amazon Bedrock with OIDC
- uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
claude_args: |
--model anthropic.claude-4-0-sonnet-20250805-v1:0
# ... other inputs
# For Google Vertex AI with OIDC
- uses: anthropics/claude-code-action@v1
with:
use_vertex: "true"
claude_args: |
--model claude-4-0-sonnet@20250805
# ... other inputs
```
## OIDC Authentication for Bedrock and Vertex
Both AWS Bedrock and GCP Vertex AI require OIDC authentication.
```yaml
# For AWS Bedrock with OIDC
- name: Configure AWS Credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: us-west-2
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
claude_args: |
--model anthropic.claude-4-0-sonnet-20250805-v1:0
# ... other inputs
permissions:
id-token: write # Required for OIDC
```
```yaml
# For GCP Vertex AI with OIDC
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: anthropics/claude-code-action@v1
with:
use_vertex: "true"
claude_args: |
--model claude-4-0-sonnet@20250805
# ... other inputs
permissions:
id-token: write # Required for OIDC
```

345
docs/configuration.md Normal file
View File

@@ -0,0 +1,345 @@
# Advanced Configuration
## Using Custom MCP Configuration
You can add custom MCP (Model Context Protocol) servers to extend Claude's capabilities using the `--mcp-config` flag in `claude_args`. These servers merge with the built-in GitHub MCP servers.
### Basic Example: Adding a Sequential Thinking Server
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--mcp-config '{"mcpServers": {"sequential-thinking": {"command": "npx", "args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]}}}'
--allowedTools mcp__sequential-thinking__sequentialthinking
# ... other inputs
```
### Passing Secrets to MCP Servers
For MCP servers that require sensitive information like API keys or tokens, you can create a configuration file with GitHub Secrets:
```yaml
- name: Create MCP Config
run: |
cat > /tmp/mcp-config.json << 'EOF'
{
"mcpServers": {
"custom-api-server": {
"command": "npx",
"args": ["-y", "@example/api-server"],
"env": {
"API_KEY": "${{ secrets.CUSTOM_API_KEY }}",
"BASE_URL": "https://api.example.com"
}
}
}
}
EOF
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--mcp-config /tmp/mcp-config.json
# ... other inputs
```
### Using Python MCP Servers with uv
For Python-based MCP servers managed with `uv`, you need to specify the directory containing your server:
```yaml
- name: Create MCP Config for Python Server
run: |
cat > /tmp/mcp-config.json << 'EOF'
{
"mcpServers": {
"my-python-server": {
"type": "stdio",
"command": "uv",
"args": [
"--directory",
"${{ github.workspace }}/path/to/server/",
"run",
"server_file.py"
]
}
}
}
EOF
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--mcp-config /tmp/mcp-config.json
--allowedTools my-python-server__<tool_name> # Replace <tool_name> with your server's tool names
# ... other inputs
```
For example, if your Python MCP server is at `mcp_servers/weather.py`, you would use:
```yaml
"args":
["--directory", "${{ github.workspace }}/mcp_servers/", "run", "weather.py"]
```
### Multiple MCP Servers
You can add multiple MCP servers by using multiple `--mcp-config` flags:
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--mcp-config /tmp/config1.json
--mcp-config /tmp/config2.json
--mcp-config '{"mcpServers": {"inline-server": {"command": "npx", "args": ["@example/server"]}}}'
# ... other inputs
```
**Important**:
- Always use GitHub Secrets (`${{ secrets.SECRET_NAME }}`) for sensitive values like API keys, tokens, or passwords. Never hardcode secrets directly in the workflow file.
- Your custom servers will override any built-in servers with the same name.
- The `claude_args` supports multiple `--mcp-config` flags that will be merged together.
## Additional Permissions for CI/CD Integration
The `additional_permissions` input allows Claude to access GitHub Actions workflow information when you grant the necessary permissions. This is particularly useful for analyzing CI/CD failures and debugging workflow issues.
### Enabling GitHub Actions Access
To allow Claude to view workflow run results, job logs, and CI status:
1. **Grant the necessary permission to your GitHub token**:
- When using the default `GITHUB_TOKEN`, add the `actions: read` permission to your workflow:
```yaml
permissions:
contents: write
pull-requests: write
issues: write
actions: read # Add this line
```
2. **Configure the action with additional permissions**:
```yaml
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
# ... other inputs
```
3. **Claude will automatically get access to CI/CD tools**:
When you enable `actions: read`, Claude can use the following MCP tools:
- `mcp__github_ci__get_ci_status` - View workflow run statuses
- `mcp__github_ci__get_workflow_run_details` - Get detailed workflow information
- `mcp__github_ci__download_job_log` - Download and analyze job logs
### Example: Debugging Failed CI Runs
```yaml
name: Claude CI Helper
on:
issue_comment:
types: [created]
permissions:
contents: write
pull-requests: write
issues: write
actions: read # Required for CI access
jobs:
claude-ci-helper:
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: |
actions: read
# Now Claude can respond to "@claude why did the CI fail?"
```
**Important Notes**:
- The GitHub token must have the `actions: read` permission in your workflow
- If the permission is missing, Claude will warn you and suggest adding it
- Currently, only `actions: read` is supported, but the format allows for future extensions
## Custom Environment Variables
You can pass custom environment variables to Claude Code execution using the `settings` input. This is useful for CI/test setups that require specific environment variables:
```yaml
- uses: anthropics/claude-code-action@v1
with:
settings: |
{
"env": {
"NODE_ENV": "test",
"CI": "true",
"DATABASE_URL": "postgres://test:test@localhost:5432/test_db"
}
}
# ... other inputs
```
These environment variables will be available to Claude Code during execution, allowing it to run tests, build processes, or other commands that depend on specific environment configurations.
## Limiting Conversation Turns
You can limit the number of back-and-forth exchanges Claude can have during task execution using the `claude_args` input. This is useful for:
- Controlling costs by preventing runaway conversations
- Setting time boundaries for automated workflows
- Ensuring predictable behavior in CI/CD pipelines
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--max-turns 5 # Limit to 5 conversation turns
# ... other inputs
```
When the turn limit is reached, Claude will stop execution gracefully. Choose a value that gives Claude enough turns to complete typical tasks while preventing excessive usage.
## Custom Tools
By default, Claude only has access to:
- File operations (reading, committing, editing files, read-only git commands)
- Comment management (creating/updating comments)
- Basic GitHub operations
Claude does **not** have access to execute arbitrary Bash commands by default. If you want Claude to run specific commands (e.g., npm install, npm test), you must explicitly allow them using the `claude_args` configuration:
**Note**: If your repository has a `.mcp.json` file in the root directory, Claude will automatically detect and use the MCP server tools defined there. However, these tools still need to be explicitly allowed.
```yaml
- uses: anthropics/claude-code-action@v1
with:
claude_args: |
--allowedTools "Bash(npm install),Bash(npm run test),Edit,Replace,NotebookEditCell"
--disallowedTools "TaskOutput,KillTask"
# ... other inputs
```
**Note**: The base GitHub tools are always included. Use `--allowedTools` to add additional tools (including specific Bash commands), and `--disallowedTools` to prevent specific tools from being used.
## Custom Model
Specify a Claude model using `claude_args`:
```yaml
- uses: anthropics/claude-code-action@v1
with:
claude_args: |
--model claude-4-0-sonnet-20250805
# ... other inputs
```
For provider-specific models:
```yaml
# AWS Bedrock
- uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
claude_args: |
--model anthropic.claude-4-0-sonnet-20250805-v1:0
# ... other inputs
# Google Vertex AI
- uses: anthropics/claude-code-action@v1
with:
use_vertex: "true"
claude_args: |
--model claude-4-0-sonnet@20250805
# ... other inputs
```
## Claude Code Settings
You can provide Claude Code settings to customize behavior such as model selection, environment variables, permissions, and hooks. Settings can be provided either as a JSON string or a path to a settings file.
### Option 1: Settings File
```yaml
- uses: anthropics/claude-code-action@v1
with:
settings: "path/to/settings.json"
# ... other inputs
```
### Option 2: Inline Settings
```yaml
- uses: anthropics/claude-code-action@v1
with:
settings: |
{
"model": "claude-opus-4-1-20250805",
"env": {
"DEBUG": "true",
"API_URL": "https://api.example.com"
},
"permissions": {
"allow": ["Bash", "Read"],
"deny": ["WebFetch"]
},
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "echo Running bash command..."
}]
}]
}
}
# ... other inputs
```
The settings support all Claude Code settings options including:
- `model`: Override the default model
- `env`: Environment variables for the session
- `permissions`: Tool usage permissions
- `hooks`: Pre/post tool execution hooks
- And more...
For a complete list of available settings and their descriptions, see the [Claude Code settings documentation](https://docs.anthropic.com/en/docs/claude-code/settings).
**Notes**:
- The `enableAllProjectMcpServers` setting is always set to `true` by this action to ensure MCP servers work correctly.
- The `claude_args` input provides direct access to Claude Code CLI arguments and takes precedence over settings.
- We recommend using `claude_args` for simple configurations and `settings` for complex configurations with hooks and environment variables.
## Migration from Deprecated Inputs
Many individual input parameters have been consolidated into `claude_args` or `settings`. Here's how to migrate:
| Old Input | New Approach |
| --------------------- | -------------------------------------------------------- |
| `allowed_tools` | Use `claude_args: "--allowedTools Tool1,Tool2"` |
| `disallowed_tools` | Use `claude_args: "--disallowedTools Tool1,Tool2"` |
| `max_turns` | Use `claude_args: "--max-turns 10"` |
| `model` | Use `claude_args: "--model claude-4-0-sonnet-20250805"` |
| `claude_env` | Use `settings` with `"env"` object |
| `custom_instructions` | Use `claude_args: "--system-prompt 'Your instructions'"` |
| `mcp_config` | Use `claude_args: "--mcp-config '{...}'"` |
| `direct_prompt` | Use `prompt` input instead |
| `override_prompt` | Use `prompt` with GitHub context variables |

113
docs/custom-automations.md Normal file
View File

@@ -0,0 +1,113 @@
# Custom Automations
These examples show how to configure Claude to act automatically based on GitHub events. When you provide a `prompt` input, the action automatically runs in agent mode without requiring manual @mentions. Without a `prompt`, it runs in interactive mode, responding to @claude mentions.
## Supported GitHub Events
This action supports the following GitHub events ([learn more GitHub event triggers](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)):
- `pull_request` - When PRs are opened or synchronized
- `issue_comment` - When comments are created on issues or PRs
- `pull_request_comment` - When comments are made on PR diffs
- `issues` - When issues are opened or assigned
- `pull_request_review` - When PR reviews are submitted
- `pull_request_review_comment` - When comments are made on PR reviews
- `repository_dispatch` - Custom events triggered via API (coming soon)
- `workflow_dispatch` - Manual workflow triggers (coming soon)
## Automated Documentation Updates
Automatically update documentation when specific files change (see [`examples/claude-pr-path-specific.yml`](../examples/claude-pr-path-specific.yml)):
```yaml
on:
pull_request:
paths:
- "src/api/**/*.ts"
steps:
- uses: anthropics/claude-code-action@v1
with:
prompt: |
Update the API documentation in README.md to reflect
the changes made to the API endpoints in this PR.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
When API files are modified, the action automatically detects that a `prompt` is provided and runs in agent mode. Claude updates your README with the latest endpoint documentation and pushes the changes back to the PR, keeping your docs in sync with your code.
## Author-Specific Code Reviews
Automatically review PRs from specific authors or external contributors (see [`examples/claude-review-from-author.yml`](../examples/claude-review-from-author.yml)):
```yaml
on:
pull_request:
types: [opened, synchronize]
jobs:
review-by-author:
if: |
github.event.pull_request.user.login == 'developer1' ||
github.event.pull_request.user.login == 'external-contributor'
steps:
- uses: anthropics/claude-code-action@v1
with:
prompt: |
Please provide a thorough review of this pull request.
Pay extra attention to coding standards, security practices,
and test coverage since this is from an external contributor.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
Perfect for automatically reviewing PRs from new team members, external contributors, or specific developers who need extra guidance. The action automatically runs in agent mode when a `prompt` is provided.
## Custom Prompt Templates
Use the `prompt` input with GitHub context variables for dynamic automation:
```yaml
- uses: anthropics/claude-code-action@v1
with:
prompt: |
Analyze PR #${{ github.event.pull_request.number }} in ${{ github.repository }} for security vulnerabilities.
Focus on:
- SQL injection risks
- XSS vulnerabilities
- Authentication bypasses
- Exposed secrets or credentials
Provide severity ratings (Critical/High/Medium/Low) for any issues found.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
You can access any GitHub context variable using the standard GitHub Actions syntax:
- `${{ github.repository }}` - The repository name
- `${{ github.event.pull_request.number }}` - PR number
- `${{ github.event.issue.number }}` - Issue number
- `${{ github.event.pull_request.title }}` - PR title
- `${{ github.event.pull_request.body }}` - PR description
- `${{ github.event.comment.body }}` - Comment text
- `${{ github.actor }}` - User who triggered the workflow
- `${{ github.base_ref }}` - Base branch for PRs
- `${{ github.head_ref }}` - Head branch for PRs
## Advanced Configuration with claude_args
For more control over Claude's behavior, use the `claude_args` input to pass CLI arguments directly:
```yaml
- uses: anthropics/claude-code-action@v1
with:
prompt: "Review this PR for performance issues"
claude_args: |
--max-turns 15
--model claude-4-0-sonnet-20250805
--allowedTools Edit,Read,Write,Bash
--system-prompt "You are a performance optimization expert. Focus on identifying bottlenecks and suggesting improvements."
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
This provides full access to Claude Code CLI capabilities while maintaining the simplified action interface.

128
docs/experimental.md Normal file
View File

@@ -0,0 +1,128 @@
# Experimental Features
**Note:** Experimental features are considered unstable and not supported for production use. They may change or be removed at any time.
## Automatic Mode Detection
The action intelligently detects the appropriate execution mode based on your workflow context, eliminating the need for manual mode configuration.
### Interactive Mode (Tag Mode)
Activated when Claude detects @mentions, issue assignments, or labels—without an explicit `prompt`.
- **Triggers**: `@claude` mentions in comments, issue assignment to claude user, label application
- **Features**: Creates tracking comments with progress checkboxes, full implementation capabilities
- **Use case**: Interactive code assistance, Q&A, and implementation requests
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# No prompt needed - responds to @claude mentions
```
### Automation Mode (Agent Mode)
Automatically activated when you provide a `prompt` input.
- **Triggers**: Any GitHub event when `prompt` input is provided
- **Features**: Direct execution without requiring @claude mentions, streamlined for automation
- **Use case**: Automated PR reviews, scheduled tasks, workflow automation
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Check for outdated dependencies and create an issue if any are found.
# Automatically runs in agent mode when prompt is provided
```
### How It Works
The action uses this logic to determine the mode:
1. **If `prompt` is provided** → Runs in **agent mode** for automation
2. **If no `prompt` but @claude is mentioned** → Runs in **tag mode** for interaction
3. **If neither** → No action is taken
This automatic detection ensures your workflows are simpler and more intuitive, without needing to understand or configure different modes.
### Advanced Mode Control
For specialized use cases, you can fine-tune behavior using `claude_args`:
```yaml
- uses: anthropics/claude-code-action@v1
with:
prompt: "Review this PR"
claude_args: |
--max-turns 20
--system-prompt "You are a code review specialist"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
## Network Restrictions
For enhanced security, you can restrict Claude's network access to specific domains only. This feature is particularly useful for:
- Enterprise environments with strict security policies
- Preventing access to external services
- Limiting Claude to only your internal APIs and services
When `experimental_allowed_domains` is set, Claude can only access the domains you explicitly list. You'll need to include the appropriate provider domains based on your authentication method.
### Provider-Specific Examples
#### If using Anthropic API or subscription
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
experimental_allowed_domains: |
.anthropic.com
```
#### If using AWS Bedrock
```yaml
- uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
experimental_allowed_domains: |
bedrock.*.amazonaws.com
bedrock-runtime.*.amazonaws.com
```
#### If using Google Vertex AI
```yaml
- uses: anthropics/claude-code-action@v1
with:
use_vertex: "true"
experimental_allowed_domains: |
*.googleapis.com
vertexai.googleapis.com
```
### Common GitHub Domains
In addition to your provider domains, you may need to include GitHub-related domains. For GitHub.com users, common domains include:
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
experimental_allowed_domains: |
.anthropic.com # For Anthropic API
.github.com
.githubusercontent.com
ghcr.io
.blob.core.windows.net
```
For GitHub Enterprise users, replace the GitHub.com domains above with your enterprise domains (e.g., `.github.company.com`, `packages.company.com`, etc.).
To determine which domains your workflow needs, you can temporarily run without restrictions and monitor the network requests, or check your GitHub Enterprise configuration for the specific services you use.

View File

@@ -41,10 +41,11 @@ By default, Claude only uses commit tools for non-destructive changes to the bra
- Never push to branches other than where it was invoked (either its own branch or the PR branch)
- Never force push or perform destructive operations
You can grant additional tools via the `allowed_tools` input if needed:
You can grant additional tools via the `claude_args` input if needed:
```yaml
allowed_tools: "Bash(git rebase:*)" # Use with caution
claude_args: |
--allowedTools "Bash(git rebase:*)" # Use with caution
```
### Why won't Claude create a pull request?
@@ -67,7 +68,7 @@ Yes! Claude can access GitHub Actions workflow runs, job logs, and test results
2. Configure the action with additional permissions:
```yaml
- uses: anthropics/claude-code-action@beta
- uses: anthropics/claude-code-action@v1
with:
additional_permissions: |
actions: read
@@ -105,36 +106,65 @@ If you need full history, you can configure this in your workflow before calling
## Configuration and Tools
### What's the difference between `direct_prompt` and `custom_instructions`?
### How does automatic mode detection work?
These inputs serve different purposes in how Claude responds:
The action intelligently detects whether to run in interactive mode or automation mode:
- **`direct_prompt`**: Bypasses trigger detection entirely. When provided, Claude executes this exact instruction regardless of comments or mentions. Perfect for automated workflows where you want Claude to perform a specific task on every run (e.g., "Update the API documentation based on changes in this PR").
- **With `prompt` input**: Runs in automation mode - executes immediately without waiting for @claude mentions
- **Without `prompt` input**: Runs in interactive mode - waits for @claude mentions in comments
- **`custom_instructions`**: Additional context added to Claude's system prompt while still respecting normal triggers. These instructions modify Claude's behavior but don't replace the triggering comment. Use this to give Claude standing instructions like "You have been granted additional tools for ...".
This automatic detection eliminates the need to manually configure modes.
Example:
```yaml
# Using direct_prompt - runs automatically without @claude mention
direct_prompt: "Review this PR for security vulnerabilities"
# Automation mode - runs automatically
prompt: "Review this PR for security vulnerabilities"
# Interactive mode - waits for @claude mention
# (no prompt provided)
```
# Using custom_instructions - still requires @claude trigger
custom_instructions: "Focus on performance implications and suggest optimizations"
### What happened to `direct_prompt` and `custom_instructions`?
**These inputs are deprecated in v1.0:**
- **`direct_prompt`** → Use `prompt` instead
- **`custom_instructions`** → Use `claude_args` with `--system-prompt`
Migration examples:
```yaml
# Old (v0.x)
direct_prompt: "Review this PR"
custom_instructions: "Focus on security"
# New (v1.0)
prompt: "Review this PR"
claude_args: |
--system-prompt "Focus on security"
```
### Why doesn't Claude execute my bash commands?
The Bash tool is **disabled by default** for security. To enable individual bash commands:
The Bash tool is **disabled by default** for security. To enable individual bash commands using `claude_args`:
```yaml
allowed_tools: "Bash(npm:*),Bash(git:*)" # Allows only npm and git commands
claude_args: |
--allowedTools "Bash(npm:*),Bash(git:*)" # Allows only npm and git commands
```
### Can Claude work across multiple repositories?
No, Claude's GitHub app token is sandboxed to the current repository only. It cannot push to any other repositories. It can, however, read public repositories, but to get access to this, you must configure it with tools to do so.
### Why aren't comments posted as claude[bot]?
Comments appear as claude[bot] when the action uses its built-in authentication. However, if you provide a `github_token` in your workflow, the action will use that token's authentication instead, causing comments to appear under a different username.
**Solution**: Remove `github_token` from your workflow file unless you're using a custom GitHub App.
**Note**: The `use_sticky_comment` feature only works with claude[bot] authentication. If you're using a custom `github_token`, sticky comments won't update properly since they expect the claude[bot] username.
## MCP Servers and Extended Functionality
### What MCP servers are available by default?
@@ -144,7 +174,7 @@ Claude Code Action automatically configures two MCP servers:
1. **GitHub MCP server**: For GitHub API operations
2. **File operations server**: For advanced file manipulation
However, tools from these servers still need to be explicitly allowed via `allowed_tools`.
However, tools from these servers still need to be explicitly allowed via `claude_args` with `--allowedTools`.
## Troubleshooting
@@ -160,7 +190,7 @@ The trigger uses word boundaries, so `@claude` must be a complete word. Variatio
1. **Always specify permissions explicitly** in your workflow file
2. **Use GitHub Secrets** for API keys - never hardcode them
3. **Be specific with `allowed_tools`** - only enable what's necessary
3. **Be specific with tool permissions** - only enable what's necessary via `claude_args`
4. **Test in a separate branch** before using on important PRs
5. **Monitor Claude's token usage** to avoid hitting API limits
6. **Review Claude's changes** carefully before merging

261
docs/migration-guide.md Normal file
View File

@@ -0,0 +1,261 @@
# Migration Guide: v0.x to v1.0
This guide helps you migrate from Claude Code Action v0.x to v1.0. The new version introduces intelligent mode detection and simplified configuration while maintaining backward compatibility for most use cases.
## Overview of Changes
### 🎯 Key Improvements in v1.0
1. **Automatic Mode Detection** - No more manual `mode` configuration
2. **Simplified Configuration** - Unified `prompt` and `claude_args` inputs
3. **Better SDK Alignment** - Closer integration with Claude Code CLI
### ⚠️ Breaking Changes
The following inputs have been deprecated and replaced:
| Deprecated Input | Replacement | Notes |
| --------------------- | -------------------------------- | --------------------------------------------- |
| `mode` | Auto-detected | Action automatically chooses based on context |
| `direct_prompt` | `prompt` | Direct drop-in replacement |
| `override_prompt` | `prompt` | Use GitHub context variables instead |
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
| `max_turns` | `claude_args: --max-turns` | Use CLI format |
| `model` | `claude_args: --model` | Specify via CLI |
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
| `claude_env` | `settings` with env object | Use settings JSON |
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
## Migration Examples
### Basic Interactive Workflow (@claude mentions)
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
mode: "tag"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
custom_instructions: "Follow our coding standards"
max_turns: "10"
allowed_tools: "Edit,Read,Write"
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--max-turns 10
--system-prompt "Follow our coding standards"
--allowedTools Edit,Read,Write
```
### Automation Workflow
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
mode: "agent"
direct_prompt: "Review this PR for security issues"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
model: "claude-3-5-sonnet-20241022"
allowed_tools: "Edit,Read,Write"
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
prompt: "Review this PR for security issues"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--model claude-4-0-sonnet-20250805
--allowedTools Edit,Read,Write
```
### Custom Template with Variables
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
override_prompt: |
Analyze PR #$PR_NUMBER in $REPOSITORY
Changed files: $CHANGED_FILES
Focus on security vulnerabilities
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
prompt: |
Analyze PR #${{ github.event.pull_request.number }} in ${{ github.repository }}
Focus on security vulnerabilities in the changed files
```
### Environment Variables
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
claude_env: |
NODE_ENV: test
CI: true
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
settings: |
{
"env": {
"NODE_ENV": "test",
"CI": "true"
}
}
```
## How Mode Detection Works
The action now automatically detects the appropriate mode:
1. **If `prompt` is provided** → Runs in **automation mode**
- Executes immediately without waiting for @claude mentions
- Perfect for scheduled tasks, PR automation, etc.
2. **If no `prompt` but @claude is mentioned** → Runs in **interactive mode**
- Waits for and responds to @claude mentions
- Creates tracking comments with progress
3. **If neither** → No action is taken
## Advanced Configuration with claude_args
The `claude_args` input provides direct access to Claude Code CLI arguments:
```yaml
claude_args: |
--max-turns 15
--model claude-4-0-sonnet-20250805
--allowedTools Edit,Read,Write,Bash
--disallowedTools WebSearch
--system-prompt "You are a senior engineer focused on code quality"
--mcp-config '{"mcpServers": {"custom": {"command": "npx", "args": ["-y", "@example/server"]}}}'
```
### Common claude_args Options
| Option | Description | Example |
| ------------------- | ------------------------ | -------------------------------------- |
| `--max-turns` | Limit conversation turns | `--max-turns 10` |
| `--model` | Specify Claude model | `--model claude-4-0-sonnet-20250805` |
| `--allowedTools` | Enable specific tools | `--allowedTools Edit,Read,Write` |
| `--disallowedTools` | Disable specific tools | `--disallowedTools WebSearch` |
| `--system-prompt` | Add system instructions | `--system-prompt "Focus on security"` |
| `--mcp-config` | Add MCP server config | `--mcp-config '{"mcpServers": {...}}'` |
## Provider-Specific Updates
### AWS Bedrock
```yaml
- uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
claude_args: |
--model anthropic.claude-4-0-sonnet-20250805-v1:0
```
### Google Vertex AI
```yaml
- uses: anthropics/claude-code-action@v1
with:
use_vertex: "true"
claude_args: |
--model claude-4-0-sonnet@20250805
```
## MCP Configuration Migration
### Adding Custom MCP Servers
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
mcp_config: |
{
"mcpServers": {
"custom-server": {
"command": "npx",
"args": ["-y", "@example/server"]
}
}
}
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
claude_args: |
--mcp-config '{"mcpServers": {"custom-server": {"command": "npx", "args": ["-y", "@example/server"]}}}'
```
You can also pass MCP configuration from a file:
```yaml
- uses: anthropics/claude-code-action@v1
with:
claude_args: |
--mcp-config /path/to/mcp-config.json
```
## Step-by-Step Migration Checklist
- [ ] Update action version from `@beta` to `@v1`
- [ ] Remove `mode` input (auto-detected now)
- [ ] Replace `direct_prompt` with `prompt`
- [ ] Replace `override_prompt` with `prompt` using GitHub context
- [ ] Move `custom_instructions` to `claude_args` with `--system-prompt`
- [ ] Convert `max_turns` to `claude_args` with `--max-turns`
- [ ] Convert `model` to `claude_args` with `--model`
- [ ] Convert `allowed_tools` to `claude_args` with `--allowedTools`
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
- [ ] Move `claude_env` to `settings` JSON format
- [ ] Move `mcp_config` to `claude_args` with `--mcp-config`
- [ ] Test workflow in a non-production environment
## Getting Help
If you encounter issues during migration:
1. Check the [FAQ](./faq.md) for common questions
2. Review [example workflows](../examples/) for reference
3. Open an [issue](https://github.com/anthropics/claude-code-action/issues) for support
## Version Compatibility
- **v0.x workflows** will continue to work but with deprecation warnings
- **v1.0** is the recommended version for all new workflows
- Future versions may remove deprecated inputs entirely

38
docs/security.md Normal file
View File

@@ -0,0 +1,38 @@
# Security
## Access Control
- **Repository Access**: The action can only be triggered by users with write access to the repository
- **Bot User Control**: By default, GitHub Apps and bots cannot trigger this action for security reasons. Use the `allowed_bots` parameter to enable specific bots or all bots
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
## GitHub App Permissions
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
- **Pull Requests**: Read and write to create PRs and push changes
- **Issues**: Read and write to respond to issues
- **Contents**: Read and write to modify repository files
## Commit Signing
All commits made by Claude through this action are automatically signed with commit signatures. This ensures the authenticity and integrity of commits, providing a verifiable trail of changes made by the action.
## ⚠️ Authentication Protection
**CRITICAL: Never hardcode your Anthropic API key or OAuth token in workflow files!**
Your authentication credentials must always be stored in GitHub secrets to prevent unauthorized access:
```yaml
# CORRECT ✅
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# OR
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# NEVER DO THIS ❌
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable!
```

146
docs/setup.md Normal file
View File

@@ -0,0 +1,146 @@
# Setup Guide
## Manual Setup (Direct API)
**Requirements**: You must be a repository admin to complete these steps.
1. Install the Claude GitHub app to your repository: https://github.com/apps/claude
2. Add authentication to your repository secrets ([Learn how to use secrets in GitHub Actions](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions)):
- Either `ANTHROPIC_API_KEY` for API key authentication
- Or `CLAUDE_CODE_OAUTH_TOKEN` for OAuth token authentication (Pro and Max users can generate this by running `claude setup-token` locally)
3. Copy the workflow file from [`examples/claude.yml`](../examples/claude.yml) into your repository's `.github/workflows/`
## Using a Custom GitHub App
If you prefer not to install the official Claude app, you can create your own GitHub App to use with this action. This gives you complete control over permissions and access.
**When you may want to use a custom GitHub App:**
- You need more restrictive permissions than the official app
- Organization policies prevent installing third-party apps
- You're using AWS Bedrock or Google Vertex AI
**Steps to create and use a custom GitHub App:**
1. **Create a new GitHub App:**
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
- Click "New GitHub App"
- Configure the app with these minimum permissions:
- **Repository permissions:**
- Contents: Read & Write
- Issues: Read & Write
- Pull requests: Read & Write
- **Account permissions:** None required
- Set "Where can this GitHub App be installed?" to your preference
- Create the app
2. **Generate and download a private key:**
- After creating the app, scroll down to "Private keys"
- Click "Generate a private key"
- Download the `.pem` file (keep this secure!)
3. **Install the app on your repository:**
- Go to the app's settings page
- Click "Install App"
- Select the repositories where you want to use Claude
4. **Add the app credentials to your repository secrets:**
- Go to your repository's Settings → Secrets and variables → Actions
- Add these secrets:
- `APP_ID`: Your GitHub App's ID (found in the app settings)
- `APP_PRIVATE_KEY`: The contents of the downloaded `.pem` file
5. **Update your workflow to use the custom app:**
```yaml
name: Claude with Custom App
on:
issue_comment:
types: [created]
# ... other triggers
jobs:
claude-response:
runs-on: ubuntu-latest
steps:
# Generate a token from your custom app
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Use Claude with your custom app's token
- uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.app-token.outputs.token }}
# ... other configuration
```
**Important notes:**
- The custom app must have read/write permissions for Issues, Pull Requests, and Contents
- Your app's token will have the exact permissions you configured, nothing more
For more information on creating GitHub Apps, see the [GitHub documentation](https://docs.github.com/en/apps/creating-github-apps).
## Security Best Practices
**⚠️ IMPORTANT: Never commit API keys directly to your repository! Always use GitHub Actions secrets.**
To securely use your Anthropic API key:
1. Add your API key as a repository secret:
- Go to your repository's Settings
- Navigate to "Secrets and variables" → "Actions"
- Click "New repository secret"
- Name it `ANTHROPIC_API_KEY`
- Paste your API key as the value
2. Reference the secret in your workflow:
```yaml
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
**Never do this:**
```yaml
# ❌ WRONG - Exposes your API key
anthropic_api_key: "sk-ant-..."
```
**Always do this:**
```yaml
# ✅ CORRECT - Uses GitHub secrets
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
This applies to all sensitive values including API keys, access tokens, and credentials.
We also recommend that you always use short-lived tokens when possible
## Setting Up GitHub Secrets
1. Go to your repository's Settings
2. Click on "Secrets and variables" → "Actions"
3. Click "New repository secret"
4. For authentication, choose one:
- API Key: Name: `ANTHROPIC_API_KEY`, Value: Your Anthropic API key (starting with `sk-ant-`)
- OAuth Token: Name: `CLAUDE_CODE_OAUTH_TOKEN`, Value: Your Claude Code OAuth token (Pro and Max users can generate this by running `claude setup-token` locally)
5. Click "Add secret"
### Best Practices for Authentication
1. ✅ Always use `${{ secrets.ANTHROPIC_API_KEY }}` or `${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}` in workflows
2. ✅ Never commit API keys or tokens to version control
3. ✅ Regularly rotate your API keys and tokens
4. ✅ Use environment secrets for organization-wide access
5. ❌ Never share API keys or tokens in pull requests or issues
6. ❌ Avoid logging workflow variables that might contain keys

213
docs/usage.md Normal file
View File

@@ -0,0 +1,213 @@
# Usage
Add a workflow file to your repository (e.g., `.github/workflows/claude.yml`):
```yaml
name: Claude Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned, labeled]
pull_request_review:
types: [submitted]
jobs:
claude-response:
runs-on: ubuntu-latest
steps:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or use OAuth token instead:
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Optional: provide a prompt for automation workflows
# prompt: "Review this PR for security issues"
# Optional: pass advanced arguments to Claude CLI
# claude_args: |
# --max-turns 10
# --model claude-4-0-sonnet-20250805
# Optional: add custom trigger phrase (default: @claude)
# trigger_phrase: "/claude"
# Optional: add assignee trigger for issues
# assignee_trigger: "claude"
# Optional: add label trigger for issues
# label_trigger: "claude"
# Optional: grant additional permissions (requires corresponding GitHub token permissions)
# additional_permissions: |
# actions: read
# Optional: allow bot users to trigger the action
# allowed_bots: "dependabot[bot],renovate[bot]"
```
## Inputs
| Input | Description | Required | Default |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
| `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" |
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
### Deprecated Inputs
These inputs are deprecated and will be removed in a future version:
| Input | Description | Migration Path |
| --------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `mode` | **DEPRECATED**: Mode is now automatically detected based on workflow context | Remove this input; the action auto-detects the correct mode |
| `direct_prompt` | **DEPRECATED**: Use `prompt` instead | Replace with `prompt` |
| `override_prompt` | **DEPRECATED**: Use `prompt` with template variables or `claude_args` with `--system-prompt` | Use `prompt` for templates or `claude_args` for system prompts |
| `custom_instructions` | **DEPRECATED**: Use `claude_args` with `--system-prompt` or include in `prompt` | Move instructions to `prompt` or use `claude_args` |
| `max_turns` | **DEPRECATED**: Use `claude_args` with `--max-turns` instead | Use `claude_args: "--max-turns 5"` |
| `model` | **DEPRECATED**: Use `claude_args` with `--model` instead | Use `claude_args: "--model claude-4-0-sonnet-20250805"` |
| `fallback_model` | **DEPRECATED**: Use `claude_args` with fallback configuration | Configure fallback in `claude_args` or `settings` |
| `allowed_tools` | **DEPRECATED**: Use `claude_args` with `--allowedTools` instead | Use `claude_args: "--allowedTools Edit,Read,Write"` |
| `disallowed_tools` | **DEPRECATED**: Use `claude_args` with `--disallowedTools` instead | Use `claude_args: "--disallowedTools WebSearch"` |
| `claude_env` | **DEPRECATED**: Use `settings` with env configuration | Configure environment in `settings` JSON |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
> **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration.
## Upgrading from v0.x?
For a comprehensive guide on migrating from v0.x to v1.0, including step-by-step instructions and examples, see our **[Migration Guide](./migration-guide.md)**.
### Quick Migration Examples
#### Interactive Workflows (with @claude mentions)
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
mode: "tag"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
custom_instructions: "Focus on security"
max_turns: "10"
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--max-turns 10
--system-prompt "Focus on security"
```
#### Automation Workflows
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
mode: "agent"
direct_prompt: "Update the API documentation"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
model: "claude-4-0-sonnet-20250805"
allowed_tools: "Edit,Read,Write"
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
prompt: "Update the API documentation"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--model claude-4-0-sonnet-20250805
--allowedTools Edit,Read,Write
```
#### Custom Templates
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
override_prompt: |
Analyze PR #$PR_NUMBER for security issues.
Focus on: $CHANGED_FILES
```
**After (v1.0):**
```yaml
- uses: anthropics/claude-code-action@v1
with:
prompt: |
Analyze PR #${{ github.event.pull_request.number }} for security issues.
Focus on the changed files in this PR.
```
## Ways to Tag @claude
These examples show how to interact with Claude using comments in PRs and issues. By default, Claude will be triggered anytime you mention `@claude`, but you can customize the exact trigger phrase using the `trigger_phrase` input in the workflow.
Claude will see the full PR context, including any comments.
### Ask Questions
Add a comment to a PR or issue:
```
@claude What does this function do and how could we improve it?
```
Claude will analyze the code and provide a detailed explanation with suggestions.
### Request Fixes
Ask Claude to implement specific changes:
```
@claude Can you add error handling to this function?
```
### Code Review
Get a thorough review:
```
@claude Please review this PR and suggest improvements
```
Claude will analyze the changes and provide feedback.
### Fix Bugs from Screenshots
Upload a screenshot of a bug and ask Claude to fix it:
```
@claude Here's a screenshot of a bug I'm seeing [upload screenshot]. Can you fix it?
```
Claude can see and analyze images, making it easy to fix visual bugs or UI issues.

View File

@@ -0,0 +1,97 @@
name: Auto Fix CI Failures (Signed Commits)
on:
workflow_run:
workflows: ["CI"]
types:
- completed
permissions:
contents: write
pull-requests: write
actions: read
issues: write
id-token: write # Required for OIDC token exchange
jobs:
auto-fix-signed:
if: |
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.pull_requests[0] &&
!startsWith(github.event.workflow_run.head_branch, 'claude-auto-fix-ci-signed-')
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: Generate fix branch name
id: branch
run: |
BRANCH_NAME="claude-auto-fix-ci-signed-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
# Don't create branch locally - MCP tools will create it via API
echo "Generated branch name: $BRANCH_NAME (will be created by MCP tools)"
- 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 (Signed Commits)
id: claude
uses: anthropics/claude-code-action@v1-dev
env:
CLAUDE_BRANCH: ${{ steps.branch.outputs.branch_name }}
BASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
with:
prompt: |
/fix-ci-signed
Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }}
Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }}
PR Number: ${{ github.event.workflow_run.pull_requests[0].number }}
Branch Name: ${{ steps.branch.outputs.branch_name }}
Base Branch: ${{ github.event.workflow_run.head_branch }}
Repository: ${{ github.repository }}
Error logs:
${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
use_commit_signing: true
claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*),mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files'"

View File

@@ -0,0 +1,148 @@
---
description: Analyze and fix CI failures with signed commits using MCP tools
allowed_tools: Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*),mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files
---
# Fix CI Failures with Signed Commits
You are tasked with analyzing CI failure logs and fixing the issues using MCP tools for signed commits. Follow these steps:
## Context Provided
$ARGUMENTS
## Important Context Information
Look for these key pieces of information in the arguments:
- **Failed CI Run URL**: Link to the failed CI run
- **Failed Jobs**: List of jobs that failed
- **PR Number**: The PR number to comment on
- **Branch Name**: The fix branch you're working on
- **Base Branch**: The original PR branch
- **Error logs**: Detailed logs from failed jobs
## CRITICAL: Use MCP Tools for Git Operations
**IMPORTANT**: You MUST use MCP tools for all git operations to ensure commits are properly signed. DO NOT use `git` commands directly via Bash.
- Use `mcp__github_file_ops__commit_files` to commit and push changes
- Use `mcp__github_file_ops__delete_files` to delete files
## 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 MCP search tools to locate the failing code:
- Use `mcp_github_file_ops_server__search_files` or `mcp_github_file_ops_server__file_search` to find failing test names or functions
- Use `mcp_github_file_ops_server__read_file` to read 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: Verify Fixes Locally
Run available verification commands using Bash:
- 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
## Step 5: Commit and Push Changes Using MCP
**CRITICAL**: You MUST use MCP tools for committing and pushing:
1. Prepare all your file changes (using Edit/MultiEdit/Write tools as needed)
2. **Use `mcp__github_file_ops__commit_files` to commit and push all changes**
- Pass the file paths you've edited in the `files` array
- Set `message` to describe the specific fixes (e.g., "Fix CI failures: remove syntax errors and format code")
- The MCP tool will automatically create the branch specified in "Branch Name:" from the context and push signed commits
**IMPORTANT**: The MCP tool will create the branch from the context automatically. The branch name from "Branch Name:" in the context will be used.
Example usage:
```
mcp__github_file_ops__commit_files with:
- files: ["src/utils/retry.ts", "src/other/file.ts"] // List of file paths you edited
- message: "Fix CI failures: [describe specific fixes]"
```
Note: The branch will be created from the Base Branch specified in the context.
## Step 6: Create PR Comment (REQUIRED - DO NOT SKIP)
**CRITICAL: You MUST create a PR comment after pushing. This step is MANDATORY.**
After successfully pushing the fixes, you MUST create a comment on the original PR to notify about the auto-fix. DO NOT end the task without completing this step.
1. Extract the PR number from the context provided in arguments (look for "PR Number:" in the context)
2. **MANDATORY**: Execute the gh CLI command below to create the comment
3. Verify the comment was created successfully
**YOU MUST RUN THIS COMMAND** (replace placeholders with actual values from context):
```bash
gh pr comment PR_NUMBER --body "## 🤖 CI Auto-Fix Available (Signed Commits)
Claude has analyzed the CI failures and prepared fixes with signed commits.
[**→ Create pull request to fix CI**](https://github.com/OWNER/REPO/compare/BASE_BRANCH...FIX_BRANCH?quick_pull=1)
_This fix was generated automatically based on the [failed CI run](FAILED_CI_RUN_URL)._"
```
**IMPORTANT REPLACEMENTS YOU MUST MAKE:**
- Replace `PR_NUMBER` with the actual PR number from "PR Number:" in context
- Replace `OWNER/REPO` with the repository from "Repository:" in context
- Replace `BASE_BRANCH` with the branch from "Base Branch:" in context
- Replace `FIX_BRANCH` with the branch from "Branch Name:" in context
- Replace `FAILED_CI_RUN_URL` with the URL from "Failed CI Run:" in context
**DO NOT SKIP THIS STEP. The task is NOT complete until the PR comment is created.**
## Step 7: Final Verification
**BEFORE CONSIDERING THE TASK COMPLETE**, verify you have:
1. ✅ Fixed all CI failures
2. ✅ Committed the changes using `mcp_github_file_ops_server__push_files`
3. ✅ Verified the branch was pushed successfully
4.**CREATED THE PR COMMENT using `gh pr comment` command from Step 6**
If you have NOT created the PR comment, go back to Step 6 and execute the command.
## Important Guidelines
- Always use MCP tools for git operations to ensure proper commit signing
- 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
- **THE TASK IS NOT COMPLETE WITHOUT THE PR COMMENT**
Begin by analyzing the failure details provided above.

View File

@@ -0,0 +1,97 @@
name: Auto Fix CI Failures
on:
workflow_run:
workflows: ["CI"]
types:
- completed
permissions:
contents: write
pull-requests: write
actions: read
issues: write
id-token: write # Required for OIDC token exchange
jobs:
auto-fix:
if: |
github.event.workflow_run.conclusion == 'failure' &&
github.event.workflow_run.pull_requests[0] &&
!startsWith(github.event.workflow_run.head_branch, 'claude-auto-fix-ci-')
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 identity
run: |
git config --global user.email "claude[bot]@users.noreply.github.com"
git config --global user.name "claude[bot]"
- 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
id: claude
uses: anthropics/claude-code-action@v1-dev
with:
prompt: |
/fix-ci
Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }}
Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }}
PR Number: ${{ github.event.workflow_run.pull_requests[0].number }}
Branch Name: ${{ steps.branch.outputs.branch_name }}
Base Branch: ${{ github.event.workflow_run.head_branch }}
Repository: ${{ github.repository }}
Error logs:
${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*)'"

View File

@@ -0,0 +1,127 @@
---
description: Analyze and fix CI failures by examining logs and making targeted fixes
allowed_tools: Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*)
---
# Fix CI Failures
You are tasked with analyzing CI failure logs and fixing the issues. Follow these steps:
## Context Provided
$ARGUMENTS
## Important Context Information
Look for these key pieces of information in the arguments:
- **Failed CI Run URL**: Link to the failed CI run
- **Failed Jobs**: List of jobs that failed
- **PR Number**: The PR number to comment on
- **Branch Name**: The fix branch you're working on
- **Base Branch**: The original PR branch
- **Error logs**: Detailed logs from failed jobs
## 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: 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
## Step 5: Commit and Push Changes
After applying ALL fixes:
1. Stage all modified files with `git add -A`
2. Commit with: `git commit -m "Fix CI failures: [describe specific fixes]"`
3. Document which CI jobs/tests were addressed
4. **CRITICAL**: Push the branch with `git push origin HEAD` - You MUST push the branch after committing
## Step 6: Create PR Comment (REQUIRED - DO NOT SKIP)
**CRITICAL: You MUST create a PR comment after pushing. This step is MANDATORY.**
After successfully pushing the fixes, you MUST create a comment on the original PR to notify about the auto-fix. DO NOT end the task without completing this step.
1. Extract the PR number from the context provided in arguments (look for "PR Number:" in the context)
2. **MANDATORY**: Execute the gh CLI command below to create the comment
3. Verify the comment was created successfully
**YOU MUST RUN THIS COMMAND** (replace placeholders with actual values from context):
```bash
gh pr comment PR_NUMBER --body "## 🤖 CI Auto-Fix Available
Claude has analyzed the CI failures and prepared fixes.
[**→ Create pull request to fix CI**](https://github.com/OWNER/REPO/compare/BASE_BRANCH...FIX_BRANCH?quick_pull=1)
_This fix was generated automatically based on the [failed CI run](FAILED_CI_RUN_URL)._"
```
**IMPORTANT REPLACEMENTS YOU MUST MAKE:**
- Replace `PR_NUMBER` with the actual PR number from "PR Number:" in context
- Replace `OWNER/REPO` with the repository from "Repository:" in context
- Replace `BASE_BRANCH` with the branch from "Base Branch:" in context
- Replace `FIX_BRANCH` with the branch from "Branch Name:" in context
- Replace `FAILED_CI_RUN_URL` with the URL from "Failed CI Run:" in context
**DO NOT SKIP THIS STEP. The task is NOT complete until the PR comment is created.**
## Step 7: Final Verification
**BEFORE CONSIDERING THE TASK COMPLETE**, verify you have:
1. ✅ Fixed all CI failures
2. ✅ Committed the changes
3. ✅ Pushed the branch with `git push origin HEAD`
4.**CREATED THE PR COMMENT using `gh pr comment` command from Step 6**
If you have NOT created the PR comment, go back to Step 6 and execute the command.
## 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
- **THE TASK IS NOT COMPLETE WITHOUT THE PR COMMENT**
Begin by analyzing the failure details provided above.

View File

@@ -0,0 +1,30 @@
name: Claude Args Example
on:
workflow_dispatch:
inputs:
prompt:
description: "Prompt for Claude"
required: true
type: string
jobs:
claude-with-custom-args:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Claude with custom arguments
uses: anthropics/claude-code-action@v1-dev
with:
prompt: ${{ github.event.inputs.prompt }}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# claude_args provides direct CLI argument control
# This allows full customization of Claude's behavior
claude_args: |
--max-turns 15
--model claude-opus-4-1-20250805
--allowedTools Edit,Read,Write,Bash
--disallowedTools WebSearch
--system-prompt "You are a senior engineer focused on code quality"

View File

@@ -1,4 +1,4 @@
name: Claude Auto Review
name: Claude PR Auto Review
on:
pull_request:
@@ -18,11 +18,10 @@ jobs:
fetch-depth: 1
- name: Automatic PR Review
uses: anthropics/claude-code-action@beta
uses: anthropics/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
direct_prompt: |
prompt: |
Please review this pull request and provide comprehensive feedback.
Focus on:
@@ -32,7 +31,10 @@ jobs:
- Security implications
- Test coverage
- Documentation updates if needed
- Verify that README.md and docs are updated for any new features or config changes
Provide constructive feedback with specific suggestions for improvement.
Use inline comments to highlight specific areas of concern.
# allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_pull_request_review_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
claude_args: |
--allowedTools "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

@@ -0,0 +1,45 @@
name: Claude Experimental Review Mode
on:
pull_request:
types: [opened, synchronize]
issue_comment:
types: [created]
jobs:
code-review:
# Run on PR events, or when someone comments "@claude review" on a PR
if: |
github.event_name == 'pull_request' ||
(github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
contains(github.event.comment.body, '@claude 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 # Full history for better diff analysis
- name: Code Review with Claude
uses: anthropics/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# github_token not needed - uses default GITHUB_TOKEN for GitHub operations
prompt: |
Review this pull request comprehensively.
Focus on:
- Code quality and maintainability
- Security vulnerabilities
- Performance issues
- Best practices and design patterns
- Test coverage gaps
Be constructive and provide specific suggestions for improvements.
Use GitHub's suggestion format when proposing code changes.

View File

@@ -1,17 +1,21 @@
name: Claude Mode Examples
name: Claude Automatic Mode Detection Examples
on:
# Common events for both modes
# Events for interactive mode (responds to @claude mentions)
issue_comment:
types: [created]
issues:
types: [opened, labeled]
pull_request:
types: [opened]
# Events for automation mode (runs with explicit prompt)
workflow_dispatch:
schedule:
- cron: "0 0 * * 0" # Weekly on Sunday
jobs:
# Tag Mode (Default) - Traditional implementation
tag-mode-example:
# Interactive Mode - Activated automatically when no prompt is provided
interactive-mode-example:
runs-on: ubuntu-latest
permissions:
contents: write
@@ -19,38 +23,32 @@ jobs:
issues: write
id-token: write
steps:
- uses: anthropics/claude-code-action@beta
- uses: anthropics/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Tag mode (default) behavior:
# Interactive mode (auto-detected when no prompt):
# - Scans for @claude mentions in comments, issues, and PRs
# - Only acts when trigger phrase is found
# - Creates tracking comments with progress checkboxes
# - Perfect for: Interactive Q&A, on-demand code changes
# Agent Mode - Automation without triggers
agent-mode-auto-review:
# Automatically review every new PR
if: github.event_name == 'pull_request' && github.event.action == 'opened'
# Automation Mode - Activated automatically when prompt is provided
automation-mode-scheduled-task:
runs-on: ubuntu-latest
permissions:
contents: read
contents: write
pull-requests: write
issues: write
id-token: write
steps:
- uses: anthropics/claude-code-action@beta
- uses: anthropics/claude-code-action@v1-dev
with:
mode: agent
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
override_prompt: |
Review this PR for code quality. Focus on:
- Potential bugs or logic errors
- Security concerns
- Performance issues
Provide specific, actionable feedback.
# Agent mode behavior:
# - NO @claude mention needed - runs immediately
# - Enables true automation (impossible with tag mode)
# - Perfect for: CI/CD integration, automatic reviews, label-based workflows
prompt: |
Check for outdated dependencies and security vulnerabilities.
Create an issue if any critical problems are found.
# Automation mode (auto-detected when prompt provided):
# - Works with any GitHub event
# - Executes immediately without waiting for @claude mentions
# - No tracking comments created
# - Perfect for: scheduled maintenance, automated reviews, CI/CD tasks

View File

@@ -24,11 +24,10 @@ jobs:
fetch-depth: 1
- name: Claude Code Review
uses: anthropics/claude-code-action@beta
uses: anthropics/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
direct_prompt: |
prompt: |
Please review this pull request focusing on the changed files.
Provide feedback on:
- Code quality and adherence to best practices

View File

@@ -23,11 +23,10 @@ jobs:
fetch-depth: 1
- name: Review PR from Specific Author
uses: anthropics/claude-code-action@beta
uses: anthropics/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
timeout_minutes: "60"
direct_prompt: |
prompt: |
Please provide a thorough review of this pull request.
Since this is from a specific author that requires careful review,

View File

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

View File

@@ -0,0 +1,63 @@
name: Issue Deduplication
on:
issues:
types: [opened]
jobs:
deduplicate:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Check for duplicate issues
uses: anthropics/claude-code-action@v1-dev
with:
prompt: |
Analyze this new issue and check if it's a duplicate of existing issues in the repository.
Issue: #${{ github.event.issue.number }}
Repository: ${{ github.repository }}
Your task:
1. Use mcp__github__get_issue to get details of the current issue (#${{ github.event.issue.number }})
2. Search for similar existing issues using mcp__github__search_issues with relevant keywords from the issue title and body
3. Compare the new issue with existing ones to identify potential duplicates
Criteria for duplicates:
- Same bug or error being reported
- Same feature request (even if worded differently)
- Same question being asked
- Issues describing the same root problem
If you find duplicates:
- Add a comment on the new issue linking to the original issue(s)
- Apply a "duplicate" label to the new issue
- Be polite and explain why it's a duplicate
- Suggest the user follow the original issue for updates
If it's NOT a duplicate:
- Don't add any comments
- You may apply appropriate topic labels based on the issue content
Use these tools:
- mcp__github__get_issue: Get issue details
- mcp__github__search_issues: Search for similar issues
- mcp__github__list_issues: List recent issues if needed
- mcp__github__create_issue_comment: Add a comment if duplicate found
- mcp__github__update_issue: Add labels
Be thorough but efficient. Focus on finding true duplicates, not just similar issues.
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--allowedTools "mcp__github__get_issue,mcp__github__search_issues,mcp__github__list_issues,mcp__github__create_issue_comment,mcp__github__update_issue,mcp__github__get_issue_comments"

75
examples/issue-triage.yml Normal file
View File

@@ -0,0 +1,75 @@
name: Issue Triage
on:
issues:
types: [opened]
jobs:
triage-issue:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Triage issue with Claude
uses: anthropics/claude-code-action@v1-dev
with:
prompt: |
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
Issue Information:
- REPO: ${{ github.repository }}
- ISSUE_NUMBER: ${{ github.event.issue.number }}
TASK OVERVIEW:
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
2. Next, use the GitHub tools to get context about the issue:
- You have access to these tools:
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
- Start by using mcp__github__get_issue to get the issue details
3. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
4. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
5. Apply the selected labels:
- Use mcp__github__update_issue to apply your selected labels
- DO NOT post any comments explaining your decision
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
IMPORTANT GUIDELINES:
- Be thorough in your analysis
- Only select labels from the provided list above
- DO NOT post any comments to the issue
- Your ONLY action should be to apply labels using mcp__github__update_issue
- It's okay to not add any labels if none are clearly applicable
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
claude_args: |
--allowedTools "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"

View File

@@ -0,0 +1,39 @@
name: Claude Commit Analysis
on:
workflow_dispatch:
inputs:
analysis_type:
description: "Type of analysis to perform"
required: true
type: choice
options:
- summarize-commit
- security-review
default: "summarize-commit"
jobs:
analyze-commit:
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: 2 # Need at least 2 commits to analyze the latest
- name: Run Claude Analysis
uses: anthropics/claude-code-action@v1-dev
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Analyze the latest commit in this repository.
${{ github.event.inputs.analysis_type == 'summarize-commit' && 'Task: Provide a clear, concise summary of what changed in the latest commit. Include the commit message, files changed, and the purpose of the changes.' || '' }}
${{ github.event.inputs.analysis_type == 'security-review' && 'Task: Review the latest commit for potential security vulnerabilities. Check for exposed secrets, insecure coding patterns, dependency vulnerabilities, or any other security concerns. Provide specific recommendations if issues are found.' || '' }}

View File

@@ -17,12 +17,14 @@
"@octokit/rest": "^21.1.1",
"@octokit/webhooks-types": "^7.6.1",
"node-fetch": "^3.3.2",
"shell-quote": "^1.8.3",
"zod": "^3.24.4"
},
"devDependencies": {
"@types/bun": "1.2.11",
"@types/node": "^20.0.0",
"@types/node-fetch": "^2.6.12",
"@types/shell-quote": "^1.7.5",
"prettier": "3.5.3",
"typescript": "^5.8.3"
}

View File

@@ -6,8 +6,8 @@ echo "Installing git hooks..."
# Make sure hooks directory exists
mkdir -p .git/hooks
# Install pre-push hook
cp scripts/pre-push .git/hooks/pre-push
chmod +x .git/hooks/pre-push
# Install pre-commit hook
cp scripts/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Git hooks installed successfully!"

View File

@@ -23,6 +23,7 @@ import { GITHUB_SERVER_URL } from "../github/api/config";
import type { Mode, ModeContext } from "../modes/types";
export type { CommonFields, PreparedContext } from "./types";
// Tag mode defaults - these tools are needed for tag mode to function
const BASE_ALLOWED_TOOLS = [
"Edit",
"MultiEdit",
@@ -32,16 +33,16 @@ const BASE_ALLOWED_TOOLS = [
"Read",
"Write",
];
const DISALLOWED_TOOLS = ["WebSearch", "WebFetch"];
export function buildAllowedToolsString(
customAllowedTools?: string[],
includeActionsTools: boolean = false,
useCommitSigning: boolean = false,
): string {
// Tag mode needs these tools to function properly
let baseTools = [...BASE_ALLOWED_TOOLS];
// Always include the comment update tool from the comment server
// Always include the comment update tool for tag mode
baseTools.push("mcp__github_comment__update_claude_comment");
// Add commit signing tools if enabled
@@ -51,7 +52,7 @@ export function buildAllowedToolsString(
"mcp__github_file_ops__delete_files",
);
} else {
// When not using commit signing, add specific Bash git commands only
// When not using commit signing, add specific Bash git commands
baseTools.push(
"Bash(git add:*)",
"Bash(git commit:*)",
@@ -60,8 +61,6 @@ export function buildAllowedToolsString(
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git rm:*)",
"Bash(git config user.name:*)",
"Bash(git config user.email:*)",
);
}
@@ -85,9 +84,10 @@ export function buildDisallowedToolsString(
customDisallowedTools?: string[],
allowedTools?: string[],
): string {
let disallowedTools = [...DISALLOWED_TOOLS];
// Tag mode: Disable WebSearch and WebFetch by default for security
let disallowedTools = ["WebSearch", "WebFetch"];
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
// If user has explicitly allowed some default disallowed tools, remove them
if (allowedTools && allowedTools.length > 0) {
disallowedTools = disallowedTools.filter(
(tool) => !allowedTools.includes(tool),
@@ -117,11 +117,7 @@ export function prepareContext(
const triggerPhrase = context.inputs.triggerPhrase || "@claude";
const assigneeTrigger = context.inputs.assigneeTrigger;
const labelTrigger = context.inputs.labelTrigger;
const customInstructions = context.inputs.customInstructions;
const allowedTools = context.inputs.allowedTools;
const disallowedTools = context.inputs.disallowedTools;
const directPrompt = context.inputs.directPrompt;
const overridePrompt = context.inputs.overridePrompt;
const prompt = context.inputs.prompt;
const isPR = context.isPR;
// Get PR/Issue number from entityNumber
@@ -154,13 +150,7 @@ export function prepareContext(
claudeCommentId,
triggerPhrase,
...(triggerUsername && { triggerUsername }),
...(customInstructions && { customInstructions }),
...(allowedTools.length > 0 && { allowedTools: allowedTools.join(",") }),
...(disallowedTools.length > 0 && {
disallowedTools: disallowedTools.join(","),
}),
...(directPrompt && { directPrompt }),
...(overridePrompt && { overridePrompt }),
...(prompt && { prompt }),
...(claudeBranch && { claudeBranch }),
};
@@ -280,7 +270,7 @@ export function prepareContext(
}
if (eventAction === "assigned") {
if (!assigneeTrigger && !directPrompt) {
if (!assigneeTrigger && !prompt) {
throw new Error(
"ASSIGNEE_TRIGGER is required for issue assigned event",
);
@@ -463,82 +453,32 @@ function getCommitInstructions(
}
}
function substitutePromptVariables(
template: string,
context: PreparedContext,
githubData: FetchDataResult,
): string {
const { contextData, comments, reviewData, changedFilesWithSHA } = githubData;
const { eventData } = context;
const variables: Record<string, string> = {
REPOSITORY: context.repository,
PR_NUMBER:
eventData.isPR && "prNumber" in eventData ? eventData.prNumber : "",
ISSUE_NUMBER:
!eventData.isPR && "issueNumber" in eventData
? eventData.issueNumber
: "",
PR_TITLE: eventData.isPR && contextData?.title ? contextData.title : "",
ISSUE_TITLE: !eventData.isPR && contextData?.title ? contextData.title : "",
PR_BODY:
eventData.isPR && contextData?.body
? formatBody(contextData.body, githubData.imageUrlMap)
: "",
ISSUE_BODY:
!eventData.isPR && contextData?.body
? formatBody(contextData.body, githubData.imageUrlMap)
: "",
PR_COMMENTS: eventData.isPR
? formatComments(comments, githubData.imageUrlMap)
: "",
ISSUE_COMMENTS: !eventData.isPR
? formatComments(comments, githubData.imageUrlMap)
: "",
REVIEW_COMMENTS: eventData.isPR
? formatReviewComments(reviewData, githubData.imageUrlMap)
: "",
CHANGED_FILES: eventData.isPR
? formatChangedFilesWithSHA(changedFilesWithSHA)
: "",
TRIGGER_COMMENT: "commentBody" in eventData ? eventData.commentBody : "",
TRIGGER_USERNAME: context.triggerUsername || "",
BRANCH_NAME:
"claudeBranch" in eventData && eventData.claudeBranch
? eventData.claudeBranch
: "baseBranch" in eventData && eventData.baseBranch
? eventData.baseBranch
: "",
BASE_BRANCH:
"baseBranch" in eventData && eventData.baseBranch
? eventData.baseBranch
: "",
EVENT_TYPE: eventData.eventName,
IS_PR: eventData.isPR ? "true" : "false",
};
let result = template;
for (const [key, value] of Object.entries(variables)) {
const regex = new RegExp(`\\$${key}`, "g");
result = result.replace(regex, value);
}
return result;
}
export function generatePrompt(
context: PreparedContext,
githubData: FetchDataResult,
useCommitSigning: boolean,
mode: Mode,
): string {
if (context.overridePrompt) {
return substitutePromptVariables(
context.overridePrompt,
context,
githubData,
);
// v1.0: Simply pass through the prompt to Claude Code
const prompt = context.prompt || "";
if (prompt) {
return prompt;
}
// Otherwise use the mode's default prompt generator
return mode.generatePrompt(context, githubData, useCommitSigning);
}
/**
* Generates the default prompt for tag mode
* @internal
*/
export function generateDefaultPrompt(
context: PreparedContext,
githubData: FetchDataResult,
useCommitSigning: boolean = false,
): string {
const {
contextData,
comments,
@@ -587,23 +527,28 @@ ${formattedBody}
${formattedComments || "No comments"}
</comments>
<review_comments>
${eventData.isPR ? formattedReviewComments || "No review comments" : ""}
</review_comments>
${
eventData.isPR
? `<review_comments>
${formattedReviewComments || "No review comments"}
</review_comments>`
: ""
}
<changed_files>
${eventData.isPR ? formattedChangedFiles || "No files changed" : ""}
</changed_files>${imagesInfo}
${
eventData.isPR
? `<changed_files>
${formattedChangedFiles || "No files changed"}
</changed_files>`
: ""
}${imagesInfo}
<event_type>${eventType}</event_type>
<is_pr>${eventData.isPR ? "true" : "false"}</is_pr>
<trigger_context>${triggerContext}</trigger_context>
<repository>${context.repository}</repository>
${
eventData.isPR
? `<pr_number>${eventData.prNumber}</pr_number>`
: `<issue_number>${eventData.issueNumber ?? ""}</issue_number>`
}
${eventData.isPR && eventData.prNumber ? `<pr_number>${eventData.prNumber}</pr_number>` : ""}
${!eventData.isPR && eventData.issueNumber ? `<issue_number>${eventData.issueNumber}</issue_number>` : ""}
<claude_comment_id>${context.claudeCommentId}</claude_comment_id>
<trigger_username>${context.triggerUsername ?? "Unknown"}</trigger_username>
<trigger_display_name>${githubData.triggerDisplayName ?? context.triggerUsername ?? "Unknown"}</trigger_display_name>
@@ -618,15 +563,6 @@ ${sanitizeContent(eventData.commentBody)}
</trigger_comment>`
: ""
}
${
context.directPrompt
? `<direct_prompt>
IMPORTANT: The following are direct instructions from the user that MUST take precedence over all other instructions and context. These instructions should guide your behavior and actions above any other considerations:
${sanitizeContent(context.directPrompt)}
</direct_prompt>`
: ""
}
${`<comment_tool_info>
IMPORTANT: You have been provided with the mcp__github_comment__update_claude_comment tool to update your comment. This tool automatically handles both issue and PR comments.
@@ -657,14 +593,13 @@ Follow these steps:
- For ISSUE_ASSIGNED: Read the entire issue body to understand the task.
- For ISSUE_LABELED: Read the entire issue body to understand the task.
${eventData.eventName === "issue_comment" || eventData.eventName === "pull_request_review_comment" || eventData.eventName === "pull_request_review" ? ` - For comment/review events: Your instructions are in the <trigger_comment> tag above.` : ""}
${context.directPrompt ? ` - CRITICAL: Direct user instructions were provided in the <direct_prompt> tag above. These are HIGH PRIORITY instructions that OVERRIDE all other context and MUST be followed exactly as written.` : ""}
- IMPORTANT: Only the comment/issue containing '${context.triggerPhrase}' has your instructions.
- Other comments may contain requests from other users, but DO NOT act on those unless the trigger comment explicitly asks you to.
- Use the Read tool to look at relevant files for better context.
- Mark this todo as complete in the comment by checking the box: - [x].
3. Understand the Request:
- Extract the actual question or request from ${context.directPrompt ? "the <direct_prompt> tag above" : eventData.eventName === "issue_comment" || eventData.eventName === "pull_request_review_comment" || eventData.eventName === "pull_request_review" ? "the <trigger_comment> tag above" : `the comment/issue that contains '${context.triggerPhrase}'`}.
- Extract the actual question or request from ${eventData.eventName === "issue_comment" || eventData.eventName === "pull_request_review_comment" || eventData.eventName === "pull_request_review" ? "the <trigger_comment> tag above" : `the comment/issue that contains '${context.triggerPhrase}'`}.
- CRITICAL: If other users requested changes in other comments, DO NOT implement those changes unless the trigger comment explicitly asks you to implement them.
- Only follow the instructions in the trigger comment - all other comments are just for context.
- IMPORTANT: Always check for and follow the repository's CLAUDE.md file(s) as they contain repo-specific instructions and guidelines that must be followed.
@@ -787,10 +722,6 @@ e. Propose a high-level plan of action, including any repo setup steps and linti
f. If you are unable to complete certain steps, such as running a linter or test suite, particularly due to missing permissions, explain this in your comment so that the user can update your \`--allowedTools\`.
`;
if (context.customInstructions) {
promptContent += `\n\nCUSTOM INSTRUCTIONS:\n${context.customInstructions}`;
}
return promptContent;
}
@@ -801,20 +732,25 @@ export async function createPrompt(
context: ParsedGitHubContext,
) {
try {
// Tag mode requires a comment ID
if (mode.name === "tag" && !modeContext.commentId) {
throw new Error("Tag mode requires a comment ID for prompt generation");
// Prepare the context for prompt generation
let claudeCommentId: string = "";
if (mode.name === "tag") {
if (!modeContext.commentId) {
throw new Error(
`${mode.name} mode requires a comment ID for prompt generation`,
);
}
claudeCommentId = modeContext.commentId.toString();
}
// Prepare the context for prompt generation
const preparedContext = prepareContext(
context,
modeContext.commentId?.toString() || "",
claudeCommentId,
modeContext.baseBranch,
modeContext.claudeBranch,
);
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true,
});
@@ -823,6 +759,7 @@ export async function createPrompt(
preparedContext,
githubData,
context.inputs.useCommitSigning,
mode,
);
// Log the final prompt to console
@@ -832,37 +769,25 @@ export async function createPrompt(
// Write the prompt file
await writeFile(
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
promptContent,
);
// Set allowed tools
const hasActionsReadPermission =
context.inputs.additionalPermissions.get("actions") === "read" &&
context.isPR;
const hasActionsReadPermission = false;
// Get mode-specific tools
const modeAllowedTools = mode.getAllowedTools();
const modeDisallowedTools = mode.getDisallowedTools();
// Combine with existing allowed tools
const combinedAllowedTools = [
...context.inputs.allowedTools,
...modeAllowedTools,
];
const combinedDisallowedTools = [
...context.inputs.disallowedTools,
...modeDisallowedTools,
];
const allAllowedTools = buildAllowedToolsString(
combinedAllowedTools,
modeAllowedTools,
hasActionsReadPermission,
context.inputs.useCommitSigning,
);
const allDisallowedTools = buildDisallowedToolsString(
combinedDisallowedTools,
combinedAllowedTools,
modeDisallowedTools,
modeAllowedTools,
);
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);

View File

@@ -3,11 +3,8 @@ export type CommonFields = {
claudeCommentId: string;
triggerPhrase: string;
triggerUsername?: string;
customInstructions?: string;
allowedTools?: string;
disallowedTools?: string;
directPrompt?: string;
overridePrompt?: string;
prompt?: string;
claudeBranch?: string;
};
type PullRequestReviewCommentEvent = {

View File

@@ -0,0 +1,58 @@
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: "",
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

@@ -372,8 +372,12 @@ export function formatGroupedContent(groupedContent: GroupedContent[]): string {
const usage = item.usage || {};
if (Object.keys(usage).length > 0) {
const inputTokens = usage.input_tokens || 0;
const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
const cacheReadTokens = usage.cache_read_input_tokens || 0;
const totalInputTokens =
inputTokens + cacheCreationTokens + cacheReadTokens;
const outputTokens = usage.output_tokens || 0;
markdown += `*Token usage: ${inputTokens} input, ${outputTokens} output*\n\n`;
markdown += `*Token usage: ${totalInputTokens} input, ${outputTokens} output*\n\n`;
}
// Only add separator if this section had content
@@ -393,7 +397,7 @@ export function formatGroupedContent(groupedContent: GroupedContent[]): string {
markdown += "---\n\n";
} else if (itemType === "final_result") {
const data = item.data || {};
const cost = (data as any).cost_usd || 0;
const cost = (data as any).total_cost_usd || (data as any).cost_usd || 0;
const duration = (data as any).duration_ms || 0;
const resultText = (data as any).result || "";

View File

@@ -7,109 +7,83 @@
import * as core from "@actions/core";
import { setupGitHubToken } from "../github/token";
import { checkHumanActor } from "../github/validation/actor";
import { checkWritePermissions } from "../github/validation/permissions";
import { createInitialComment } from "../github/operations/comments/create-initial";
import { setupBranch } from "../github/operations/branch";
import { configureGitAuth } from "../github/operations/git-config";
import { prepareMcpConfig } from "../mcp/install-mcp-server";
import { createOctokit } from "../github/api/client";
import { fetchGitHubData } from "../github/data/fetcher";
import { parseGitHubContext } from "../github/context";
import { parseGitHubContext, isEntityContext } from "../github/context";
import { getMode } from "../modes/registry";
import { createPrompt } from "../create-prompt";
import { prepare } from "../prepare";
import { collectActionInputsPresence } from "./collect-inputs";
async function run() {
try {
// Step 1: Setup GitHub token
collectActionInputsPresence();
// Parse GitHub context first to enable mode detection
const context = parseGitHubContext();
// Auto-detect mode based on context
const mode = getMode(context);
// Setup GitHub token
const githubToken = await setupGitHubToken();
const octokit = createOctokit(githubToken);
// Step 2: Parse GitHub context (once for all operations)
const context = parseGitHubContext();
// Step 3: Check write permissions
const hasWritePermissions = await checkWritePermissions(
octokit.rest,
context,
);
if (!hasWritePermissions) {
throw new Error(
"Actor does not have write permissions to the repository",
// Step 3: Check write permissions (only for entity contexts)
if (isEntityContext(context)) {
const hasWritePermissions = await checkWritePermissions(
octokit.rest,
context,
);
if (!hasWritePermissions) {
throw new Error(
"Actor does not have write permissions to the repository",
);
}
}
// Step 4: Get mode and check trigger conditions
const mode = getMode(context.inputs.mode);
// 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;
}
// Step 5: Check if actor is human
await checkHumanActor(octokit.rest, context);
// Step 6: Create initial tracking comment (mode-aware)
// Some modes (e.g., agent mode) may not need tracking comments
let commentId: number | undefined;
let commentData:
| Awaited<ReturnType<typeof createInitialComment>>
| undefined;
if (mode.shouldCreateTrackingComment()) {
commentData = await createInitialComment(octokit.rest, context);
commentId = commentData.id;
}
// Step 7: Fetch GitHub data (once for both branch setup and prompt creation)
const githubData = await fetchGitHubData({
octokits: octokit,
repository: `${context.repository.owner}/${context.repository.repo}`,
prNumber: context.entityNumber.toString(),
isPR: context.isPR,
triggerUsername: context.actor,
// Step 5: Use the new modular prepare function
const result = await prepare({
context,
octokit,
mode,
githubToken,
});
// Step 8: Setup branch
const branchInfo = await setupBranch(octokit, githubData, context);
// MCP config is handled by individual modes (tag/agent) and included in their claude_args output
// Step 9: Configure git authentication if not using commit signing
if (!context.inputs.useCommitSigning) {
try {
await configureGitAuth(githubToken, context, commentData?.user || null);
} catch (error) {
console.error("Failed to configure git authentication:", error);
throw error;
// 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) {
const modeContext = mode.prepareContext(context, {
commentId: result.commentId,
baseBranch: result.branchInfo.baseBranch,
claudeBranch: result.branchInfo.claudeBranch,
});
const systemPrompt = mode.getSystemPrompt(modeContext);
if (systemPrompt) {
core.exportVariable("APPEND_SYSTEM_PROMPT", systemPrompt);
}
}
// Step 10: Create prompt file
const modeContext = mode.prepareContext(context, {
commentId,
baseBranch: branchInfo.baseBranch,
claudeBranch: branchInfo.claudeBranch,
});
await createPrompt(mode, modeContext, githubData, context);
// Step 11: 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.inputs.allowedTools,
context,
});
core.setOutput("mcp_config", mcpConfig);
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
core.setFailed(`Prepare step failed with error: ${errorMessage}`);

View File

@@ -9,6 +9,7 @@ import {
import {
parseGitHubContext,
isPullRequestReviewCommentEvent,
isEntityContext,
} from "../github/context";
import { GITHUB_SERVER_URL } from "../github/api/config";
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
@@ -23,7 +24,14 @@ async function run() {
const triggerUsername = process.env.TRIGGER_USERNAME;
const context = parseGitHubContext();
// This script is only called for entity-based events
if (!isEntityContext(context)) {
throw new Error("update-comment-link requires an entity context");
}
const { owner, repo } = context.repository;
const octokit = createOctokit(githubToken);
const serverUrl = GITHUB_SERVER_URL;

View File

@@ -46,6 +46,7 @@ export const PR_QUERY = `
login
}
createdAt
isMinimized
}
}
reviews(first: 100) {
@@ -69,6 +70,7 @@ export const PR_QUERY = `
login
}
createdAt
isMinimized
}
}
}
@@ -98,6 +100,7 @@ export const ISSUE_QUERY = `
login
}
createdAt
isMinimized
}
}
}

View File

@@ -6,13 +6,58 @@ import type {
PullRequestEvent,
PullRequestReviewEvent,
PullRequestReviewCommentEvent,
WorkflowRunEvent,
} from "@octokit/webhooks-types";
import type { ModeName } from "../modes/types";
import { DEFAULT_MODE, isValidMode } from "../modes/registry";
// Custom types for GitHub Actions events that aren't webhooks
export type WorkflowDispatchEvent = {
action?: never;
inputs?: Record<string, any>;
ref?: string;
repository: {
name: string;
owner: {
login: string;
};
};
sender: {
login: string;
};
workflow: string;
};
export type ParsedGitHubContext = {
export type ScheduleEvent = {
action?: never;
schedule?: string;
repository: {
name: string;
owner: {
login: string;
};
};
};
// Event name constants for better maintainability
const ENTITY_EVENT_NAMES = [
"issues",
"issue_comment",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
] as const;
const AUTOMATION_EVENT_NAMES = [
"workflow_dispatch",
"schedule",
"workflow_run",
] as const;
// Derive types from constants for better maintainability
type EntityEventName = (typeof ENTITY_EVENT_NAMES)[number];
type AutomationEventName = (typeof AUTOMATION_EVENT_NAMES)[number];
// Common fields shared by all context types
type BaseContext = {
runId: string;
eventName: string;
eventAction?: string;
repository: {
owner: string;
@@ -20,6 +65,22 @@ export type ParsedGitHubContext = {
full_name: string;
};
actor: string;
inputs: {
prompt: string;
triggerPhrase: string;
assigneeTrigger: string;
labelTrigger: string;
baseBranch?: string;
branchPrefix: string;
useStickyComment: boolean;
useCommitSigning: boolean;
allowedBots: string;
};
};
// Context for entity-based events (issues, PRs, comments)
export type ParsedGitHubContext = BaseContext & {
eventName: EntityEventName;
payload:
| IssuesEvent
| IssueCommentEvent
@@ -28,35 +89,22 @@ export type ParsedGitHubContext = {
| PullRequestReviewCommentEvent;
entityNumber: number;
isPR: boolean;
inputs: {
mode: ModeName;
triggerPhrase: string;
assigneeTrigger: string;
labelTrigger: string;
allowedTools: string[];
disallowedTools: string[];
customInstructions: string;
directPrompt: string;
overridePrompt: string;
baseBranch?: string;
branchPrefix: string;
useStickyComment: boolean;
additionalPermissions: Map<string, string>;
useCommitSigning: boolean;
};
};
export function parseGitHubContext(): ParsedGitHubContext {
const context = github.context;
// Context for automation events (workflow_dispatch, schedule, workflow_run)
export type AutomationContext = BaseContext & {
eventName: AutomationEventName;
payload: WorkflowDispatchEvent | ScheduleEvent | WorkflowRunEvent;
};
const modeInput = process.env.MODE ?? DEFAULT_MODE;
if (!isValidMode(modeInput)) {
throw new Error(`Invalid mode: ${modeInput}.`);
}
// Union type for all contexts
export type GitHubContext = ParsedGitHubContext | AutomationContext;
export function parseGitHubContext(): GitHubContext {
const context = github.context;
const commonFields = {
runId: process.env.GITHUB_RUN_ID!,
eventName: context.eventName,
eventAction: context.payload.action,
repository: {
owner: context.repo.owner,
@@ -65,134 +113,143 @@ export function parseGitHubContext(): ParsedGitHubContext {
},
actor: context.actor,
inputs: {
mode: modeInput as ModeName,
prompt: process.env.PROMPT || "",
triggerPhrase: process.env.TRIGGER_PHRASE ?? "@claude",
assigneeTrigger: process.env.ASSIGNEE_TRIGGER ?? "",
labelTrigger: process.env.LABEL_TRIGGER ?? "",
allowedTools: parseMultilineInput(process.env.ALLOWED_TOOLS ?? ""),
disallowedTools: parseMultilineInput(process.env.DISALLOWED_TOOLS ?? ""),
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
directPrompt: process.env.DIRECT_PROMPT ?? "",
overridePrompt: process.env.OVERRIDE_PROMPT ?? "",
baseBranch: process.env.BASE_BRANCH,
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
additionalPermissions: parseAdditionalPermissions(
process.env.ADDITIONAL_PERMISSIONS ?? "",
),
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
allowedBots: process.env.ALLOWED_BOTS ?? "",
},
};
switch (context.eventName) {
case "issues": {
const payload = context.payload as IssuesEvent;
return {
...commonFields,
payload: context.payload as IssuesEvent,
entityNumber: (context.payload as IssuesEvent).issue.number,
eventName: "issues",
payload,
entityNumber: payload.issue.number,
isPR: false,
};
}
case "issue_comment": {
const payload = context.payload as IssueCommentEvent;
return {
...commonFields,
payload: context.payload as IssueCommentEvent,
entityNumber: (context.payload as IssueCommentEvent).issue.number,
isPR: Boolean(
(context.payload as IssueCommentEvent).issue.pull_request,
),
eventName: "issue_comment",
payload,
entityNumber: payload.issue.number,
isPR: Boolean(payload.issue.pull_request),
};
}
case "pull_request": {
const payload = context.payload as PullRequestEvent;
return {
...commonFields,
payload: context.payload as PullRequestEvent,
entityNumber: (context.payload as PullRequestEvent).pull_request.number,
eventName: "pull_request",
payload,
entityNumber: payload.pull_request.number,
isPR: true,
};
}
case "pull_request_review": {
const payload = context.payload as PullRequestReviewEvent;
return {
...commonFields,
payload: context.payload as PullRequestReviewEvent,
entityNumber: (context.payload as PullRequestReviewEvent).pull_request
.number,
eventName: "pull_request_review",
payload,
entityNumber: payload.pull_request.number,
isPR: true,
};
}
case "pull_request_review_comment": {
const payload = context.payload as PullRequestReviewCommentEvent;
return {
...commonFields,
payload: context.payload as PullRequestReviewCommentEvent,
entityNumber: (context.payload as PullRequestReviewCommentEvent)
.pull_request.number,
eventName: "pull_request_review_comment",
payload,
entityNumber: payload.pull_request.number,
isPR: true,
};
}
case "workflow_dispatch": {
return {
...commonFields,
eventName: "workflow_dispatch",
payload: context.payload as unknown as WorkflowDispatchEvent,
};
}
case "schedule": {
return {
...commonFields,
eventName: "schedule",
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}`);
}
}
export function parseMultilineInput(s: string): string[] {
return s
.split(/,|[\n\r]+/)
.map((tool) => tool.replace(/#.+$/, ""))
.map((tool) => tool.trim())
.filter((tool) => tool.length > 0);
}
export function parseAdditionalPermissions(s: string): Map<string, string> {
const permissions = new Map<string, string>();
if (!s || !s.trim()) {
return permissions;
}
const lines = s.trim().split("\n");
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine) {
const [key, value] = trimmedLine.split(":").map((part) => part.trim());
if (key && value) {
permissions.set(key, value);
}
}
}
return permissions;
}
export function isIssuesEvent(
context: ParsedGitHubContext,
context: GitHubContext,
): context is ParsedGitHubContext & { payload: IssuesEvent } {
return context.eventName === "issues";
}
export function isIssueCommentEvent(
context: ParsedGitHubContext,
context: GitHubContext,
): context is ParsedGitHubContext & { payload: IssueCommentEvent } {
return context.eventName === "issue_comment";
}
export function isPullRequestEvent(
context: ParsedGitHubContext,
context: GitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestEvent } {
return context.eventName === "pull_request";
}
export function isPullRequestReviewEvent(
context: ParsedGitHubContext,
context: GitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestReviewEvent } {
return context.eventName === "pull_request_review";
}
export function isPullRequestReviewCommentEvent(
context: ParsedGitHubContext,
context: GitHubContext,
): context is ParsedGitHubContext & { payload: PullRequestReviewCommentEvent } {
return context.eventName === "pull_request_review_comment";
}
export function isIssuesAssignedEvent(
context: ParsedGitHubContext,
context: GitHubContext,
): context is ParsedGitHubContext & { payload: IssuesAssignedEvent } {
return isIssuesEvent(context) && context.eventAction === "assigned";
}
// Type guard to check if context is an entity context (has entityNumber and isPR)
export function isEntityContext(
context: GitHubContext,
): context is ParsedGitHubContext {
return ENTITY_EVENT_NAMES.includes(context.eventName as EntityEventName);
}
// Type guard to check if context is an automation context
export function isAutomationContext(
context: GitHubContext,
): context is AutomationContext {
return AUTOMATION_EVENT_NAMES.includes(
context.eventName as AutomationEventName,
);
}

View File

@@ -134,7 +134,7 @@ export async function fetchGitHubData({
// Prepare all comments for image processing
const issueComments: CommentWithImages[] = comments
.filter((c) => c.body)
.filter((c) => c.body && !c.isMinimized)
.map((c) => ({
type: "issue_comment" as const,
id: c.databaseId,
@@ -154,7 +154,7 @@ export async function fetchGitHubData({
const reviewComments: CommentWithImages[] =
reviewData?.nodes
?.flatMap((r) => r.comments?.nodes ?? [])
.filter((c) => c.body)
.filter((c) => c.body && !c.isMinimized)
.map((c) => ({
type: "review_comment" as const,
id: c.databaseId,

View File

@@ -50,6 +50,7 @@ export function formatComments(
imageUrlMap?: Map<string, string>,
): string {
return comments
.filter((comment) => !comment.isMinimized)
.map((comment) => {
let body = comment.body;
@@ -96,6 +97,7 @@ export function formatReviewComments(
review.comments.nodes.length > 0
) {
const comments = review.comments.nodes
.filter((comment) => !comment.isMinimized)
.map((comment) => {
let body = comment.body;
@@ -110,7 +112,9 @@ export function formatReviewComments(
return ` [Comment on ${comment.path}:${comment.line || "?"}]: ${body}`;
})
.join("\n");
reviewOutput += `\n${comments}`;
if (comments) {
reviewOutput += `\n${comments}`;
}
}
return reviewOutput;

View File

@@ -6,7 +6,7 @@
*/
import { $ } from "bun";
import type { ParsedGitHubContext } from "../context";
import type { GitHubContext } from "../context";
import { GITHUB_SERVER_URL } from "../api/config";
type GitUser = {
@@ -16,7 +16,7 @@ type GitUser = {
export async function configureGitAuth(
githubToken: string,
context: ParsedGitHubContext,
context: GitHubContext,
user: GitUser | null,
) {
console.log("Configuring git authentication for non-signing mode");

View File

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

View File

@@ -10,6 +10,7 @@ export type GitHubComment = {
body: string;
author: GitHubAuthor;
createdAt: string;
isMinimized?: boolean;
};
export type GitHubReviewComment = GitHubComment & {

View File

@@ -3,11 +3,17 @@ import path from "path";
import type { Octokits } from "../api/client";
import { GITHUB_SERVER_URL } from "../api/config";
const escapedUrl = GITHUB_SERVER_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const IMAGE_REGEX = new RegExp(
`!\\[[^\\]]*\\]\\((${GITHUB_SERVER_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\/user-attachments\\/assets\\/[^)]+)\\)`,
`!\\[[^\\]]*\\]\\((${escapedUrl}\\/user-attachments\\/assets\\/[^)]+)\\)`,
"g",
);
const HTML_IMG_REGEX = new RegExp(
`<img[^>]+src=["']([^"']*${escapedUrl}\\/user-attachments\\/assets\\/[^"']+)["'][^>]*>`,
"gi",
);
type IssueComment = {
type: "issue_comment";
id: string;
@@ -63,8 +69,16 @@ export async function downloadCommentImages(
}> = [];
for (const comment of comments) {
const imageMatches = [...comment.body.matchAll(IMAGE_REGEX)];
const urls = imageMatches.map((match) => match[1] as string);
// Extract URLs from Markdown format
const markdownMatches = [...comment.body.matchAll(IMAGE_REGEX)];
const markdownUrls = markdownMatches.map((match) => match[1] as string);
// Extract URLs from HTML format
const htmlMatches = [...comment.body.matchAll(HTML_IMG_REGEX)];
const htmlUrls = htmlMatches.map((match) => match[1] as string);
// Combine and deduplicate URLs
const urls = [...new Set([...markdownUrls, ...htmlUrls])];
if (urls.length > 0) {
commentsWithImages.push({ comment, urls });

View File

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

View File

@@ -21,9 +21,42 @@ export async function checkHumanActor(
console.log(`Actor type: ${actorType}`);
// Check bot permissions if actor is not a User
if (actorType !== "User") {
const allowedBots = githubContext.inputs.allowedBots;
// Check if all bots are allowed
if (allowedBots.trim() === "*") {
console.log(
`All bots are allowed, skipping human actor check for: ${githubContext.actor}`,
);
return;
}
// Parse allowed bots list
const allowedBotsList = allowedBots
.split(",")
.map((bot) =>
bot
.trim()
.toLowerCase()
.replace(/\[bot\]$/, ""),
)
.filter((bot) => bot.length > 0);
const botName = githubContext.actor.toLowerCase().replace(/\[bot\]$/, "");
// Check if specific bot is allowed
if (allowedBotsList.includes(botName)) {
console.log(
`Bot ${botName} is in allowed list, skipping human actor check`,
);
return;
}
// Bot not allowed
throw new Error(
`Workflow initiated by non-human actor: ${githubContext.actor} (type: ${actorType}).`,
`Workflow initiated by non-human actor: ${botName} (type: ${actorType}). Add bot to allowed_bots list or use '*' to allow all bots.`,
);
}

View File

@@ -17,6 +17,12 @@ export async function checkWritePermissions(
try {
core.info(`Checking permissions for actor: ${actor}`);
// Check if the actor is a GitHub App (bot user)
if (actor.endsWith("[bot]")) {
core.info(`Actor is a GitHub App: ${actor}`);
return true;
}
// Check permissions directly using the permission endpoint
const response = await octokit.repos.getCollaboratorPermissionLevel({
owner: repository.owner,

View File

@@ -13,12 +13,12 @@ import type { ParsedGitHubContext } from "../context";
export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
const {
inputs: { assigneeTrigger, labelTrigger, triggerPhrase, directPrompt },
inputs: { assigneeTrigger, labelTrigger, triggerPhrase, prompt },
} = context;
// If direct prompt is provided, always trigger
if (directPrompt) {
console.log(`Direct prompt provided, triggering action`);
// If prompt is provided, always trigger
if (prompt) {
console.log(`Prompt provided, triggering action`);
return true;
}

View File

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

View File

@@ -3,8 +3,9 @@
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFile } from "fs/promises";
import { readFile, stat } from "fs/promises";
import { join } from "path";
import { constants } from "fs";
import fetch from "node-fetch";
import { GITHUB_API_URL } from "../github/api/config";
import { retryWithBackoff } from "../utils/retry";
@@ -162,6 +163,34 @@ async function getOrCreateBranchRef(
return baseSha;
}
// Get the appropriate Git file mode for a file
async function getFileMode(filePath: string): Promise<string> {
try {
const fileStat = await stat(filePath);
if (fileStat.isFile()) {
// Check if execute bit is set for user
if (fileStat.mode & constants.S_IXUSR) {
return "100755"; // Executable file
} else {
return "100644"; // Regular file
}
} else if (fileStat.isDirectory()) {
return "040000"; // Directory (tree)
} else if (fileStat.isSymbolicLink()) {
return "120000"; // Symbolic link
} else {
// Fallback for unknown file types
return "100644";
}
} catch (error) {
// If we can't stat the file, default to regular file
console.warn(
`Could not determine file mode for ${filePath}, using default: ${error}`,
);
return "100644";
}
}
// Commit files tool
server.tool(
"commit_files",
@@ -223,6 +252,9 @@ server.tool(
? filePath
: join(REPO_DIR, filePath);
// Get the proper file mode based on file permissions
const fileMode = await getFileMode(fullPath);
// Check if file is binary (images, etc.)
const isBinaryFile =
/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test(
@@ -261,7 +293,7 @@ server.tool(
// Return tree entry with blob SHA
return {
path: filePath,
mode: "100644",
mode: fileMode,
type: "blob",
sha: blobData.sha,
};
@@ -270,7 +302,7 @@ server.tool(
const content = await readFile(fullPath, "utf-8");
return {
path: filePath,
mode: "100644",
mode: fileMode,
type: "blob",
content: content,
};
@@ -353,15 +385,22 @@ server.tool(
if (!updateRefResponse.ok) {
const errorText = await updateRefResponse.text();
// Provide a more helpful error message for 403 permission errors
if (updateRefResponse.status === 403) {
const permissionError = new Error(
`Permission denied: Unable to push commits to branch '${branch}'. ` +
`Please rebase your branch from the main/master branch to allow Claude to commit.\n\n` +
`Original error: ${errorText}`,
);
throw permissionError;
}
// For other errors, use the original message
const error = new Error(
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
);
// Only retry on 403 errors - these are the intermittent failures we're targeting
if (updateRefResponse.status === 403) {
throw error;
}
// For non-403 errors, fail immediately without retry
console.error("Non-retryable error:", updateRefResponse.status);
throw error;
@@ -559,16 +598,23 @@ server.tool(
if (!updateRefResponse.ok) {
const errorText = await updateRefResponse.text();
// Provide a more helpful error message for 403 permission errors
if (updateRefResponse.status === 403) {
console.log("Received 403 error, will retry...");
const permissionError = new Error(
`Permission denied: Unable to push commits to branch '${branch}'. ` +
`Please rebase your branch from the main/master branch to allow Claude to commit.\n\n` +
`Original error: ${errorText}`,
);
throw permissionError;
}
// For other errors, use the original message
const error = new Error(
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
);
// Only retry on 403 errors - these are the intermittent failures we're targeting
if (updateRefResponse.status === 403) {
console.log("Received 403 error, will retry...");
throw error;
}
// For non-403 errors, fail immediately without retry
console.error("Non-retryable error:", updateRefResponse.status);
throw error;

View File

@@ -0,0 +1,184 @@
#!/usr/bin/env node
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;
const REPO_NAME = process.env.REPO_NAME;
const PR_NUMBER = process.env.PR_NUMBER;
if (!REPO_OWNER || !REPO_NAME || !PR_NUMBER) {
console.error(
"Error: REPO_OWNER, REPO_NAME, and PR_NUMBER environment variables are required",
);
process.exit(1);
}
// GitHub Inline Comment MCP Server - Provides inline PR comment functionality
// Provides an inline comment tool without exposing full PR review capabilities, so that
// Claude can't accidentally approve a PR
const server = new McpServer({
name: "GitHub Inline Comment Server",
version: "0.0.1",
});
server.tool(
"create_inline_comment",
"Create an inline comment on a specific line or lines in a PR file",
{
path: z
.string()
.describe("The file path to comment on (e.g., 'src/index.js')"),
body: z
.string()
.describe(
"The comment text (supports markdown and GitHub code suggestion blocks). " +
"For code suggestions, use: ```suggestion\\nreplacement code\\n```. " +
"IMPORTANT: The suggestion block will REPLACE the ENTIRE line range (single line or startLine to line). " +
"Ensure the replacement is syntactically complete and valid - it must work as a drop-in replacement for the selected lines.",
),
line: z
.number()
.nonnegative()
.optional()
.describe(
"Line number for single-line comments (required if startLine is not provided)",
),
startLine: z
.number()
.nonnegative()
.optional()
.describe(
"Start line for multi-line comments (use with line parameter for the end line)",
),
side: z
.enum(["LEFT", "RIGHT"])
.optional()
.default("RIGHT")
.describe(
"Side of the diff to comment on: LEFT (old code) or RIGHT (new code)",
),
commit_id: z
.string()
.optional()
.describe(
"Specific commit SHA to comment on (defaults to latest commit)",
),
},
async ({ path, body, line, startLine, side, commit_id }) => {
try {
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
throw new Error("GITHUB_TOKEN environment variable is required");
}
const owner = REPO_OWNER;
const repo = REPO_NAME;
const pull_number = parseInt(PR_NUMBER, 10);
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(
"Either 'line' for single-line comments or both 'startLine' and 'line' for multi-line comments must be provided",
);
}
// If only line is provided, it's a single-line comment
// If both startLine and line are provided, it's a multi-line comment
const isSingleLine = !startLine;
const pr = await octokit.pulls.get({
owner,
repo,
pull_number,
});
const params: Parameters<
typeof octokit.rest.pulls.createReviewComment
>[0] = {
owner,
repo,
pull_number,
body: sanitizedBody,
path,
side: side || "RIGHT",
commit_id: commit_id || pr.data.head.sha,
};
if (isSingleLine) {
// Single-line comment
params.line = line;
} else {
// Multi-line comment
params.start_line = startLine;
params.start_side = side || "RIGHT";
params.line = line;
}
const result = await octokit.rest.pulls.createReviewComment(params);
return {
content: [
{
type: "text",
text: JSON.stringify(
{
success: true,
comment_id: result.data.id,
html_url: result.data.html_url,
path: result.data.path,
line: result.data.line || result.data.original_line,
message: `Inline comment created successfully on ${path}${isSingleLine ? ` at line ${line}` : ` from line ${startLine} to ${line}`}`,
},
null,
2,
),
},
],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
// Provide more helpful error messages for common issues
let helpMessage = "";
if (errorMessage.includes("Validation Failed")) {
helpMessage =
"\n\nThis usually means the line number doesn't exist in the diff or the file path is incorrect. Make sure you're commenting on lines that are part of the PR's changes.";
} else if (errorMessage.includes("Not Found")) {
helpMessage =
"\n\nThis usually means the PR number, repository, or file path is incorrect.";
}
return {
content: [
{
type: "text",
text: `Error creating inline comment: ${errorMessage}${helpMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
async function runServer() {
const transport = new StdioServerTransport();
await server.connect(transport);
process.on("exit", () => {
server.close();
});
}
runServer().catch(console.error);

View File

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

View File

@@ -1,30 +1,32 @@
import type { Mode } from "../types";
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 { configureGitAuth } from "../../github/operations/git-config";
/**
* Agent mode implementation.
*
* This mode is designed for automation and workflow_dispatch scenarios.
* It always triggers (no checking), allows highly flexible configurations,
* and works well with override_prompt for custom workflows.
*
* In the future, this mode could restrict certain tools for safety in automation contexts,
* e.g., disallowing WebSearch or limiting file system operations.
* This mode runs whenever an explicit prompt is provided in the workflow configuration.
* It bypasses the standard @claude mention checking and comment tracking used by tag mode,
* providing direct access to Claude Code for automation workflows.
*/
export const agentMode: Mode = {
name: "agent",
description: "Automation mode that always runs without trigger checking",
description: "Direct automation mode for explicit prompts",
shouldTrigger() {
return true;
shouldTrigger(context) {
// Only trigger when an explicit prompt is provided
return !!context.inputs?.prompt;
},
prepareContext(context, data) {
prepareContext(context) {
// Agent mode doesn't use comment tracking or branch management
return {
mode: "agent",
githubContext: context,
commentId: data?.commentId,
baseBranch: data?.baseBranch,
claudeBranch: data?.claudeBranch,
};
},
@@ -39,4 +41,112 @@ export const agentMode: Mode = {
shouldCreateTrackingComment() {
return false;
},
async prepare({
context,
githubToken,
octokit,
}: ModeOptions): Promise<ModeResult> {
// Configure git authentication for agent mode (same as tag mode)
if (!context.inputs.useCommitSigning) {
try {
// Get the authenticated user (will be claude[bot] when using Claude App token)
const { data: authenticatedUser } =
await octokit.rest.users.getAuthenticated();
const user = {
login: authenticatedUser.login,
id: authenticatedUser.id,
};
// Use the shared git configuration function
await configureGitAuth(githubToken, context, user);
} catch (error) {
console.error("Failed to configure git authentication:", error);
// Continue anyway - git operations may still work with default config
}
}
// Create prompt directory
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
recursive: true,
});
// Write the prompt file - use the user's prompt directly
const promptContent =
context.inputs.prompt ||
`Repository: ${context.repository.owner}/${context.repository.repo}`;
await writeFile(
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
promptContent,
);
// Parse allowed tools from user's claude_args
const userClaudeArgs = process.env.CLAUDE_ARGS || "";
const allowedTools = parseAllowedTools(userClaudeArgs);
// Check for branch info from environment variables (useful for auto-fix workflows)
const claudeBranch = process.env.CLAUDE_BRANCH || undefined;
const baseBranch =
process.env.BASE_BRANCH || context.inputs.baseBranch || "main";
// Detect current branch from GitHub environment
const currentBranch =
claudeBranch ||
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: baseBranch,
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}'`;
}
// Append user's claude_args (which may have more --mcp-config flags)
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();
core.setOutput("claude_args", claudeArgs);
return {
commentId: undefined,
branchInfo: {
baseBranch: baseBranch,
currentBranch: baseBranch, // Use base branch as current when creating new branch
claudeBranch: claudeBranch,
},
mcpConfig: ourMcpConfig,
};
},
generatePrompt(context: PreparedContext): string {
// Agent mode uses prompt field
if (context.prompt) {
return context.prompt;
}
// Minimal fallback - repository is a string in PreparedContext
return `Repository: ${context.repository}`;
},
getSystemPrompt() {
// Agent mode doesn't need additional system prompts
return undefined;
},
};

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

66
src/modes/detector.ts Normal file
View File

@@ -0,0 +1,66 @@
import type { GitHubContext } from "../github/context";
import {
isEntityContext,
isIssueCommentEvent,
isPullRequestReviewCommentEvent,
} from "../github/context";
import { checkContainsTrigger } from "../github/validation/trigger";
export type AutoDetectedMode = "tag" | "agent";
export function detectMode(context: GitHubContext): AutoDetectedMode {
// If prompt is provided, use agent mode for direct execution
if (context.inputs?.prompt) {
return "agent";
}
// Check for @claude mentions (tag mode)
if (isEntityContext(context)) {
if (
isIssueCommentEvent(context) ||
isPullRequestReviewCommentEvent(context)
) {
if (checkContainsTrigger(context)) {
return "tag";
}
}
if (context.eventName === "issues") {
if (checkContainsTrigger(context)) {
return "tag";
}
}
}
// Default to agent mode (which won't trigger without a prompt)
return "agent";
}
export function getModeDescription(mode: AutoDetectedMode): string {
switch (mode) {
case "tag":
return "Interactive mode triggered by @claude mentions";
case "agent":
return "Direct automation mode for explicit prompts";
default:
return "Unknown mode";
}
}
export function shouldUseTrackingComment(mode: AutoDetectedMode): boolean {
return mode === "tag";
}
export function getDefaultPromptForMode(
mode: AutoDetectedMode,
context: GitHubContext,
): string | undefined {
switch (mode) {
case "tag":
return undefined;
case "agent":
return context.inputs?.prompt;
default:
return undefined;
}
}

View File

@@ -1,45 +1,45 @@
/**
* Mode Registry for claude-code-action
* Mode Registry for claude-code-action v1.0
*
* This module provides access to all available execution modes.
*
* To add a new mode:
* 1. Add the mode name to VALID_MODES below
* 2. Create the mode implementation in a new directory (e.g., src/modes/new-mode/)
* 3. Import and add it to the modes object below
* 4. Update action.yml description to mention the new mode
* This module provides access to all available execution modes and handles
* automatic mode detection based on GitHub event types.
*/
import type { Mode, ModeName } from "./types";
import { tagMode } from "./tag";
import { agentMode } from "./agent";
import type { GitHubContext } from "../github/context";
import { detectMode, type AutoDetectedMode } from "./detector";
export const DEFAULT_MODE = "tag" as const;
export const VALID_MODES = ["tag", "agent"] as const;
/**
* All available modes.
* Add new modes here as they are created.
* All available modes in v1.0
*/
const modes = {
tag: tagMode,
agent: agentMode,
} as const satisfies Record<ModeName, Mode>;
} as const satisfies Record<AutoDetectedMode, Mode>;
/**
* Retrieves a mode by name.
* @param name The mode name to retrieve
* @returns The requested mode
* @throws Error if the mode is not found
* Automatically detects and retrieves the appropriate mode based on the GitHub context.
* In v1.0, modes are auto-selected based on event type.
* @param context The GitHub context
* @returns The appropriate mode for the context
*/
export function getMode(name: ModeName): Mode {
const mode = modes[name];
export function getMode(context: GitHubContext): Mode {
const modeName = detectMode(context);
console.log(
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
);
const mode = modes[modeName];
if (!mode) {
const validModes = VALID_MODES.join("', '");
throw new Error(
`Invalid mode '${name}'. Valid modes are: '${validModes}'. Please check your workflow configuration.`,
`Mode '${modeName}' not found. This should not happen. Please report this issue.`,
);
}
return mode;
}
@@ -49,5 +49,6 @@ export function getMode(name: ModeName): Mode {
* @returns True if the name is a valid mode name
*/
export function isValidMode(name: string): name is ModeName {
return VALID_MODES.includes(name as ModeName);
const validModes = ["tag", "agent"];
return validModes.includes(name);
}

View File

@@ -1,5 +1,16 @@
import type { Mode } from "../types";
import * as core from "@actions/core";
import type { Mode, ModeOptions, ModeResult } from "../types";
import { checkContainsTrigger } from "../../github/validation/trigger";
import { checkHumanActor } from "../../github/validation/actor";
import { createInitialComment } from "../../github/operations/comments/create-initial";
import { setupBranch } from "../../github/operations/branch";
import { configureGitAuth } from "../../github/operations/git-config";
import { prepareMcpConfig } from "../../mcp/install-mcp-server";
import { fetchGitHubData } from "../../github/data/fetcher";
import { createPrompt, generateDefaultPrompt } from "../../create-prompt";
import { isEntityContext } from "../../github/context";
import type { PreparedContext } from "../../create-prompt/types";
import type { FetchDataResult } from "../../github/data/fetcher";
/**
* Tag mode implementation.
@@ -13,6 +24,10 @@ export const tagMode: Mode = {
description: "Traditional implementation mode triggered by @claude mentions",
shouldTrigger(context) {
// Tag mode only handles entity events
if (!isEntityContext(context)) {
return false;
}
return checkContainsTrigger(context);
},
@@ -37,4 +52,136 @@ export const tagMode: Mode = {
shouldCreateTrackingComment() {
return true;
},
async prepare({
context,
octokit,
githubToken,
}: ModeOptions): Promise<ModeResult> {
// Tag mode only handles entity-based events
if (!isEntityContext(context)) {
throw new Error("Tag mode requires entity context");
}
// Check if actor is human
await checkHumanActor(octokit.rest, context);
// Create initial tracking comment
const commentData = await createInitialComment(octokit.rest, context);
const commentId = commentData.id;
const githubData = await fetchGitHubData({
octokits: octokit,
repository: `${context.repository.owner}/${context.repository.repo}`,
prNumber: context.entityNumber.toString(),
isPR: context.isPR,
triggerUsername: context.actor,
});
// Setup branch
const branchInfo = await setupBranch(octokit, githubData, context);
// Configure git authentication if not using commit signing
if (!context.inputs.useCommitSigning) {
try {
await configureGitAuth(githubToken, context, commentData.user);
} catch (error) {
console.error("Failed to configure git authentication:", error);
throw error;
}
}
// Create prompt file
const modeContext = this.prepareContext(context, {
commentId,
baseBranch: branchInfo.baseBranch,
claudeBranch: branchInfo.claudeBranch,
});
await createPrompt(tagMode, modeContext, githubData, context);
// Get our GitHub MCP servers configuration
const ourMcpConfig = await prepareMcpConfig({
githubToken,
owner: context.repository.owner,
repo: context.repository.repo,
branch: branchInfo.claudeBranch || branchInfo.currentBranch,
baseBranch: branchInfo.baseBranch,
claudeCommentId: commentId.toString(),
allowedTools: [],
context,
});
// Don't output mcp_config separately anymore - include in claude_args
// Build claude_args for tag mode with required tools
// Tag mode REQUIRES these tools to function properly
const tagModeTools = [
"Edit",
"MultiEdit",
"Glob",
"Grep",
"LS",
"Read",
"Write",
"mcp__github_comment__update_claude_comment",
];
// Add git commands when not using commit signing
if (!context.inputs.useCommitSigning) {
tagModeTools.push(
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(git push:*)",
"Bash(git status:*)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Bash(git rm:*)",
);
} else {
// When using commit signing, use MCP file ops tools
tagModeTools.push(
"mcp__github_file_ops__commit_files",
"mcp__github_file_ops__delete_files",
);
}
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 required tools for tag mode
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
// Append user's claude_args (which may have more --mcp-config flags)
if (userClaudeArgs) {
claudeArgs += ` ${userClaudeArgs}`;
}
core.setOutput("claude_args", claudeArgs.trim());
return {
commentId,
branchInfo,
mcpConfig: ourMcpConfig,
};
},
generatePrompt(
context: PreparedContext,
githubData: FetchDataResult,
useCommitSigning: boolean,
): string {
return generateDefaultPrompt(context, githubData, useCommitSigning);
},
getSystemPrompt() {
// Tag mode doesn't need additional system prompts
return undefined;
},
};

View File

@@ -1,10 +1,13 @@
import type { ParsedGitHubContext } from "../github/context";
import type { GitHubContext } from "../github/context";
import type { PreparedContext } from "../create-prompt/types";
import type { FetchDataResult } from "../github/data/fetcher";
import type { Octokits } from "../github/api/client";
export type ModeName = "tag" | "agent";
export type ModeContext = {
mode: ModeName;
githubContext: ParsedGitHubContext;
githubContext: GitHubContext;
commentId?: number;
baseBranch?: string;
claudeBranch?: string;
@@ -22,8 +25,8 @@ export type ModeData = {
* and tracking comment creation.
*
* Current modes include:
* - 'tag': Traditional implementation triggered by mentions/assignments
* - 'agent': For automation with no trigger checking
* - 'tag': Interactive mode triggered by @claude mentions
* - 'agent': Direct automation mode triggered by explicit prompts
*/
export type Mode = {
name: ModeName;
@@ -32,12 +35,12 @@ export type Mode = {
/**
* Determines if this mode should trigger based on the GitHub context
*/
shouldTrigger(context: ParsedGitHubContext): boolean;
shouldTrigger(context: GitHubContext): boolean;
/**
* Prepares the mode context with any additional data needed for prompt generation
*/
prepareContext(context: ParsedGitHubContext, data?: ModeData): ModeContext;
prepareContext(context: GitHubContext, data?: ModeData): ModeContext;
/**
* Returns the list of tools that should be allowed for this mode
@@ -53,4 +56,45 @@ export type Mode = {
* Determines if this mode should create a tracking comment
*/
shouldCreateTrackingComment(): boolean;
/**
* Generates the prompt for this mode.
* @returns The complete prompt string
*/
generatePrompt(
context: PreparedContext,
githubData: FetchDataResult,
useCommitSigning: boolean,
): string;
/**
* Prepares the GitHub environment for this mode.
* Each mode decides how to handle different event types.
* @returns PrepareResult with commentId, branchInfo, and mcpConfig
*/
prepare(options: ModeOptions): Promise<ModeResult>;
/**
* Returns an optional system prompt to append to Claude's base system prompt.
* This allows modes to add mode-specific instructions.
* @returns The system prompt string or undefined if no additional prompt is needed
*/
getSystemPrompt?(context: ModeContext): string | undefined;
};
// Define types for mode prepare method
export type ModeOptions = {
context: GitHubContext;
octokit: Octokits;
githubToken: string;
};
export type ModeResult = {
commentId?: number;
branchInfo: {
baseBranch: string;
claudeBranch?: string;
currentBranch: string;
};
mcpConfig: string;
};

20
src/prepare/index.ts Normal file
View File

@@ -0,0 +1,20 @@
/**
* Main prepare module that delegates to the mode's prepare method
*/
import type { PrepareOptions, PrepareResult } from "./types";
export async function prepare(options: PrepareOptions): Promise<PrepareResult> {
const { mode, context, octokit, githubToken } = options;
console.log(
`Preparing with mode: ${mode.name} for event: ${context.eventName}`,
);
// Delegate to the mode's prepare method
return mode.prepare({
context,
octokit,
githubToken,
});
}

20
src/prepare/types.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { GitHubContext } from "../github/context";
import type { Octokits } from "../github/api/client";
import type { Mode } from "../modes/types";
export type PrepareResult = {
commentId?: number;
branchInfo: {
baseBranch: string;
claudeBranch?: string;
currentBranch: string;
};
mcpConfig: string;
};
export type PrepareOptions = {
context: GitHubContext;
octokit: Octokits;
mode: Mode;
githubToken: string;
};

96
test/actor.test.ts Normal file
View File

@@ -0,0 +1,96 @@
#!/usr/bin/env bun
import { describe, test, expect } from "bun:test";
import { checkHumanActor } from "../src/github/validation/actor";
import type { Octokit } from "@octokit/rest";
import { createMockContext } from "./mockContext";
function createMockOctokit(userType: string): Octokit {
return {
users: {
getByUsername: async () => ({
data: {
type: userType,
},
}),
},
} as unknown as Octokit;
}
describe("checkHumanActor", () => {
test("should pass for human actor", async () => {
const mockOctokit = createMockOctokit("User");
const context = createMockContext();
context.actor = "human-user";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should throw error for bot actor when not allowed", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "test-bot[bot]";
context.inputs.allowedBots = "";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: test-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should pass for bot actor when all bots allowed", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "test-bot[bot]";
context.inputs.allowedBots = "*";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should pass for specific bot when in allowed list", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "dependabot[bot]";
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should pass for specific bot when in allowed list (without [bot])", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "dependabot[bot]";
context.inputs.allowedBots = "dependabot,renovate";
await expect(
checkHumanActor(mockOctokit, context),
).resolves.toBeUndefined();
});
test("should throw error for bot not in allowed list", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "other-bot[bot]";
context.inputs.allowedBots = "dependabot[bot],renovate[bot]";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
test("should throw error for bot not in allowed list (without [bot])", async () => {
const mockOctokit = createMockOctokit("Bot");
const context = createMockContext();
context.actor = "other-bot[bot]";
context.inputs.allowedBots = "dependabot,renovate";
await expect(checkHumanActor(mockOctokit, context)).rejects.toThrow(
"Workflow initiated by non-human actor: other-bot (type: Bot). Add bot to allowed_bots list or use '*' to allow all bots.",
);
});
});

View File

@@ -3,13 +3,37 @@
import { describe, test, expect } from "bun:test";
import {
generatePrompt,
generateDefaultPrompt,
getEventTypeAndContext,
buildAllowedToolsString,
buildDisallowedToolsString,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
import type { Mode } from "../src/modes/types";
describe("generatePrompt", () => {
// Create a mock tag mode that uses the default prompt
const mockTagMode: Mode = {
name: "tag",
description: "Tag mode",
shouldTrigger: () => true,
prepareContext: (context) => ({ mode: "tag", githubContext: context }),
getAllowedTools: () => [],
getDisallowedTools: () => [],
shouldCreateTrackingComment: () => true,
generatePrompt: (context, githubData, useCommitSigning) =>
generateDefaultPrompt(context, githubData, useCommitSigning),
prepare: async () => ({
commentId: 123,
branchInfo: {
baseBranch: "main",
currentBranch: "main",
claudeBranch: undefined,
},
mcpConfig: "{}",
}),
};
const mockGitHubData = {
contextData: {
title: "Test PR",
@@ -117,7 +141,7 @@ describe("generatePrompt", () => {
imageUrlMap: new Map<string, string>(),
};
test("should generate prompt for issue_comment event", () => {
test("should generate prompt for issue_comment event", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -133,7 +157,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
@@ -148,7 +177,7 @@ describe("generatePrompt", () => {
expect(prompt).not.toContain("filename\tstatus\tadditions\tdeletions\tsha"); // since it's not a PR
});
test("should generate prompt for pull_request_review event", () => {
test("should generate prompt for pull_request_review event", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -161,7 +190,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
@@ -172,7 +206,7 @@ describe("generatePrompt", () => {
); // from review comments
});
test("should generate prompt for issue opened event", () => {
test("should generate prompt for issue opened event", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -187,7 +221,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
expect(prompt).toContain(
@@ -199,7 +238,7 @@ describe("generatePrompt", () => {
expect(prompt).toContain("The target-branch should be 'main'");
});
test("should generate prompt for issue assigned event", () => {
test("should generate prompt for issue assigned event", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -215,7 +254,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
expect(prompt).toContain(
@@ -226,7 +270,7 @@ describe("generatePrompt", () => {
);
});
test("should generate prompt for issue labeled event", () => {
test("should generate prompt for issue labeled event", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -242,7 +286,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
expect(prompt).toContain(
@@ -253,33 +302,9 @@ describe("generatePrompt", () => {
);
});
test("should include direct prompt when provided", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
directPrompt: "Fix the bug in the login form",
eventData: {
eventName: "issues",
eventAction: "opened",
isPR: false,
issueNumber: "789",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101-1200",
},
};
// Removed test - direct_prompt field no longer supported in v1.0
const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("<direct_prompt>");
expect(prompt).toContain("Fix the bug in the login form");
expect(prompt).toContain("</direct_prompt>");
expect(prompt).toContain(
"CRITICAL: Direct user instructions were provided in the <direct_prompt> tag above. These are HIGH PRIORITY instructions that OVERRIDE all other context and MUST be followed exactly as written.",
);
});
test("should generate prompt for pull_request event", () => {
test("should generate prompt for pull_request event", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -292,7 +317,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
@@ -300,12 +330,11 @@ describe("generatePrompt", () => {
expect(prompt).toContain("pull request opened");
});
test("should include custom instructions when provided", () => {
test("should generate prompt for issue comment without custom fields", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
customInstructions: "Always use TypeScript",
eventData: {
eventName: "issue_comment",
commentId: "67890",
@@ -317,17 +346,24 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("CUSTOM INSTRUCTIONS:\nAlways use TypeScript");
// Verify prompt generates successfully without custom instructions
expect(prompt).toContain("@claude please fix this");
expect(prompt).not.toContain("CUSTOM INSTRUCTIONS");
});
test("should use override_prompt when provided", () => {
test("should use override_prompt when provided", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
overridePrompt: "Simple prompt for $REPOSITORY PR #$PR_NUMBER",
prompt: "Simple prompt for reviewing PR",
eventData: {
eventName: "pull_request",
eventAction: "opened",
@@ -336,19 +372,25 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toBe("Simple prompt for owner/repo PR #123");
// v1.0: Prompt is passed through as-is
expect(prompt).toBe("Simple prompt for reviewing PR");
expect(prompt).not.toContain("You are Claude, an AI assistant");
});
test("should substitute all variables in override_prompt", () => {
test("should pass through prompt without variable substitution", async () => {
const envVars: PreparedContext = {
repository: "test/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
triggerUsername: "john-doe",
overridePrompt: `Repository: $REPOSITORY
prompt: `Repository: $REPOSITORY
PR: $PR_NUMBER
Title: $PR_TITLE
Body: $PR_BODY
@@ -371,29 +413,30 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("Repository: test/repo");
expect(prompt).toContain("PR: 456");
expect(prompt).toContain("Title: Test PR");
expect(prompt).toContain("Body: This is a test PR");
expect(prompt).toContain("Comments: ");
expect(prompt).toContain("Review Comments: ");
expect(prompt).toContain("Changed Files: ");
expect(prompt).toContain("Trigger Comment: Please review this code");
expect(prompt).toContain("Username: john-doe");
expect(prompt).toContain("Branch: feature-branch");
expect(prompt).toContain("Base: main");
expect(prompt).toContain("Event: pull_request_review_comment");
expect(prompt).toContain("Is PR: true");
// v1.0: Variables are NOT substituted - prompt is passed as-is to Claude Code
expect(prompt).toContain("Repository: $REPOSITORY");
expect(prompt).toContain("PR: $PR_NUMBER");
expect(prompt).toContain("Title: $PR_TITLE");
expect(prompt).toContain("Body: $PR_BODY");
expect(prompt).toContain("Branch: $BRANCH_NAME");
expect(prompt).toContain("Base: $BASE_BRANCH");
expect(prompt).toContain("Username: $TRIGGER_USERNAME");
expect(prompt).toContain("Comment: $TRIGGER_COMMENT");
});
test("should handle override_prompt for issues", () => {
test("should handle override_prompt for issues", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
overridePrompt: "Issue #$ISSUE_NUMBER: $ISSUE_TITLE in $REPOSITORY",
prompt: "Review issue and provide feedback",
eventData: {
eventName: "issues",
eventAction: "opened",
@@ -418,18 +461,23 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, issueGitHubData, false);
const prompt = await generatePrompt(
envVars,
issueGitHubData,
false,
mockTagMode,
);
expect(prompt).toBe("Issue #789: Bug: Login form broken in owner/repo");
// v1.0: Prompt is passed through as-is
expect(prompt).toBe("Review issue and provide feedback");
});
test("should handle empty values in override_prompt substitution", () => {
test("should handle prompt without substitution", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
overridePrompt:
"PR: $PR_NUMBER, Issue: $ISSUE_NUMBER, Comment: $TRIGGER_COMMENT",
prompt: "PR: $PR_NUMBER, Issue: $ISSUE_NUMBER, Comment: $TRIGGER_COMMENT",
eventData: {
eventName: "pull_request",
eventAction: "opened",
@@ -438,12 +486,20 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toBe("PR: 123, Issue: , Comment: ");
// v1.0: No substitution - passed as-is
expect(prompt).toBe(
"PR: $PR_NUMBER, Issue: $ISSUE_NUMBER, Comment: $TRIGGER_COMMENT",
);
});
test("should not substitute variables when override_prompt is not provided", () => {
test("should not substitute variables when override_prompt is not provided", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -458,13 +514,18 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
});
test("should include trigger username when provided", () => {
test("should include trigger username when provided", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -481,7 +542,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
// With commit signing disabled, co-author info appears in git commit instructions
@@ -490,7 +556,7 @@ describe("generatePrompt", () => {
);
});
test("should include PR-specific instructions only for PR events", () => {
test("should include PR-specific instructions only for PR events", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -503,7 +569,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain PR-specific instructions (git commands when not using signing)
expect(prompt).toContain("git push");
@@ -519,7 +590,7 @@ describe("generatePrompt", () => {
expect(prompt).not.toContain("Create a PR](https://github.com/");
});
test("should include Issue-specific instructions only for Issue events", () => {
test("should include Issue-specific instructions only for Issue events", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -534,7 +605,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain Issue-specific instructions
expect(prompt).toContain(
@@ -557,7 +633,7 @@ describe("generatePrompt", () => {
);
});
test("should use actual branch name for issue comments", () => {
test("should use actual branch name for issue comments", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -573,7 +649,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain the actual branch name with timestamp
expect(prompt).toContain(
@@ -587,7 +668,7 @@ describe("generatePrompt", () => {
);
});
test("should handle closed PR with new branch", () => {
test("should handle closed PR with new branch", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -603,7 +684,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain branch-specific instructions like issues
expect(prompt).toContain(
@@ -626,7 +712,7 @@ describe("generatePrompt", () => {
);
});
test("should handle open PR without new branch", () => {
test("should handle open PR without new branch", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -641,7 +727,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain open PR instructions (git commands when not using signing)
expect(prompt).toContain("git push");
@@ -657,7 +748,7 @@ describe("generatePrompt", () => {
);
});
test("should handle PR review on closed PR with new branch", () => {
test("should handle PR review on closed PR with new branch", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -672,7 +763,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain new branch instructions
expect(prompt).toContain(
@@ -684,7 +780,7 @@ describe("generatePrompt", () => {
expect(prompt).toContain("Reference to the original PR");
});
test("should handle PR review comment on closed PR with new branch", () => {
test("should handle PR review comment on closed PR with new branch", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -700,7 +796,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain new branch instructions
expect(prompt).toContain(
@@ -713,7 +814,7 @@ describe("generatePrompt", () => {
);
});
test("should handle pull_request event on closed PR with new branch", () => {
test("should handle pull_request event on closed PR with new branch", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -728,7 +829,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain new branch instructions
expect(prompt).toContain(
@@ -738,7 +844,7 @@ describe("generatePrompt", () => {
expect(prompt).toContain("Reference to the original PR");
});
test("should include git commands when useCommitSigning is false", () => {
test("should include git commands when useCommitSigning is false", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -752,7 +858,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should have git command instructions
expect(prompt).toContain("Use git commands via the Bash tool");
@@ -767,7 +878,7 @@ describe("generatePrompt", () => {
expect(prompt).not.toContain("mcp__github_file_ops__commit_files");
});
test("should include commit signing tools when useCommitSigning is true", () => {
test("should include commit signing tools when useCommitSigning is true", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -781,7 +892,12 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, true);
const prompt = await generatePrompt(
envVars,
mockGitHubData,
true,
mockTagMode,
);
// Should have commit signing tool instructions
expect(prompt).toContain("mcp__github_file_ops__commit_files");
@@ -795,7 +911,7 @@ describe("generatePrompt", () => {
});
describe("getEventTypeAndContext", () => {
test("should return correct type and context for pull_request_review_comment", () => {
test("should return correct type and context for pull_request_review_comment", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -814,7 +930,7 @@ describe("getEventTypeAndContext", () => {
expect(result.triggerContext).toBe("PR review comment with '@claude'");
});
test("should return correct type and context for issue assigned", () => {
test("should return correct type and context for issue assigned", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -836,7 +952,7 @@ describe("getEventTypeAndContext", () => {
expect(result.triggerContext).toBe("issue assigned to 'claude-bot'");
});
test("should return correct type and context for issue labeled", () => {
test("should return correct type and context for issue labeled", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
@@ -858,12 +974,12 @@ describe("getEventTypeAndContext", () => {
expect(result.triggerContext).toBe("issue labeled with 'claude-task'");
});
test("should return correct type and context for issue assigned without assigneeTrigger", () => {
test("should return correct type and context for issue assigned without assigneeTrigger", async () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
directPrompt: "Please assess this issue",
prompt: "Please assess this issue",
eventData: {
eventName: "issues",
eventAction: "assigned",
@@ -871,7 +987,7 @@ describe("getEventTypeAndContext", () => {
issueNumber: "999",
baseBranch: "main",
claudeBranch: "claude/issue-999-20240101-1200",
// No assigneeTrigger when using directPrompt
// No assigneeTrigger when using prompt
},
};
@@ -883,7 +999,7 @@ describe("getEventTypeAndContext", () => {
});
describe("buildAllowedToolsString", () => {
test("should return correct tools for regular events (default no signing)", () => {
test("should return correct tools for regular events (default no signing)", async () => {
const result = buildAllowedToolsString();
// The base tools should be in the result
@@ -905,7 +1021,7 @@ describe("buildAllowedToolsString", () => {
expect(result).not.toContain("mcp__github_file_ops__delete_files");
});
test("should return correct tools with default parameters", () => {
test("should return correct tools with default parameters", async () => {
const result = buildAllowedToolsString([], false, false);
// The base tools should be in the result
@@ -926,7 +1042,7 @@ describe("buildAllowedToolsString", () => {
expect(result).not.toContain("mcp__github_file_ops__delete_files");
});
test("should append custom tools when provided", () => {
test("should append custom tools when provided", async () => {
const customTools = ["Tool1", "Tool2", "Tool3"];
const result = buildAllowedToolsString(customTools);
@@ -947,7 +1063,7 @@ describe("buildAllowedToolsString", () => {
expect(basePlusCustom).toContain("Tool3");
});
test("should include GitHub Actions tools when includeActionsTools is true", () => {
test("should include GitHub Actions tools when includeActionsTools is true", async () => {
const result = buildAllowedToolsString([], true);
// Base tools should be present
@@ -960,7 +1076,7 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("mcp__github_ci__download_job_log");
});
test("should include both custom and Actions tools when both provided", () => {
test("should include both custom and Actions tools when both provided", async () => {
const customTools = ["Tool1", "Tool2"];
const result = buildAllowedToolsString(customTools, true);
@@ -977,7 +1093,7 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("mcp__github_ci__download_job_log");
});
test("should include commit signing tools when useCommitSigning is true", () => {
test("should include commit signing tools when useCommitSigning is true", async () => {
const result = buildAllowedToolsString([], false, true);
// Base tools should be present
@@ -998,7 +1114,7 @@ describe("buildAllowedToolsString", () => {
expect(result).not.toContain("Bash(");
});
test("should include specific Bash git commands when useCommitSigning is false", () => {
test("should include specific Bash git commands when useCommitSigning is false", async () => {
const result = buildAllowedToolsString([], false, false);
// Base tools should be present
@@ -1017,8 +1133,6 @@ describe("buildAllowedToolsString", () => {
expect(result).toContain("Bash(git diff:*)");
expect(result).toContain("Bash(git log:*)");
expect(result).toContain("Bash(git rm:*)");
expect(result).toContain("Bash(git config user.name:*)");
expect(result).toContain("Bash(git config user.email:*)");
// Comment tool from minimal server should be included
expect(result).toContain("mcp__github_comment__update_claude_comment");
@@ -1028,7 +1142,7 @@ describe("buildAllowedToolsString", () => {
expect(result).not.toContain("mcp__github_file_ops__delete_files");
});
test("should handle all combinations of options", () => {
test("should handle all combinations of options", async () => {
const customTools = ["CustomTool1", "CustomTool2"];
const result = buildAllowedToolsString(customTools, true, false);
@@ -1052,7 +1166,7 @@ describe("buildAllowedToolsString", () => {
});
describe("buildDisallowedToolsString", () => {
test("should return base disallowed tools when no custom tools provided", () => {
test("should return base disallowed tools when no custom tools provided", async () => {
const result = buildDisallowedToolsString();
// The base disallowed tools should be in the result
@@ -1060,7 +1174,7 @@ describe("buildDisallowedToolsString", () => {
expect(result).toContain("WebFetch");
});
test("should append custom disallowed tools when provided", () => {
test("should append custom disallowed tools when provided", async () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const result = buildDisallowedToolsString(customDisallowedTools);
@@ -1078,7 +1192,7 @@ describe("buildDisallowedToolsString", () => {
expect(parts).toContain("BadTool2");
});
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
test("should remove hardcoded disallowed tools if they are in allowed tools", async () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const allowedTools = ["WebSearch", "SomeOtherTool"];
const result = buildDisallowedToolsString(
@@ -1097,7 +1211,7 @@ describe("buildDisallowedToolsString", () => {
expect(result).toContain("BadTool2");
});
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
test("should remove all hardcoded disallowed tools if they are all in allowed tools", async () => {
const allowedTools = ["WebSearch", "WebFetch", "SomeOtherTool"];
const result = buildDisallowedToolsString(undefined, allowedTools);
@@ -1109,7 +1223,7 @@ describe("buildDisallowedToolsString", () => {
expect(result).toBe("");
});
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
test("should handle custom disallowed tools when all hardcoded tools are overridden", async () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const allowedTools = ["WebSearch", "WebFetch"];
const result = buildDisallowedToolsString(

View File

@@ -252,6 +252,63 @@ describe("formatComments", () => {
`[user1 at 2023-01-01T00:00:00Z]: Image: ![](https://github.com/user-attachments/assets/test.png)`,
);
});
test("filters out minimized comments", () => {
const comments: GitHubComment[] = [
{
id: "1",
databaseId: "100001",
body: "Normal comment",
author: { login: "user1" },
createdAt: "2023-01-01T00:00:00Z",
isMinimized: false,
},
{
id: "2",
databaseId: "100002",
body: "Minimized comment",
author: { login: "user2" },
createdAt: "2023-01-02T00:00:00Z",
isMinimized: true,
},
{
id: "3",
databaseId: "100003",
body: "Another normal comment",
author: { login: "user3" },
createdAt: "2023-01-03T00:00:00Z",
},
];
const result = formatComments(comments);
expect(result).toBe(
`[user1 at 2023-01-01T00:00:00Z]: Normal comment\n\n[user3 at 2023-01-03T00:00:00Z]: Another normal comment`,
);
});
test("returns empty string when all comments are minimized", () => {
const comments: GitHubComment[] = [
{
id: "1",
databaseId: "100001",
body: "Minimized comment 1",
author: { login: "user1" },
createdAt: "2023-01-01T00:00:00Z",
isMinimized: true,
},
{
id: "2",
databaseId: "100002",
body: "Minimized comment 2",
author: { login: "user2" },
createdAt: "2023-01-02T00:00:00Z",
isMinimized: true,
},
];
const result = formatComments(comments);
expect(result).toBe("");
});
});
describe("formatReviewComments", () => {
@@ -517,6 +574,159 @@ describe("formatReviewComments", () => {
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: ![](https://github.com/user-attachments/assets/test.png)`,
);
});
test("filters out minimized review comments", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "Review with mixed comments",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Normal review comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: false,
},
{
id: "comment2",
databaseId: "200002",
body: "Minimized review comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
{
id: "comment3",
databaseId: "200003",
body: "Another normal comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/main.ts",
line: 10,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview with mixed comments\n [Comment on src/index.ts:42]: Normal review comment\n [Comment on src/main.ts:10]: Another normal comment`,
);
});
test("returns review with only body when all review comments are minimized", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "Review body only",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Minimized comment 1",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: true,
},
{
id: "comment2",
databaseId: "200002",
body: "Minimized comment 2",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body only`,
);
});
test("handles multiple reviews with mixed minimized comments", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "First review",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Good comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: false,
},
],
},
},
{
id: "review2",
databaseId: "300002",
author: { login: "reviewer2" },
body: "Second review",
state: "COMMENTED",
submittedAt: "2023-01-02T00:00:00Z",
comments: {
nodes: [
{
id: "comment2",
databaseId: "200002",
body: "Spam comment",
author: { login: "reviewer2" },
createdAt: "2023-01-02T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nFirst review\n [Comment on src/index.ts:42]: Good comment\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: COMMENTED\nSecond review`,
);
});
});
describe("formatChangedFiles", () => {

View File

@@ -28,7 +28,7 @@ if __name__ == "__main__":
print(result)
```
*Token usage: 100 input, 75 output*
*Token usage: 150 input, 75 output*
---
@@ -47,7 +47,7 @@ I can see the debug print statement that needs to be removed. Let me fix this by
**→** File successfully edited. The debug print statement has been removed.
*Token usage: 200 input, 50 output*
*Token usage: 300 input, 50 output*
---
@@ -70,7 +70,7 @@ Perfect! I've successfully removed the debug print statement from the function.
**→** Successfully posted review comment to PR #123
*Token usage: 150 input, 80 output*
*Token usage: 225 input, 80 output*
---
@@ -82,7 +82,7 @@ Great! I've successfully completed the requested task:
The debug print statement has been removed as requested by the reviewers.
*Token usage: 180 input, 60 output*
*Token usage: 270 input, 60 output*
---
@@ -91,5 +91,3 @@ The debug print statement has been removed as requested by the reviewers.
Successfully removed debug print statement from file and added review comment to document the change.
**Cost:** $0.0347 | **Duration:** 18.8s

View File

@@ -1,115 +0,0 @@
import { describe, it, expect } from "bun:test";
import {
parseMultilineInput,
parseAdditionalPermissions,
} from "../../src/github/context";
describe("parseMultilineInput", () => {
it("should parse a comma-separated string", () => {
const input = `Bash(bun install),Bash(bun test:*),Bash(bun typecheck)`;
const result = parseMultilineInput(input);
expect(result).toEqual([
"Bash(bun install)",
"Bash(bun test:*)",
"Bash(bun typecheck)",
]);
});
it("should parse multiline string", () => {
const input = `Bash(bun install)
Bash(bun test:*)
Bash(bun typecheck)`;
const result = parseMultilineInput(input);
expect(result).toEqual([
"Bash(bun install)",
"Bash(bun test:*)",
"Bash(bun typecheck)",
]);
});
it("should parse comma-separated multiline line", () => {
const input = `Bash(bun install),Bash(bun test:*)
Bash(bun typecheck)`;
const result = parseMultilineInput(input);
expect(result).toEqual([
"Bash(bun install)",
"Bash(bun test:*)",
"Bash(bun typecheck)",
]);
});
it("should ignore comments", () => {
const input = `Bash(bun install),
Bash(bun test:*) # For testing
# For type checking
Bash(bun typecheck)
`;
const result = parseMultilineInput(input);
expect(result).toEqual([
"Bash(bun install)",
"Bash(bun test:*)",
"Bash(bun typecheck)",
]);
});
it("should parse an empty string", () => {
const input = "";
const result = parseMultilineInput(input);
expect(result).toEqual([]);
});
});
describe("parseAdditionalPermissions", () => {
it("should parse single permission", () => {
const input = "actions: read";
const result = parseAdditionalPermissions(input);
expect(result.get("actions")).toBe("read");
expect(result.size).toBe(1);
});
it("should parse multiple permissions", () => {
const input = `actions: read
packages: write
contents: read`;
const result = parseAdditionalPermissions(input);
expect(result.get("actions")).toBe("read");
expect(result.get("packages")).toBe("write");
expect(result.get("contents")).toBe("read");
expect(result.size).toBe(3);
});
it("should handle empty string", () => {
const input = "";
const result = parseAdditionalPermissions(input);
expect(result.size).toBe(0);
});
it("should handle whitespace and empty lines", () => {
const input = `
actions: read
packages: write
`;
const result = parseAdditionalPermissions(input);
expect(result.get("actions")).toBe("read");
expect(result.get("packages")).toBe("write");
expect(result.size).toBe(2);
});
it("should ignore lines without colon separator", () => {
const input = `actions: read
invalid line
packages: write`;
const result = parseAdditionalPermissions(input);
expect(result.get("actions")).toBe("read");
expect(result.get("packages")).toBe("write");
expect(result.size).toBe(2);
});
it("should trim whitespace around keys and values", () => {
const input = " actions : read ";
const result = parseAdditionalPermissions(input);
expect(result.get("actions")).toBe("read");
expect(result.size).toBe(1);
});
});

View File

@@ -662,4 +662,255 @@ describe("downloadCommentImages", () => {
);
expect(result.get(imageUrl2)).toBeUndefined();
});
test("should detect and download images from HTML img tags", async () => {
const mockOctokit = createMockOctokit();
const imageUrl =
"https://github.com/user-attachments/assets/html-image.png";
const signedUrl =
"https://private-user-images.githubusercontent.com/html.png?jwt=token";
// Mock octokit response
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
// Mock fetch for image download
const mockArrayBuffer = new ArrayBuffer(8);
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
arrayBuffer: async () => mockArrayBuffer,
} as Response);
const comments: CommentWithImages[] = [
{
type: "issue_comment",
id: "777",
body: `Here's an HTML image: <img src="${imageUrl}" alt="test">`,
},
];
const result = await downloadCommentImages(
mockOctokit,
"owner",
"repo",
comments,
);
expect(mockOctokit.rest.issues.getComment).toHaveBeenCalledWith({
owner: "owner",
repo: "repo",
comment_id: 777,
mediaType: { format: "full+json" },
});
expect(fetchSpy).toHaveBeenCalledWith(signedUrl);
expect(fsWriteFileSpy).toHaveBeenCalledWith(
"/tmp/github-images/image-1704067200000-0.png",
Buffer.from(mockArrayBuffer),
);
expect(result.size).toBe(1);
expect(result.get(imageUrl)).toBe(
"/tmp/github-images/image-1704067200000-0.png",
);
expect(consoleLogSpy).toHaveBeenCalledWith(
"Found 1 image(s) in issue_comment 777",
);
expect(consoleLogSpy).toHaveBeenCalledWith(`Downloading ${imageUrl}...`);
expect(consoleLogSpy).toHaveBeenCalledWith(
"✓ Saved: /tmp/github-images/image-1704067200000-0.png",
);
});
test("should handle HTML img tags with different quote styles", async () => {
const mockOctokit = createMockOctokit();
const imageUrl1 =
"https://github.com/user-attachments/assets/single-quote.jpg";
const imageUrl2 =
"https://github.com/user-attachments/assets/double-quote.png";
const signedUrl1 =
"https://private-user-images.githubusercontent.com/single.jpg?jwt=token1";
const signedUrl2 =
"https://private-user-images.githubusercontent.com/double.png?jwt=token2";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
});
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
arrayBuffer: async () => new ArrayBuffer(8),
} as Response);
const comments: CommentWithImages[] = [
{
type: "issue_comment",
id: "888",
body: `Single quote: <img src='${imageUrl1}' alt="test"> and double quote: <img src="${imageUrl2}" alt="test">`,
},
];
const result = await downloadCommentImages(
mockOctokit,
"owner",
"repo",
comments,
);
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(result.size).toBe(2);
expect(result.get(imageUrl1)).toBe(
"/tmp/github-images/image-1704067200000-0.jpg",
);
expect(result.get(imageUrl2)).toBe(
"/tmp/github-images/image-1704067200000-1.png",
);
expect(consoleLogSpy).toHaveBeenCalledWith(
"Found 2 image(s) in issue_comment 888",
);
});
test("should handle mixed Markdown and HTML images", async () => {
const mockOctokit = createMockOctokit();
const markdownUrl =
"https://github.com/user-attachments/assets/markdown.png";
const htmlUrl = "https://github.com/user-attachments/assets/html.jpg";
const signedUrl1 =
"https://private-user-images.githubusercontent.com/md.png?jwt=token1";
const signedUrl2 =
"https://private-user-images.githubusercontent.com/html.jpg?jwt=token2";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl1}"><img src="${signedUrl2}">`,
},
});
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
arrayBuffer: async () => new ArrayBuffer(8),
} as Response);
const comments: CommentWithImages[] = [
{
type: "issue_comment",
id: "999",
body: `Markdown: ![test](${markdownUrl}) and HTML: <img src="${htmlUrl}" alt="test">`,
},
];
const result = await downloadCommentImages(
mockOctokit,
"owner",
"repo",
comments,
);
expect(fetchSpy).toHaveBeenCalledTimes(2);
expect(result.size).toBe(2);
expect(result.get(markdownUrl)).toBe(
"/tmp/github-images/image-1704067200000-0.png",
);
expect(result.get(htmlUrl)).toBe(
"/tmp/github-images/image-1704067200000-1.jpg",
);
expect(consoleLogSpy).toHaveBeenCalledWith(
"Found 2 image(s) in issue_comment 999",
);
});
test("should deduplicate identical URLs from Markdown and HTML", async () => {
const mockOctokit = createMockOctokit();
const imageUrl = "https://github.com/user-attachments/assets/duplicate.png";
const signedUrl =
"https://private-user-images.githubusercontent.com/dup.png?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
arrayBuffer: async () => new ArrayBuffer(8),
} as Response);
const comments: CommentWithImages[] = [
{
type: "issue_comment",
id: "1000",
body: `Same image twice: ![test](${imageUrl}) and <img src="${imageUrl}" alt="test">`,
},
];
const result = await downloadCommentImages(
mockOctokit,
"owner",
"repo",
comments,
);
expect(fetchSpy).toHaveBeenCalledTimes(1); // Only downloaded once
expect(result.size).toBe(1);
expect(result.get(imageUrl)).toBe(
"/tmp/github-images/image-1704067200000-0.png",
);
expect(consoleLogSpy).toHaveBeenCalledWith(
"Found 1 image(s) in issue_comment 1000",
);
});
test("should handle HTML img tags with additional attributes", async () => {
const mockOctokit = createMockOctokit();
const imageUrl =
"https://github.com/user-attachments/assets/complex-tag.webp";
const signedUrl =
"https://private-user-images.githubusercontent.com/complex.webp?jwt=token";
// @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
data: {
body_html: `<img src="${signedUrl}">`,
},
});
fetchSpy = spyOn(global, "fetch").mockResolvedValue({
ok: true,
arrayBuffer: async () => new ArrayBuffer(8),
} as Response);
const comments: CommentWithImages[] = [
{
type: "issue_comment",
id: "1001",
body: `Complex tag: <img class="image" src="${imageUrl}" alt="test image" width="100" height="200">`,
},
];
const result = await downloadCommentImages(
mockOctokit,
"owner",
"repo",
comments,
);
expect(fetchSpy).toHaveBeenCalledTimes(1);
expect(result.size).toBe(1);
expect(result.get(imageUrl)).toBe(
"/tmp/github-images/image-1704067200000-0.webp",
);
expect(consoleLogSpy).toHaveBeenCalledWith(
"Found 1 image(s) in issue_comment 1001",
);
});
});

View File

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

View File

@@ -1,4 +1,7 @@
import type { ParsedGitHubContext } from "../src/github/context";
import type {
ParsedGitHubContext,
AutomationContext,
} from "../src/github/context";
import type {
IssuesEvent,
IssueCommentEvent,
@@ -8,23 +11,14 @@ import type {
} from "@octokit/webhooks-types";
const defaultInputs = {
mode: "tag" as const,
prompt: "",
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
anthropicModel: "claude-3-7-sonnet-20250219",
allowedTools: [] as string[],
disallowedTools: [] as string[],
customInstructions: "",
directPrompt: "",
overridePrompt: "",
useBedrock: false,
useVertex: false,
timeoutMinutes: 30,
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map<string, string>(),
useCommitSigning: false,
allowedBots: "",
};
const defaultRepository = {
@@ -33,12 +27,16 @@ const defaultRepository = {
full_name: "test-owner/test-repo",
};
type MockContextOverrides = Omit<Partial<ParsedGitHubContext>, "inputs"> & {
inputs?: Partial<ParsedGitHubContext["inputs"]>;
};
export const createMockContext = (
overrides: Partial<ParsedGitHubContext> = {},
overrides: MockContextOverrides = {},
): ParsedGitHubContext => {
const baseContext: ParsedGitHubContext = {
runId: "1234567890",
eventName: "",
eventName: "issue_comment", // Default to a valid entity event
eventAction: "",
repository: defaultRepository,
actor: "test-actor",
@@ -48,11 +46,35 @@ export const createMockContext = (
inputs: defaultInputs,
};
if (overrides.inputs) {
overrides.inputs = { ...defaultInputs, ...overrides.inputs };
}
const mergedInputs = overrides.inputs
? { ...defaultInputs, ...overrides.inputs }
: defaultInputs;
return { ...baseContext, ...overrides };
return { ...baseContext, ...overrides, inputs: mergedInputs };
};
type MockAutomationOverrides = Omit<Partial<AutomationContext>, "inputs"> & {
inputs?: Partial<AutomationContext["inputs"]>;
};
export const createMockAutomationContext = (
overrides: MockAutomationOverrides = {},
): AutomationContext => {
const baseContext: AutomationContext = {
runId: "1234567890",
eventName: "workflow_dispatch",
eventAction: undefined,
repository: defaultRepository,
actor: "test-actor",
payload: {} as any,
inputs: defaultInputs,
};
const mergedInputs = overrides.inputs
? { ...defaultInputs, ...overrides.inputs }
: defaultInputs;
return { ...baseContext, ...overrides, inputs: mergedInputs };
};
export const mockIssueOpenedContext: ParsedGitHubContext = {

View File

@@ -1,82 +1,169 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
import { agentMode } from "../../src/modes/agent";
import type { ParsedGitHubContext } from "../../src/github/context";
import { createMockContext } from "../mockContext";
import type { GitHubContext } from "../../src/github/context";
import { createMockContext, createMockAutomationContext } from "../mockContext";
import * as core from "@actions/core";
describe("Agent Mode", () => {
let mockContext: ParsedGitHubContext;
let mockContext: GitHubContext;
let exportVariableSpy: any;
let setOutputSpy: any;
beforeEach(() => {
mockContext = createMockContext({
mockContext = createMockAutomationContext({
eventName: "workflow_dispatch",
isPR: false,
});
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
() => {},
);
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
});
test("agent mode has correct properties and behavior", () => {
// Basic properties
afterEach(() => {
exportVariableSpy?.mockClear();
setOutputSpy?.mockClear();
exportVariableSpy?.mockRestore();
setOutputSpy?.mockRestore();
});
test("agent mode has correct properties", () => {
expect(agentMode.name).toBe("agent");
expect(agentMode.description).toBe(
"Automation mode that always runs without trigger checking",
"Direct automation mode for explicit prompts",
);
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
// Tool methods return empty arrays
expect(agentMode.getAllowedTools()).toEqual([]);
expect(agentMode.getDisallowedTools()).toEqual([]);
// Always triggers regardless of context
const contextWithoutTrigger = createMockContext({
eventName: "workflow_dispatch",
isPR: false,
inputs: {
...createMockContext().inputs,
triggerPhrase: "@claude",
},
payload: {} as any,
});
expect(agentMode.shouldTrigger(contextWithoutTrigger)).toBe(true);
});
test("prepareContext includes all required data", () => {
const data = {
commentId: 789,
baseBranch: "develop",
claudeBranch: "claude/automated-task",
};
const context = agentMode.prepareContext(mockContext, data);
expect(context.mode).toBe("agent");
expect(context.githubContext).toBe(mockContext);
expect(context.commentId).toBe(789);
expect(context.baseBranch).toBe("develop");
expect(context.claudeBranch).toBe("claude/automated-task");
});
test("prepareContext works without data", () => {
test("prepareContext returns minimal data", () => {
const context = agentMode.prepareContext(mockContext);
expect(context.mode).toBe("agent");
expect(context.githubContext).toBe(mockContext);
expect(context.commentId).toBeUndefined();
expect(context.baseBranch).toBeUndefined();
expect(context.claudeBranch).toBeUndefined();
// Agent mode doesn't use comment tracking or branch management
expect(Object.keys(context)).toEqual(["mode", "githubContext"]);
});
test("agent mode triggers for all event types", () => {
const events = [
"push",
"schedule",
"workflow_dispatch",
"repository_dispatch",
test("agent mode only triggers when prompt is provided", () => {
// Should NOT trigger for automation events without prompt
const workflowDispatchContext = createMockAutomationContext({
eventName: "workflow_dispatch",
});
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(false);
const scheduleContext = createMockAutomationContext({
eventName: "schedule",
});
expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
// Should NOT trigger for entity events without prompt
const entityEvents = [
"issue_comment",
"pull_request",
];
"pull_request_review",
"issues",
] as const;
events.forEach((eventName) => {
const context = createMockContext({ eventName, isPR: false });
expect(agentMode.shouldTrigger(context)).toBe(true);
entityEvents.forEach((eventName) => {
const contextNoPrompt = createMockContext({ eventName });
expect(agentMode.shouldTrigger(contextNoPrompt)).toBe(false);
});
// Should trigger for ANY event when prompt is provided
const allEvents = [
"workflow_dispatch",
"schedule",
"issue_comment",
"pull_request",
"pull_request_review",
"issues",
] as const;
allEvents.forEach((eventName) => {
const contextWithPrompt =
eventName === "workflow_dispatch" || eventName === "schedule"
? createMockAutomationContext({
eventName,
inputs: { prompt: "Do something" },
})
: createMockContext({
eventName,
inputs: { prompt: "Do something" },
});
expect(agentMode.shouldTrigger(contextWithPrompt)).toBe(true);
});
});
test("prepare method passes through claude_args", async () => {
// Clear any previous calls before this test
exportVariableSpy.mockClear();
setOutputSpy.mockClear();
const contextWithCustomArgs = createMockAutomationContext({
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";
const mockOctokit = {} as any;
const result = await agentMode.prepare({
context: contextWithCustomArgs,
octokit: mockOctokit,
githubToken: "test-token",
});
// Verify claude_args includes MCP config and user args
const callArgs = setOutputSpy.mock.calls[0];
expect(callArgs[0]).toBe("claude_args");
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
expect(result).toEqual({
commentId: undefined,
branchInfo: {
baseBranch: "main",
currentBranch: "main",
claudeBranch: undefined,
},
mcpConfig: expect.any(String),
});
// 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 () => {
const contextWithPrompts = createMockAutomationContext({
eventName: "workflow_dispatch",
});
// In v1-dev, we only have the unified prompt field
contextWithPrompts.inputs.prompt = "Custom prompt content";
const mockOctokit = {} as any;
await agentMode.prepare({
context: contextWithPrompts,
octokit: mockOctokit,
githubToken: "test-token",
});
// Note: We can't easily test file creation in this unit test,
// but we can verify the method completes without errors
// Agent mode now includes MCP config even with empty user args
const callArgs = setOutputSpy.mock.calls[0];
expect(callArgs[0]).toBe("claude_args");
expect(callArgs[1]).toContain("--mcp-config");
});
});

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

@@ -1,29 +1,116 @@
import { describe, test, expect } from "bun:test";
import { getMode, isValidMode } from "../../src/modes/registry";
import type { ModeName } from "../../src/modes/types";
import { tagMode } from "../../src/modes/tag";
import { agentMode } from "../../src/modes/agent";
import { tagMode } from "../../src/modes/tag";
import { createMockContext, createMockAutomationContext } from "../mockContext";
describe("Mode Registry", () => {
test("getMode returns tag mode by default", () => {
const mode = getMode("tag");
expect(mode).toBe(tagMode);
expect(mode.name).toBe("tag");
const mockContext = createMockContext({
eventName: "issue_comment",
payload: {
action: "created",
comment: {
body: "Test comment without trigger",
},
} as any,
});
test("getMode returns agent mode", () => {
const mode = getMode("agent");
const mockWorkflowDispatchContext = createMockAutomationContext({
eventName: "workflow_dispatch",
});
const mockScheduleContext = createMockAutomationContext({
eventName: "schedule",
});
test("getMode auto-detects agent mode for issue_comment without trigger", () => {
const mode = getMode(mockContext);
// Agent mode is the default when no trigger is found
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode throws error for invalid mode", () => {
const invalidMode = "invalid" as unknown as ModeName;
expect(() => getMode(invalidMode)).toThrow(
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent'. Please check your workflow configuration.",
);
test("getMode auto-detects agent mode for workflow_dispatch", () => {
const mode = getMode(mockWorkflowDispatchContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
// Removed test - explicit mode override no longer supported in v1.0
test("getMode auto-detects agent for workflow_dispatch", () => {
const mode = getMode(mockWorkflowDispatchContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode auto-detects agent for schedule event", () => {
const mode = getMode(mockScheduleContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
// Removed test - legacy mode names no longer supported in v1.0
test("getMode auto-detects agent mode for PR opened", () => {
const prContext = createMockContext({
eventName: "pull_request",
payload: { action: "opened" } as any,
isPR: true,
});
const mode = getMode(prContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode uses agent mode when prompt is provided, even with @claude mention", () => {
const contextWithPrompt = createMockContext({
eventName: "issue_comment",
payload: {
action: "created",
comment: {
body: "@claude please help",
},
} as any,
inputs: {
prompt: "/review",
} as any,
});
const mode = getMode(contextWithPrompt);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode uses tag mode for @claude mention without prompt", () => {
// Ensure PROMPT env var is not set (clean up from previous tests)
const originalPrompt = process.env.PROMPT;
delete process.env.PROMPT;
const contextWithMention = createMockContext({
eventName: "issue_comment",
payload: {
action: "created",
comment: {
body: "@claude please help",
},
} as any,
inputs: {
triggerPhrase: "@claude",
prompt: "",
} as any,
});
const mode = getMode(contextWithMention);
expect(mode).toBe(tagMode);
expect(mode.name).toBe("tag");
// Restore original value if it existed
if (originalPrompt !== undefined) {
process.env.PROMPT = originalPrompt;
}
});
// Removed test - explicit mode override no longer supported in v1.0
test("isValidMode returns true for all valid modes", () => {
expect(isValidMode("tag")).toBe(true);
expect(isValidMode("agent")).toBe(true);

View File

@@ -60,19 +60,14 @@ describe("checkWritePermissions", () => {
entityNumber: 1,
isPR: false,
inputs: {
mode: "tag",
prompt: "",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
directPrompt: "",
overridePrompt: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
allowedBots: "",
},
});
@@ -126,6 +121,16 @@ describe("checkWritePermissions", () => {
);
});
test("should return true for bot user", async () => {
const mockOctokit = createMockOctokit("none");
const context = createContext();
context.actor = "test-bot[bot]";
const result = await checkWritePermissions(mockOctokit, context);
expect(result).toBe(true);
});
test("should throw error when permission check fails", async () => {
const error = new Error("API error");
const mockOctokit = {

View File

@@ -220,13 +220,13 @@ describe("parseEnvVarsWithContext", () => {
).toThrow("BASE_BRANCH is required for issues event");
});
test("should allow issue assigned event with direct_prompt and no assigneeTrigger", () => {
test("should allow issue assigned event with prompt and no assigneeTrigger", () => {
const contextWithDirectPrompt = createMockContext({
...mockIssueAssignedContext,
inputs: {
...mockIssueAssignedContext.inputs,
assigneeTrigger: "", // No assignee trigger
directPrompt: "Please assess this issue", // But direct prompt is provided
prompt: "Please assess this issue", // But prompt is provided
},
});
@@ -239,7 +239,7 @@ describe("parseEnvVarsWithContext", () => {
expect(result.eventData.eventName).toBe("issues");
expect(result.eventData.isPR).toBe(false);
expect(result.directPrompt).toBe("Please assess this issue");
expect(result.prompt).toBe("Please assess this issue");
if (
result.eventData.eventName === "issues" &&
result.eventData.eventAction === "assigned"
@@ -249,13 +249,13 @@ describe("parseEnvVarsWithContext", () => {
}
});
test("should throw error when neither assigneeTrigger nor directPrompt provided for issue assigned event", () => {
test("should throw error when neither assigneeTrigger nor prompt provided for issue assigned event", () => {
const contextWithoutTriggers = createMockContext({
...mockIssueAssignedContext,
inputs: {
...mockIssueAssignedContext.inputs,
assigneeTrigger: "", // No assignee trigger
directPrompt: "", // No direct prompt
prompt: "", // No prompt
},
});
@@ -270,44 +270,23 @@ describe("parseEnvVarsWithContext", () => {
});
});
describe("optional fields", () => {
test("should include custom instructions when provided", () => {
describe("context generation", () => {
test("should generate context without legacy fields", () => {
process.env = BASE_ENV;
const contextWithCustomInstructions = createMockContext({
const context = createMockContext({
...mockPullRequestCommentContext,
inputs: {
...mockPullRequestCommentContext.inputs,
customInstructions: "Be concise",
},
});
const result = prepareContext(contextWithCustomInstructions, "12345");
const result = prepareContext(context, "12345");
expect(result.customInstructions).toBe("Be concise");
// Verify context is created without legacy fields
expect(result.repository).toBe("test-owner/test-repo");
expect(result.claudeCommentId).toBe("12345");
expect(result.triggerPhrase).toBe("/claude");
expect((result as any).customInstructions).toBeUndefined();
expect((result as any).allowedTools).toBeUndefined();
});
test("should include allowed tools when provided", () => {
process.env = BASE_ENV;
const contextWithAllowedTools = createMockContext({
...mockPullRequestCommentContext,
inputs: {
...mockPullRequestCommentContext.inputs,
allowedTools: ["Tool1", "Tool2"],
},
});
const result = prepareContext(contextWithAllowedTools, "12345");
expect(result.allowedTools).toBe("Tool1,Tool2");
});
});
test("should throw error for unsupported event type", () => {
process.env = BASE_ENV;
const unsupportedContext = createMockContext({
eventName: "unsupported_event",
eventAction: "whatever",
});
expect(() => prepareContext(unsupportedContext, "12345")).toThrow(
"Unsupported event type: unsupported_event",
);
});
});

View File

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

View File

@@ -22,31 +22,26 @@ import type {
import type { ParsedGitHubContext } from "../src/github/context";
describe("checkContainsTrigger", () => {
describe("direct prompt trigger", () => {
it("should return true when direct prompt is provided", () => {
describe("prompt trigger", () => {
it("should return true when prompt is provided", () => {
const context = createMockContext({
eventName: "issues",
eventAction: "opened",
inputs: {
mode: "tag",
prompt: "Fix the bug in the login form",
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "Fix the bug in the login form",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
allowedBots: "",
},
});
expect(checkContainsTrigger(context)).toBe(true);
});
it("should return false when direct prompt is empty", () => {
it("should return false when prompt is empty", () => {
const context = createMockContext({
eventName: "issues",
eventAction: "opened",
@@ -61,19 +56,14 @@ describe("checkContainsTrigger", () => {
},
} as IssuesEvent,
inputs: {
mode: "tag",
prompt: "",
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
allowedBots: "",
},
});
expect(checkContainsTrigger(context)).toBe(false);
@@ -278,19 +268,14 @@ describe("checkContainsTrigger", () => {
},
} as PullRequestEvent,
inputs: {
mode: "tag",
prompt: "",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
allowedBots: "",
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -312,19 +297,14 @@ describe("checkContainsTrigger", () => {
},
} as PullRequestEvent,
inputs: {
mode: "tag",
prompt: "",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
allowedBots: "",
},
});
expect(checkContainsTrigger(context)).toBe(true);
@@ -346,19 +326,14 @@ describe("checkContainsTrigger", () => {
},
} as PullRequestEvent,
inputs: {
mode: "tag",
prompt: "",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
allowedBots: "",
},
});
expect(checkContainsTrigger(context)).toBe(false);
@@ -474,17 +449,6 @@ describe("checkContainsTrigger", () => {
});
});
});
describe("non-matching events", () => {
it("should return false for non-matching event type", () => {
const context = createMockContext({
eventName: "push",
eventAction: "created",
payload: {} as any,
});
expect(checkContainsTrigger(context)).toBe(false);
});
});
});
describe("escapeRegExp", () => {