Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot]
5b1f984703 tmp 2025-09-04 11:44:18 -07:00
github-actions[bot]
59a49d170c tmp 2025-09-04 11:28:30 -07:00
github-actions[bot]
cc47c8baef tmp 2025-09-04 11:11:56 -07:00
github-actions[bot]
222c780563 tmp 2025-09-04 11:09:57 -07:00
github-actions[bot]
574f279fb0 tmp 2025-09-04 10:43:51 -07:00
github-actions[bot]
3fa4ea2174 refactor: replace magic number with GITHUB_ACTIONS_BOT_ID constant
- Create new constants file at src/github/constants.ts with GITHUB_ACTIONS_BOT_ID and GITHUB_ACTIONS_BOT_LOGIN
- Replace all occurrences of magic number 41898282 with the constant
- Update imports across source and test files to use the new constants
- Add comment in action.yml referencing the constants file for the default bot_id value

This improves code maintainability by centralizing the GitHub Actions bot ID definition.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 10:36:22 -07:00
github-actions[bot]
532c5e257d feat: add bot_id input to handle GitHub App authentication errors
Adds a new optional bot_id input parameter that defaults to the github-actions[bot] ID (41898282). This resolves the "403 Resource not accessible by integration" error that occurs when using GitHub App installation tokens, which cannot access the /user endpoint.

Changes:
- Add bot_id input to action.yml with default value
- Update context parsing to include bot_id from environment
- Modify agent mode to use bot_id when available, avoiding API calls that fail with GitHub App tokens
- Add clear error handling for GitHub App token limitations
- Update documentation in usage.md and faq.md
- Fix test mocks to include bot_id field

This allows users to specify a custom bot user ID or use the default github-actions[bot] ID automatically, preventing 403 errors in automation workflows.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-04 09:12:52 -07:00
68 changed files with 348 additions and 4353 deletions

View File

@@ -1,61 +0,0 @@
---
name: code-quality-reviewer
description: Use this agent when you need to review code for quality, maintainability, and adherence to best practices. Examples:\n\n- After implementing a new feature or function:\n user: 'I've just written a function to process user authentication'\n assistant: 'Let me use the code-quality-reviewer agent to analyze the authentication function for code quality and best practices'\n\n- When refactoring existing code:\n user: 'I've refactored the payment processing module'\n assistant: 'I'll launch the code-quality-reviewer agent to ensure the refactored code maintains high quality standards'\n\n- Before committing significant changes:\n user: 'I've completed the API endpoint implementations'\n assistant: 'Let me use the code-quality-reviewer agent to review the endpoints for proper error handling and maintainability'\n\n- When uncertain about code quality:\n user: 'Can you check if this validation logic is robust enough?'\n assistant: 'I'll use the code-quality-reviewer agent to thoroughly analyze the validation logic'
tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash
model: inherit
---
You are an expert code quality reviewer with deep expertise in software engineering best practices, clean code principles, and maintainable architecture. Your role is to provide thorough, constructive code reviews focused on quality, readability, and long-term maintainability.
When reviewing code, you will:
**Clean Code Analysis:**
- Evaluate naming conventions for clarity and descriptiveness
- Assess function and method sizes for single responsibility adherence
- Check for code duplication and suggest DRY improvements
- Identify overly complex logic that could be simplified
- Verify proper separation of concerns
**Error Handling & Edge Cases:**
- Identify missing error handling for potential failure points
- Evaluate the robustness of input validation
- Check for proper handling of null/undefined values
- Assess edge case coverage (empty arrays, boundary conditions, etc.)
- Verify appropriate use of try-catch blocks and error propagation
**Readability & Maintainability:**
- Evaluate code structure and organization
- Check for appropriate use of comments (avoiding over-commenting obvious code)
- Assess the clarity of control flow
- Identify magic numbers or strings that should be constants
- Verify consistent code style and formatting
**TypeScript-Specific Considerations** (when applicable):
- Prefer `type` over `interface` as per project standards
- Avoid unnecessary use of underscores for unused variables
- Ensure proper type safety and avoid `any` types when possible
**Best Practices:**
- Evaluate adherence to SOLID principles
- Check for proper use of design patterns where appropriate
- Assess performance implications of implementation choices
- Verify security considerations (input sanitization, sensitive data handling)
**Review Structure:**
Provide your analysis in this format:
- Start with a brief summary of overall code quality
- Organize findings by severity (critical, important, minor)
- Provide specific examples with line references when possible
- Suggest concrete improvements with code examples
- Highlight positive aspects and good practices observed
- End with actionable recommendations prioritized by impact
Be constructive and educational in your feedback. When identifying issues, explain why they matter and how they impact code quality. Focus on teaching principles that will improve future code, not just fixing current issues.
If the code is well-written, acknowledge this and provide suggestions for potential enhancements rather than forcing criticism. Always maintain a professional, helpful tone that encourages continuous improvement.

View File

@@ -1,56 +0,0 @@
---
name: documentation-accuracy-reviewer
description: Use this agent when you need to verify that code documentation is accurate, complete, and up-to-date. Specifically use this agent after: implementing new features that require documentation updates, modifying existing APIs or functions, completing a logical chunk of code that needs documentation review, or when preparing code for review/release. Examples: 1) User: 'I just added a new authentication module with several public methods' → Assistant: 'Let me use the documentation-accuracy-reviewer agent to verify the documentation is complete and accurate for your new authentication module.' 2) User: 'Please review the documentation for the payment processing functions I just wrote' → Assistant: 'I'll launch the documentation-accuracy-reviewer agent to check your payment processing documentation.' 3) After user completes a feature implementation → Assistant: 'Now that the feature is complete, I'll use the documentation-accuracy-reviewer agent to ensure all documentation is accurate and up-to-date.'
tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash
model: inherit
---
You are an expert technical documentation reviewer with deep expertise in code documentation standards, API documentation best practices, and technical writing. Your primary responsibility is to ensure that code documentation accurately reflects implementation details and provides clear, useful information to developers.
When reviewing documentation, you will:
**Code Documentation Analysis:**
- Verify that all public functions, methods, and classes have appropriate documentation comments
- Check that parameter descriptions match actual parameter types and purposes
- Ensure return value documentation accurately describes what the code returns
- Validate that examples in documentation actually work with the current implementation
- Confirm that edge cases and error conditions are properly documented
- Check for outdated comments that reference removed or modified functionality
**README Verification:**
- Cross-reference README content with actual implemented features
- Verify installation instructions are current and complete
- Check that usage examples reflect the current API
- Ensure feature lists accurately represent available functionality
- Validate that configuration options documented in README match actual code
- Identify any new features missing from README documentation
**API Documentation Review:**
- Verify endpoint descriptions match actual implementation
- Check request/response examples for accuracy
- Ensure authentication requirements are correctly documented
- Validate parameter types, constraints, and default values
- Confirm error response documentation matches actual error handling
- Check that deprecated endpoints are properly marked
**Quality Standards:**
- Flag documentation that is vague, ambiguous, or misleading
- Identify missing documentation for public interfaces
- Note inconsistencies between documentation and implementation
- Suggest improvements for clarity and completeness
- Ensure documentation follows project-specific standards from CLAUDE.md
**Review Structure:**
Provide your analysis in this format:
- Start with a summary of overall documentation quality
- List specific issues found, categorized by type (code comments, README, API docs)
- For each issue, provide: file/location, current state, recommended fix
- Prioritize issues by severity (critical inaccuracies vs. minor improvements)
- End with actionable recommendations
You will be thorough but focused, identifying genuine documentation issues rather than stylistic preferences. When documentation is accurate and complete, acknowledge this clearly. If you need to examine specific files or code sections to verify documentation accuracy, request access to those resources. Always consider the target audience (developers using the code) and ensure documentation serves their needs effectively.

View File

@@ -1,53 +0,0 @@
---
name: performance-reviewer
description: Use this agent when you need to analyze code for performance issues, bottlenecks, and resource efficiency. Examples: After implementing database queries or API calls, when optimizing existing features, after writing data processing logic, when investigating slow application behavior, or when completing any code that involves loops, network requests, or memory-intensive operations.
tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash
model: inherit
---
You are an elite performance optimization specialist with deep expertise in identifying and resolving performance bottlenecks across all layers of software systems. Your mission is to conduct thorough performance reviews that uncover inefficiencies and provide actionable optimization recommendations.
When reviewing code, you will:
**Performance Bottleneck Analysis:**
- Examine algorithmic complexity and identify O(n²) or worse operations that could be optimized
- Detect unnecessary computations, redundant operations, or repeated work
- Identify blocking operations that could benefit from asynchronous execution
- Review loop structures for inefficient iterations or nested loops that could be flattened
- Check for premature optimization vs. legitimate performance concerns
**Network Query Efficiency:**
- Analyze database queries for N+1 problems and missing indexes
- Review API calls for batching opportunities and unnecessary round trips
- Check for proper use of pagination, filtering, and projection in data fetching
- Identify opportunities for caching, memoization, or request deduplication
- Examine connection pooling and resource reuse patterns
- Verify proper error handling that doesn't cause retry storms
**Memory and Resource Management:**
- Detect potential memory leaks from unclosed connections, event listeners, or circular references
- Review object lifecycle management and garbage collection implications
- Identify excessive memory allocation or large object creation in loops
- Check for proper cleanup in cleanup functions, destructors, or finally blocks
- Analyze data structure choices for memory efficiency
- Review file handles, database connections, and other resource cleanup
**Review Structure:**
Provide your analysis in this format:
1. **Critical Issues**: Immediate performance problems requiring attention
2. **Optimization Opportunities**: Improvements that would yield measurable benefits
3. **Best Practice Recommendations**: Preventive measures for future performance
4. **Code Examples**: Specific before/after snippets demonstrating improvements
For each issue identified:
- Specify the exact location (file, function, line numbers)
- Explain the performance impact with estimated complexity or resource usage
- Provide concrete, implementable solutions
- Prioritize recommendations by impact vs. effort
If code appears performant, confirm this explicitly and note any particularly well-optimized sections. Always consider the specific runtime environment and scale requirements when making recommendations.

View File

@@ -1,59 +0,0 @@
---
name: security-code-reviewer
description: Use this agent when you need to review code for security vulnerabilities, input validation issues, or authentication/authorization flaws. Examples: After implementing authentication logic, when adding user input handling, after writing API endpoints that process external data, or when integrating third-party libraries. The agent should be called proactively after completing security-sensitive code sections like login systems, data validation layers, or permission checks.
tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash
model: inherit
---
You are an elite security code reviewer with deep expertise in application security, threat modeling, and secure coding practices. Your mission is to identify and prevent security vulnerabilities before they reach production.
When reviewing code, you will:
**Security Vulnerability Assessment**
- Systematically scan for OWASP Top 10 vulnerabilities (injection flaws, broken authentication, sensitive data exposure, XXE, broken access control, security misconfiguration, XSS, insecure deserialization, using components with known vulnerabilities, insufficient logging)
- Identify potential SQL injection, NoSQL injection, and command injection vulnerabilities
- Check for cross-site scripting (XSS) vulnerabilities in any user-facing output
- Look for cross-site request forgery (CSRF) protection gaps
- Examine cryptographic implementations for weak algorithms or improper key management
- Identify potential race conditions and time-of-check-time-of-use (TOCTOU) vulnerabilities
**Input Validation and Sanitization**
- Verify all user inputs are properly validated against expected formats and ranges
- Ensure input sanitization occurs at appropriate boundaries (client-side validation is supplementary, never primary)
- Check for proper encoding when outputting user data
- Validate that file uploads have proper type checking, size limits, and content validation
- Ensure API parameters are validated for type, format, and business logic constraints
- Look for potential path traversal vulnerabilities in file operations
**Authentication and Authorization Review**
- Verify authentication mechanisms use secure, industry-standard approaches
- Check for proper session management (secure cookies, appropriate timeouts, session invalidation)
- Ensure passwords are properly hashed using modern algorithms (bcrypt, Argon2, PBKDF2)
- Validate that authorization checks occur at every protected resource access
- Look for privilege escalation opportunities
- Check for insecure direct object references (IDOR)
- Verify proper implementation of role-based or attribute-based access control
**Analysis Methodology**
1. First, identify the security context and attack surface of the code
2. Map data flows from untrusted sources to sensitive operations
3. Examine each security-critical operation for proper controls
4. Consider both common vulnerabilities and context-specific threats
5. Evaluate defense-in-depth measures
**Review Structure:**
Provide findings in order of severity (Critical, High, Medium, Low, Informational):
- **Vulnerability Description**: Clear explanation of the security issue
- **Location**: Specific file, function, and line numbers
- **Impact**: Potential consequences if exploited
- **Remediation**: Concrete steps to fix the vulnerability with code examples when helpful
- **References**: Relevant CWE numbers or security standards
If no security issues are found, provide a brief summary confirming the review was completed and highlighting any positive security practices observed.
Always consider the principle of least privilege, defense in depth, and fail securely. When uncertain about a potential vulnerability, err on the side of caution and flag it for further investigation.

View File

@@ -1,52 +0,0 @@
---
name: test-coverage-reviewer
description: Use this agent when you need to review testing implementation and coverage. Examples: After writing a new feature implementation, use this agent to verify test coverage. When refactoring code, use this agent to ensure tests still adequately cover all scenarios. After completing a module, use this agent to identify missing test cases and edge conditions.
tools: Glob, Grep, Read, WebFetch, TodoWrite, WebSearch, BashOutput, KillBash
model: inherit
---
You are an expert QA engineer and testing specialist with deep expertise in test-driven development, code coverage analysis, and quality assurance best practices. Your role is to conduct thorough reviews of test implementations to ensure comprehensive coverage and robust quality validation.
When reviewing code for testing, you will:
**Analyze Test Coverage:**
- Examine the ratio of test code to production code
- Identify untested code paths, branches, and edge cases
- Verify that all public APIs and critical functions have corresponding tests
- Check for coverage of error handling and exception scenarios
- Assess coverage of boundary conditions and input validation
**Evaluate Test Quality:**
- Review test structure and organization (arrange-act-assert pattern)
- Verify tests are isolated, independent, and deterministic
- Check for proper use of mocks, stubs, and test doubles
- Ensure tests have clear, descriptive names that document behavior
- Validate that assertions are specific and meaningful
- Identify brittle tests that may break with minor refactoring
**Identify Missing Test Scenarios:**
- List untested edge cases and boundary conditions
- Highlight missing integration test scenarios
- Point out uncovered error paths and failure modes
- Suggest performance and load testing opportunities
- Recommend security-related test cases where applicable
**Provide Actionable Feedback:**
- Prioritize findings by risk and impact
- Suggest specific test cases to add with example implementations
- Recommend refactoring opportunities to improve testability
- Identify anti-patterns and suggest corrections
**Review Structure:**
Provide your analysis in this format:
- **Coverage Analysis**: Summary of current test coverage with specific gaps
- **Quality Assessment**: Evaluation of existing test quality with examples
- **Missing Scenarios**: Prioritized list of untested cases
- **Recommendations**: Concrete actions to improve test suite
Be thorough but practical - focus on tests that provide real value and catch actual bugs. Consider the testing pyramid and ensure appropriate balance between unit, integration, and end-to-end tests.

View File

@@ -1,60 +0,0 @@
---
allowed-tools: Bash(gh label list:*),Bash(gh issue view:*),Bash(gh issue edit:*),Bash(gh search:*)
description: Apply labels to GitHub issues
---
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 gh commands to get context about the issue:
- Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
- Use `gh search issues` to find similar issues that might provide context for proper categorization
- You have access to these Bash commands:
- Bash(gh label list:\*) - to get available labels
- Bash(gh issue view:\*) - to view issue details
- Bash(gh issue edit:\*) - to apply labels to the issue
- Bash(gh search:\*) - to search for similar issues
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
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
- Consider platform labels (android, ios) if applicable
- If you find similar issues using gh search, 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 `gh issue edit` 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 gh issue edit
- It's okay to not add any labels if none are clearly applicable
---

View File

@@ -1,20 +0,0 @@
---
allowed-tools: Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)
description: Review a pull request
---
Perform a comprehensive code review using subagents for key areas:
- code-quality-reviewer
- performance-reviewer
- test-coverage-reviewer
- documentation-accuracy-reviewer
- security-code-reviewer
Instruct each to only provide noteworthy feedback. Once they finish, review the feedback and post only the feedback that you also deem noteworthy.
Provide feedback using inline comments for specific issues.
Use top-level comments for general observations or praise.
Keep feedback concise.
---

View File

@@ -1,15 +0,0 @@
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "bun run format"
}
],
"matcher": "Edit|Write|MultiEdit"
}
]
}
}

View File

@@ -9,7 +9,7 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:
@@ -24,7 +24,7 @@ jobs:
prettier: prettier:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v1 - uses: oven-sh/setup-bun@v1
with: with:
@@ -39,7 +39,7 @@ jobs:
typecheck: typecheck:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2 - uses: oven-sh/setup-bun@v2
with: with:

View File

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

@@ -25,7 +25,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@@ -14,14 +14,95 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup GitHub MCP Server
run: |
mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
{
"mcpServers": {
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-efef8ae"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
}
}
}
EOF
- name: Create triage prompt
run: |
mkdir -p /tmp/claude-prompts
cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF'
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
Issue Information:
- REPO: ${{ github.repository }}
- ISSUE_NUMBER: ${{ github.event.issue.number }}
TASK OVERVIEW:
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
2. Next, use the GitHub tools to get context about the issue:
- You have access to these tools:
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
- Start by using mcp__github__get_issue to get the issue details
3. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
4. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
5. Apply the selected labels:
- Use mcp__github__update_issue to apply your selected labels
- DO NOT post any comments explaining your decision
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
IMPORTANT GUIDELINES:
- Be thorough in your analysis
- Only select labels from the provided list above
- DO NOT post any comments to the issue
- Your ONLY action should be to apply labels using mcp__github__update_issue
- It's okay to not add any labels if none are clearly applicable
EOF
- name: Run Claude Code for Issue Triage - name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-action@main uses: anthropics/claude-code-base-action@v1
with: with:
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER${{ github.event.issue.number }}" prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues claude_args: |
github_token: ${{ secrets.GITHUB_TOKEN }} --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

@@ -19,7 +19,7 @@ jobs:
next_version: ${{ steps.next_version.outputs.next_version }} next_version: ${{ steps.next_version.outputs.next_version }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -91,7 +91,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -116,7 +116,7 @@ jobs:
environment: production environment: production
steps: steps:
- name: Checkout base-action repo - name: Checkout base-action repo
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
repository: anthropics/claude-code-base-action repository: anthropics/claude-code-base-action
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }} token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}

View File

@@ -67,7 +67,7 @@ jobs:
uses: ./base-action uses: ./base-action
with: with:
prompt: | prompt: |
Run the command `echo $HOME` to check the home directory path Use Bash to echo "This should not work"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
settings: | settings: |
{ {
@@ -163,7 +163,7 @@ jobs:
uses: ./base-action uses: ./base-action
with: with:
prompt: | prompt: |
Run the command `echo $HOME` to check the home directory path Use Bash to echo "This should not work from file"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
settings: "test-settings.json" settings: "test-settings.json"

View File

@@ -1,349 +0,0 @@
name: Test Structured Outputs
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
test-basic-types:
name: Test Basic Type Conversions
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test with explicit values
id: test
uses: ./base-action
with:
prompt: |
Run this command: echo "test"
Then return EXACTLY these values:
- text_field: "hello"
- number_field: 42
- boolean_true: true
- boolean_false: false
json_schema: |
{
"type": "object",
"properties": {
"text_field": {"type": "string"},
"number_field": {"type": "number"},
"boolean_true": {"type": "boolean"},
"boolean_false": {"type": "boolean"}
},
"required": ["text_field", "number_field", "boolean_true", "boolean_false"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash"
- name: Verify outputs
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Test string pass-through
TEXT_FIELD=$(echo "$OUTPUT" | jq -r '.text_field')
if [ "$TEXT_FIELD" != "hello" ]; then
echo "❌ String: expected 'hello', got '$TEXT_FIELD'"
exit 1
fi
# Test number → string conversion
NUMBER_FIELD=$(echo "$OUTPUT" | jq -r '.number_field')
if [ "$NUMBER_FIELD" != "42" ]; then
echo "❌ Number: expected '42', got '$NUMBER_FIELD'"
exit 1
fi
# Test boolean → "true" conversion
BOOLEAN_TRUE=$(echo "$OUTPUT" | jq -r '.boolean_true')
if [ "$BOOLEAN_TRUE" != "true" ]; then
echo "❌ Boolean true: expected 'true', got '$BOOLEAN_TRUE'"
exit 1
fi
# Test boolean → "false" conversion
BOOLEAN_FALSE=$(echo "$OUTPUT" | jq -r '.boolean_false')
if [ "$BOOLEAN_FALSE" != "false" ]; then
echo "❌ Boolean false: expected 'false', got '$BOOLEAN_FALSE'"
exit 1
fi
echo "✅ All basic type conversions correct"
test-complex-types:
name: Test Arrays and Objects
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test complex types
id: test
uses: ./base-action
with:
prompt: |
Run: echo "ready"
Return EXACTLY:
- items: ["apple", "banana", "cherry"]
- config: {"key": "value", "count": 3}
- empty_array: []
json_schema: |
{
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"type": "string"}
},
"config": {"type": "object"},
"empty_array": {"type": "array"}
},
"required": ["items", "config", "empty_array"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash"
- name: Verify JSON stringification
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Arrays should be JSON stringified
if ! echo "$OUTPUT" | jq -e '.items | length == 3' > /dev/null; then
echo "❌ Array not properly formatted"
echo "$OUTPUT" | jq '.items'
exit 1
fi
# Objects should be JSON stringified
if ! echo "$OUTPUT" | jq -e '.config.key == "value"' > /dev/null; then
echo "❌ Object not properly formatted"
echo "$OUTPUT" | jq '.config'
exit 1
fi
# Empty arrays should work
if ! echo "$OUTPUT" | jq -e '.empty_array | length == 0' > /dev/null; then
echo "❌ Empty array not properly formatted"
echo "$OUTPUT" | jq '.empty_array'
exit 1
fi
echo "✅ All complex types handled correctly"
test-edge-cases:
name: Test Edge Cases
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test edge cases
id: test
uses: ./base-action
with:
prompt: |
Run: echo "test"
Return EXACTLY:
- zero: 0
- empty_string: ""
- negative: -5
- decimal: 3.14
json_schema: |
{
"type": "object",
"properties": {
"zero": {"type": "number"},
"empty_string": {"type": "string"},
"negative": {"type": "number"},
"decimal": {"type": "number"}
},
"required": ["zero", "empty_string", "negative", "decimal"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash"
- name: Verify edge cases
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Zero should be "0", not empty or falsy
ZERO=$(echo "$OUTPUT" | jq -r '.zero')
if [ "$ZERO" != "0" ]; then
echo "❌ Zero: expected '0', got '$ZERO'"
exit 1
fi
# Empty string should be empty (not "null" or missing)
EMPTY_STRING=$(echo "$OUTPUT" | jq -r '.empty_string')
if [ "$EMPTY_STRING" != "" ]; then
echo "❌ Empty string: expected '', got '$EMPTY_STRING'"
exit 1
fi
# Negative numbers should work
NEGATIVE=$(echo "$OUTPUT" | jq -r '.negative')
if [ "$NEGATIVE" != "-5" ]; then
echo "❌ Negative: expected '-5', got '$NEGATIVE'"
exit 1
fi
# Decimals should preserve precision
DECIMAL=$(echo "$OUTPUT" | jq -r '.decimal')
if [ "$DECIMAL" != "3.14" ]; then
echo "❌ Decimal: expected '3.14', got '$DECIMAL'"
exit 1
fi
echo "✅ All edge cases handled correctly"
test-name-sanitization:
name: Test Output Name Sanitization
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Test special characters in field names
id: test
uses: ./base-action
with:
prompt: |
Run: echo "test"
Return EXACTLY: {test-result: "passed", item_count: 10}
json_schema: |
{
"type": "object",
"properties": {
"test-result": {"type": "string"},
"item_count": {"type": "number"}
},
"required": ["test-result", "item_count"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash"
- name: Verify sanitized names work
run: |
# Parse the structured_output JSON
OUTPUT='${{ steps.test.outputs.structured_output }}'
# Hyphens should be preserved in the JSON
TEST_RESULT=$(echo "$OUTPUT" | jq -r '.["test-result"]')
if [ "$TEST_RESULT" != "passed" ]; then
echo "❌ Hyphenated name failed: expected 'passed', got '$TEST_RESULT'"
exit 1
fi
# Underscores should work
ITEM_COUNT=$(echo "$OUTPUT" | jq -r '.item_count')
if [ "$ITEM_COUNT" != "10" ]; then
echo "❌ Underscore name failed: expected '10', got '$ITEM_COUNT'"
exit 1
fi
echo "✅ Name sanitization works"
test-execution-file-structure:
name: Test Execution File Format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
- name: Run with structured output
id: test
uses: ./base-action
with:
prompt: "Run: echo 'complete'. Return: {done: true}"
json_schema: |
{
"type": "object",
"properties": {
"done": {"type": "boolean"}
},
"required": ["done"]
}
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash"
- name: Verify execution file contains structured_output
run: |
FILE="${{ steps.test.outputs.execution_file }}"
# Check file exists
if [ ! -f "$FILE" ]; then
echo "❌ Execution file missing"
exit 1
fi
# Check for structured_output field
if ! jq -e '.[] | select(.type == "result") | .structured_output' "$FILE" > /dev/null; then
echo "❌ No structured_output in execution file"
cat "$FILE"
exit 1
fi
# Verify the actual value
DONE=$(jq -r '.[] | select(.type == "result") | .structured_output.done' "$FILE")
if [ "$DONE" != "true" ]; then
echo "❌ Wrong value in execution file"
exit 1
fi
echo "✅ Execution file format correct"
test-summary:
name: Summary
runs-on: ubuntu-latest
needs:
- test-basic-types
- test-complex-types
- test-edge-cases
- test-name-sanitization
- test-execution-file-structure
if: always()
steps:
- name: Generate Summary
run: |
echo "# Structured Output Tests (Optimized)" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Fast, deterministic tests using explicit prompts" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Test | Result |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Basic Types | ${{ needs.test-basic-types.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Complex Types | ${{ needs.test-complex-types.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Edge Cases | ${{ needs.test-edge-cases.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Name Sanitization | ${{ needs.test-name-sanitization.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
echo "| Execution File | ${{ needs.test-execution-file-structure.result == 'success' && '✅ PASS' || '❌ FAIL' }} |" >> $GITHUB_STEP_SUMMARY
# Check if all passed
ALL_PASSED=${{
needs.test-basic-types.result == 'success' &&
needs.test-complex-types.result == 'success' &&
needs.test-edge-cases.result == 'success' &&
needs.test-name-sanitization.result == 'success' &&
needs.test-execution-file-structure.result == 'success'
}}
if [ "$ALL_PASSED" = "true" ]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "## ✅ All Tests Passed" >> $GITHUB_STEP_SUMMARY
else
echo "" >> $GITHUB_STEP_SUMMARY
echo "## ❌ Some Tests Failed" >> $GITHUB_STEP_SUMMARY
exit 1
fi

View File

@@ -13,7 +13,6 @@ A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs an
- 💬 **PR/Issue Integration**: Works seamlessly with GitHub comments and PR reviews - 💬 **PR/Issue Integration**: Works seamlessly with GitHub comments and PR reviews
- 🛠️ **Flexible Tool Access**: Access to GitHub APIs and file operations (additional tools can be enabled via configuration) - 🛠️ **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 - 📋 **Progress Tracking**: Visual progress indicators with checkboxes that dynamically update as Claude completes tasks
- 📊 **Structured Outputs**: Get validated JSON results that automatically become GitHub Action outputs for complex automations
- 🏃 **Runs on Your Infrastructure**: The action executes entirely on your own GitHub runner (Anthropic API calls go to your chosen provider) - 🏃 **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 - ⚙️ **Simplified Configuration**: Unified `prompt` and `claude_args` inputs provide clean, powerful configuration aligned with Claude Code SDK

View File

@@ -27,10 +27,6 @@ inputs:
description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots." description: "Comma-separated list of allowed bot usernames, or '*' to allow all bots. Empty string (default) allows no bots."
required: false required: false
default: "" default: ""
allowed_non_write_users:
description: "Comma-separated list of usernames to allow without write permissions, or '*' to allow all users. Only works when github_token input is provided. WARNING: Use with extreme caution - this bypasses security checks and should only be used for workflows with very limited permissions (e.g., issue labeling)."
required: false
default: ""
# Claude Code configuration # Claude Code configuration
prompt: prompt:
@@ -101,22 +97,6 @@ inputs:
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." 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 required: false
default: "" default: ""
show_full_output:
description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments."
required: false
default: "false"
plugins:
description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')"
required: false
default: ""
plugin_marketplaces:
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
required: false
default: ""
json_schema:
description: "JSON schema for structured output validation. When provided, Claude will return validated JSON matching this schema. All fields are available in the structured_output output as a JSON string (use fromJSON() or jq to access fields)."
required: false
default: ""
outputs: outputs:
execution_file: execution_file:
@@ -168,7 +148,6 @@ runs:
BRANCH_PREFIX: ${{ inputs.branch_prefix }} BRANCH_PREFIX: ${{ inputs.branch_prefix }}
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }} OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
ALLOWED_BOTS: ${{ inputs.allowed_bots }} ALLOWED_BOTS: ${{ inputs.allowed_bots }}
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
GITHUB_RUN_ID: ${{ github.run_id }} GITHUB_RUN_ID: ${{ github.run_id }}
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }} USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }} DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
@@ -178,7 +157,6 @@ runs:
TRACK_PROGRESS: ${{ inputs.track_progress }} TRACK_PROGRESS: ${{ inputs.track_progress }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }} ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
CLAUDE_ARGS: ${{ inputs.claude_args }} CLAUDE_ARGS: ${{ inputs.claude_args }}
JSON_SCHEMA: ${{ inputs.json_schema }}
ALL_INPUTS: ${{ toJson(inputs) }} ALL_INPUTS: ${{ toJson(inputs) }}
- name: Install Base Action Dependencies - name: Install Base Action Dependencies
@@ -194,7 +172,7 @@ runs:
# Install Claude Code if no custom executable is provided # Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..." echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.42 curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
@@ -230,10 +208,6 @@ runs:
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }} 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_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
JSON_SCHEMA: ${{ inputs.json_schema }}
# Model configuration # Model configuration
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
@@ -244,7 +218,6 @@ runs:
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }} CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }} ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }}
CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }} CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }}
CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }} CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }}
@@ -280,7 +253,7 @@ runs:
GITHUB_EVENT_NAME: ${{ github.event_name }} GITHUB_EVENT_NAME: ${{ github.event_name }}
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }} TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }} CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review_comment' }} IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }} BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }} CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }} OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}

View File

@@ -50,7 +50,7 @@ This is a GitHub Action that allows running Claude Code within GitHub workflows.
- Unit tests for configuration logic - Unit tests for configuration logic
- Integration tests for prompt preparation - Integration tests for prompt preparation
- Full workflow tests in `.github/workflows/test-base-action.yml` - Full workflow tests in `.github/workflows/test-action.yml`
## Important Technical Details ## Important Technical Details

View File

@@ -85,32 +85,29 @@ Add the following to your workflow file:
## Inputs ## Inputs
| Input | Description | Required | Default | | Input | Description | Required | Default |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- | | ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
| `prompt` | The prompt to send to Claude Code | No\* | '' | | `prompt` | The prompt to send to Claude Code | No\* | '' |
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' | | `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' | | `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' | | `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' |
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' | | `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' |
| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' | | `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' |
| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' | | `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' |
| `system_prompt` | Override system prompt | No | '' | | `system_prompt` | Override system prompt | No | '' |
| `append_system_prompt` | Append to system prompt | No | '' | | `append_system_prompt` | Append to system prompt | No | '' |
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' | | `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' | | `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' | | `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 | '' | | `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' | | `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 | '' | | `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' | | `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' | | `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' |
| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' | | `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' |
| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#-full-output-security-warning)) | No | 'false'\*\* |
\*Either `prompt` or `prompt_file` must be provided, but not both. \*Either `prompt` or `prompt_file` must be provided, but not both.
\*\*`show_full_output` is automatically enabled when GitHub Actions debug mode is active. See [security documentation](../docs/security.md#-full-output-security-warning) for important security considerations.
## Outputs ## Outputs
| Output | Description | | Output | Description |
@@ -339,7 +336,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0

View File

@@ -24,10 +24,6 @@ inputs:
description: "Additional arguments to pass directly to Claude CLI (e.g., '--max-turns 3 --mcp-config /path/to/config.json')" description: "Additional arguments to pass directly to Claude CLI (e.g., '--max-turns 3 --mcp-config /path/to/config.json')"
required: false required: false
default: "" default: ""
allowed_tools:
description: "Comma-separated list of allowed tools (e.g., 'Read,Write,Bash'). Passed as --allowedTools to Claude CLI"
required: false
default: ""
# Authentication settings # Authentication settings
anthropic_api_key: anthropic_api_key:
@@ -59,26 +55,6 @@ inputs:
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." 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 required: false
default: "" default: ""
show_full_output:
description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments."
required: false
default: "false"
plugins:
description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')"
required: false
default: ""
plugin_marketplaces:
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
required: false
default: ""
json_schema:
description: |
JSON schema for structured output validation. Claude must return JSON matching this schema
or the action will fail. All fields are returned in a single structured_output JSON string.
Access outputs via: fromJSON(steps.<step-id>.outputs.structured_output).<field_name>
required: false
default: ""
outputs: outputs:
conclusion: conclusion:
@@ -87,9 +63,6 @@ outputs:
execution_file: execution_file:
description: "Path to the JSON file containing Claude Code execution log" description: "Path to the JSON file containing Claude Code execution log"
value: ${{ steps.run_claude.outputs.execution_file }} value: ${{ steps.run_claude.outputs.execution_file }}
structured_output:
description: "JSON string containing all structured output fields (use fromJSON() or jq to parse)"
value: ${{ steps.run_claude.outputs.structured_output }}
runs: runs:
using: "composite" using: "composite"
@@ -126,7 +99,7 @@ runs:
run: | run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..." echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.45 curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH # Add the directory containing the custom executable to PATH
@@ -153,17 +126,11 @@ runs:
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }} INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }} INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }} INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
INPUT_ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
JSON_SCHEMA: ${{ inputs.json_schema }}
# Provider configuration # Provider configuration
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }} CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }} ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
ANTHROPIC_CUSTOM_HEADERS: ${{ env.ANTHROPIC_CUSTOM_HEADERS }}
# Only set provider flags if explicitly true, since any value (including "false") is truthy # Only set provider flags if explicitly true, since any value (including "false") is truthy
CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }} CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }}
CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }} CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }}

View File

@@ -32,7 +32,7 @@ jobs:
"--rm", "--rm",
"-e", "-e",
"GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-23fa0dd" "ghcr.io/github/github-mcp-server:sha-7aced2b"
], ],
"env": { "env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"

View File

@@ -1,196 +0,0 @@
{
"name": "@anthropic-ai/claude-code-base-action",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@anthropic-ai/claude-code-base-action",
"version": "1.0.0",
"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"
}
},
"node_modules/@actions/core": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.11.1.tgz",
"integrity": "sha512-hXJCSrkwfA46Vd9Z3q4cpEpHB1rL5NG04+/rbqW9d3+CSvtB1tYe8UTpAlixa1vj0m/ULglfEK2UKxMGxCxv5A==",
"license": "MIT",
"dependencies": {
"@actions/exec": "^1.1.1",
"@actions/http-client": "^2.0.1"
}
},
"node_modules/@actions/exec": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.1.1.tgz",
"integrity": "sha512-+sCcHHbVdk93a0XT19ECtO/gIXoxvdsgQLzb2fE2/5sIZmWQuluYyjPQtrtTHdU1YzTZ7bAPN4sITq2xi1679w==",
"license": "MIT",
"dependencies": {
"@actions/io": "^1.0.1"
}
},
"node_modules/@actions/http-client": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.2.3.tgz",
"integrity": "sha512-mx8hyJi/hjFvbPokCg4uRd4ZX78t+YyRPtnKWwIl+RzNaVuFpQHfmlGVfsKEJN8LwTCvL+DfVgAM04XaHkm6bA==",
"license": "MIT",
"dependencies": {
"tunnel": "^0.0.6",
"undici": "^5.25.4"
}
},
"node_modules/@actions/io": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.1.3.tgz",
"integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==",
"license": "MIT"
},
"node_modules/@fastify/busboy": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
"integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
"license": "MIT",
"engines": {
"node": ">=14"
}
},
"node_modules/@types/bun": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.3.1.tgz",
"integrity": "sha512-4jNMk2/K9YJtfqwoAa28c8wK+T7nvJFOjxI4h/7sORWcypRNxBpr+TPNaCfVWq70tLCJsqoFwcf0oI0JU/fvMQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"bun-types": "1.3.1"
}
},
"node_modules/@types/node": {
"version": "20.19.23",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz",
"integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"node_modules/@types/react": {
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
},
"node_modules/@types/shell-quote": {
"version": "1.7.5",
"resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.5.tgz",
"integrity": "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==",
"dev": true,
"license": "MIT"
},
"node_modules/bun-types": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.3.1.tgz",
"integrity": "sha512-NMrcy7smratanWJ2mMXdpatalovtxVggkj11bScuWuiOoXTiKIu2eVS1/7qbyI/4yHedtsn175n4Sm4JcdHLXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
},
"peerDependencies": {
"@types/react": "^19"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT",
"peer": true
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/tunnel": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==",
"license": "MIT",
"engines": {
"node": ">=0.6.11 <=0.7.0 || >=0.7.3"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},
"engines": {
"node": ">=14.0"
}
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
}
}
}

View File

@@ -5,7 +5,6 @@ import { preparePrompt } from "./prepare-prompt";
import { runClaude } from "./run-claude"; import { runClaude } from "./run-claude";
import { setupClaudeCodeSettings } from "./setup-claude-code-settings"; import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
import { validateEnvironmentVariables } from "./validate-env"; import { validateEnvironmentVariables } from "./validate-env";
import { installPlugins } from "./install-plugins";
async function run() { async function run() {
try { try {
@@ -16,34 +15,13 @@ async function run() {
undefined, // homeDir undefined, // homeDir
); );
// Install Claude Code plugins if specified
await installPlugins(
process.env.INPUT_PLUGIN_MARKETPLACES,
process.env.INPUT_PLUGINS,
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
);
const promptConfig = await preparePrompt({ const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "", prompt: process.env.INPUT_PROMPT || "",
promptFile: process.env.INPUT_PROMPT_FILE || "", promptFile: process.env.INPUT_PROMPT_FILE || "",
}); });
// Build claudeArgs with JSON schema if provided
let claudeArgs = process.env.INPUT_CLAUDE_ARGS || "";
// Add allowed tools if specified
if (process.env.INPUT_ALLOWED_TOOLS) {
claudeArgs += ` --allowedTools "${process.env.INPUT_ALLOWED_TOOLS}"`;
}
// Add JSON schema if specified (no escaping - parseShellArgs handles it)
if (process.env.JSON_SCHEMA) {
// Wrap in single quotes for parseShellArgs
claudeArgs += ` --json-schema '${process.env.JSON_SCHEMA}'`;
}
await runClaude(promptConfig.path, { await runClaude(promptConfig.path, {
claudeArgs: claudeArgs.trim(), claudeArgs: process.env.INPUT_CLAUDE_ARGS,
allowedTools: process.env.INPUT_ALLOWED_TOOLS, allowedTools: process.env.INPUT_ALLOWED_TOOLS,
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS, disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
maxTurns: process.env.INPUT_MAX_TURNS, maxTurns: process.env.INPUT_MAX_TURNS,
@@ -55,7 +33,6 @@ async function run() {
model: process.env.ANTHROPIC_MODEL, model: process.env.ANTHROPIC_MODEL,
pathToClaudeCodeExecutable: pathToClaudeCodeExecutable:
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE, process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
}); });
} catch (error) { } catch (error) {
core.setFailed(`Action failed with error: ${error}`); core.setFailed(`Action failed with error: ${error}`);

View File

@@ -1,222 +0,0 @@
import { spawn, ChildProcess } from "child_process";
const PLUGIN_NAME_REGEX = /^[@a-zA-Z0-9_\-\/\.]+$/;
const MAX_PLUGIN_NAME_LENGTH = 512;
const PATH_TRAVERSAL_REGEX =
/\.\.\/|\/\.\.|\.\/|\/\.|(?:^|\/)\.\.$|(?:^|\/)\.$|\.\.(?![0-9])/;
const MARKETPLACE_URL_REGEX =
/^https:\/\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]+\.git$/;
/**
* Validates a marketplace URL for security issues
* @param url - The marketplace URL to validate
* @throws {Error} If the URL is invalid
*/
function validateMarketplaceUrl(url: string): void {
const normalized = url.trim();
if (!normalized) {
throw new Error("Marketplace URL cannot be empty");
}
if (!MARKETPLACE_URL_REGEX.test(normalized)) {
throw new Error(`Invalid marketplace URL format: ${url}`);
}
// Additional check for valid URL structure
try {
new URL(normalized);
} catch {
throw new Error(`Invalid marketplace URL: ${url}`);
}
}
/**
* Validates a plugin name for security issues
* @param pluginName - The plugin name to validate
* @throws {Error} If the plugin name is invalid
*/
function validatePluginName(pluginName: string): void {
// Normalize Unicode to prevent homoglyph attacks (e.g., fullwidth dots, Unicode slashes)
const normalized = pluginName.normalize("NFC");
if (normalized.length > MAX_PLUGIN_NAME_LENGTH) {
throw new Error(`Plugin name too long: ${normalized.substring(0, 50)}...`);
}
if (!PLUGIN_NAME_REGEX.test(normalized)) {
throw new Error(`Invalid plugin name format: ${pluginName}`);
}
// Prevent path traversal attacks with single efficient regex check
if (PATH_TRAVERSAL_REGEX.test(normalized)) {
throw new Error(`Invalid plugin name format: ${pluginName}`);
}
}
/**
* Parse a newline-separated list of marketplace URLs and return an array of validated URLs
* @param marketplaces - Newline-separated list of marketplace Git URLs
* @returns Array of validated marketplace URLs (empty array if none provided)
*/
function parseMarketplaces(marketplaces?: string): string[] {
const trimmed = marketplaces?.trim();
if (!trimmed) {
return [];
}
// Split by newline and process each URL
return trimmed
.split("\n")
.map((url) => url.trim())
.filter((url) => {
if (url.length === 0) return false;
validateMarketplaceUrl(url);
return true;
});
}
/**
* Parse a newline-separated list of plugin names and return an array of trimmed, non-empty plugin names
* Validates plugin names to prevent command injection and path traversal attacks
* Allows: letters, numbers, @, -, _, /, . (common npm/scoped package characters)
* Disallows: path traversal (../, ./), shell metacharacters, and consecutive dots
* @param plugins - Newline-separated list of plugin names, or undefined/empty to return empty array
* @returns Array of validated plugin names (empty array if none provided)
* @throws {Error} If any plugin name fails validation
*/
function parsePlugins(plugins?: string): string[] {
const trimmedPlugins = plugins?.trim();
if (!trimmedPlugins) {
return [];
}
// Split by newline and process each plugin
return trimmedPlugins
.split("\n")
.map((p) => p.trim())
.filter((p) => {
if (p.length === 0) return false;
validatePluginName(p);
return true;
});
}
/**
* Executes a Claude Code CLI command with proper error handling
* @param claudeExecutable - Path to the Claude executable
* @param args - Command arguments to pass to the executable
* @param errorContext - Context string for error messages (e.g., "Failed to install plugin 'foo'")
* @returns Promise that resolves when the command completes successfully
* @throws {Error} If the command fails to execute
*/
async function executeClaudeCommand(
claudeExecutable: string,
args: string[],
errorContext: string,
): Promise<void> {
return new Promise((resolve, reject) => {
const childProcess: ChildProcess = spawn(claudeExecutable, args, {
stdio: "inherit",
});
childProcess.on("close", (code: number | null) => {
if (code === 0) {
resolve();
} else if (code === null) {
reject(new Error(`${errorContext}: process terminated by signal`));
} else {
reject(new Error(`${errorContext} (exit code: ${code})`));
}
});
childProcess.on("error", (err: Error) => {
reject(new Error(`${errorContext}: ${err.message}`));
});
});
}
/**
* Installs a single Claude Code plugin
* @param pluginName - The name of the plugin to install
* @param claudeExecutable - Path to the Claude executable
* @returns Promise that resolves when the plugin is installed successfully
* @throws {Error} If the plugin installation fails
*/
async function installPlugin(
pluginName: string,
claudeExecutable: string,
): Promise<void> {
console.log(`Installing plugin: ${pluginName}`);
return executeClaudeCommand(
claudeExecutable,
["plugin", "install", pluginName],
`Failed to install plugin '${pluginName}'`,
);
}
/**
* Adds a Claude Code plugin marketplace
* @param claudeExecutable - Path to the Claude executable
* @param marketplaceUrl - The marketplace Git URL to add
* @returns Promise that resolves when the marketplace add command completes
* @throws {Error} If the command fails to execute
*/
async function addMarketplace(
claudeExecutable: string,
marketplaceUrl: string,
): Promise<void> {
console.log(`Adding marketplace: ${marketplaceUrl}`);
return executeClaudeCommand(
claudeExecutable,
["plugin", "marketplace", "add", marketplaceUrl],
`Failed to add marketplace '${marketplaceUrl}'`,
);
}
/**
* Installs Claude Code plugins from a newline-separated list
* @param marketplacesInput - Newline-separated list of marketplace Git URLs
* @param pluginsInput - Newline-separated list of plugin names
* @param claudeExecutable - Path to the Claude executable (defaults to "claude")
* @returns Promise that resolves when all plugins are installed
* @throws {Error} If any plugin fails validation or installation (stops on first error)
*/
export async function installPlugins(
marketplacesInput?: string,
pluginsInput?: string,
claudeExecutable?: string,
): Promise<void> {
// Resolve executable path with explicit fallback
const resolvedExecutable = claudeExecutable || "claude";
// Parse and add all marketplaces before installing plugins
const marketplaces = parseMarketplaces(marketplacesInput);
if (marketplaces.length > 0) {
console.log(`Adding ${marketplaces.length} marketplace(s)...`);
for (const marketplace of marketplaces) {
await addMarketplace(resolvedExecutable, marketplace);
console.log(`✓ Successfully added marketplace: ${marketplace}`);
}
} else {
console.log("No marketplaces specified, skipping marketplace setup");
}
const plugins = parsePlugins(pluginsInput);
if (plugins.length > 0) {
console.log(`Installing ${plugins.length} plugin(s)...`);
for (const plugin of plugins) {
await installPlugin(plugin, resolvedExecutable);
console.log(`✓ Successfully installed: ${plugin}`);
}
} else {
console.log("No plugins specified, skipping plugins installation");
}
}

View File

@@ -1,7 +1,7 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import { exec } from "child_process"; import { exec } from "child_process";
import { promisify } from "util"; import { promisify } from "util";
import { unlink, writeFile, stat, readFile } from "fs/promises"; import { unlink, writeFile, stat } from "fs/promises";
import { createWriteStream } from "fs"; import { createWriteStream } from "fs";
import { spawn } from "child_process"; import { spawn } from "child_process";
import { parse as parseShellArgs } from "shell-quote"; import { parse as parseShellArgs } from "shell-quote";
@@ -12,59 +12,6 @@ const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`;
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`; const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"]; const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
/**
* Sanitizes JSON output to remove sensitive information when full output is disabled
* Returns a safe summary message or null if the message should be completely suppressed
*/
function sanitizeJsonOutput(
jsonObj: any,
showFullOutput: boolean,
): string | null {
if (showFullOutput) {
// In full output mode, return the full JSON
return JSON.stringify(jsonObj, null, 2);
}
// In non-full-output mode, provide minimal safe output
const type = jsonObj.type;
const subtype = jsonObj.subtype;
// System initialization - safe to show
if (type === "system" && subtype === "init") {
return JSON.stringify(
{
type: "system",
subtype: "init",
message: "Claude Code initialized",
model: jsonObj.model || "unknown",
},
null,
2,
);
}
// Result messages - Always show the final result
if (type === "result") {
// These messages contain the final result and should always be visible
return JSON.stringify(
{
type: "result",
subtype: jsonObj.subtype,
is_error: jsonObj.is_error,
duration_ms: jsonObj.duration_ms,
num_turns: jsonObj.num_turns,
total_cost_usd: jsonObj.total_cost_usd,
permission_denials: jsonObj.permission_denials,
},
null,
2,
);
}
// For any other message types, suppress completely in non-full-output mode
return null;
}
export type ClaudeOptions = { export type ClaudeOptions = {
claudeArgs?: string; claudeArgs?: string;
model?: string; model?: string;
@@ -77,7 +24,6 @@ export type ClaudeOptions = {
appendSystemPrompt?: string; appendSystemPrompt?: string;
claudeEnv?: string; claudeEnv?: string;
fallbackModel?: string; fallbackModel?: string;
showFullOutput?: string;
}; };
type PreparedConfig = { type PreparedConfig = {
@@ -122,48 +68,6 @@ export function prepareRunConfig(
}; };
} }
/**
* Parses structured_output from execution file and sets GitHub Action outputs
* Only runs if json_schema was explicitly provided by the user
* Exported for testing
*/
export async function parseAndSetStructuredOutputs(
executionFile: string,
): Promise<void> {
try {
const content = await readFile(executionFile, "utf-8");
const messages = JSON.parse(content) as {
type: string;
structured_output?: Record<string, unknown>;
}[];
// Search backwards - result is typically last or second-to-last message
const result = messages.findLast(
(m) => m.type === "result" && m.structured_output,
);
if (!result?.structured_output) {
throw new Error(
`json_schema was provided but Claude did not return structured_output.\n` +
`Found ${messages.length} messages. Result exists: ${!!result}\n`,
);
}
// Set the complete structured output as a single JSON string
// This works around GitHub Actions limitation that composite actions can't have dynamic outputs
const structuredOutputJson = JSON.stringify(result.structured_output);
core.setOutput("structured_output", structuredOutputJson);
core.info(
`Set structured_output with ${Object.keys(result.structured_output).length} field(s)`,
);
} catch (error) {
if (error instanceof Error) {
throw error; // Preserve original error and stack trace
}
throw new Error(`Failed to parse structured outputs: ${error}`);
}
}
export async function runClaude(promptPath: string, options: ClaudeOptions) { export async function runClaude(promptPath: string, options: ClaudeOptions) {
const config = prepareRunConfig(promptPath, options); const config = prepareRunConfig(promptPath, options);
@@ -234,27 +138,12 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
pipeStream.destroy(); pipeStream.destroy();
}); });
// Determine if full output should be shown
// Show full output if explicitly set to "true" OR if GitHub Actions debug mode is enabled
const isDebugMode = process.env.ACTIONS_STEP_DEBUG === "true";
let showFullOutput = options.showFullOutput === "true" || isDebugMode;
if (isDebugMode && options.showFullOutput !== "false") {
console.log("Debug mode detected - showing full output");
showFullOutput = true;
} else if (!showFullOutput) {
console.log("Running Claude Code (full output hidden for security)...");
console.log(
"Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.",
);
}
// Capture output for parsing execution metrics // Capture output for parsing execution metrics
let output = ""; let output = "";
claudeProcess.stdout.on("data", (data) => { claudeProcess.stdout.on("data", (data) => {
const text = data.toString(); const text = data.toString();
// Try to parse as JSON and handle based on verbose setting // Try to parse as JSON and pretty print if it's on a single line
const lines = text.split("\n"); const lines = text.split("\n");
lines.forEach((line: string, index: number) => { lines.forEach((line: string, index: number) => {
if (line.trim() === "") return; if (line.trim() === "") return;
@@ -262,24 +151,17 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
try { try {
// Check if this line is a JSON object // Check if this line is a JSON object
const parsed = JSON.parse(line); const parsed = JSON.parse(line);
const sanitizedOutput = sanitizeJsonOutput(parsed, showFullOutput); const prettyJson = JSON.stringify(parsed, null, 2);
process.stdout.write(prettyJson);
if (sanitizedOutput) { if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write(sanitizedOutput); process.stdout.write("\n");
if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write("\n");
}
} }
} catch (e) { } catch (e) {
// Not a JSON object // Not a JSON object, print as is
if (showFullOutput) { process.stdout.write(line);
// In full output mode, print as is if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write(line); process.stdout.write("\n");
if (index < lines.length - 1 || text.endsWith("\n")) {
process.stdout.write("\n");
}
} }
// In non-full-output mode, suppress non-JSON output
} }
}); });
@@ -350,23 +232,8 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
core.warning(`Failed to process output for execution metrics: ${e}`); core.warning(`Failed to process output for execution metrics: ${e}`);
} }
core.setOutput("execution_file", EXECUTION_FILE);
// Parse and set structured outputs only if user provided json_schema
if (process.env.JSON_SCHEMA) {
try {
await parseAndSetStructuredOutputs(EXECUTION_FILE);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
core.setFailed(errorMessage);
core.setOutput("conclusion", "failure");
process.exit(1);
}
}
// Set conclusion to success if we reached here
core.setOutput("conclusion", "success"); core.setOutput("conclusion", "success");
core.setOutput("execution_file", EXECUTION_FILE);
} else { } else {
core.setOutput("conclusion", "failure"); core.setOutput("conclusion", "failure");

View File

@@ -9,4 +9,4 @@ fi
# Run the test workflow locally # Run the test workflow locally
# You'll need to provide your ANTHROPIC_API_KEY # You'll need to provide your ANTHROPIC_API_KEY
echo "Running action locally with act..." echo "Running action locally with act..."
act push --secret ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" -W .github/workflows/test-base-action.yml --container-architecture linux/amd64 act push --secret ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" -W .github/workflows/test-action.yml --container-architecture linux/amd64

View File

@@ -1,599 +0,0 @@
#!/usr/bin/env bun
import { describe, test, expect, mock, spyOn, afterEach } from "bun:test";
import { installPlugins } from "../src/install-plugins";
import * as childProcess from "child_process";
describe("installPlugins", () => {
let spawnSpy: ReturnType<typeof spyOn> | undefined;
afterEach(() => {
// Restore original spawn after each test
if (spawnSpy) {
spawnSpy.mockRestore();
}
});
function createMockSpawn(
exitCode: number | null = 0,
shouldError: boolean = false,
) {
const mockProcess = {
on: mock((event: string, handler: Function) => {
if (event === "close" && !shouldError) {
// Simulate successful close
setTimeout(() => handler(exitCode), 0);
} else if (event === "error" && shouldError) {
// Simulate error
setTimeout(() => handler(new Error("spawn error")), 0);
}
return mockProcess;
}),
};
spawnSpy = spyOn(childProcess, "spawn").mockImplementation(
() => mockProcess as any,
);
return spawnSpy;
}
test("should not call spawn when no plugins are specified", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "");
expect(spy).not.toHaveBeenCalled();
});
test("should not call spawn when plugins is undefined", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, undefined);
expect(spy).not.toHaveBeenCalled();
});
test("should not call spawn when plugins is only whitespace", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, " ");
expect(spy).not.toHaveBeenCalled();
});
test("should install a single plugin with default executable", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "test-plugin");
expect(spy).toHaveBeenCalledTimes(1);
// Only call: install plugin (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
test("should install multiple plugins sequentially", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "plugin1\nplugin2\nplugin3");
expect(spy).toHaveBeenCalledTimes(3);
// Install plugins (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "plugin1"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin2"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
3,
"claude",
["plugin", "install", "plugin3"],
{ stdio: "inherit" },
);
});
test("should use custom claude executable path when provided", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "test-plugin", "/custom/path/to/claude");
expect(spy).toHaveBeenCalledTimes(1);
// Only call: install plugin (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"/custom/path/to/claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
test("should trim whitespace from plugin names before installation", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, " plugin1 \n plugin2 ");
expect(spy).toHaveBeenCalledTimes(2);
// Install plugins (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "plugin1"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin2"],
{ stdio: "inherit" },
);
});
test("should skip empty entries in plugin list", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "plugin1\n\nplugin2");
expect(spy).toHaveBeenCalledTimes(2);
// Install plugins (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "plugin1"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin2"],
{ stdio: "inherit" },
);
});
test("should handle plugin installation error and throw", async () => {
createMockSpawn(1, false); // Exit code 1
await expect(installPlugins(undefined, "failing-plugin")).rejects.toThrow(
"Failed to install plugin 'failing-plugin' (exit code: 1)",
);
});
test("should handle null exit code (process terminated by signal)", async () => {
createMockSpawn(null, false); // Exit code null (terminated by signal)
await expect(
installPlugins(undefined, "terminated-plugin"),
).rejects.toThrow(
"Failed to install plugin 'terminated-plugin': process terminated by signal",
);
});
test("should stop installation on first error", async () => {
const spy = createMockSpawn(1, false); // Exit code 1
await expect(
installPlugins(undefined, "plugin1\nplugin2\nplugin3"),
).rejects.toThrow("Failed to install plugin 'plugin1' (exit code: 1)");
// Should only try to install first plugin before failing
expect(spy).toHaveBeenCalledTimes(1);
});
test("should handle plugins with special characters in names", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "org/plugin-name\n@scope/plugin");
expect(spy).toHaveBeenCalledTimes(2);
// Install plugins (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "org/plugin-name"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "@scope/plugin"],
{ stdio: "inherit" },
);
});
test("should handle spawn errors", async () => {
createMockSpawn(0, true); // Trigger error event
await expect(installPlugins(undefined, "test-plugin")).rejects.toThrow(
"Failed to install plugin 'test-plugin': spawn error",
);
});
test("should install plugins with custom executable and multiple plugins", async () => {
const spy = createMockSpawn();
await installPlugins(
undefined,
"plugin-a\nplugin-b",
"/usr/local/bin/claude-custom",
);
expect(spy).toHaveBeenCalledTimes(2);
// Install plugins (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"/usr/local/bin/claude-custom",
["plugin", "install", "plugin-a"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"/usr/local/bin/claude-custom",
["plugin", "install", "plugin-b"],
{ stdio: "inherit" },
);
});
test("should reject plugin names with command injection attempts", async () => {
const spy = createMockSpawn();
// Should throw due to invalid characters (semicolon and spaces)
await expect(
installPlugins(undefined, "plugin-name; rm -rf /"),
).rejects.toThrow("Invalid plugin name format");
// Mock should never be called because validation fails first
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with path traversal using ../", async () => {
const spy = createMockSpawn();
await expect(
installPlugins(undefined, "../../../malicious-plugin"),
).rejects.toThrow("Invalid plugin name format");
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with path traversal using ./", async () => {
const spy = createMockSpawn();
await expect(
installPlugins(undefined, "./../../@scope/package"),
).rejects.toThrow("Invalid plugin name format");
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with consecutive dots", async () => {
const spy = createMockSpawn();
await expect(installPlugins(undefined, ".../.../package")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject plugin names with hidden path traversal", async () => {
const spy = createMockSpawn();
await expect(installPlugins(undefined, "package/../other")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should accept plugin names with single dots in version numbers", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "plugin-v1.0.2");
expect(spy).toHaveBeenCalledTimes(1);
// Only call: install plugin (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "plugin-v1.0.2"],
{ stdio: "inherit" },
);
});
test("should accept plugin names with multiple dots in semantic versions", async () => {
const spy = createMockSpawn();
await installPlugins(undefined, "@scope/plugin-v1.0.0-beta.1");
expect(spy).toHaveBeenCalledTimes(1);
// Only call: install plugin (no marketplace without explicit marketplace input)
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "@scope/plugin-v1.0.0-beta.1"],
{ stdio: "inherit" },
);
});
test("should reject Unicode homoglyph path traversal attempts", async () => {
const spy = createMockSpawn();
// Using fullwidth dots (U+FF0E) and fullwidth solidus (U+FF0F)
await expect(installPlugins(undefined, "malicious")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject path traversal at end of path", async () => {
const spy = createMockSpawn();
await expect(installPlugins(undefined, "package/..")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject single dot directory reference", async () => {
const spy = createMockSpawn();
await expect(installPlugins(undefined, "package/.")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
test("should reject path traversal in middle of path", async () => {
const spy = createMockSpawn();
await expect(installPlugins(undefined, "package/../other")).rejects.toThrow(
"Invalid plugin name format",
);
expect(spy).not.toHaveBeenCalled();
});
// Marketplace functionality tests
test("should add a single marketplace before installing plugins", async () => {
const spy = createMockSpawn();
await installPlugins(
"https://github.com/user/marketplace.git",
"test-plugin",
);
expect(spy).toHaveBeenCalledTimes(2);
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/user/marketplace.git",
],
{ stdio: "inherit" },
);
// Second call: install plugin
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
test("should add multiple marketplaces with newline separation", async () => {
const spy = createMockSpawn();
await installPlugins(
"https://github.com/user/m1.git\nhttps://github.com/user/m2.git",
"test-plugin",
);
expect(spy).toHaveBeenCalledTimes(3); // 2 marketplaces + 1 plugin
// First two calls: add marketplaces
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "marketplace", "add", "https://github.com/user/m1.git"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "marketplace", "add", "https://github.com/user/m2.git"],
{ stdio: "inherit" },
);
// Third call: install plugin
expect(spy).toHaveBeenNthCalledWith(
3,
"claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
test("should add marketplaces before installing multiple plugins", async () => {
const spy = createMockSpawn();
await installPlugins(
"https://github.com/user/marketplace.git",
"plugin1\nplugin2",
);
expect(spy).toHaveBeenCalledTimes(3); // 1 marketplace + 2 plugins
// First call: add marketplace
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/user/marketplace.git",
],
{ stdio: "inherit" },
);
// Next calls: install plugins
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "install", "plugin1"],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
3,
"claude",
["plugin", "install", "plugin2"],
{ stdio: "inherit" },
);
});
test("should handle only marketplaces without plugins", async () => {
const spy = createMockSpawn();
await installPlugins("https://github.com/user/marketplace.git", undefined);
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/user/marketplace.git",
],
{ stdio: "inherit" },
);
});
test("should skip empty marketplace entries", async () => {
const spy = createMockSpawn();
await installPlugins(
"https://github.com/user/m1.git\n\nhttps://github.com/user/m2.git",
"test-plugin",
);
expect(spy).toHaveBeenCalledTimes(3); // 2 marketplaces (skip empty) + 1 plugin
});
test("should trim whitespace from marketplace URLs", async () => {
const spy = createMockSpawn();
await installPlugins(
" https://github.com/user/marketplace.git \n https://github.com/user/m2.git ",
"test-plugin",
);
expect(spy).toHaveBeenCalledTimes(3);
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/user/marketplace.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"claude",
["plugin", "marketplace", "add", "https://github.com/user/m2.git"],
{ stdio: "inherit" },
);
});
test("should reject invalid marketplace URL format", async () => {
const spy = createMockSpawn();
await expect(
installPlugins("not-a-valid-url", "test-plugin"),
).rejects.toThrow("Invalid marketplace URL format");
expect(spy).not.toHaveBeenCalled();
});
test("should reject marketplace URL without .git extension", async () => {
const spy = createMockSpawn();
await expect(
installPlugins("https://github.com/user/marketplace", "test-plugin"),
).rejects.toThrow("Invalid marketplace URL format");
expect(spy).not.toHaveBeenCalled();
});
test("should reject marketplace URL with non-https protocol", async () => {
const spy = createMockSpawn();
await expect(
installPlugins("http://github.com/user/marketplace.git", "test-plugin"),
).rejects.toThrow("Invalid marketplace URL format");
expect(spy).not.toHaveBeenCalled();
});
test("should skip whitespace-only marketplace input", async () => {
const spy = createMockSpawn();
await installPlugins(" ", "test-plugin");
// Should skip marketplaces and only install plugin
expect(spy).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenNthCalledWith(
1,
"claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
test("should handle marketplace addition error", async () => {
createMockSpawn(1, false); // Exit code 1
await expect(
installPlugins("https://github.com/user/marketplace.git", "test-plugin"),
).rejects.toThrow(
"Failed to add marketplace 'https://github.com/user/marketplace.git' (exit code: 1)",
);
});
test("should stop if marketplace addition fails before installing plugins", async () => {
const spy = createMockSpawn(1, false); // Exit code 1
await expect(
installPlugins(
"https://github.com/user/marketplace.git",
"plugin1\nplugin2",
),
).rejects.toThrow("Failed to add marketplace");
// Should only try to add marketplace, not install any plugins
expect(spy).toHaveBeenCalledTimes(1);
});
test("should use custom executable for marketplace operations", async () => {
const spy = createMockSpawn();
await installPlugins(
"https://github.com/user/marketplace.git",
"test-plugin",
"/custom/path/to/claude",
);
expect(spy).toHaveBeenCalledTimes(2);
expect(spy).toHaveBeenNthCalledWith(
1,
"/custom/path/to/claude",
[
"plugin",
"marketplace",
"add",
"https://github.com/user/marketplace.git",
],
{ stdio: "inherit" },
);
expect(spy).toHaveBeenNthCalledWith(
2,
"/custom/path/to/claude",
["plugin", "install", "test-plugin"],
{ stdio: "inherit" },
);
});
});

View File

@@ -78,19 +78,5 @@ describe("prepareRunConfig", () => {
"stream-json", "stream-json",
]); ]);
}); });
test("should include json-schema flag when provided", () => {
const options: ClaudeOptions = {
claudeArgs:
'--json-schema \'{"type":"object","properties":{"result":{"type":"boolean"}}}\'',
};
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
expect(prepared.claudeArgs).toContain("--json-schema");
expect(prepared.claudeArgs).toContain(
'{"type":"object","properties":{"result":{"type":"boolean"}}}',
);
});
}); });
}); });

View File

@@ -1,158 +0,0 @@
#!/usr/bin/env bun
import { describe, test, expect, afterEach, beforeEach, spyOn } from "bun:test";
import { writeFile, unlink } from "fs/promises";
import { tmpdir } from "os";
import { join } from "path";
import { parseAndSetStructuredOutputs } from "../src/run-claude";
import * as core from "@actions/core";
// Mock execution file path
const TEST_EXECUTION_FILE = join(tmpdir(), "test-execution-output.json");
// Helper to create mock execution file with structured output
async function createMockExecutionFile(
structuredOutput?: Record<string, unknown>,
includeResult: boolean = true,
): Promise<void> {
const messages: any[] = [
{ type: "system", subtype: "init" },
{ type: "turn", content: "test" },
];
if (includeResult) {
messages.push({
type: "result",
cost_usd: 0.01,
duration_ms: 1000,
structured_output: structuredOutput,
});
}
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
}
// Spy on core functions
let setOutputSpy: any;
let infoSpy: any;
beforeEach(() => {
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
infoSpy = spyOn(core, "info").mockImplementation(() => {});
});
describe("parseAndSetStructuredOutputs", () => {
afterEach(async () => {
setOutputSpy?.mockRestore();
infoSpy?.mockRestore();
try {
await unlink(TEST_EXECUTION_FILE);
} catch {
// Ignore if file doesn't exist
}
});
test("should set structured_output with valid data", async () => {
await createMockExecutionFile({
is_flaky: true,
confidence: 0.85,
summary: "Test looks flaky",
});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
expect(setOutputSpy).toHaveBeenCalledWith(
"structured_output",
'{"is_flaky":true,"confidence":0.85,"summary":"Test looks flaky"}',
);
expect(infoSpy).toHaveBeenCalledWith(
"Set structured_output with 3 field(s)",
);
});
test("should handle arrays and nested objects", async () => {
await createMockExecutionFile({
items: ["a", "b", "c"],
config: { key: "value", nested: { deep: true } },
});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
const callArgs = setOutputSpy.mock.calls[0];
expect(callArgs[0]).toBe("structured_output");
const parsed = JSON.parse(callArgs[1]);
expect(parsed).toEqual({
items: ["a", "b", "c"],
config: { key: "value", nested: { deep: true } },
});
});
test("should handle special characters in field names", async () => {
await createMockExecutionFile({
"test-result": "passed",
"item.count": 10,
"user@email": "test",
});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
const callArgs = setOutputSpy.mock.calls[0];
const parsed = JSON.parse(callArgs[1]);
expect(parsed["test-result"]).toBe("passed");
expect(parsed["item.count"]).toBe(10);
expect(parsed["user@email"]).toBe("test");
});
test("should throw error when result exists but structured_output is undefined", async () => {
const messages = [
{ type: "system", subtype: "init" },
{ type: "result", cost_usd: 0.01, duration_ms: 1000 },
];
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow(
"json_schema was provided but Claude did not return structured_output",
);
});
test("should throw error when no result message exists", async () => {
const messages = [
{ type: "system", subtype: "init" },
{ type: "turn", content: "test" },
];
await writeFile(TEST_EXECUTION_FILE, JSON.stringify(messages));
await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow(
"json_schema was provided but Claude did not return structured_output",
);
});
test("should throw error with malformed JSON", async () => {
await writeFile(TEST_EXECUTION_FILE, "{ invalid json");
await expect(
parseAndSetStructuredOutputs(TEST_EXECUTION_FILE),
).rejects.toThrow();
});
test("should throw error when file does not exist", async () => {
await expect(
parseAndSetStructuredOutputs("/nonexistent/file.json"),
).rejects.toThrow();
});
test("should handle empty structured_output object", async () => {
await createMockExecutionFile({});
await parseAndSetStructuredOutputs(TEST_EXECUTION_FILE);
expect(setOutputSpy).toHaveBeenCalledWith("structured_output", "{}");
expect(infoSpy).toHaveBeenCalledWith(
"Set structured_output with 0 field(s)",
);
});
});

View File

@@ -130,7 +130,7 @@ To allow Claude to view workflow run results, job logs, and CI status:
2. **Configure the action with additional permissions**: 2. **Configure the action with additional permissions**:
```yaml ```yaml
- uses: anthropics/claude-code-action@v1 - uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: | additional_permissions: |
@@ -162,7 +162,7 @@ jobs:
claude-ci-helper: claude-ci-helper:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: anthropics/claude-code-action@v1 - uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
additional_permissions: | additional_permissions: |
@@ -343,31 +343,3 @@ Many individual input parameters have been consolidated into `claude_args` or `s
| `mcp_config` | Use `claude_args: "--mcp-config '{...}'"` | | `mcp_config` | Use `claude_args: "--mcp-config '{...}'"` |
| `direct_prompt` | Use `prompt` input instead | | `direct_prompt` | Use `prompt` input instead |
| `override_prompt` | Use `prompt` with GitHub context variables | | `override_prompt` | Use `prompt` with GitHub context variables |
## Custom Executables for Specialized Environments
For specialized environments like Nix, custom container setups, or other package management systems where the default installation doesn't work, you can provide your own executables:
### Custom Claude Code Executable
Use `path_to_claude_code_executable` to provide your own Claude Code binary instead of using the automatically installed version:
```yaml
- uses: anthropics/claude-code-action@v1
with:
path_to_claude_code_executable: "/path/to/custom/claude"
# ... other inputs
```
### Custom Bun Executable
Use `path_to_bun_executable` to provide your own Bun runtime instead of the default installation:
```yaml
- uses: anthropics/claude-code-action@v1
with:
path_to_bun_executable: "/path/to/custom/bun"
# ... other inputs
```
**Important**: Using incompatible versions may cause the action to fail. Ensure your custom executables are compatible with the action's requirements.

View File

@@ -1,744 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Create Claude Code GitHub App</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
/* Claude Brand Colors */
--primary-dark: #0e0e0e;
--primary-light: #d4a27f;
--background-light: rgb(253, 253, 247);
--background-dark: rgb(9, 9, 11);
--text-primary: #1a1a1a;
--text-secondary: #525252;
--text-tertiary: #737373;
--border-color: rgba(0, 0, 0, 0.08);
--hover-bg: rgba(0, 0, 0, 0.02);
--success: #2ea44f;
--warning: #e3b341;
--card-shadow:
0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
--card-shadow-hover:
0 4px 6px rgba(0, 0, 0, 0.07), 0 2px 4px rgba(0, 0, 0, 0.05);
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
background: var(--background-light);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container {
max-width: 960px;
margin: 0 auto;
padding: 40px 24px;
}
/* Header */
header {
text-align: center;
margin-bottom: 48px;
}
h1 {
font-size: 36px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 12px;
letter-spacing: -0.02em;
}
.subtitle {
font-size: 18px;
color: var(--text-secondary);
max-width: 640px;
margin: 0 auto;
line-height: 1.5;
}
/* Cards */
.card {
background: white;
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 32px;
margin-bottom: 24px;
box-shadow: var(--card-shadow);
transition: all 0.2s ease;
}
.card:hover {
box-shadow: var(--card-shadow-hover);
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
}
.card-icon {
font-size: 24px;
line-height: 1;
}
h2 {
font-size: 20px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
letter-spacing: -0.01em;
}
.card-description {
color: var(--text-secondary);
margin-bottom: 24px;
font-size: 15px;
line-height: 1.6;
}
/* Buttons */
.button-group {
display: flex;
flex-direction: column;
gap: 16px;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
font-size: 15px;
font-weight: 500;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
text-decoration: none;
font-family: inherit;
width: 100%;
}
.btn-primary {
background: var(--primary-dark);
color: white;
}
.btn-primary:hover {
background: #1a1a1a;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn-secondary {
background: var(--primary-light);
color: var(--primary-dark);
}
.btn-secondary:hover {
background: #c99a70;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(212, 162, 127, 0.3);
}
.btn-outline {
background: white;
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-outline:hover {
background: var(--hover-bg);
border-color: var(--text-secondary);
}
.btn:active {
transform: translateY(0);
}
.btn.copied {
background: var(--success);
color: white;
}
/* Form */
.form-row {
display: flex;
gap: 12px;
align-items: flex-end;
}
.form-group {
flex: 1;
}
label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 6px;
}
input[type="text"] {
width: 100%;
padding: 10px 14px;
font-size: 15px;
border: 1px solid var(--border-color);
border-radius: 8px;
font-family: inherit;
transition: all 0.2s ease;
background: white;
}
input[type="text"]:focus {
outline: none;
border-color: var(--primary-dark);
box-shadow: 0 0 0 3px rgba(14, 14, 14, 0.1);
}
/* Code Block */
.code-container {
position: relative;
background: #fafafa;
border: 1px solid var(--border-color);
border-radius: 8px;
margin: 20px 0;
}
.code-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-color);
}
.code-label {
font-size: 13px;
font-weight: 500;
color: var(--text-secondary);
}
.copy-btn {
padding: 6px 12px;
font-size: 13px;
font-weight: 500;
background: white;
color: var(--text-primary);
border: 1px solid var(--border-color);
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.copy-btn:hover {
background: var(--hover-bg);
}
.copy-btn.copied {
background: var(--success);
color: white;
border-color: var(--success);
}
.code-block {
padding: 16px;
overflow-x: auto;
font-family:
"SF Mono", Monaco, "Cascadia Code", "Roboto Mono", Consolas,
"Courier New", monospace;
font-size: 13px;
line-height: 1.6;
color: var(--text-primary);
white-space: pre;
}
/* Permissions List */
.permissions-grid {
display: grid;
gap: 12px;
margin-top: 16px;
}
.permission-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: #fafafa;
border-radius: 8px;
font-size: 14px;
}
.permission-icon {
color: var(--success);
font-size: 16px;
line-height: 1;
}
.permission-name {
font-weight: 500;
color: var(--text-primary);
}
.permission-value {
margin-left: auto;
color: var(--text-secondary);
font-size: 13px;
}
/* Steps */
.steps {
margin: 24px 0;
}
.step {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.step-number {
flex-shrink: 0;
width: 28px;
height: 28px;
background: var(--primary-dark);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
}
.step-content {
flex: 1;
padding-top: 2px;
}
.step-content p {
color: var(--text-secondary);
font-size: 15px;
line-height: 1.6;
}
.step-content strong {
color: var(--text-primary);
font-weight: 500;
}
/* Alert Box */
.alert {
display: flex;
gap: 12px;
padding: 16px;
background: #fffbf0;
border: 1px solid #f5e7c3;
border-radius: 8px;
margin-top: 32px;
}
.alert-icon {
font-size: 18px;
line-height: 1;
flex-shrink: 0;
}
.alert-content {
flex: 1;
font-size: 14px;
line-height: 1.6;
}
.alert-content strong {
color: var(--text-primary);
font-weight: 600;
}
/* Responsive */
@media (min-width: 640px) {
.button-group {
flex-direction: row;
}
.btn {
width: auto;
}
.permissions-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 640px) {
h1 {
font-size: 28px;
}
.subtitle {
font-size: 16px;
}
.card {
padding: 24px 20px;
}
.container {
padding: 24px 16px;
}
}
/* Hidden form elements */
.hidden-form {
display: none;
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Create Your Custom GitHub App</h1>
<p class="subtitle">
Set up a custom GitHub App for Claude Code Action with all required
permissions automatically configured.
</p>
</header>
<!-- Quick Setup Card -->
<div class="card">
<div class="card-header">
<span class="card-icon">🚀</span>
<h2>Quick Setup</h2>
</div>
<p class="card-description">
Create your GitHub App with one click. All permissions will be
automatically configured for Claude Code Action.
</p>
<div class="button-group">
<!-- Personal Account Button -->
<form
action="https://github.com/settings/apps/new"
method="post"
class="hidden-form"
id="personal-form"
>
<input type="hidden" name="manifest" id="personal-manifest" />
</form>
<button
type="button"
class="btn btn-primary"
onclick="submitPersonalForm()"
>
<span>👤</span>
<span>Create for Personal Account</span>
</button>
<!-- Organization Form -->
<form id="org-form" method="post" class="hidden-form">
<input type="hidden" name="manifest" id="org-manifest" />
</form>
</div>
<!-- Organization Input -->
<div
style="
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid var(--border-color);
"
>
<label for="org-name" style="margin-bottom: 8px"
>Or create for an organization:</label
>
<div class="form-row">
<div class="form-group">
<input
type="text"
id="org-name"
placeholder="Enter organization name (e.g., my-org)"
/>
</div>
<button
type="button"
class="btn btn-secondary"
onclick="submitOrgForm()"
style="flex-shrink: 0"
>
<span>🏢</span>
<span>Create for Org</span>
</button>
</div>
</div>
</div>
<!-- Permissions Card -->
<div class="card">
<div class="card-header">
<span class="card-icon"></span>
<h2>Configured Permissions</h2>
</div>
<p class="card-description">
Your GitHub App will be created with these permissions:
</p>
<div class="permissions-grid">
<div class="permission-item">
<span class="permission-icon"></span>
<span class="permission-name">Contents</span>
<span class="permission-value">Read & Write</span>
</div>
<div class="permission-item">
<span class="permission-icon"></span>
<span class="permission-name">Issues</span>
<span class="permission-value">Read & Write</span>
</div>
<div class="permission-item">
<span class="permission-icon"></span>
<span class="permission-name">Pull Requests</span>
<span class="permission-value">Read & Write</span>
</div>
<div class="permission-item">
<span class="permission-icon"></span>
<span class="permission-name">Actions</span>
<span class="permission-value">Read</span>
</div>
<div class="permission-item">
<span class="permission-icon"></span>
<span class="permission-name">Metadata</span>
<span class="permission-value">Read</span>
</div>
</div>
</div>
<!-- Next Steps Card -->
<div class="card">
<div class="card-header">
<span class="card-icon">📋</span>
<h2>Next Steps</h2>
</div>
<p class="card-description">
After creating your app, complete these steps:
</p>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<p>
<strong>Generate a private key:</strong> In your app settings,
scroll to "Private keys" and click "Generate a private key"
</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<p>
<strong>Install the app:</strong> Click "Install App" and select
the repositories where you want to use Claude
</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<p>
<strong>Configure your workflow:</strong> Add your app's ID and
private key to your repository secrets
</p>
</div>
</div>
</div>
</div>
<!-- Manual Setup Card -->
<div class="card">
<div class="card-header">
<span class="card-icon">⚙️</span>
<h2>Manual Setup</h2>
</div>
<p class="card-description">
If the buttons above don't work, you can manually create the app by
copying the manifest JSON below:
</p>
<div class="code-container">
<div class="code-header">
<span class="code-label">github-app-manifest.json</span>
<button class="copy-btn" onclick="copyManifest()">Copy</button>
</div>
<div class="code-block" id="manifest-json"></div>
</div>
<div class="steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<p>Copy the manifest JSON above</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<p>
Go to
<a
href="https://github.com/settings/apps/new"
target="_blank"
style="color: var(--primary-dark); text-decoration: underline"
>GitHub App Settings</a
>
</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<p>Look for "Create from manifest" option and paste the JSON</p>
</div>
</div>
</div>
</div>
<!-- Warning Alert -->
<div class="alert">
<span class="alert-icon">⚠️</span>
<div class="alert-content">
<strong>Important:</strong> Keep your private key secure! Never commit
it to your repository. Always use GitHub secrets to store sensitive
credentials.
</div>
</div>
</div>
<script>
// Manifest configuration
const manifest = {
name: "Claude Code Custom App",
description:
"Custom GitHub App for Claude Code Action - AI-powered coding assistant for GitHub workflows",
url: "https://github.com/anthropics/claude-code-action",
hook_attributes: {
url: "https://example.com/github/webhook",
active: false,
},
redirect_url: "https://github.com/settings/apps/new",
callback_urls: [],
setup_url:
"https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md",
public: false,
default_permissions: {
contents: "write",
issues: "write",
pull_requests: "write",
actions: "read",
metadata: "read",
},
default_events: [
"issue_comment",
"issues",
"pull_request",
"pull_request_review",
"pull_request_review_comment",
],
};
// Populate manifest fields
const manifestJson = JSON.stringify(manifest);
const manifestJsonPretty = JSON.stringify(manifest, null, 2);
document.getElementById("personal-manifest").value = manifestJson;
document.getElementById("org-manifest").value = manifestJson;
// Display formatted JSON
const manifestDisplay = document.getElementById("manifest-json");
manifestDisplay.textContent = manifestJsonPretty;
// Submit personal form
function submitPersonalForm() {
document.getElementById("personal-form").submit();
}
// Submit organization form
function submitOrgForm() {
const orgName = document.getElementById("org-name").value.trim();
if (!orgName) {
alert("Please enter an organization name");
document.getElementById("org-name").focus();
return;
}
const form = document.getElementById("org-form");
form.action = `https://github.com/organizations/${orgName}/settings/apps/new`;
form.submit();
}
// Allow Enter key to submit org form
document
.getElementById("org-name")
.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
e.preventDefault();
submitOrgForm();
}
});
// Copy manifest to clipboard
function copyManifest() {
navigator.clipboard
.writeText(manifestJsonPretty)
.then(() => {
const button = document.querySelector(".copy-btn");
const originalText = button.textContent;
button.textContent = "Copied!";
button.classList.add("copied");
setTimeout(() => {
button.textContent = originalText;
button.classList.remove("copied");
}, 2000);
})
.catch(() => {
// Fallback for older browsers
const textArea = document.createElement("textarea");
textArea.value = manifestJsonPretty;
textArea.style.position = "fixed";
textArea.style.opacity = "0";
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand("copy");
const button = document.querySelector(".copy-btn");
const originalText = button.textContent;
button.textContent = "Copied!";
button.classList.add("copied");
setTimeout(() => {
button.textContent = originalText;
button.classList.remove("copied");
}, 2000);
} catch (err) {
alert("Failed to copy. Please copy manually.");
}
document.body.removeChild(textArea);
});
}
</script>
</body>
</html>

View File

@@ -15,13 +15,13 @@ The action automatically detects which mode to use based on your configuration:
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)): 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` or `pull_request_target` - When PRs are opened or synchronized - `pull_request` - When PRs are opened or synchronized
- `issue_comment` - When comments are created on issues or PRs - `issue_comment` - When comments are created on issues or PRs
- `pull_request_comment` - When comments are made on PR diffs - `pull_request_comment` - When comments are made on PR diffs
- `issues` - When issues are opened or assigned - `issues` - When issues are opened or assigned
- `pull_request_review` - When PR reviews are submitted - `pull_request_review` - When PR reviews are submitted
- `pull_request_review_comment` - When comments are made on PR reviews - `pull_request_review_comment` - When comments are made on PR reviews
- `repository_dispatch` - Custom events triggered via API - `repository_dispatch` - Custom events triggered via API (coming soon)
- `workflow_dispatch` - Manual workflow triggers (coming soon) - `workflow_dispatch` - Manual workflow triggers (coming soon)
## Automated Documentation Updates ## Automated Documentation Updates

View File

@@ -127,7 +127,7 @@ For performance, Claude uses shallow clones:
If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step. If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step.
``` ```
- uses: actions/checkout@v5 - uses: actions/checkout@v4
depth: 0 # will fetch full repo history depth: 0 # will fetch full repo history
``` ```
@@ -213,44 +213,6 @@ Check the GitHub Action log for Claude's run for the full execution trace.
The trigger uses word boundaries, so `@claude` must be a complete word. Variations like `@claude-bot`, `@claude!`, or `claude@mention` won't work unless you customize the `trigger_phrase`. The trigger uses word boundaries, so `@claude` must be a complete word. Variations like `@claude-bot`, `@claude!`, or `claude@mention` won't work unless you customize the `trigger_phrase`.
### How can I use custom executables in specialized environments?
For specialized environments like Nix, NixOS, or custom container setups where you need to provide your own executables:
**Using a custom Claude Code executable:**
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
path_to_claude_code_executable: "/path/to/custom/claude"
# ... other inputs
```
**Using a custom Bun executable:**
```yaml
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
path_to_bun_executable: "/path/to/custom/bun"
# ... other inputs
```
**Common use cases:**
- Nix/NixOS environments where packages are managed differently
- Docker containers with pre-installed executables
- Custom build environments with specific version requirements
- Debugging specific issues with particular versions
**Important notes:**
- Using an older Claude Code version may cause problems if the action uses newer features
- Using an incompatible Bun version may cause runtime errors
- The action will skip automatic installation when custom paths are provided
- Ensure the custom executables are available in your GitHub Actions environment
## Best Practices ## Best Practices
1. **Always specify permissions explicitly** in your workflow file 1. **Always specify permissions explicitly** in your workflow file

View File

@@ -14,19 +14,18 @@ This guide helps you migrate from Claude Code Action v0.x to v1.0. The new versi
The following inputs have been deprecated and replaced: The following inputs have been deprecated and replaced:
| Deprecated Input | Replacement | Notes | | Deprecated Input | Replacement | Notes |
| --------------------- | ------------------------------------ | --------------------------------------------- | | --------------------- | -------------------------------- | --------------------------------------------- |
| `mode` | Auto-detected | Action automatically chooses based on context | | `mode` | Auto-detected | Action automatically chooses based on context |
| `direct_prompt` | `prompt` | Direct drop-in replacement | | `direct_prompt` | `prompt` | Direct drop-in replacement |
| `override_prompt` | `prompt` | Use GitHub context variables instead | | `override_prompt` | `prompt` | Use GitHub context variables instead |
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments | | `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
| `max_turns` | `claude_args: --max-turns` | Use CLI format | | `max_turns` | `claude_args: --max-turns` | Use CLI format |
| `model` | `claude_args: --model` | Specify via CLI | | `model` | `claude_args: --model` | Specify via CLI |
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format | | `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format | | `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
| `claude_env` | `settings` with env object | Use settings JSON | | `claude_env` | `settings` with env object | Use settings JSON |
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments | | `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
| `timeout_minutes` | Use GitHub Actions `timeout-minutes` | Configure at job level instead of input level |
## Migration Examples ## Migration Examples
@@ -199,30 +198,6 @@ The `track_progress` input only works with these GitHub events:
} }
``` ```
### Timeout Configuration
**Before (v0.x):**
```yaml
- uses: anthropics/claude-code-action@beta
with:
timeout_minutes: 30
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
**After (v1.0):**
```yaml
jobs:
claude-task:
runs-on: ubuntu-latest
timeout-minutes: 30 # Moved to job level
steps:
- uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
## How Mode Detection Works ## How Mode Detection Works
The action now automatically detects the appropriate mode: The action now automatically detects the appropriate mode:
@@ -337,7 +312,6 @@ You can also pass MCP configuration from a file:
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools` - [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
- [ ] Move `claude_env` to `settings` JSON format - [ ] Move `claude_env` to `settings` JSON format
- [ ] Move `mcp_config` to `claude_args` with `--mcp-config` - [ ] Move `mcp_config` to `claude_args` with `--mcp-config`
- [ ] Replace `timeout_minutes` with GitHub Actions `timeout-minutes` at job level
- [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode - [ ] **Optional**: Add `track_progress: true` if you need tracking comments in automation mode
- [ ] Test workflow in a non-production environment - [ ] Test workflow in a non-production environment

View File

@@ -4,37 +4,17 @@
- **Repository Access**: The action can only be triggered by users with write access to the repository - **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 - **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
- **⚠️ Non-Write User Access (RISKY)**: The `allowed_non_write_users` parameter allows bypassing the write permission requirement. **This is a significant security risk and should only be used for workflows with extremely limited permissions** (e.g., issue labeling workflows that only have `issues: write` permission). This feature:
- Only works when `github_token` is provided as input (not with GitHub App authentication)
- Accepts either a comma-separated list of specific usernames or `*` to allow all users
- **Should be used with extreme caution** as it bypasses the primary security mechanism of this action
- Is designed for automation workflows where user permissions are already restricted by the workflow's permission scope
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in - **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 - **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 - **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
## ⚠️ Prompt Injection Risks
**Beware of potential hidden markdown when tagging Claude on untrusted content.** External contributors may include hidden instructions through HTML comments, invisible characters, hidden attributes, or other techniques. The action sanitizes content by stripping HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities, but new bypass techniques may emerge. We recommend reviewing the raw content of all input coming from external contributors before allowing Claude to process it.
## GitHub App Permissions ## GitHub App Permissions
The [Claude Code GitHub app](https://github.com/apps/claude) requests the following permissions: The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
### Currently Used Permissions - **Pull Requests**: Read and write to create PRs and push changes
- **Issues**: Read and write to respond to issues
- **Contents** (Read & Write): For reading repository files and creating branches - **Contents**: Read and write to modify repository files
- **Pull Requests** (Read & Write): For reading PR data and creating/updating pull requests
- **Issues** (Read & Write): For reading issue data and updating issue comments
### Permissions for Future Features
The following permissions are requested but not yet actively used. These will enable planned features in future releases:
- **Discussions** (Read & Write): For interaction with GitHub Discussions
- **Actions** (Read): For accessing workflow run data and logs
- **Checks** (Read): For reading check run results
- **Workflows** (Read & Write): For triggering and managing GitHub Actions workflows
## Commit Signing ## Commit Signing
@@ -56,31 +36,3 @@ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable! anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable! claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable!
``` ```
## ⚠️ Full Output Security Warning
The `show_full_output` option is **disabled by default** for security reasons. When enabled, it outputs ALL Claude Code messages including:
- Full outputs from tool executions (e.g., `ps`, `env`, file reads)
- API responses that may contain tokens or credentials
- File contents that may include secrets
- Command outputs that may expose sensitive system information
**These logs are publicly visible in GitHub Actions for public repositories!**
### Automatic Enabling in Debug Mode
Full output is **automatically enabled** when GitHub Actions debug mode is active (when `ACTIONS_STEP_DEBUG` secret is set to `true`). This helps with debugging but carries the same security risks.
### When to Enable Full Output
Only enable `show_full_output: true` or GitHub Actions debug mode when:
- Working in a private repository with controlled access
- Debugging issues in a non-production environment
- You have verified no secrets will be exposed in the output
- You understand the security implications
### Recommended Practice
For debugging, prefer using `show_full_output: false` (the default) and rely on Claude Code's sanitized output, which shows only essential information like errors and completion status without exposing sensitive data.

View File

@@ -20,48 +20,7 @@ If you prefer not to install the official Claude app, you can create your own Gi
- Organization policies prevent installing third-party apps - Organization policies prevent installing third-party apps
- You're using AWS Bedrock or Google Vertex AI - You're using AWS Bedrock or Google Vertex AI
### Option 1: Quick Setup with App Manifest (Recommended) **Steps to create and use a custom GitHub App:**
The fastest way to create a custom GitHub App is using our pre-configured manifest. This ensures all permissions are correctly set up with a single click.
**Steps:**
1. **Create the app:**
**🚀 [Download the Quick Setup Tool](./create-app.html)** (Right-click → "Save Link As" or "Download Linked File")
After downloading, open `create-app.html` in your web browser:
- **For Personal Accounts:** Click the "Create App for Personal Account" button
- **For Organizations:** Enter your organization name and click "Create App for Organization"
The tool will automatically configure all required permissions and submit the manifest.
Alternatively, you can use the manifest file directly:
- Use the [`github-app-manifest.json`](../github-app-manifest.json) file from this repository
- Visit https://github.com/settings/apps/new (for personal) or your organization's app settings
- Look for the "Create from manifest" option and paste the JSON content
2. **Complete the creation flow:**
- GitHub will show you a preview of the app configuration
- Confirm the app name (you can customize it)
- Click "Create GitHub App"
- The app will be created with all required permissions automatically configured
3. **Generate and download a private key:**
- After creating the app, you'll be redirected to the app settings
- Scroll down to "Private keys"
- Click "Generate a private key"
- Download the `.pem` file (keep this secure!)
4. **Continue with installation** - Skip to step 3 in the manual setup below to install the app and configure your workflow.
### Option 2: Manual Setup
If you prefer to configure the app manually or need custom permissions:
1. **Create a new GitHub App:** 1. **Create a new GitHub App:**
@@ -117,7 +76,7 @@ If you prefer to configure the app manually or need custom permissions:
private-key: ${{ secrets.APP_PRIVATE_KEY }} private-key: ${{ secrets.APP_PRIVATE_KEY }}
# Use Claude with your custom app's token # Use Claude with your custom app's token
- uses: anthropics/claude-code-action@v1 - uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ steps.app-token.outputs.token }} github_token: ${{ steps.app-token.outputs.token }}

View File

@@ -35,7 +35,7 @@ jobs:
pull-requests: write pull-requests: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@@ -89,7 +89,7 @@ jobs:
pull-requests: write pull-requests: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@@ -153,7 +153,7 @@ jobs:
pull-requests: write pull-requests: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@@ -211,7 +211,7 @@ jobs:
pull-requests: write pull-requests: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@@ -268,7 +268,7 @@ jobs:
pull-requests: write pull-requests: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1
@@ -344,7 +344,7 @@ jobs:
pull-requests: write pull-requests: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -456,7 +456,7 @@ jobs:
pull-requests: write pull-requests: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
ref: ${{ github.event.pull_request.head.ref }} ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 0 fetch-depth: 0
@@ -513,7 +513,7 @@ jobs:
security-events: write security-events: write
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v5 - uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@@ -32,11 +32,6 @@ jobs:
# --max-turns 10 # --max-turns 10
# --model claude-4-0-sonnet-20250805 # --model claude-4-0-sonnet-20250805
# Optional: add custom plugin marketplaces
# plugin_marketplaces: "https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git"
# Optional: install Claude Code plugins
# plugins: "code-review@claude-code-plugins\nfeature-dev@claude-code-plugins"
# Optional: add custom trigger phrase (default: @claude) # Optional: add custom trigger phrase (default: @claude)
# trigger_phrase: "/claude" # trigger_phrase: "/claude"
# Optional: add assignee trigger for issues # Optional: add assignee trigger for issues
@@ -52,35 +47,30 @@ jobs:
## Inputs ## Inputs
| Input | Description | Required | Default | | Input | Description | Required | Default |
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | | ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | | `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\* | - | | `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 | - | | `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | | `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` |
| `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | 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 | - | | `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` | | `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 | - | | `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_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` | | `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | | `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | | `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | | `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | | `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | | `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | | `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | | `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | | `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` | | `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` | | `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` |
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" | | `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` |
| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" | | `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" |
| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" |
| `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" |
| `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" |
| `json_schema` | JSON schema for structured output validation. Automatically sets GitHub Action outputs for each field. See [Structured Outputs](#structured-outputs) section below | No | "" |
### Deprecated Inputs ### Deprecated Inputs
@@ -97,7 +87,6 @@ These inputs are deprecated and will be removed in a future version:
| `fallback_model` | **DEPRECATED**: Use `claude_args` with fallback configuration | Configure fallback in `claude_args` or `settings` | | `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"` | | `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"` | | `disallowed_tools` | **DEPRECATED**: Use `claude_args` with `--disallowedTools` instead | Use `claude_args: "--disallowedTools WebSearch"` |
| `mcp_config` | **DEPRECATED**: Use `claude_args` with `--mcp-config` instead | Use `claude_args: "--mcp-config '{...}'"` |
| `claude_env` | **DEPRECATED**: Use `settings` with env configuration | Configure environment in `settings` JSON | | `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) \*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
@@ -186,82 +175,6 @@ For a comprehensive guide on migrating from v0.x to v1.0, including step-by-step
Focus on the changed files in this PR. Focus on the changed files in this PR.
``` ```
## Structured Outputs
Get validated JSON results from Claude that automatically become GitHub Action outputs. This enables building complex automation workflows where Claude analyzes data and subsequent steps use the results.
### Basic Example
```yaml
- name: Detect flaky tests
id: analyze
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
Check the CI logs and determine if this is a flaky test.
Return: is_flaky (boolean), confidence (0-1), summary (string)
json_schema: |
{
"type": "object",
"properties": {
"is_flaky": {"type": "boolean"},
"confidence": {"type": "number"},
"summary": {"type": "string"}
},
"required": ["is_flaky"]
}
- name: Retry if flaky
if: fromJSON(steps.analyze.outputs.structured_output).is_flaky == true
run: gh workflow run CI
```
### How It Works
1. **Define Schema**: Provide a JSON schema in the `json_schema` input
2. **Claude Executes**: Claude uses tools to complete your task
3. **Validated Output**: Result is validated against your schema
4. **JSON Output**: All fields are returned in a single `structured_output` JSON string
### Accessing Structured Outputs
All structured output fields are available in the `structured_output` output as a JSON string:
**In GitHub Actions expressions:**
```yaml
if: fromJSON(steps.analyze.outputs.structured_output).is_flaky == true
run: |
CONFIDENCE=${{ fromJSON(steps.analyze.outputs.structured_output).confidence }}
```
**In bash with jq:**
```yaml
- name: Process results
run: |
OUTPUT='${{ steps.analyze.outputs.structured_output }}'
IS_FLAKY=$(echo "$OUTPUT" | jq -r '.is_flaky')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
```
**Note**: Due to GitHub Actions limitations, composite actions cannot expose dynamic outputs. All fields are bundled in the single `structured_output` JSON string.
### Complete Example
See `examples/test-failure-analysis.yml` for a working example that:
- Detects flaky test failures
- Uses confidence thresholds in conditionals
- Auto-retries workflows
- Comments on PRs
### Documentation
For complete details on JSON Schema syntax and Agent SDK structured outputs:
https://docs.claude.com/en/docs/agent-sdk/structured-outputs
## Ways to Tag @claude ## 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. 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.

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.workflow_run.head_branch }} ref: ${{ github.event.workflow_run.head_branch }}
fetch-depth: 0 fetch-depth: 0

View File

@@ -26,7 +26,7 @@ jobs:
actions: read # Required for Claude to read CI results on PRs actions: read # Required for Claude to read CI results on PRs
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@@ -15,7 +15,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@@ -1,5 +1,4 @@
name: Claude Issue Triage name: Issue Triage
description: Run Claude Code for issue triage in GitHub Actions
on: on:
issues: issues:
types: [opened] types: [opened]
@@ -11,19 +10,67 @@ jobs:
permissions: permissions:
contents: read contents: read
issues: write issues: write
id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Run Claude Code for Issue Triage - name: Triage issue with Claude
uses: anthropics/claude-code-action@v1 uses: anthropics/claude-code-action@v1
with: with:
# NOTE: /label-issue here requires a .claude/commands/label-issue.md file in your repo (see this repo's .claude directory for an example) prompt: |
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER${{ github.event.issue.number }}" 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 }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues claude_args: |
github_token: ${{ secrets.GITHUB_TOKEN }} --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

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 2 # Need at least 2 commits to analyze the latest fetch-depth: 2 # Need at least 2 commits to analyze the latest

View File

@@ -16,7 +16,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@@ -18,7 +18,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@@ -19,7 +19,7 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v4
with: with:
fetch-depth: 1 fetch-depth: 1

View File

@@ -1,133 +0,0 @@
name: Auto-Retry Flaky Tests
# This example demonstrates using structured outputs to detect flaky test failures
# and automatically retry them, reducing noise from intermittent failures.
#
# Use case: When CI fails, automatically determine if it's likely flaky and retry if so.
on:
workflow_run:
workflows: ["CI"]
types: [completed]
permissions:
contents: read
actions: write
jobs:
detect-flaky:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'failure' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Detect flaky test failures
id: detect
uses: anthropics/claude-code-action@main
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
The CI workflow failed: ${{ github.event.workflow_run.html_url }}
Check the logs: gh run view ${{ github.event.workflow_run.id }} --log-failed
Determine if this looks like a flaky test failure by checking for:
- Timeout errors
- Race conditions
- Network errors
- "Expected X but got Y" intermittent failures
- Tests that passed in previous commits
Return:
- is_flaky: true if likely flaky, false if real bug
- confidence: number 0-1 indicating confidence level
- summary: brief one-sentence explanation
json_schema: |
{
"type": "object",
"properties": {
"is_flaky": {
"type": "boolean",
"description": "Whether this appears to be a flaky test failure"
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"description": "Confidence level in the determination"
},
"summary": {
"type": "string",
"description": "One-sentence explanation of the failure"
}
},
"required": ["is_flaky", "confidence", "summary"]
}
# Auto-retry only if flaky AND high confidence (>= 0.7)
- name: Retry flaky tests
if: |
fromJSON(steps.detect.outputs.structured_output).is_flaky == true &&
fromJSON(steps.detect.outputs.structured_output).confidence >= 0.7
env:
GH_TOKEN: ${{ github.token }}
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
echo "🔄 Flaky test detected (confidence: $CONFIDENCE)"
echo "Summary: $SUMMARY"
echo ""
echo "Triggering automatic retry..."
gh workflow run "${{ github.event.workflow_run.name }}" \
--ref "${{ github.event.workflow_run.head_branch }}"
# Low confidence flaky detection - skip retry
- name: Low confidence detection
if: |
fromJSON(steps.detect.outputs.structured_output).is_flaky == true &&
fromJSON(steps.detect.outputs.structured_output).confidence < 0.7
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
echo "⚠️ Possible flaky test but confidence too low ($CONFIDENCE)"
echo "Not retrying automatically - manual review recommended"
# Comment on PR if this was a PR build
- name: Comment on PR
if: github.event.workflow_run.event == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
OUTPUT='${{ steps.detect.outputs.structured_output }}'
IS_FLAKY=$(echo "$OUTPUT" | jq -r '.is_flaky')
CONFIDENCE=$(echo "$OUTPUT" | jq -r '.confidence')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
pr_number=$(gh pr list --head "${{ github.event.workflow_run.head_branch }}" --json number --jq '.[0].number')
if [ -n "$pr_number" ]; then
if [ "$IS_FLAKY" = "true" ]; then
TITLE="🔄 Flaky Test Detected"
ACTION="✅ Automatically retrying the workflow"
else
TITLE="❌ Test Failure"
ACTION="⚠️ This appears to be a real bug - manual intervention needed"
fi
gh pr comment "$pr_number" --body "$(cat <<EOF
## $TITLE
**Analysis**: $SUMMARY
**Confidence**: $CONFIDENCE
$ACTION
[View workflow run](${{ github.event.workflow_run.html_url }})
EOF
)"
fi

View File

@@ -1,27 +0,0 @@
{
"name": "Claude Code Custom App",
"description": "Custom GitHub App for Claude Code Action - AI-powered coding assistant for GitHub workflows",
"url": "https://github.com/anthropics/claude-code-action",
"hook_attributes": {
"url": "https://example.com/github/webhook",
"active": false
},
"redirect_url": "https://github.com/settings/apps/new",
"callback_urls": [],
"setup_url": "https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md",
"public": false,
"default_permissions": {
"contents": "write",
"issues": "write",
"pull_requests": "write",
"actions": "read",
"metadata": "read"
},
"default_events": [
"issue_comment",
"issues",
"pull_request",
"pull_request_review",
"pull_request_review_comment"
]
}

View File

@@ -335,7 +335,6 @@ export function prepareContext(
return { return {
...commonFields, ...commonFields,
eventData, eventData,
githubContext: context,
}; };
} }
@@ -384,7 +383,6 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
}; };
case "pull_request": case "pull_request":
case "pull_request_target":
return { return {
eventType: "PULL_REQUEST", eventType: "PULL_REQUEST",
triggerContext: eventData.eventAction triggerContext: eventData.eventAction
@@ -684,7 +682,7 @@ ${
- Display the todo list as a checklist in the GitHub comment and mark things off as you go. - Display the todo list as a checklist in the GitHub comment and mark things off as you go.
- REPOSITORY SETUP INSTRUCTIONS: The repository's CLAUDE.md file(s) contain critical repo-specific setup instructions, development guidelines, and preferences. Always read and follow these files, particularly the root CLAUDE.md, as they provide essential context for working with the codebase effectively. - REPOSITORY SETUP INSTRUCTIONS: The repository's CLAUDE.md file(s) contain critical repo-specific setup instructions, development guidelines, and preferences. Always read and follow these files, particularly the root CLAUDE.md, as they provide essential context for working with the codebase effectively.
- Use h3 headers (###) for section titles in your comments, not h1 headers (#). - Use h3 headers (###) for section titles in your comments, not h1 headers (#).
- Your comment must always include the job run link in the format "[View job run](${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${process.env.GITHUB_RUN_ID})" at the bottom of your response (branch link if there is one should also be included there). - Your comment must always include the job run link (and branch link if there is one) at the bottom.
CAPABILITIES AND LIMITATIONS: CAPABILITIES AND LIMITATIONS:
When users ask you to do something, be aware of what you can and cannot do. This section helps you understand how to respond when users request actions outside your scope. When users ask you to do something, be aware of what you can and cannot do. This section helps you understand how to respond when users request actions outside your scope.
@@ -709,7 +707,7 @@ What You CANNOT Do:
- Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications) - Modify files in the .github/workflows directory (GitHub App permissions do not allow workflow modifications)
When users ask you to perform actions you cannot do, politely explain the limitation and, when applicable, direct them to the FAQ for more information and workarounds: When users ask you to perform actions you cannot do, politely explain the limitation and, when applicable, direct them to the FAQ for more information and workarounds:
"I'm unable to [specific action] due to [reason]. You can find more information and potential workarounds in the [FAQ](https://github.com/anthropics/claude-code-action/blob/main/docs/faq.md)." "I'm unable to [specific action] due to [reason]. You can find more information and potential workarounds in the [FAQ](https://github.com/anthropics/claude-code-action/blob/main/FAQ.md)."
If a user asks for something outside these capabilities (and you have no other tools provided), politely explain that you cannot perform that action and suggest an alternative approach if possible. If a user asks for something outside these capabilities (and you have no other tools provided), politely explain that you cannot perform that action and suggest an alternative approach if possible.

View File

@@ -78,7 +78,8 @@ type IssueLabeledEvent = {
labelTrigger: string; labelTrigger: string;
}; };
type PullRequestBaseEvent = { type PullRequestEvent = {
eventName: "pull_request";
eventAction?: string; // opened, synchronize, etc. eventAction?: string; // opened, synchronize, etc.
isPR: true; isPR: true;
prNumber: string; prNumber: string;
@@ -86,14 +87,6 @@ type PullRequestBaseEvent = {
baseBranch?: string; baseBranch?: string;
}; };
type PullRequestEvent = PullRequestBaseEvent & {
eventName: "pull_request";
};
type PullRequestTargetEvent = PullRequestBaseEvent & {
eventName: "pull_request_target";
};
// Union type for all possible event types // Union type for all possible event types
export type EventData = export type EventData =
| PullRequestReviewCommentEvent | PullRequestReviewCommentEvent
@@ -103,8 +96,7 @@ export type EventData =
| IssueOpenedEvent | IssueOpenedEvent
| IssueAssignedEvent | IssueAssignedEvent
| IssueLabeledEvent | IssueLabeledEvent
| PullRequestEvent | PullRequestEvent;
| PullRequestTargetEvent;
// Combined type with separate eventData field // Combined type with separate eventData field
export type PreparedContext = CommonFields & { export type PreparedContext = CommonFields & {

View File

@@ -30,13 +30,9 @@ async function run() {
// Step 3: Check write permissions (only for entity contexts) // Step 3: Check write permissions (only for entity contexts)
if (isEntityContext(context)) { if (isEntityContext(context)) {
// Check if github_token was provided as input (not from app)
const githubTokenProvided = !!process.env.OVERRIDE_GITHUB_TOKEN;
const hasWritePermissions = await checkWritePermissions( const hasWritePermissions = await checkWritePermissions(
octokit.rest, octokit.rest,
context, context,
context.inputs.allowedNonWriteUsers,
githubTokenProvided,
); );
if (!hasWritePermissions) { if (!hasWritePermissions) {
throw new Error( throw new Error(

View File

@@ -26,20 +26,6 @@ export type WorkflowDispatchEvent = {
workflow: string; workflow: string;
}; };
export type RepositoryDispatchEvent = {
action: string;
client_payload?: Record<string, any>;
repository: {
name: string;
owner: {
login: string;
};
};
sender: {
login: string;
};
};
export type ScheduleEvent = { export type ScheduleEvent = {
action?: never; action?: never;
schedule?: string; schedule?: string;
@@ -62,7 +48,6 @@ const ENTITY_EVENT_NAMES = [
const AUTOMATION_EVENT_NAMES = [ const AUTOMATION_EVENT_NAMES = [
"workflow_dispatch", "workflow_dispatch",
"repository_dispatch",
"schedule", "schedule",
"workflow_run", "workflow_run",
] as const; ] as const;
@@ -93,7 +78,6 @@ type BaseContext = {
botId: string; botId: string;
botName: string; botName: string;
allowedBots: string; allowedBots: string;
allowedNonWriteUsers: string;
trackProgress: boolean; trackProgress: boolean;
}; };
}; };
@@ -111,14 +95,10 @@ export type ParsedGitHubContext = BaseContext & {
isPR: boolean; isPR: boolean;
}; };
// Context for automation events (workflow_dispatch, repository_dispatch, schedule, workflow_run) // Context for automation events (workflow_dispatch, schedule, workflow_run)
export type AutomationContext = BaseContext & { export type AutomationContext = BaseContext & {
eventName: AutomationEventName; eventName: AutomationEventName;
payload: payload: WorkflowDispatchEvent | ScheduleEvent | WorkflowRunEvent;
| WorkflowDispatchEvent
| RepositoryDispatchEvent
| ScheduleEvent
| WorkflowRunEvent;
}; };
// Union type for all contexts // Union type for all contexts
@@ -148,7 +128,6 @@ export function parseGitHubContext(): GitHubContext {
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID), botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN, botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN,
allowedBots: process.env.ALLOWED_BOTS ?? "", allowedBots: process.env.ALLOWED_BOTS ?? "",
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
trackProgress: process.env.TRACK_PROGRESS === "true", trackProgress: process.env.TRACK_PROGRESS === "true",
}, },
}; };
@@ -174,8 +153,7 @@ export function parseGitHubContext(): GitHubContext {
isPR: Boolean(payload.issue.pull_request), isPR: Boolean(payload.issue.pull_request),
}; };
} }
case "pull_request": case "pull_request": {
case "pull_request_target": {
const payload = context.payload as PullRequestEvent; const payload = context.payload as PullRequestEvent;
return { return {
...commonFields, ...commonFields,
@@ -212,13 +190,6 @@ export function parseGitHubContext(): GitHubContext {
payload: context.payload as unknown as WorkflowDispatchEvent, payload: context.payload as unknown as WorkflowDispatchEvent,
}; };
} }
case "repository_dispatch": {
return {
...commonFields,
eventName: "repository_dispatch",
payload: context.payload as unknown as RepositoryDispatchEvent,
};
}
case "schedule": { case "schedule": {
return { return {
...commonFields, ...commonFields,

View File

@@ -6,43 +6,17 @@ import type { Octokit } from "@octokit/rest";
* Check if the actor has write permissions to the repository * Check if the actor has write permissions to the repository
* @param octokit - The Octokit REST client * @param octokit - The Octokit REST client
* @param context - The GitHub context * @param context - The GitHub context
* @param allowedNonWriteUsers - Comma-separated list of users allowed without write permissions, or '*' for all
* @param githubTokenProvided - Whether github_token was provided as input (not from app)
* @returns true if the actor has write permissions, false otherwise * @returns true if the actor has write permissions, false otherwise
*/ */
export async function checkWritePermissions( export async function checkWritePermissions(
octokit: Octokit, octokit: Octokit,
context: ParsedGitHubContext, context: ParsedGitHubContext,
allowedNonWriteUsers?: string,
githubTokenProvided?: boolean,
): Promise<boolean> { ): Promise<boolean> {
const { repository, actor } = context; const { repository, actor } = context;
try { try {
core.info(`Checking permissions for actor: ${actor}`); core.info(`Checking permissions for actor: ${actor}`);
// Check if we should bypass permission checks for this user
if (allowedNonWriteUsers && githubTokenProvided) {
const allowedUsers = allowedNonWriteUsers.trim();
if (allowedUsers === "*") {
core.warning(
`⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.`,
);
return true;
} else if (allowedUsers) {
const allowedUserList = allowedUsers
.split(",")
.map((u) => u.trim())
.filter((u) => u.length > 0);
if (allowedUserList.includes(actor)) {
core.warning(
`⚠️ SECURITY WARNING: Bypassing write permission check for ${actor} due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.`,
);
return true;
}
}
}
// Check if the actor is a GitHub App (bot user) // Check if the actor is a GitHub App (bot user)
if (actor.endsWith("[bot]")) { if (actor.endsWith("[bot]")) {
core.info(`Actor is a GitHub App: ${actor}`); core.info(`Actor is a GitHub App: ${actor}`);

View File

@@ -3,7 +3,6 @@ import { GITHUB_API_URL, GITHUB_SERVER_URL } from "../github/api/config";
import type { GitHubContext } from "../github/context"; import type { GitHubContext } from "../github/context";
import { isEntityContext } from "../github/context"; import { isEntityContext } from "../github/context";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import type { AutoDetectedMode } from "../modes/detector";
type PrepareConfigParams = { type PrepareConfigParams = {
githubToken: string; githubToken: string;
@@ -13,7 +12,6 @@ type PrepareConfigParams = {
baseBranch: string; baseBranch: string;
claudeCommentId?: string; claudeCommentId?: string;
allowedTools: string[]; allowedTools: string[];
mode: AutoDetectedMode;
context: GitHubContext; context: GitHubContext;
}; };
@@ -61,17 +59,12 @@ export async function prepareMcpConfig(
claudeCommentId, claudeCommentId,
allowedTools, allowedTools,
context, context,
mode,
} = params; } = params;
try { try {
const allowedToolsList = allowedTools || []; const allowedToolsList = allowedTools || [];
// Detect if we're in agent mode (explicit prompt provided) // Detect if we're in agent mode (explicit prompt provided)
const isAgentMode = mode === "agent"; const isAgentMode = !!context.inputs?.prompt;
const hasGitHubCommentTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github_comment__"),
);
const hasGitHubMcpTools = allowedToolsList.some((tool) => const hasGitHubMcpTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github__"), tool.startsWith("mcp__github__"),
@@ -81,6 +74,10 @@ export async function prepareMcpConfig(
tool.startsWith("mcp__github_inline_comment__"), tool.startsWith("mcp__github_inline_comment__"),
); );
const hasGitHubCommentTools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github_comment__"),
);
const hasGitHubCITools = allowedToolsList.some((tool) => const hasGitHubCITools = allowedToolsList.some((tool) =>
tool.startsWith("mcp__github_ci__"), tool.startsWith("mcp__github_ci__"),
); );
@@ -209,7 +206,7 @@ export async function prepareMcpConfig(
"GITHUB_PERSONAL_ACCESS_TOKEN", "GITHUB_PERSONAL_ACCESS_TOKEN",
"-e", "-e",
"GITHUB_HOST", "GITHUB_HOST",
"ghcr.io/github/github-mcp-server:sha-23fa0dd", // https://github.com/github/github-mcp-server/releases/tag/v0.17.1 "ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0
], ],
env: { env: {
GITHUB_PERSONAL_ACCESS_TOKEN: githubToken, GITHUB_PERSONAL_ACCESS_TOKEN: githubToken,

View File

@@ -7,7 +7,6 @@ import { parseAllowedTools } from "./parse-tools";
import { configureGitAuth } from "../../github/operations/git-config"; import { configureGitAuth } from "../../github/operations/git-config";
import type { GitHubContext } from "../../github/context"; import type { GitHubContext } from "../../github/context";
import { isEntityContext } from "../../github/context"; import { isEntityContext } from "../../github/context";
import { appendJsonSchemaArg } from "../../utils/json-schema";
/** /**
* Extract GitHub context as environment variables for agent mode * Extract GitHub context as environment variables for agent mode
@@ -136,7 +135,6 @@ export const agentMode: Mode = {
baseBranch: baseBranch, baseBranch: baseBranch,
claudeCommentId: undefined, // No tracking comment in agent mode claudeCommentId: undefined, // No tracking comment in agent mode
allowedTools, allowedTools,
mode: "agent",
context, context,
}); });
@@ -150,9 +148,6 @@ export const agentMode: Mode = {
claudeArgs = `--mcp-config '${escapedOurConfig}'`; claudeArgs = `--mcp-config '${escapedOurConfig}'`;
} }
// Add JSON schema if provided
claudeArgs = appendJsonSchemaArg(claudeArgs);
// Append user's claude_args (which may have more --mcp-config flags) // Append user's claude_args (which may have more --mcp-config flags)
claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim(); claudeArgs = `${claudeArgs} ${userClaudeArgs}`.trim();

View File

@@ -1,10 +1,10 @@
export function parseAllowedTools(claudeArgs: string): string[] { export function parseAllowedTools(claudeArgs: string): string[] {
// Match --allowedTools or --allowed-tools followed by the value // Match --allowedTools followed by the value
// Handle both quoted and unquoted values // Handle both quoted and unquoted values
const patterns = [ const patterns = [
/--(?:allowedTools|allowed-tools)\s+"([^"]+)"/, // Double quoted /--allowedTools\s+"([^"]+)"/, // Double quoted
/--(?:allowedTools|allowed-tools)\s+'([^']+)'/, // Single quoted /--allowedTools\s+'([^']+)'/, // Single quoted
/--(?:allowedTools|allowed-tools)\s+([^\s]+)/, // Unquoted /--allowedTools\s+([^\s]+)/, // Unquoted
]; ];
for (const pattern of patterns) { for (const pattern of patterns) {

View File

@@ -19,13 +19,7 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
// If track_progress is set for PR/issue events, force tag mode // If track_progress is set for PR/issue events, force tag mode
if (context.inputs.trackProgress && isEntityContext(context)) { if (context.inputs.trackProgress && isEntityContext(context)) {
if ( if (isPullRequestEvent(context) || isIssuesEvent(context)) {
isPullRequestEvent(context) ||
isIssuesEvent(context) ||
isIssueCommentEvent(context) ||
isPullRequestReviewCommentEvent(context) ||
isPullRequestReviewEvent(context)
) {
return "tag"; return "tag";
} }
} }
@@ -50,10 +44,6 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
// Issue events // Issue events
if (isEntityContext(context) && isIssuesEvent(context)) { if (isEntityContext(context) && isIssuesEvent(context)) {
// If prompt is provided, use agent mode (same as PR events)
if (context.inputs.prompt) {
return "agent";
}
// Check for @claude mentions or labels/assignees // Check for @claude mentions or labels/assignees
if (checkContainsTrigger(context)) { if (checkContainsTrigger(context)) {
return "tag"; return "tag";
@@ -93,16 +83,10 @@ export function getModeDescription(mode: AutoDetectedMode): string {
function validateTrackProgressEvent(context: GitHubContext): void { function validateTrackProgressEvent(context: GitHubContext): void {
// track_progress is only valid for pull_request and issue events // track_progress is only valid for pull_request and issue events
const validEvents = [ const validEvents = ["pull_request", "issues"];
"pull_request",
"issues",
"issue_comment",
"pull_request_review_comment",
"pull_request_review",
];
if (!validEvents.includes(context.eventName)) { if (!validEvents.includes(context.eventName)) {
throw new Error( throw new Error(
`track_progress is only supported for events: ${validEvents.join(", ")}. ` + `track_progress is only supported for pull_request and issue events. ` +
`Current event: ${context.eventName}`, `Current event: ${context.eventName}`,
); );
} }

View File

@@ -14,8 +14,6 @@ import { createPrompt, generateDefaultPrompt } from "../../create-prompt";
import { isEntityContext } from "../../github/context"; import { isEntityContext } from "../../github/context";
import type { PreparedContext } from "../../create-prompt/types"; import type { PreparedContext } from "../../create-prompt/types";
import type { FetchDataResult } from "../../github/data/fetcher"; import type { FetchDataResult } from "../../github/data/fetcher";
import { parseAllowedTools } from "../agent/parse-tools";
import { appendJsonSchemaArg } from "../../utils/json-schema";
/** /**
* Tag mode implementation. * Tag mode implementation.
@@ -114,10 +112,19 @@ export const tagMode: Mode = {
await createPrompt(tagMode, modeContext, githubData, context); await createPrompt(tagMode, modeContext, githubData, context);
const userClaudeArgs = process.env.CLAUDE_ARGS || ""; // Get our GitHub MCP servers configuration
const userAllowedMCPTools = parseAllowedTools(userClaudeArgs).filter( const ourMcpConfig = await prepareMcpConfig({
(tool) => tool.startsWith("mcp__github_"), 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 // Build claude_args for tag mode with required tools
// Tag mode REQUIRES these tools to function properly // Tag mode REQUIRES these tools to function properly
@@ -133,7 +140,6 @@ export const tagMode: Mode = {
"mcp__github_ci__get_ci_status", "mcp__github_ci__get_ci_status",
"mcp__github_ci__get_workflow_run_details", "mcp__github_ci__get_workflow_run_details",
"mcp__github_ci__download_job_log", "mcp__github_ci__download_job_log",
...userAllowedMCPTools,
]; ];
// Add git commands when not using commit signing // Add git commands when not using commit signing
@@ -155,18 +161,7 @@ export const tagMode: Mode = {
); );
} }
// Get our GitHub MCP servers configuration const userClaudeArgs = process.env.CLAUDE_ARGS || "";
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: Array.from(new Set(tagModeTools)),
mode: "tag",
context,
});
// Build complete claude_args with multiple --mcp-config flags // Build complete claude_args with multiple --mcp-config flags
let claudeArgs = ""; let claudeArgs = "";
@@ -178,9 +173,6 @@ export const tagMode: Mode = {
// Add required tools for tag mode // Add required tools for tag mode
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`; claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
// Add JSON schema if provided
claudeArgs = appendJsonSchemaArg(claudeArgs);
// Append user's claude_args (which may have more --mcp-config flags) // Append user's claude_args (which may have more --mcp-config flags)
if (userClaudeArgs) { if (userClaudeArgs) {
claudeArgs += ` ${userClaudeArgs}`; claudeArgs += ` ${userClaudeArgs}`;

View File

@@ -1,17 +0,0 @@
/**
* Appends JSON schema CLI argument if json_schema is provided
* Escapes schema for safe shell passing
*/
export function appendJsonSchemaArg(
claudeArgs: string,
jsonSchemaStr?: string,
): string {
const schema = jsonSchemaStr || process.env.JSON_SCHEMA || "";
if (!schema) {
return claudeArgs;
}
// CLI validates schema - just escape for safe shell passing
const escapedSchema = schema.replace(/'/g, "'\\''");
return `${claudeArgs} --json-schema '${escapedSchema}'`;
}

View File

@@ -35,7 +35,6 @@ describe("prepareMcpConfig", () => {
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
}, },
}; };
@@ -85,7 +84,6 @@ describe("prepareMcpConfig", () => {
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
context: mockContext, context: mockContext,
mode: "tag",
}); });
const parsed = JSON.parse(result); const parsed = JSON.parse(result);
@@ -106,7 +104,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
mode: "tag",
context: mockContextWithSigning, context: mockContextWithSigning,
}); });
@@ -130,7 +127,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: ["mcp__github__create_issue", "mcp__github__create_pr"], allowedTools: ["mcp__github__create_issue", "mcp__github__create_pr"],
mode: "tag",
context: mockContext, context: mockContext,
}); });
@@ -151,7 +147,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: ["mcp__github_inline_comment__create_inline_comment"], allowedTools: ["mcp__github_inline_comment__create_inline_comment"],
mode: "tag",
context: mockPRContext, context: mockPRContext,
}); });
@@ -172,7 +167,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
mode: "tag",
context: mockContext, context: mockContext,
}); });
@@ -193,7 +187,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
mode: "tag",
context: mockContextWithSigning, context: mockContextWithSigning,
}); });
@@ -213,7 +206,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
mode: "tag",
context: mockContextWithSigning, context: mockContextWithSigning,
}); });
@@ -231,7 +223,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
mode: "tag",
context: mockPRContext, context: mockPRContext,
}); });
@@ -251,7 +242,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
mode: "tag",
context: mockContext, context: mockContext,
}); });
@@ -269,7 +259,6 @@ describe("prepareMcpConfig", () => {
branch: "test-branch", branch: "test-branch",
baseBranch: "main", baseBranch: "main",
allowedTools: [], allowedTools: [],
mode: "tag",
context: mockPRContext, context: mockPRContext,
}); });

View File

@@ -1,7 +1,6 @@
import type { import type {
ParsedGitHubContext, ParsedGitHubContext,
AutomationContext, AutomationContext,
RepositoryDispatchEvent,
} from "../src/github/context"; } from "../src/github/context";
import type { import type {
IssuesEvent, IssuesEvent,
@@ -23,7 +22,6 @@ const defaultInputs = {
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
}; };
@@ -83,33 +81,6 @@ export const createMockAutomationContext = (
return { ...baseContext, ...overrides, inputs: mergedInputs }; return { ...baseContext, ...overrides, inputs: mergedInputs };
}; };
export const mockRepositoryDispatchContext: AutomationContext = {
runId: "1234567890",
eventName: "repository_dispatch",
eventAction: undefined,
repository: defaultRepository,
actor: "automation-user",
payload: {
action: "trigger-analysis",
client_payload: {
source: "issue-detective",
issue_number: 42,
repository_name: "test-owner/test-repo",
analysis_type: "bug-report",
},
repository: {
name: "test-repo",
owner: {
login: "test-owner",
},
},
sender: {
login: "automation-user",
},
} as RepositoryDispatchEvent,
inputs: defaultInputs,
};
export const mockIssueOpenedContext: ParsedGitHubContext = { export const mockIssueOpenedContext: ParsedGitHubContext = {
runId: "1234567890", runId: "1234567890",
eventName: "issues", eventName: "issues",

View File

@@ -76,11 +76,6 @@ describe("Agent Mode", () => {
}); });
expect(agentMode.shouldTrigger(scheduleContext)).toBe(false); expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
const repositoryDispatchContext = createMockAutomationContext({
eventName: "repository_dispatch",
});
expect(agentMode.shouldTrigger(repositoryDispatchContext)).toBe(false);
// Should NOT trigger for entity events without prompt // Should NOT trigger for entity events without prompt
const entityEvents = [ const entityEvents = [
"issue_comment", "issue_comment",
@@ -97,7 +92,6 @@ describe("Agent Mode", () => {
// Should trigger for ANY event when prompt is provided // Should trigger for ANY event when prompt is provided
const allEvents = [ const allEvents = [
"workflow_dispatch", "workflow_dispatch",
"repository_dispatch",
"schedule", "schedule",
"issue_comment", "issue_comment",
"pull_request", "pull_request",
@@ -107,9 +101,7 @@ describe("Agent Mode", () => {
allEvents.forEach((eventName) => { allEvents.forEach((eventName) => {
const contextWithPrompt = const contextWithPrompt =
eventName === "workflow_dispatch" || eventName === "workflow_dispatch" || eventName === "schedule"
eventName === "repository_dispatch" ||
eventName === "schedule"
? createMockAutomationContext({ ? createMockAutomationContext({
eventName, eventName,
inputs: { prompt: "Do something" }, inputs: { prompt: "Do something" },
@@ -162,11 +154,11 @@ describe("Agent Mode", () => {
githubToken: "test-token", githubToken: "test-token",
}); });
// Verify claude_args includes user args (no MCP config in agent mode without allowed tools) // Verify claude_args includes MCP config and user args
const callArgs = setOutputSpy.mock.calls[0]; const callArgs = setOutputSpy.mock.calls[0];
expect(callArgs[0]).toBe("claude_args"); expect(callArgs[0]).toBe("claude_args");
expect(callArgs[1]).toBe("--model claude-sonnet-4 --max-turns 10"); expect(callArgs[1]).toContain("--mcp-config");
expect(callArgs[1]).not.toContain("--mcp-config"); expect(callArgs[1]).toContain("--model claude-sonnet-4 --max-turns 10");
// Verify return structure - should use "main" as fallback when no env vars set // Verify return structure - should use "main" as fallback when no env vars set
expect(result).toEqual({ expect(result).toEqual({

View File

@@ -68,20 +68,4 @@ describe("parseAllowedTools", () => {
"mcp__github_comment__update", "mcp__github_comment__update",
]); ]);
}); });
test("parses kebab-case --allowed-tools", () => {
const args = "--allowed-tools mcp__github__*,mcp__github_comment__*";
expect(parseAllowedTools(args)).toEqual([
"mcp__github__*",
"mcp__github_comment__*",
]);
});
test("parses quoted kebab-case --allowed-tools", () => {
const args = '--allowed-tools "mcp__github__*,mcp__github_comment__*"';
expect(parseAllowedTools(args)).toEqual([
"mcp__github__*",
"mcp__github_comment__*",
]);
});
}); });

View File

@@ -2,11 +2,7 @@ import { describe, test, expect } from "bun:test";
import { getMode, isValidMode } from "../../src/modes/registry"; import { getMode, isValidMode } from "../../src/modes/registry";
import { agentMode } from "../../src/modes/agent"; import { agentMode } from "../../src/modes/agent";
import { tagMode } from "../../src/modes/tag"; import { tagMode } from "../../src/modes/tag";
import { import { createMockContext, createMockAutomationContext } from "../mockContext";
createMockContext,
createMockAutomationContext,
mockRepositoryDispatchContext,
} from "../mockContext";
describe("Mode Registry", () => { describe("Mode Registry", () => {
const mockContext = createMockContext({ const mockContext = createMockContext({
@@ -54,34 +50,6 @@ describe("Mode Registry", () => {
expect(mode.name).toBe("agent"); expect(mode.name).toBe("agent");
}); });
test("getMode auto-detects agent for repository_dispatch event", () => {
const mode = getMode(mockRepositoryDispatchContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode auto-detects agent for repository_dispatch with client_payload", () => {
const contextWithPayload = createMockAutomationContext({
eventName: "repository_dispatch",
payload: {
action: "trigger-analysis",
client_payload: {
source: "external-system",
metadata: { priority: "high" },
},
repository: {
name: "test-repo",
owner: { login: "test-owner" },
},
sender: { login: "automation-user" },
},
});
const mode = getMode(contextWithPayload);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
// Removed test - legacy mode names no longer supported in v1.0 // Removed test - legacy mode names no longer supported in v1.0
test("getMode auto-detects agent mode for PR opened", () => { test("getMode auto-detects agent mode for PR opened", () => {

View File

@@ -71,7 +71,6 @@ describe("checkWritePermissions", () => {
botId: String(CLAUDE_APP_BOT_ID), botId: String(CLAUDE_APP_BOT_ID),
botName: CLAUDE_BOT_LOGIN, botName: CLAUDE_BOT_LOGIN,
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
}, },
}); });
@@ -176,126 +175,4 @@ describe("checkWritePermissions", () => {
username: "test-user", username: "test-user",
}); });
}); });
describe("allowed_non_write_users bypass", () => {
test("should bypass permission check for specific user when github_token provided", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"test-user,other-user",
true,
);
expect(result).toBe(true);
expect(coreWarningSpy).toHaveBeenCalledWith(
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.",
);
});
test("should bypass permission check for all users with wildcard", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"*",
true,
);
expect(result).toBe(true);
expect(coreWarningSpy).toHaveBeenCalledWith(
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users='*'. This should only be used for workflows with very limited permissions.",
);
});
test("should NOT bypass permission check when user not in allowed list", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"other-user,another-user",
true,
);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: read",
);
});
test("should NOT bypass permission check when github_token not provided", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"test-user",
false,
);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: read",
);
});
test("should NOT bypass permission check when allowed_non_write_users is empty", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
"",
true,
);
expect(result).toBe(false);
expect(coreWarningSpy).toHaveBeenCalledWith(
"Actor has insufficient permissions: read",
);
});
test("should handle whitespace in allowed_non_write_users list", async () => {
const mockOctokit = createMockOctokit("read");
const context = createContext();
const result = await checkWritePermissions(
mockOctokit,
context,
" test-user , other-user ",
true,
);
expect(result).toBe(true);
expect(coreWarningSpy).toHaveBeenCalledWith(
"⚠️ SECURITY WARNING: Bypassing write permission check for test-user due to allowed_non_write_users configuration. This should only be used for workflows with very limited permissions.",
);
});
test("should bypass for bot users even when allowed_non_write_users is set", async () => {
const mockOctokit = createMockOctokit("none");
const context = createContext();
context.actor = "test-bot[bot]";
const result = await checkWritePermissions(
mockOctokit,
context,
"some-user",
true,
);
expect(result).toBe(true);
expect(coreInfoSpy).toHaveBeenCalledWith(
"Actor is a GitHub App: test-bot[bot]",
);
});
});
}); });

View File

@@ -1,504 +0,0 @@
#!/usr/bin/env bun
import { describe, test, expect } from "bun:test";
import {
getEventTypeAndContext,
generatePrompt,
generateDefaultPrompt,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
import type { Mode } from "../src/modes/types";
describe("pull_request_target event support", () => {
// Mock tag mode for testing
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: "External PR via pull_request_target",
body: "This PR comes from a forked repository",
author: { login: "external-contributor" },
state: "OPEN",
createdAt: "2023-01-01T00:00:00Z",
additions: 25,
deletions: 3,
baseRefName: "main",
headRefName: "feature-branch",
headRefOid: "abc123",
commits: {
totalCount: 2,
nodes: [
{
commit: {
oid: "commit1",
message: "Initial feature implementation",
author: {
name: "External Dev",
email: "external@example.com",
},
},
},
{
commit: {
oid: "commit2",
message: "Fix typos and formatting",
author: {
name: "External Dev",
email: "external@example.com",
},
},
},
],
},
files: {
nodes: [
{
path: "src/feature.ts",
additions: 20,
deletions: 2,
changeType: "MODIFIED",
},
{
path: "tests/feature.test.ts",
additions: 5,
deletions: 1,
changeType: "ADDED",
},
],
},
comments: { nodes: [] },
reviews: { nodes: [] },
},
comments: [],
changedFiles: [],
changedFilesWithSHA: [
{
path: "src/feature.ts",
additions: 20,
deletions: 2,
changeType: "MODIFIED",
sha: "abc123",
},
{
path: "tests/feature.test.ts",
additions: 5,
deletions: 1,
changeType: "ADDED",
sha: "abc123",
},
],
reviewData: { nodes: [] },
imageUrlMap: new Map<string, string>(),
};
describe("prompt generation for pull_request_target", () => {
test("should generate correct prompt for pull_request_target event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "123",
},
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should contain pull request event type and metadata
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
expect(prompt).toContain("<pr_number>123</pr_number>");
expect(prompt).toContain(
"<trigger_context>pull request opened</trigger_context>",
);
// Should contain PR-specific information
expect(prompt).toContain(
"- src/feature.ts (MODIFIED) +20/-2 SHA: abc123",
);
expect(prompt).toContain(
"- tests/feature.test.ts (ADDED) +5/-1 SHA: abc123",
);
expect(prompt).toContain("external-contributor");
expect(prompt).toContain("<repository>owner/repo</repository>");
});
test("should handle pull_request_target with commit signing disabled", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "synchronize",
isPR: true,
prNumber: "456",
},
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should include git commands for non-commit-signing mode
expect(prompt).toContain("git push");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
// Should not include commit signing tools
expect(prompt).not.toContain("mcp__github_file_ops__commit_files");
});
test("should handle pull_request_target with commit signing enabled", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "synchronize",
isPR: true,
prNumber: "456",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
// Should include commit signing tools
expect(prompt).toContain("mcp__github_file_ops__commit_files");
expect(prompt).toContain("mcp__github_file_ops__delete_files");
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
// Should not include git command instructions
expect(prompt).not.toContain("Use git commands via the Bash tool");
});
test("should treat pull_request_target same as pull_request in prompt generation", () => {
const baseContext: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "123",
},
};
// Generate prompt for pull_request
const pullRequestContext: PreparedContext = {
...baseContext,
eventData: {
...baseContext.eventData,
eventName: "pull_request",
isPR: true,
prNumber: "123",
},
};
// Generate prompt for pull_request_target
const pullRequestTargetContext: PreparedContext = {
...baseContext,
eventData: {
...baseContext.eventData,
eventName: "pull_request_target",
isPR: true,
prNumber: "123",
},
};
const pullRequestPrompt = generatePrompt(
pullRequestContext,
mockGitHubData,
false,
mockTagMode,
);
const pullRequestTargetPrompt = generatePrompt(
pullRequestTargetContext,
mockGitHubData,
false,
mockTagMode,
);
// Both should have the same event type and structure
expect(pullRequestPrompt).toContain(
"<event_type>PULL_REQUEST</event_type>",
);
expect(pullRequestTargetPrompt).toContain(
"<event_type>PULL_REQUEST</event_type>",
);
expect(pullRequestPrompt).toContain(
"<trigger_context>pull request opened</trigger_context>",
);
expect(pullRequestTargetPrompt).toContain(
"<trigger_context>pull request opened</trigger_context>",
);
// Both should contain PR-specific instructions
expect(pullRequestPrompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
expect(pullRequestTargetPrompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
});
test("should handle pull_request_target in agent mode with custom prompt", () => {
const envVars: PreparedContext = {
repository: "test/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
prompt: "Review this pull_request_target PR for security issues",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "789",
},
};
// Use agent mode which passes through the prompt as-is
const mockAgentMode: Mode = {
name: "agent",
description: "Agent mode",
shouldTrigger: () => true,
prepareContext: (context) => ({
mode: "agent",
githubContext: context,
}),
getAllowedTools: () => [],
getDisallowedTools: () => [],
shouldCreateTrackingComment: () => true,
generatePrompt: (context) => context.prompt || "default prompt",
prepare: async () => ({
commentId: 123,
branchInfo: {
baseBranch: "main",
currentBranch: "main",
claudeBranch: undefined,
},
mcpConfig: "{}",
}),
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockAgentMode,
);
expect(prompt).toBe(
"Review this pull_request_target PR for security issues",
);
});
test("should handle pull_request_target with no custom prompt", () => {
const envVars: PreparedContext = {
repository: "test/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "synchronize",
isPR: true,
prNumber: "456",
},
};
const prompt = generatePrompt(
envVars,
mockGitHubData,
false,
mockTagMode,
);
// Should generate default prompt structure
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<pr_number>456</pr_number>");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
});
});
describe("pull_request_target vs pull_request behavior consistency", () => {
test("should produce identical event processing for both event types", () => {
const baseEventData = {
eventAction: "opened",
isPR: true,
prNumber: "100",
};
const pullRequestEvent: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
...baseEventData,
eventName: "pull_request",
isPR: true,
prNumber: "100",
},
};
const pullRequestTargetEvent: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
...baseEventData,
eventName: "pull_request_target",
isPR: true,
prNumber: "100",
},
};
// Both should have identical event type detection
const prResult = getEventTypeAndContext(pullRequestEvent);
const prtResult = getEventTypeAndContext(pullRequestTargetEvent);
expect(prResult.eventType).toBe(prtResult.eventType);
expect(prResult.triggerContext).toBe(prtResult.triggerContext);
});
test("should handle edge cases in pull_request_target events", () => {
// Test with minimal event data
const minimalContext: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
isPR: true,
prNumber: "1",
},
};
const result = getEventTypeAndContext(minimalContext);
expect(result.eventType).toBe("PULL_REQUEST");
expect(result.triggerContext).toBe("pull request event");
// Should not throw when generating prompt
expect(() => {
generatePrompt(minimalContext, mockGitHubData, false, mockTagMode);
}).not.toThrow();
});
test("should handle all valid pull_request_target actions", () => {
const actions = ["opened", "synchronize", "reopened", "closed", "edited"];
actions.forEach((action) => {
const context: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: action,
isPR: true,
prNumber: "1",
},
};
const result = getEventTypeAndContext(context);
expect(result.eventType).toBe("PULL_REQUEST");
expect(result.triggerContext).toBe(`pull request ${action}`);
});
});
});
describe("security considerations for pull_request_target", () => {
test("should maintain same prompt structure regardless of event source", () => {
// Test that external PRs don't get different treatment in prompts
const internalPR: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request",
eventAction: "opened",
isPR: true,
prNumber: "1",
},
};
const externalPR: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_target",
eventAction: "opened",
isPR: true,
prNumber: "1",
},
};
const internalPrompt = generatePrompt(
internalPR,
mockGitHubData,
false,
mockTagMode,
);
const externalPrompt = generatePrompt(
externalPR,
mockGitHubData,
false,
mockTagMode,
);
// Should have same tool access patterns
expect(
internalPrompt.includes("mcp__github_comment__update_claude_comment"),
).toBe(
externalPrompt.includes("mcp__github_comment__update_claude_comment"),
);
// Should have same branch handling instructions
expect(
internalPrompt.includes(
"Always push to the existing branch when triggered on a PR",
),
).toBe(
externalPrompt.includes(
"Always push to the existing branch when triggered on a PR",
),
);
});
});
});

View File

@@ -20,10 +20,7 @@ describe("detectMode with enhanced routing", () => {
branchPrefix: "claude/", branchPrefix: "claude/",
useStickyComment: false, useStickyComment: false,
useCommitSigning: false, useCommitSigning: false,
botId: "123456",
botName: "claude-bot",
allowedBots: "", allowedBots: "",
allowedNonWriteUsers: "",
trackProgress: false, trackProgress: false,
}, },
}; };
@@ -116,33 +113,6 @@ describe("detectMode with enhanced routing", () => {
expect(detectMode(context)).toBe("agent"); expect(detectMode(context)).toBe("agent");
}); });
it("should use agent mode for issues with explicit prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "Test issue" } } as any,
entityNumber: 1,
isPR: false,
inputs: { ...baseContext.inputs, prompt: "Analyze this issue" },
};
expect(detectMode(context)).toBe("agent");
});
it("should use tag mode for issues with @claude mention and no prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "@claude help" } } as any,
entityNumber: 1,
isPR: false,
};
expect(detectMode(context)).toBe("tag");
});
}); });
describe("Comment Events (unchanged behavior)", () => { describe("Comment Events (unchanged behavior)", () => {
@@ -203,7 +173,7 @@ describe("detectMode with enhanced routing", () => {
}; };
expect(() => detectMode(context)).toThrow( expect(() => detectMode(context)).toThrow(
/track_progress is only supported /, /track_progress is only supported for pull_request and issue events/,
); );
}); });