Compare commits
317 Commits
ashwin/tes
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef50f123a3 | ||
|
|
b3c0320e7e | ||
|
|
c93e8fe879 | ||
|
|
11a9dadd19 | ||
|
|
567fe954a4 | ||
|
|
2da6cfae68 | ||
|
|
e58dfa5555 | ||
|
|
6ee201f023 | ||
|
|
b4d6741327 | ||
|
|
4e5d8b13ca | ||
|
|
5d5c10a4f3 | ||
|
|
632a368e81 | ||
|
|
4c682d8b65 | ||
|
|
38ec876110 | ||
|
|
0d2971c794 | ||
|
|
c68f82cb11 | ||
|
|
78758edf84 | ||
|
|
c3d45e8e94 | ||
|
|
931e620273 | ||
|
|
905d4eb99a | ||
|
|
5fb899572b | ||
|
|
c3bf66dbc2 | ||
|
|
3943183052 | ||
|
|
65f29cf68e | ||
|
|
1c8b699d43 | ||
|
|
ff49ec5fd6 | ||
|
|
25474bfe8b | ||
|
|
b47fd721da | ||
|
|
c26cb6427d | ||
|
|
657fb7c9c9 | ||
|
|
2ff1acb3ee | ||
|
|
b2fdd80112 | ||
|
|
26ddc358fe | ||
|
|
398370690e | ||
|
|
6cad158a17 | ||
|
|
0f1fe5ef85 | ||
|
|
6e2bd52842 | ||
|
|
3534c326a5 | ||
|
|
6685b26dfb | ||
|
|
5150ea9643 | ||
|
|
eb8baa46af | ||
|
|
f328a5c889 | ||
|
|
b15d4751a6 | ||
|
|
d5db8208f9 | ||
|
|
d8af4e9f01 | ||
|
|
f37c786ad3 | ||
|
|
21b0f0f9aa | ||
|
|
27f549ae64 | ||
|
|
263993d836 | ||
|
|
85133eeab2 | ||
|
|
1eddb334cf | ||
|
|
0432df8bfe | ||
|
|
ba026a3e56 | ||
|
|
c95e735eb1 | ||
|
|
58dbe8ed68 | ||
|
|
c281e17d7f | ||
|
|
408a40e7c2 | ||
|
|
bee87b3258 | ||
|
|
32156b120b | ||
|
|
7225f045c6 | ||
|
|
88c168b39e | ||
|
|
e7b588b6ea | ||
|
|
094bd24d57 | ||
|
|
3ac52d0da9 | ||
|
|
0ee1beea58 | ||
|
|
ff9acae588 | ||
|
|
6062f37096 | ||
|
|
df37d2f076 | ||
|
|
1ba15be4f0 | ||
|
|
9ddce40de8 | ||
|
|
1b422b3517 | ||
|
|
4c044bb2f5 | ||
|
|
cd77b50d2b | ||
|
|
0e80d3c5b8 | ||
|
|
f956510b1a | ||
|
|
5d0cc745cd | ||
|
|
567be3da98 | ||
|
|
eb99fb38f0 | ||
|
|
33fbb80626 | ||
|
|
3428ca8991 | ||
|
|
26ec041249 | ||
|
|
1fc90f3ed9 | ||
|
|
e763fe78de | ||
|
|
5f8e5bfe5b | ||
|
|
73367208d0 | ||
|
|
64c7a0ef71 | ||
|
|
220272d388 | ||
|
|
ba7fa4bcf0 | ||
|
|
1dd74842e5 | ||
|
|
273fe82540 | ||
|
|
e750645f1b | ||
|
|
cd4b150a2a | ||
|
|
ade221fd1c | ||
|
|
48fe7dd592 | ||
|
|
6ae1b29ba2 | ||
|
|
7af3506741 | ||
|
|
35a9e0292d | ||
|
|
fa3312a107 | ||
|
|
dd8541688d | ||
|
|
edd85d6153 | ||
|
|
0cf5eeec4f | ||
|
|
e6cb7a7ce3 | ||
|
|
2f8ba26a21 | ||
|
|
cc5ef44546 | ||
|
|
f6a1c4c1b4 | ||
|
|
68cfeead18 | ||
|
|
f5088835af | ||
|
|
ea36d6abde | ||
|
|
c22f7c3f9d | ||
|
|
f669191d7d | ||
|
|
8c383c5de3 | ||
|
|
1bb0e7464b | ||
|
|
23ed4cb53d | ||
|
|
21e3fe0542 | ||
|
|
b433f16b30 | ||
|
|
7695f7866a | ||
|
|
d5b01b6843 | ||
|
|
6c61301d8e | ||
|
|
db388438c1 | ||
|
|
b113f49a56 | ||
|
|
7057f3318b | ||
|
|
f09dc9a6a3 | ||
|
|
006aaf2935 | ||
|
|
9a3c761f54 | ||
|
|
6867bb3ab0 | ||
|
|
98af40b63c | ||
|
|
4ce5f178c2 | ||
|
|
fab4258c6e | ||
|
|
70e16deb18 | ||
|
|
0ed5eeaa54 | ||
|
|
01e756b34e | ||
|
|
ff34ce0ff0 | ||
|
|
2817c54db8 | ||
|
|
d01eedd981 | ||
|
|
49046e0709 | ||
|
|
32ac7269f2 | ||
|
|
fe72061e16 | ||
|
|
231bd75b71 | ||
|
|
4126f9d975 | ||
|
|
ba45bb9506 | ||
|
|
0c704179b5 | ||
|
|
f64219702d | ||
|
|
8341a564b0 | ||
|
|
2804b4174b | ||
|
|
2316a9a8db | ||
|
|
49cfcf8107 | ||
|
|
e208124d29 | ||
|
|
ba60ef7ba2 | ||
|
|
f3c892ca8d | ||
|
|
6e896a06bb | ||
|
|
a017b830c0 | ||
|
|
75f52e56b2 | ||
|
|
1bbc9e7ff7 | ||
|
|
625ea1519c | ||
|
|
a9171f0ced | ||
|
|
4778aeae4c | ||
|
|
b6e5a9f27a | ||
|
|
5d91d7d217 | ||
|
|
90006bcae7 | ||
|
|
005436f51d | ||
|
|
1b8ee3b941 | ||
|
|
c247cb152d | ||
|
|
cefa60067a | ||
|
|
7a708f68fa | ||
|
|
5da7ba548c | ||
|
|
964b8355fb | ||
|
|
c83d67a9b9 | ||
|
|
c9ec2b02b4 | ||
|
|
63ea7e3174 | ||
|
|
653f9cd7a3 | ||
|
|
b17b541bbc | ||
|
|
7e4bf87b1c | ||
|
|
154d0de144 | ||
|
|
3ba9f7c8c2 | ||
|
|
e5b07416ea | ||
|
|
b89827f8d1 | ||
|
|
7145c3e051 | ||
|
|
db4548b597 | ||
|
|
0d19335299 | ||
|
|
95be46676d | ||
|
|
f98c1a5aa8 | ||
|
|
b0c32b65f9 | ||
|
|
d7b6d50442 | ||
|
|
f375cabfab | ||
|
|
9acae263e7 | ||
|
|
67bf0594ce | ||
|
|
b58533dbe0 | ||
|
|
bda9bf08de | ||
|
|
79b343c094 | ||
|
|
609c388361 | ||
|
|
f0c8eb2980 | ||
|
|
68a0348c20 | ||
|
|
dc06a34646 | ||
|
|
a3bb51dac1 | ||
|
|
6610520549 | ||
|
|
e2eb96f51d | ||
|
|
05c95aed79 | ||
|
|
bb4a3f68f7 | ||
|
|
2acd1f7011 | ||
|
|
469fc9c1a4 | ||
|
|
90da6b6e15 | ||
|
|
752ba96ea1 | ||
|
|
66bf95c07f | ||
|
|
6337623ebb | ||
|
|
6d79044f1d | ||
|
|
a7e4c51380 | ||
|
|
7febbb006b | ||
|
|
798cf0988d | ||
|
|
8458f4399d | ||
|
|
f9b2917716 | ||
|
|
f092d4cefd | ||
|
|
c2edeab4c3 | ||
|
|
4318310481 | ||
|
|
11571151c4 | ||
|
|
70193f466c | ||
|
|
9db20ef677 | ||
|
|
6902c227aa | ||
|
|
e45f28fae7 | ||
|
|
8c4e1e7eb1 | ||
|
|
906bd89c74 | ||
|
|
08f88abe2b | ||
|
|
14ab4250bb | ||
|
|
c7fdd19642 | ||
|
|
92d173475f | ||
|
|
108e982900 | ||
|
|
7bb53ae6ee | ||
|
|
804b418b93 | ||
|
|
500439cb9b | ||
|
|
4cda0ef6d1 | ||
|
|
037b85d0d2 | ||
|
|
8a1c437175 | ||
|
|
56c8ae7d88 | ||
|
|
f4d737af0b | ||
|
|
29fe50368c | ||
|
|
8ad13bd20b | ||
|
|
7b914ae5c0 | ||
|
|
d4c09790f5 | ||
|
|
5033c581bb | ||
|
|
f8749bd14b | ||
|
|
f30f5eecfc | ||
|
|
fc4013af38 | ||
|
|
96524b7ffe | ||
|
|
fd20c95358 | ||
|
|
d808160c26 | ||
|
|
3eacedbeb7 | ||
|
|
f52f12eba5 | ||
|
|
4a85933f25 | ||
|
|
ba6edd55ef | ||
|
|
06461dddff | ||
|
|
c2a94eead0 | ||
|
|
1c0c3eaced | ||
|
|
23d2d6c6b4 | ||
|
|
e8bad57227 | ||
|
|
0a6d62601b | ||
|
|
777ffcbfc9 | ||
|
|
dc58efed33 | ||
|
|
e5437bfbc5 | ||
|
|
b2dd1006a0 | ||
|
|
ac1a3207f3 | ||
|
|
521d069da7 | ||
|
|
7e4b782d5f | ||
|
|
4fb0ef3be0 | ||
|
|
14ac8aa20e | ||
|
|
90d189f3ab | ||
|
|
9c09b26b2d | ||
|
|
2086c977a5 | ||
|
|
851ef5b84e | ||
|
|
1ce8153c18 | ||
|
|
00391ab25e | ||
|
|
426380f01b | ||
|
|
77f51d2905 | ||
|
|
7e5b42b197 | ||
|
|
1b7c7a77d3 | ||
|
|
bd70a3ef2b | ||
|
|
f4954b5256 | ||
|
|
93f8ab56c2 | ||
|
|
93028b410e | ||
|
|
838d4d9d25 | ||
|
|
7ed3b616d5 | ||
|
|
09ea2f00e1 | ||
|
|
455b943dd7 | ||
|
|
063d17ebb2 | ||
|
|
2e92922dd6 | ||
|
|
a5528eec74 | ||
|
|
1d4650c102 | ||
|
|
86d6f44e34 | ||
|
|
c1adac956c | ||
|
|
f197e7bfd5 | ||
|
|
89f9131f6c | ||
|
|
b78e1c0244 | ||
|
|
abf075daf2 | ||
|
|
a3ff61d47a | ||
|
|
1b7eb924f1 | ||
|
|
0f7dfed927 | ||
|
|
11a01b7183 | ||
|
|
69dec299f8 | ||
|
|
1a8e7d330a | ||
|
|
9975f36410 | ||
|
|
c1ffc8a0e8 | ||
|
|
13e47489f4 | ||
|
|
765fadc6a6 | ||
|
|
fd2c17f101 | ||
|
|
a4a723b927 | ||
|
|
d22fa6061b | ||
|
|
63f1c772bd | ||
|
|
fb823f6dd6 | ||
|
|
9e9123239f | ||
|
|
791fcb9fd1 | ||
|
|
9365bbe4af | ||
|
|
2e6fc44bd4 | ||
|
|
a6ca65328b | ||
|
|
ce697c0d4c | ||
|
|
b60e3f0e60 | ||
|
|
3ed14485f8 | ||
|
|
45408b4058 | ||
|
|
1f8cfe7658 | ||
|
|
a6888c03f2 |
61
.claude/agents/code-quality-reviewer.md
Normal file
61
.claude/agents/code-quality-reviewer.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
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.
|
||||
56
.claude/agents/documentation-accuracy-reviewer.md
Normal file
56
.claude/agents/documentation-accuracy-reviewer.md
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
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.
|
||||
53
.claude/agents/performance-reviewer.md
Normal file
53
.claude/agents/performance-reviewer.md
Normal file
@ -0,0 +1,53 @@
|
||||
---
|
||||
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.
|
||||
59
.claude/agents/security-code-reviewer.md
Normal file
59
.claude/agents/security-code-reviewer.md
Normal file
@ -0,0 +1,59 @@
|
||||
---
|
||||
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.
|
||||
52
.claude/agents/test-coverage-reviewer.md
Normal file
52
.claude/agents/test-coverage-reviewer.md
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
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.
|
||||
61
.claude/commands/label-issue.md
Normal file
61
.claude/commands/label-issue.md
Normal file
@ -0,0 +1,61 @@
|
||||
---
|
||||
allowed-tools: Bash(./scripts/gh.sh:*),Bash(./scripts/edit-issue-labels.sh:*)
|
||||
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: `./scripts/gh.sh label list`. Run exactly this command with nothing else.
|
||||
|
||||
2. Next, use gh wrapper commands to get context about the issue:
|
||||
|
||||
- Use `./scripts/gh.sh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
|
||||
- Use `./scripts/gh.sh search issues` to find similar issues that might provide context for proper categorization
|
||||
- `./scripts/gh.sh` is a wrapper for `gh` CLI. Example commands:
|
||||
- `./scripts/gh.sh label list` — fetch all available labels
|
||||
- `./scripts/gh.sh issue view 123` — view issue details
|
||||
- `./scripts/gh.sh issue view 123 --comments` — view with comments
|
||||
- `./scripts/gh.sh search issues "query" --limit 10` — search for issues
|
||||
- `./scripts/edit-issue-labels.sh` — apply labels to the issue
|
||||
|
||||
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 ./scripts/gh.sh label list
|
||||
- Consider platform labels (android, ios) if applicable
|
||||
- If you find similar issues using ./scripts/gh.sh 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 `./scripts/edit-issue-labels.sh --add-label LABEL1 --add-label LABEL2` to apply your selected labels (issue number is read from the workflow event)
|
||||
- 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 ./scripts/edit-issue-labels.sh
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
|
||||
---
|
||||
20
.claude/commands/review-pr.md
Normal file
20
.claude/commands/review-pr.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
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.
|
||||
|
||||
---
|
||||
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PostToolUse": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "bun run format"
|
||||
}
|
||||
],
|
||||
"matcher": "Edit|Write|MultiEdit"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
132
.github/workflows/bump-claude-code-version.yml
vendored
132
.github/workflows/bump-claude-code-version.yml
vendored
@ -1,132 +0,0 @@
|
||||
name: Bump Claude Code Version
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [bump_claude_code_version]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Claude Code version to bump to"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
bump-version:
|
||||
name: Bump Claude Code Version
|
||||
runs-on: ubuntu-latest
|
||||
environment: release
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4
|
||||
with:
|
||||
token: ${{ secrets.RELEASE_PAT }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get version from event payload
|
||||
id: get_version
|
||||
run: |
|
||||
# Get version from either repository_dispatch or workflow_dispatch
|
||||
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
||||
NEW_VERSION="${CLIENT_PAYLOAD_VERSION}"
|
||||
else
|
||||
NEW_VERSION="${INPUT_VERSION}"
|
||||
fi
|
||||
|
||||
# Sanitize the version to avoid issues enabled by problematic characters
|
||||
NEW_VERSION=$(echo "$NEW_VERSION" | tr -d '`;$(){}[]|&<>' | tr -s ' ' '-')
|
||||
|
||||
if [ -z "$NEW_VERSION" ]; then
|
||||
echo "Error: version not provided"
|
||||
exit 1
|
||||
fi
|
||||
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
|
||||
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
INPUT_VERSION: ${{ inputs.version }}
|
||||
CLIENT_PAYLOAD_VERSION: ${{ github.event.client_payload.version }}
|
||||
|
||||
- name: Create branch and update base-action/action.yml
|
||||
run: |
|
||||
# Variables
|
||||
TIMESTAMP=$(date +'%Y%m%d-%H%M%S')
|
||||
BRANCH_NAME="bump-claude-code-${{ env.NEW_VERSION }}-$TIMESTAMP"
|
||||
|
||||
echo "BRANCH_NAME=$BRANCH_NAME" >> $GITHUB_ENV
|
||||
|
||||
# Get the default branch
|
||||
DEFAULT_BRANCH=$(gh api repos/${GITHUB_REPOSITORY} --jq '.default_branch')
|
||||
echo "DEFAULT_BRANCH=$DEFAULT_BRANCH" >> $GITHUB_ENV
|
||||
|
||||
# Get the latest commit SHA from the default branch
|
||||
BASE_SHA=$(gh api repos/${GITHUB_REPOSITORY}/git/refs/heads/$DEFAULT_BRANCH --jq '.object.sha')
|
||||
|
||||
# Create a new branch
|
||||
gh api \
|
||||
--method POST \
|
||||
repos/${GITHUB_REPOSITORY}/git/refs \
|
||||
-f ref="refs/heads/$BRANCH_NAME" \
|
||||
-f sha="$BASE_SHA"
|
||||
|
||||
# Get the current base-action/action.yml content
|
||||
ACTION_CONTENT=$(gh api repos/${GITHUB_REPOSITORY}/contents/base-action/action.yml?ref=$DEFAULT_BRANCH --jq '.content' | base64 -d)
|
||||
|
||||
# Update the Claude Code version in the npm install command
|
||||
UPDATED_CONTENT=$(echo "$ACTION_CONTENT" | sed -E "s/(npm install -g @anthropic-ai\/claude-code@)[0-9]+\.[0-9]+\.[0-9]+/\1${{ env.NEW_VERSION }}/")
|
||||
|
||||
# Verify the change would be made
|
||||
if ! echo "$UPDATED_CONTENT" | grep -q "@anthropic-ai/claude-code@${{ env.NEW_VERSION }}"; then
|
||||
echo "Error: Failed to update Claude Code version in content"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the current SHA of base-action/action.yml for the update API call
|
||||
FILE_SHA=$(gh api repos/${GITHUB_REPOSITORY}/contents/base-action/action.yml?ref=$DEFAULT_BRANCH --jq '.sha')
|
||||
|
||||
# Create the updated base-action/action.yml content in base64
|
||||
echo "$UPDATED_CONTENT" | base64 > action.yml.b64
|
||||
|
||||
# Commit the updated base-action/action.yml via GitHub API
|
||||
gh api \
|
||||
--method PUT \
|
||||
repos/${GITHUB_REPOSITORY}/contents/base-action/action.yml \
|
||||
-f message="chore: bump Claude Code version to ${{ env.NEW_VERSION }}" \
|
||||
-F content=@action.yml.b64 \
|
||||
-f sha="$FILE_SHA" \
|
||||
-f branch="$BRANCH_NAME"
|
||||
|
||||
echo "Successfully created branch and updated Claude Code version to ${{ env.NEW_VERSION }}"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
|
||||
- name: Create Pull Request
|
||||
run: |
|
||||
# Determine trigger type for PR body
|
||||
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
||||
TRIGGER_INFO="repository dispatch event"
|
||||
else
|
||||
TRIGGER_INFO="manual workflow dispatch by @${GITHUB_ACTOR}"
|
||||
fi
|
||||
|
||||
# Create PR body with proper YAML escape
|
||||
printf -v PR_BODY "## Bump Claude Code to ${{ env.NEW_VERSION }}\n\nThis PR updates the Claude Code version in base-action/action.yml to ${{ env.NEW_VERSION }}.\n\n### Changes\n- Updated Claude Code version from current to \`${{ env.NEW_VERSION }}\`\n\n### Triggered by\n- $TRIGGER_INFO\n\n🤖 This PR was automatically created by the bump-claude-code-version workflow."
|
||||
|
||||
echo "Creating PR with gh pr create command"
|
||||
PR_URL=$(gh pr create \
|
||||
--repo "${GITHUB_REPOSITORY}" \
|
||||
--title "chore: bump Claude Code version to ${{ env.NEW_VERSION }}" \
|
||||
--body "$PR_BODY" \
|
||||
--base "${DEFAULT_BRANCH}" \
|
||||
--head "${BRANCH_NAME}")
|
||||
|
||||
echo "PR created successfully: $PR_URL"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
GITHUB_ACTOR: ${{ github.actor }}
|
||||
DEFAULT_BRANCH: ${{ env.DEFAULT_BRANCH }}
|
||||
BRANCH_NAME: ${{ env.BRANCH_NAME }}
|
||||
37
.github/workflows/ci-all.yml
vendored
Normal file
37
.github/workflows/ci-all.yml
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
# Orchestrates all CI workflows - runs on PRs, pushes to main, and manual dispatch
|
||||
# Individual test workflows are called as reusable workflows
|
||||
name: CI All
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
uses: ./.github/workflows/ci.yml
|
||||
|
||||
test-base-action:
|
||||
uses: ./.github/workflows/test-base-action.yml
|
||||
secrets: inherit # Required for ANTHROPIC_API_KEY
|
||||
|
||||
test-custom-executables:
|
||||
uses: ./.github/workflows/test-custom-executables.yml
|
||||
secrets: inherit
|
||||
|
||||
test-mcp-servers:
|
||||
uses: ./.github/workflows/test-mcp-servers.yml
|
||||
secrets: inherit
|
||||
|
||||
test-settings:
|
||||
uses: ./.github/workflows/test-settings.yml
|
||||
secrets: inherit
|
||||
|
||||
test-structured-output:
|
||||
uses: ./.github/workflows/test-structured-output.yml
|
||||
secrets: inherit
|
||||
9
.github/workflows/ci.yml
vendored
9
.github/workflows/ci.yml
vendored
@ -1,15 +1,14 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
@ -24,7 +23,7 @@ jobs:
|
||||
prettier:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
@ -39,7 +38,7 @@ jobs:
|
||||
typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
with:
|
||||
|
||||
29
.github/workflows/claude-review.yml
vendored
29
.github/workflows/claude-review.yml
vendored
@ -1,33 +1,28 @@
|
||||
name: Auto review PRs
|
||||
name: PR Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
auto-review:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Auto review PR
|
||||
uses: anthropics/claude-code-action@main
|
||||
- name: PR Review with Progress Tracking
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
direct_prompt: |
|
||||
Please review this PR. Look at the changes and provide thoughtful feedback on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Suggestions for improvements
|
||||
- Overall architecture and design decisions
|
||||
- Documentation consistency: Verify that README.md and other documentation files are updated to reflect any code changes (especially new inputs, features, or configuration options)
|
||||
|
||||
Be constructive and specific in your feedback. Give inline comments where applicable.
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
allowed_tools: "mcp__github__create_pending_pull_request_review,mcp__github__add_comment_to_pending_review,mcp__github__submit_pending_pull_request_review,mcp__github__get_pull_request_diff"
|
||||
|
||||
prompt: "/review-pr REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }}"
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment"
|
||||
--model "claude-opus-4-7"
|
||||
|
||||
38
.github/workflows/claude-test.yml
vendored
38
.github/workflows/claude-test.yml
vendored
@ -1,38 +0,0 @@
|
||||
# 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 }}
|
||||
12
.github/workflows/claude.yml
vendored
12
.github/workflows/claude.yml
vendored
@ -19,21 +19,21 @@ jobs:
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
issues: read
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1
|
||||
uses: anthropics/claude-code-action@main
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
|
||||
--model "claude-opus-4-1-20250805"
|
||||
--model "claude-opus-4-7"
|
||||
|
||||
95
.github/workflows/issue-triage.yml
vendored
95
.github/workflows/issue-triage.yml
vendored
@ -14,95 +14,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
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
|
||||
uses: anthropics/claude-code-base-action@v1
|
||||
with:
|
||||
prompt: $(cat /tmp/claude-prompts/triage-prompt.txt)
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues
|
||||
--mcp-config /tmp/mcp-config/mcp-servers.json
|
||||
uses: anthropics/claude-code-action@main
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'
|
||||
with:
|
||||
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
47
.github/workflows/non-write-users-check.yml
vendored
Normal file
47
.github/workflows/non-write-users-check.yml
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
name: Non-write Users Check
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/**"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
allowed-non-write-check:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- run: |
|
||||
DIFF=$(gh pr diff "$PR_NUMBER" -R "$REPO" || true)
|
||||
|
||||
if ! echo "$DIFF" | grep -qE '^diff --git a/\.github/.*\.ya?ml'; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
MATCHES=$(echo "$DIFF" | grep "^+.*allowed_non_write_users" || true)
|
||||
|
||||
if [ -z "$MATCHES" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
EXISTING=$(gh pr view "$PR_NUMBER" -R "$REPO" --json comments --jq '.comments[].body' \
|
||||
| grep -c "<!-- non-write-users-check -->" || true)
|
||||
|
||||
if [ "$EXISTING" -gt 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
gh pr comment "$PR_NUMBER" -R "$REPO" --body '<!-- non-write-users-check -->
|
||||
**`allowed_non_write_users` detected**
|
||||
|
||||
This PR adds or modifies `allowed_non_write_users`, which allows users without write access to trigger Claude Code Action workflows. This can introduce security risks.
|
||||
|
||||
If this is a new flow, please make sure you actually need `allowed_non_write_users`. If you are editing an existing workflow, double check that you are not adding new Claude permissions which might lead to a vulnerability.
|
||||
|
||||
See existing workflows in this repo for safe usage examples, or contact the AppSec team.'
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
REPO: ${{ github.repository }}
|
||||
110
.github/workflows/release.yml
vendored
110
.github/workflows/release.yml
vendored
@ -8,10 +8,23 @@ on:
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
workflow_run:
|
||||
workflows: ["CI All"]
|
||||
types:
|
||||
- completed
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
runs-on: ubuntu-latest
|
||||
# Run if: manual dispatch OR (CI All succeeded AND commit is a version bump)
|
||||
if: |
|
||||
github.event_name == 'workflow_dispatch' ||
|
||||
(github.event.workflow_run.conclusion == 'success' &&
|
||||
github.event.workflow_run.head_branch == 'main' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
startsWith(github.event.workflow_run.head_commit.message, 'chore: bump Claude Code to'))
|
||||
environment: production
|
||||
permissions:
|
||||
contents: write
|
||||
@ -19,7 +32,7 @@ jobs:
|
||||
next_version: ${{ steps.next_version.outputs.next_version }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -84,14 +97,15 @@ jobs:
|
||||
|
||||
update-major-tag:
|
||||
needs: create-release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
# Skip for dry runs (workflow_run events are never dry runs)
|
||||
if: github.event_name == 'workflow_run' || !inputs.dry_run
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -109,48 +123,48 @@ jobs:
|
||||
|
||||
echo "Updated $major_version tag to point to $next_version"
|
||||
|
||||
release-base-action:
|
||||
needs: create-release
|
||||
if: ${{ !inputs.dry_run }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Checkout base-action repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: anthropics/claude-code-base-action
|
||||
token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
fetch-depth: 0
|
||||
|
||||
# - name: Create and push tag
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# # Create the version tag
|
||||
# git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
||||
# git push origin "$next_version"
|
||||
|
||||
# # Update the beta tag
|
||||
# git tag -fa beta -m "Update beta tag to ${next_version}"
|
||||
# git push origin beta --force
|
||||
|
||||
# - name: Create GitHub release
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
|
||||
# # Create the release
|
||||
# gh release create "$next_version" \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --title "$next_version" \
|
||||
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
|
||||
# --latest=false
|
||||
|
||||
# # Update beta release to be latest
|
||||
# gh release edit beta \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --latest
|
||||
# release-base-action:
|
||||
# needs: create-release
|
||||
# if: ${{ !inputs.dry_run }}
|
||||
# runs-on: ubuntu-latest
|
||||
# environment: production
|
||||
# steps:
|
||||
# - name: Checkout base-action repo
|
||||
# uses: actions/checkout@v6
|
||||
# with:
|
||||
# repository: anthropics/claude-code-base-action
|
||||
# token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
# fetch-depth: 0
|
||||
#
|
||||
# - name: Create and push tag
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
#
|
||||
# git config user.name "github-actions[bot]"
|
||||
# git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
#
|
||||
# # Create the version tag
|
||||
# git tag -a "$next_version" -m "Release $next_version - synced from claude-code-action"
|
||||
# git push origin "$next_version"
|
||||
#
|
||||
# # Update the beta tag
|
||||
# git tag -fa beta -m "Update beta tag to ${next_version}"
|
||||
# git push origin beta --force
|
||||
#
|
||||
# - name: Create GitHub release
|
||||
# env:
|
||||
# GH_TOKEN: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }}
|
||||
# run: |
|
||||
# next_version="${{ needs.create-release.outputs.next_version }}"
|
||||
#
|
||||
# # Create the release
|
||||
# gh release create "$next_version" \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --title "$next_version" \
|
||||
# --notes "Release $next_version - synced from anthropics/claude-code-action" \
|
||||
# --latest=false
|
||||
#
|
||||
# # Update beta release to be latest
|
||||
# gh release edit beta \
|
||||
# --repo anthropics/claude-code-base-action \
|
||||
# --latest
|
||||
|
||||
4
.github/workflows/sync-base-action.yml
vendored
4
.github/workflows/sync-base-action.yml
vendored
@ -94,5 +94,5 @@ jobs:
|
||||
echo "✅ Successfully synced \`base-action\` directory to [anthropics/claude-code-base-action](https://github.com/anthropics/claude-code-base-action)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Source commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/anthropics/claude-code-action/commit/${GITHUB_SHA})" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Triggered by**: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Actor**: @${{ github.actor }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Triggered by**: $GITHUB_EVENT_NAME" >> $GITHUB_STEP_SUMMARY
|
||||
echo "- **Actor**: @$GITHUB_ACTOR" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
4
.github/workflows/test-base-action.yml
vendored
4
.github/workflows/test-base-action.yml
vendored
@ -1,9 +1,6 @@
|
||||
name: Test Claude Code Action
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@ -11,6 +8,7 @@ on:
|
||||
description: "Test prompt for Claude"
|
||||
required: false
|
||||
default: "List the files in the current directory starting with 'package'"
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test-inline-prompt:
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
name: Test Custom Executables
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test-custom-executables:
|
||||
|
||||
4
.github/workflows/test-mcp-servers.yml
vendored
4
.github/workflows/test-mcp-servers.yml
vendored
@ -1,11 +1,9 @@
|
||||
name: Test MCP Servers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test-mcp-integration:
|
||||
|
||||
8
.github/workflows/test-settings.yml
vendored
8
.github/workflows/test-settings.yml
vendored
@ -1,11 +1,9 @@
|
||||
name: Test Settings Feature
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
test-settings-inline-allow:
|
||||
@ -67,7 +65,7 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "This should not work"
|
||||
Run the command `echo $HOME` to check the home directory path
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
settings: |
|
||||
{
|
||||
@ -163,7 +161,7 @@ jobs:
|
||||
uses: ./base-action
|
||||
with:
|
||||
prompt: |
|
||||
Use Bash to echo "This should not work from file"
|
||||
Run the command `echo $HOME` to check the home directory path
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
settings: "test-settings.json"
|
||||
|
||||
|
||||
305
.github/workflows/test-structured-output.yml
vendored
Normal file
305
.github/workflows/test-structured-output.yml
vendored
Normal file
@ -0,0 +1,305 @@
|
||||
name: Test Structured Outputs
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
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
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--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"]}'
|
||||
|
||||
- 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: []
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"items":{"type":"array","items":{"type":"string"}},"config":{"type":"object"},"empty_array":{"type":"array"}},"required":["items","config","empty_array"]}'
|
||||
|
||||
- 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
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"zero":{"type":"number"},"empty_string":{"type":"string"},"negative":{"type":"number"},"decimal":{"type":"number"}},"required":["zero","empty_string","negative","decimal"]}'
|
||||
|
||||
- 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}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"test-result":{"type":"string"},"item_count":{"type":"number"}},"required":["test-result","item_count"]}'
|
||||
|
||||
- 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}"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools Bash
|
||||
--json-schema '{"type":"object","properties":{"done":{"type":"boolean"}},"required":["done"]}'
|
||||
|
||||
- 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
|
||||
142
CLAUDE.md
142
CLAUDE.md
@ -1,136 +1,44 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Development Tools
|
||||
|
||||
- Runtime: Bun 1.2.11
|
||||
- TypeScript with strict configuration
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Available npm/bun scripts from package.json:
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Test
|
||||
bun test
|
||||
|
||||
# Formatting
|
||||
bun run format # Format code with prettier
|
||||
bun run format:check # Check code formatting
|
||||
|
||||
# Type checking
|
||||
bun run typecheck # Run TypeScript type checker
|
||||
bun test # Run tests
|
||||
bun run typecheck # TypeScript type checking
|
||||
bun run format # Format with prettier
|
||||
bun run format:check # Check formatting
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
## What This Is
|
||||
|
||||
This is a GitHub Action that enables Claude to interact with GitHub PRs and issues. The action operates in two main phases:
|
||||
A GitHub Action that lets Claude respond to `@claude` mentions on issues/PRs (tag mode) or run tasks via `prompt` input (agent mode). Mode is auto-detected: if `prompt` is provided, it's agent mode; if triggered by a comment/issue event with `@claude`, it's tag mode. See `src/modes/detector.ts`.
|
||||
|
||||
### Phase 1: Preparation (`src/entrypoints/prepare.ts`)
|
||||
## How It Runs
|
||||
|
||||
1. **Authentication Setup**: Establishes GitHub token via OIDC or GitHub App
|
||||
2. **Permission Validation**: Verifies actor has write permissions
|
||||
3. **Trigger Detection**: Uses mode-specific logic to determine if Claude should respond
|
||||
4. **Context Creation**: Prepares GitHub context and initial tracking comment
|
||||
Single entrypoint: `src/entrypoints/run.ts` orchestrates everything — prepare (auth, permissions, trigger check, branch/comment creation), install Claude Code CLI, execute Claude via `base-action/` functions (imported directly, not subprocess), then cleanup (update tracking comment, write step summary). SSH signing cleanup and token revocation are separate `always()` steps in `action.yml`.
|
||||
|
||||
### Phase 2: Execution (`base-action/`)
|
||||
`base-action/` is also published standalone as `@anthropic-ai/claude-code-base-action`. Don't break its public API. It reads config from `INPUT_`-prefixed env vars (set by `action.yml`), not from action inputs directly.
|
||||
|
||||
The `base-action/` directory contains the core Claude Code execution logic, which serves a dual purpose:
|
||||
## Key Concepts
|
||||
|
||||
- **Standalone Action**: Published separately as `@anthropic-ai/claude-code-base-action` for direct use
|
||||
- **Inner Logic**: Used internally by this GitHub Action after preparation phase completes
|
||||
**Auth priority**: `github_token` input (user-provided) > GitHub App OIDC token (default). The `claude_code_oauth_token` and `anthropic_api_key` are for the Claude API, not GitHub. Token setup lives in `src/github/token.ts`.
|
||||
|
||||
Execution steps:
|
||||
**Mode lifecycle**: `detectMode()` in `src/modes/detector.ts` picks the mode name ("tag" or "agent"). Trigger checking and prepare dispatch are inlined in `run.ts`: tag mode calls `prepareTagMode()` from `src/modes/tag/`, agent mode calls `prepareAgentMode()` from `src/modes/agent/`.
|
||||
|
||||
1. **MCP Server Setup**: Installs and configures GitHub MCP server for tool access
|
||||
2. **Prompt Generation**: Creates context-rich prompts from GitHub data
|
||||
3. **Claude Integration**: Executes via multiple providers (Anthropic API, AWS Bedrock, Google Vertex AI)
|
||||
4. **Result Processing**: Updates comments and creates branches/PRs as needed
|
||||
**Prompt construction**: Tag mode's `prepareTagMode()` builds the prompt by fetching GitHub data (`src/github/data/fetcher.ts`), formatting it as markdown (`src/github/data/formatter.ts`), and writing it to a temp file via `createPrompt()`. Agent mode writes the user's prompt directly. The prompt includes issue/PR body, comments, diff, and CI status. This is the most important part of the action — it's what Claude sees.
|
||||
|
||||
### Key Architectural Components
|
||||
## Things That Will Bite You
|
||||
|
||||
#### Mode System (`src/modes/`)
|
||||
|
||||
- **Tag Mode** (`tag/`): Responds to `@claude` mentions and issue assignments
|
||||
- **Agent Mode** (`agent/`): Direct execution when explicit prompt is provided
|
||||
- Extensible registry pattern in `modes/registry.ts`
|
||||
|
||||
#### GitHub Integration (`src/github/`)
|
||||
|
||||
- **Context Parsing** (`context.ts`): Unified GitHub event handling
|
||||
- **Data Fetching** (`data/fetcher.ts`): Retrieves PR/issue data via GraphQL/REST
|
||||
- **Data Formatting** (`data/formatter.ts`): Converts GitHub data to Claude-readable format
|
||||
- **Branch Operations** (`operations/branch.ts`): Handles branch creation and cleanup
|
||||
- **Comment Management** (`operations/comments/`): Creates and updates tracking comments
|
||||
|
||||
#### MCP Server Integration (`src/mcp/`)
|
||||
|
||||
- **GitHub Actions Server** (`github-actions-server.ts`): Workflow and CI access
|
||||
- **GitHub Comment Server** (`github-comment-server.ts`): Comment operations
|
||||
- **GitHub File Operations** (`github-file-ops-server.ts`): File system access
|
||||
- Auto-installation and configuration in `install-mcp-server.ts`
|
||||
|
||||
#### Authentication & Security (`src/github/`)
|
||||
|
||||
- **Token Management** (`token.ts`): OIDC token exchange and GitHub App authentication
|
||||
- **Permission Validation** (`validation/permissions.ts`): Write access verification
|
||||
- **Actor Validation** (`validation/actor.ts`): Human vs bot detection
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── entrypoints/ # Action entry points
|
||||
│ ├── prepare.ts # Main preparation logic
|
||||
│ ├── update-comment-link.ts # Post-execution comment updates
|
||||
│ └── format-turns.ts # Claude conversation formatting
|
||||
├── github/ # GitHub integration layer
|
||||
│ ├── api/ # REST/GraphQL clients
|
||||
│ ├── data/ # Data fetching and formatting
|
||||
│ ├── operations/ # Branch, comment, git operations
|
||||
│ ├── validation/ # Permission and trigger validation
|
||||
│ └── utils/ # Image downloading, sanitization
|
||||
├── modes/ # Execution modes
|
||||
│ ├── tag/ # @claude mention mode
|
||||
│ ├── agent/ # Automation mode
|
||||
│ └── registry.ts # Mode selection logic
|
||||
├── mcp/ # MCP server implementations
|
||||
├── prepare/ # Preparation orchestration
|
||||
└── utils/ # Shared utilities
|
||||
```
|
||||
|
||||
## Important Implementation Notes
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
- Uses GitHub OIDC token exchange for secure authentication
|
||||
- Supports custom GitHub Apps via `APP_ID` and `APP_PRIVATE_KEY`
|
||||
- Falls back to official Claude GitHub App if no custom app provided
|
||||
|
||||
### MCP Server Architecture
|
||||
|
||||
- Each MCP server has specific GitHub API access patterns
|
||||
- Servers are auto-installed in `~/.claude/mcp/github-{type}-server/`
|
||||
- Configuration merged with user-provided MCP config via `mcp_config` input
|
||||
|
||||
### Mode System Design
|
||||
|
||||
- Modes implement `Mode` interface with `shouldTrigger()` and `prepare()` methods
|
||||
- Registry validates mode compatibility with GitHub event types
|
||||
- Agent mode triggers when explicit prompt is provided
|
||||
|
||||
### Comment Threading
|
||||
|
||||
- Single tracking comment updated throughout execution
|
||||
- Progress indicated via dynamic checkboxes
|
||||
- Links to job runs and created branches/PRs
|
||||
- Sticky comment option for consolidated PR comments
|
||||
- **Strict TypeScript**: `noUnusedLocals` and `noUnusedParameters` are enabled. Typecheck will fail on unused variables.
|
||||
- **Discriminated unions for GitHub context**: `GitHubContext` is a union type — call `isEntityContext(context)` before accessing entity-specific fields like `context.issue` or `context.pullRequest`.
|
||||
- **Token lifecycle matters**: The GitHub App token is obtained early and revoked in a separate `always()` step in `action.yml`. If you move token revocation into `run.ts`, it won't run if the process crashes. Same for SSH signing cleanup.
|
||||
- **Error phase attribution**: The catch block in `run.ts` uses `prepareCompleted` to distinguish prepare failures from execution failures. The tracking comment shows different messages for each.
|
||||
- **`action.yml` outputs reference step IDs**: Outputs like `execution_file`, `branch_name`, `github_token` reference `steps.run.outputs.*`. If you rename the step ID, update the outputs section too.
|
||||
- **Integration testing** happens in a separate repo (`install-test`), not here. The tests in this repo are unit tests.
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Use Bun-specific TypeScript configuration with `moduleResolution: "bundler"`
|
||||
- Strict TypeScript with `noUnusedLocals` and `noUnusedParameters` enabled
|
||||
- Prefer explicit error handling with detailed error messages
|
||||
- Use discriminated unions for GitHub context types
|
||||
- Implement retry logic for GitHub API operations via `utils/retry.ts`
|
||||
- Runtime is Bun, not Node. Use `bun test`, not `jest`.
|
||||
- `moduleResolution: "bundler"` — imports don't need `.js` extensions.
|
||||
- GitHub API calls should use retry logic (`src/utils/retry.ts`).
|
||||
- MCP servers are auto-installed at runtime to `~/.claude/mcp/github-{type}-server/`.
|
||||
|
||||
24
README.md
24
README.md
@ -2,7 +2,7 @@
|
||||
|
||||
# Claude Code Action
|
||||
|
||||
A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs and issues that can answer questions and implement code changes. This action intelligently detects when to activate based on your workflow context—whether responding to @claude mentions, issue assignments, or executing automation tasks with explicit prompts. It supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, and Google Vertex AI.
|
||||
A general-purpose [Claude Code](https://claude.ai/code) action for GitHub PRs and issues that can answer questions and implement code changes. This action intelligently detects when to activate based on your workflow context—whether responding to @claude mentions, issue assignments, or executing automation tasks with explicit prompts. It supports multiple authentication methods including Anthropic direct API, Amazon Bedrock, Google Vertex AI, and Microsoft Foundry.
|
||||
|
||||
## Features
|
||||
|
||||
@ -13,6 +13,7 @@ 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
|
||||
- 🛠️ **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
|
||||
- 📊 **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)
|
||||
- ⚙️ **Simplified Configuration**: Unified `prompt` and `claude_args` inputs provide clean, powerful configuration aligned with Claude Code SDK
|
||||
|
||||
@ -29,17 +30,34 @@ This command will guide you through setting up the GitHub app and required secre
|
||||
**Note**:
|
||||
|
||||
- You must be a repository admin to install the GitHub app and add secrets
|
||||
- This quickstart method is only available for direct Anthropic API users. For AWS Bedrock or Google Vertex AI setup, see [docs/cloud-providers.md](./docs/cloud-providers.md).
|
||||
- This quickstart method is only available for direct Anthropic API users. For AWS Bedrock, Google Vertex AI, or Microsoft Foundry setup, see [docs/cloud-providers.md](./docs/cloud-providers.md).
|
||||
|
||||
## 📚 Solutions & Use Cases
|
||||
|
||||
Looking for specific automation patterns? Check our **[Solutions Guide](./docs/solutions.md)** for complete working examples including:
|
||||
|
||||
- **🔍 Automatic PR Code Review** - Full review automation
|
||||
- **📂 Path-Specific Reviews** - Trigger on critical file changes
|
||||
- **👥 External Contributor Reviews** - Special handling for new contributors
|
||||
- **📝 Custom Review Checklists** - Enforce team standards
|
||||
- **🔄 Scheduled Maintenance** - Automated repository health checks
|
||||
- **🏷️ Issue Triage & Labeling** - Automatic categorization
|
||||
- **📖 Documentation Sync** - Keep docs updated with code changes
|
||||
- **🔒 Security-Focused Reviews** - OWASP-aligned security analysis
|
||||
- **📊 DIY Progress Tracking** - Create tracking comments in automation mode
|
||||
|
||||
Each solution includes complete working examples, configuration details, and expected outcomes.
|
||||
|
||||
## Documentation
|
||||
|
||||
- **[Solutions Guide](./docs/solutions.md)** - **🎯 Ready-to-use automation patterns**
|
||||
- **[Migration Guide](./docs/migration-guide.md)** - **⭐ Upgrading from v0.x to v1.0**
|
||||
- [Setup Guide](./docs/setup.md) - Manual setup, custom GitHub apps, and security best practices
|
||||
- [Usage Guide](./docs/usage.md) - Basic usage, workflow configuration, and input parameters
|
||||
- [Custom Automations](./docs/custom-automations.md) - Examples of automated workflows and custom prompts
|
||||
- [Configuration](./docs/configuration.md) - MCP servers, permissions, environment variables, and advanced settings
|
||||
- [Experimental Features](./docs/experimental.md) - Execution modes and network restrictions
|
||||
- [Cloud Providers](./docs/cloud-providers.md) - AWS Bedrock and Google Vertex AI setup
|
||||
- [Cloud Providers](./docs/cloud-providers.md) - AWS Bedrock, Google Vertex AI, and Microsoft Foundry setup
|
||||
- [Capabilities & Limitations](./docs/capabilities-and-limitations.md) - What Claude can and cannot do
|
||||
- [Security](./docs/security.md) - Access control, permissions, and commit signing
|
||||
- [FAQ](./docs/faq.md) - Common questions and troubleshooting
|
||||
|
||||
331
action.yml
331
action.yml
@ -23,8 +23,33 @@ inputs:
|
||||
description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)"
|
||||
required: false
|
||||
default: "claude/"
|
||||
branch_name_template:
|
||||
description: "Template for branch naming. Available variables: {{prefix}}, {{entityType}}, {{entityNumber}}, {{timestamp}}, {{sha}}, {{label}}, {{description}}. {{label}} will be first label from the issue/PR, or {{entityType}} as a fallback. {{description}} will be the first 5 words of the issue/PR title in kebab-case. Default: '{{prefix}}{{entityType}}-{{entityNumber}}-{{timestamp}}'"
|
||||
required: false
|
||||
default: ""
|
||||
allowed_bots:
|
||||
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. WARNING: On public repos with '*', external Apps may be able to invoke this action with prompts they control. See docs/security.md."
|
||||
required: false
|
||||
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).
|
||||
|
||||
SECURITY: Processing untrusted content exposes the workflow to prompt injection. When this
|
||||
input is set, Claude does a best-effort scrub of Anthropic, cloud, and GitHub Actions secrets
|
||||
from subprocess environments. This reduces but does not eliminate prompt injection risk -
|
||||
only use for workflows with very limited permissions and validate all outputs.
|
||||
required: false
|
||||
default: ""
|
||||
include_comments_by_actor:
|
||||
description: "Comma-separated list of actor usernames to INCLUDE in comments. Supports wildcards: '*[bot]' matches all bots, 'dependabot[bot]' matches specific bot. Empty (default) includes all actors."
|
||||
required: false
|
||||
default: ""
|
||||
exclude_comments_by_actor:
|
||||
description: "Comma-separated list of actor usernames to EXCLUDE from comments. Supports wildcards: '*[bot]' matches all bots, 'renovate[bot]' matches specific bot. Empty (default) excludes none. If actor is in both lists, exclusion takes priority."
|
||||
required: false
|
||||
default: ""
|
||||
|
||||
@ -40,7 +65,7 @@ inputs:
|
||||
|
||||
# Auth configuration
|
||||
anthropic_api_key:
|
||||
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)"
|
||||
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex/Foundry)"
|
||||
required: false
|
||||
claude_code_oauth_token:
|
||||
description: "Claude Code OAuth token (alternative to anthropic_api_key)"
|
||||
@ -56,6 +81,10 @@ inputs:
|
||||
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
default: "false"
|
||||
use_foundry:
|
||||
description: "Use Microsoft Foundry with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
claude_args:
|
||||
description: "Additional arguments to pass directly to Claude CLI"
|
||||
@ -69,18 +98,34 @@ inputs:
|
||||
description: "Use just one comment to deliver issue/PR comments"
|
||||
required: false
|
||||
default: "false"
|
||||
classify_inline_comments:
|
||||
description: "Buffer inline comments without confirmed=true and classify them (real review vs test/probe) before posting after the session ends. Set to 'false' to post all inline comments immediately (pre-buffering behavior)."
|
||||
required: false
|
||||
default: "true"
|
||||
use_commit_signing:
|
||||
description: "Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands"
|
||||
required: false
|
||||
default: "false"
|
||||
ssh_signing_key:
|
||||
description: "SSH private key for signing commits. When provided, git will be configured to use SSH signing. Takes precedence over use_commit_signing."
|
||||
required: false
|
||||
default: ""
|
||||
bot_id:
|
||||
description: "GitHub user ID to use for git operations (defaults to Claude's bot ID)"
|
||||
required: false
|
||||
default: "41898282" # Claude's bot ID - see src/github/constants.ts
|
||||
bot_name:
|
||||
description: "GitHub username to use for git operations (defaults to Claude's bot name)"
|
||||
required: false
|
||||
default: "claude[bot]"
|
||||
track_progress:
|
||||
description: "Force tag mode with tracking comments for pull_request and issue events. Only applicable to pull_request (opened, synchronize, ready_for_review, reopened) and issue (opened, edited, labeled, assigned) events."
|
||||
required: false
|
||||
default: "false"
|
||||
experimental_allowed_domains:
|
||||
description: "Restrict network access to these domains only (newline-separated). If not set, no restrictions are applied. Provider domains are auto-detected."
|
||||
include_fix_links:
|
||||
description: "Include 'Fix this' links in PR code review feedback that open Claude Code with context to fix the identified issue"
|
||||
required: false
|
||||
default: ""
|
||||
default: "true"
|
||||
path_to_claude_code_executable:
|
||||
description: "Optional path to a custom Claude Code executable. If provided, skips automatic installation and uses this executable instead. WARNING: Using an older version may cause problems if the action begins taking advantage of new Claude Code features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
|
||||
required: false
|
||||
@ -89,48 +134,119 @@ 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."
|
||||
required: false
|
||||
default: ""
|
||||
display_report:
|
||||
description: "Whether to display the Claude Code Report in GitHub Step Summary. Set to 'false' to disable when using custom formatting solutions. WARNING: This outputs Claude-authored content in the GitHub Step Summary. This should only be used in cases where the action is used solely with trusted input."
|
||||
required: false
|
||||
default: "false"
|
||||
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: ""
|
||||
|
||||
outputs:
|
||||
execution_file:
|
||||
description: "Path to the Claude Code execution output file"
|
||||
value: ${{ steps.claude-code.outputs.execution_file }}
|
||||
value: ${{ steps.run.outputs.execution_file }}
|
||||
branch_name:
|
||||
description: "The branch created by Claude Code for this execution"
|
||||
value: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
||||
value: ${{ steps.run.outputs.branch_name }}
|
||||
github_token:
|
||||
description: "The GitHub token used by the action (Claude App token if available)"
|
||||
value: ${{ steps.prepare.outputs.github_token }}
|
||||
value: ${{ steps.run.outputs.github_token }}
|
||||
structured_output:
|
||||
description: "JSON string containing all structured output fields when --json-schema is provided in claude_args. Use fromJSON() to parse: fromJSON(steps.id.outputs.structured_output).field_name"
|
||||
value: ${{ steps.run.outputs.structured_output }}
|
||||
session_id:
|
||||
description: "The Claude Code session ID that can be used with --resume to continue this conversation"
|
||||
value: ${{ steps.run.outputs.session_id }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Bun
|
||||
if: inputs.path_to_bun_executable == ''
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # https://github.com/oven-sh/setup-bun/releases/tag/v2.2.0
|
||||
with:
|
||||
bun-version: 1.2.11
|
||||
bun-version: 1.3.6
|
||||
token: ${{ inputs.github_token || github.token }}
|
||||
|
||||
- name: Setup Custom Bun Path
|
||||
if: inputs.path_to_bun_executable != ''
|
||||
shell: bash
|
||||
env:
|
||||
PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||
run: |
|
||||
echo "Using custom Bun executable: ${{ inputs.path_to_bun_executable }}"
|
||||
echo "Using custom Bun executable: $PATH_TO_BUN_EXECUTABLE"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
BUN_DIR=$(dirname "${{ inputs.path_to_bun_executable }}")
|
||||
BUN_DIR=$(dirname "$PATH_TO_BUN_EXECUTABLE")
|
||||
echo "$BUN_DIR" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${GITHUB_ACTION_PATH}
|
||||
bun install
|
||||
bun install --production
|
||||
|
||||
- name: Prepare action
|
||||
id: prepare
|
||||
- name: Install subprocess isolation dependencies
|
||||
# Install subprocess isolation dependencies when processing content from non-write users.
|
||||
# Best-effort: skips on non-Linux or when sudo/apt unavailable (self-hosted runners).
|
||||
if: ${{ inputs.allowed_non_write_users != '' && runner.os == 'Linux' }}
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts
|
||||
if [ "${CLAUDE_CODE_SUBPROCESS_ENV_SCRUB:-}" = "0" ]; then
|
||||
echo "Subprocess isolation opted out via CLAUDE_CODE_SUBPROCESS_ENV_SCRUB=0"
|
||||
exit 0
|
||||
fi
|
||||
if command -v apt-get >/dev/null && command -v sudo >/dev/null; then
|
||||
for i in 1 2 3; do
|
||||
sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends bubblewrap socat && break
|
||||
echo "apt-get attempt $i failed, retrying..."
|
||||
sleep 5
|
||||
done
|
||||
fi
|
||||
# Ubuntu 24.04+ restricts unprivileged user namespaces via AppArmor.
|
||||
# The sysctl doesn't exist on older kernels — that's fine.
|
||||
if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ] && command -v sudo >/dev/null; then
|
||||
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
|
||||
fi
|
||||
|
||||
- name: Pin bun binary for post-steps
|
||||
if: ${{ inputs.allowed_non_write_users != '' }}
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: |
|
||||
# Keep a copy of the bun binary alongside the action's own files so
|
||||
# post-steps use the same version that was on PATH at action start.
|
||||
mkdir -p "$GITHUB_ACTION_PATH/bin"
|
||||
cp "$(command -v bun)" "$GITHUB_ACTION_PATH/bin/bun"
|
||||
|
||||
- name: Prepend system bin dirs to PATH
|
||||
if: ${{ inputs.allowed_non_write_users != '' && runner.os != 'Windows' }}
|
||||
continue-on-error: true
|
||||
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
|
||||
run: |
|
||||
echo "/usr/bin" >> "$GITHUB_PATH"
|
||||
echo "/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Run Claude Code Action
|
||||
id: run
|
||||
shell: bash
|
||||
run: |
|
||||
bun --no-env-file \
|
||||
--config="${GITHUB_ACTION_PATH}/bunfig.toml" \
|
||||
--tsconfig-override="${GITHUB_ACTION_PATH}/tsconfig.json" \
|
||||
run ${GITHUB_ACTION_PATH}/src/entrypoints/run.ts
|
||||
env:
|
||||
# Prepare inputs
|
||||
MODE: ${{ inputs.mode }}
|
||||
PROMPT: ${{ inputs.prompt }}
|
||||
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
|
||||
@ -138,84 +254,58 @@ runs:
|
||||
LABEL_TRIGGER: ${{ inputs.label_trigger }}
|
||||
BASE_BRANCH: ${{ inputs.base_branch }}
|
||||
BRANCH_PREFIX: ${{ inputs.branch_prefix }}
|
||||
BRANCH_NAME_TEMPLATE: ${{ inputs.branch_name_template }}
|
||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||
ALLOWED_BOTS: ${{ inputs.allowed_bots }}
|
||||
ALLOWED_NON_WRITE_USERS: ${{ inputs.allowed_non_write_users }}
|
||||
CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: ${{ env.CLAUDE_CODE_SUBPROCESS_ENV_SCRUB || (inputs.allowed_non_write_users != '' && '1') || '' }}
|
||||
CLAUDE_CODE_SCRIPT_CAPS: ${{ env.CLAUDE_CODE_SCRIPT_CAPS || '' }}
|
||||
INCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.include_comments_by_actor }}
|
||||
EXCLUDE_COMMENTS_BY_ACTOR: ${{ inputs.exclude_comments_by_actor }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||
CLASSIFY_INLINE_COMMENTS: ${{ inputs.classify_inline_comments }}
|
||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||
SSH_SIGNING_KEY: ${{ inputs.ssh_signing_key }}
|
||||
BOT_ID: ${{ inputs.bot_id }}
|
||||
BOT_NAME: ${{ inputs.bot_name }}
|
||||
TRACK_PROGRESS: ${{ inputs.track_progress }}
|
||||
INCLUDE_FIX_LINKS: ${{ inputs.include_fix_links }}
|
||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||
CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||
ALL_INPUTS: ${{ toJson(inputs) }}
|
||||
|
||||
- name: Install Base Action Dependencies
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Installing base-action dependencies..."
|
||||
cd ${GITHUB_ACTION_PATH}/base-action
|
||||
bun install
|
||||
echo "Base-action dependencies installed"
|
||||
cd -
|
||||
|
||||
# Install Claude Code if no custom executable is provided
|
||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||
echo "Installing Claude Code..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.96
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}")
|
||||
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
- name: Setup Network Restrictions
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && inputs.experimental_allowed_domains != ''
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ${GITHUB_ACTION_PATH}/scripts/setup-network-restrictions.sh
|
||||
${GITHUB_ACTION_PATH}/scripts/setup-network-restrictions.sh
|
||||
env:
|
||||
EXPERIMENTAL_ALLOWED_DOMAINS: ${{ inputs.experimental_allowed_domains }}
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude-code
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
# Run the base-action
|
||||
bun run ${GITHUB_ACTION_PATH}/base-action/src/index.ts
|
||||
env:
|
||||
# Base-action inputs
|
||||
CLAUDE_CODE_ACTION: "1"
|
||||
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
||||
INPUT_SETTINGS: ${{ inputs.settings }}
|
||||
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
|
||||
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
||||
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
||||
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
|
||||
DISPLAY_REPORT: ${{ inputs.display_report }}
|
||||
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
|
||||
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||
|
||||
# Model configuration
|
||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||
NODE_VERSION: ${{ env.NODE_VERSION }}
|
||||
DETAILED_PERMISSION_MESSAGES: "1"
|
||||
|
||||
# Provider configuration
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
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_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }}
|
||||
CLAUDE_CODE_USE_FOUNDRY: ${{ inputs.use_foundry == 'true' && '1' || '' }}
|
||||
|
||||
# AWS configuration
|
||||
AWS_REGION: ${{ env.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
|
||||
AWS_BEARER_TOKEN_BEDROCK: ${{ env.AWS_BEARER_TOKEN_BEDROCK }}
|
||||
ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL || (env.AWS_REGION && format('https://bedrock-runtime.{0}.amazonaws.com', env.AWS_REGION)) }}
|
||||
|
||||
# GCP configuration
|
||||
@ -229,55 +319,94 @@ runs:
|
||||
VERTEX_REGION_CLAUDE_3_5_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_5_SONNET }}
|
||||
VERTEX_REGION_CLAUDE_3_7_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_7_SONNET }}
|
||||
|
||||
- name: Update comment with job link
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
|
||||
shell: bash
|
||||
run: |
|
||||
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
|
||||
env:
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }}
|
||||
CLAUDE_COMMENT_ID: ${{ steps.prepare.outputs.claude_comment_id }}
|
||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||
GITHUB_EVENT_NAME: ${{ github.event_name }}
|
||||
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
|
||||
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
|
||||
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
|
||||
BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
|
||||
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
||||
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
||||
TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }}
|
||||
PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }}
|
||||
PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }}
|
||||
USE_STICKY_COMMENT: ${{ inputs.use_sticky_comment }}
|
||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||
TRACK_PROGRESS: ${{ inputs.track_progress }}
|
||||
# Microsoft Foundry configuration
|
||||
ANTHROPIC_FOUNDRY_RESOURCE: ${{ env.ANTHROPIC_FOUNDRY_RESOURCE }}
|
||||
ANTHROPIC_FOUNDRY_BASE_URL: ${{ env.ANTHROPIC_FOUNDRY_BASE_URL }}
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: ${{ env.ANTHROPIC_DEFAULT_SONNET_MODEL }}
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${{ env.ANTHROPIC_DEFAULT_HAIKU_MODEL }}
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }}
|
||||
|
||||
- name: Display Claude Code Report
|
||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
||||
# MCP configuration — these env vars are read directly from process.env by the
|
||||
# Claude CLI subprocess. They must be listed explicitly here because this step's
|
||||
# env: block shadows the calling workflow's job-level env vars (GitHub Actions
|
||||
# composite action behavior). Set these in your workflow's job-level env: or via
|
||||
# a prior step that writes to $GITHUB_ENV.
|
||||
MCP_TIMEOUT: ${{ env.MCP_TIMEOUT }}
|
||||
MCP_TOOL_TIMEOUT: ${{ env.MCP_TOOL_TIMEOUT }}
|
||||
MAX_MCP_OUTPUT_TOKENS: ${{ env.MAX_MCP_OUTPUT_TOKENS }}
|
||||
|
||||
# Telemetry configuration
|
||||
CLAUDE_CODE_ENABLE_TELEMETRY: ${{ env.CLAUDE_CODE_ENABLE_TELEMETRY }}
|
||||
OTEL_METRICS_EXPORTER: ${{ env.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_LOGS_EXPORTER: ${{ env.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ env.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ env.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ env.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ env.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
OTEL_LOGS_EXPORT_INTERVAL: ${{ env.OTEL_LOGS_EXPORT_INTERVAL }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: ${{ env.OTEL_RESOURCE_ATTRIBUTES }}
|
||||
|
||||
- name: Re-prepend system bin dirs to PATH
|
||||
if: ${{ always() && inputs.allowed_non_write_users != '' && runner.os != 'Windows' }}
|
||||
continue-on-error: true
|
||||
shell: /bin/bash --noprofile --norc -e -o pipefail {0}
|
||||
env:
|
||||
BASH_ENV: ""
|
||||
LD_PRELOAD: ""
|
||||
LD_LIBRARY_PATH: ""
|
||||
NODE_OPTIONS: ""
|
||||
DYLD_INSERT_LIBRARIES: ""
|
||||
DYLD_PRELOAD: ""
|
||||
DYLD_LIBRARY_PATH: ""
|
||||
DYLD_FRAMEWORK_PATH: ""
|
||||
run: |
|
||||
echo "/usr/bin" >> "$GITHUB_PATH"
|
||||
echo "/bin" >> "$GITHUB_PATH"
|
||||
{
|
||||
echo "BASH_ENV="
|
||||
echo "LD_PRELOAD="
|
||||
echo "LD_LIBRARY_PATH="
|
||||
echo "DYLD_INSERT_LIBRARIES="
|
||||
echo "DYLD_PRELOAD="
|
||||
echo "DYLD_LIBRARY_PATH="
|
||||
echo "DYLD_FRAMEWORK_PATH="
|
||||
} >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cleanup SSH signing key
|
||||
if: always() && inputs.ssh_signing_key != ''
|
||||
shell: bash
|
||||
run: |
|
||||
# Try to format the turns, but if it fails, dump the raw JSON
|
||||
if bun run ${{ github.action_path }}/src/entrypoints/format-turns.ts "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY 2>/dev/null; then
|
||||
echo "Successfully formatted Claude Code report"
|
||||
else
|
||||
echo "## Claude Code Report (Raw Output)" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "Failed to format output (please report). Here's the raw JSON:" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```json' >> $GITHUB_STEP_SUMMARY
|
||||
cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY
|
||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
BUN_BIN="${GITHUB_ACTION_PATH}/bin/bun"
|
||||
[ -x "$BUN_BIN" ] || BUN_BIN="bun"
|
||||
"$BUN_BIN" --no-env-file \
|
||||
--config="${GITHUB_ACTION_PATH}/bunfig.toml" \
|
||||
--tsconfig-override="${GITHUB_ACTION_PATH}/tsconfig.json" \
|
||||
run ${GITHUB_ACTION_PATH}/src/entrypoints/cleanup-ssh-signing.ts
|
||||
|
||||
- name: Post buffered inline comments
|
||||
if: always() && inputs.classify_inline_comments != 'false'
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ steps.run.outputs.github_token || inputs.github_token || github.token }}
|
||||
REPO_OWNER: ${{ github.event.repository.owner.login }}
|
||||
REPO_NAME: ${{ github.event.repository.name }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
run: |
|
||||
BUN_BIN="${GITHUB_ACTION_PATH}/bin/bun"
|
||||
[ -x "$BUN_BIN" ] || BUN_BIN="bun"
|
||||
"$BUN_BIN" --no-env-file \
|
||||
--config="${GITHUB_ACTION_PATH}/bunfig.toml" \
|
||||
--tsconfig-override="${GITHUB_ACTION_PATH}/tsconfig.json" \
|
||||
run ${GITHUB_ACTION_PATH}/src/entrypoints/post-buffered-inline-comments.ts
|
||||
|
||||
- name: Revoke app token
|
||||
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
|
||||
if: always() && inputs.github_token == '' && steps.run.outputs.github_token != '' && steps.run.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L \
|
||||
-X DELETE \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ steps.prepare.outputs.GITHUB_TOKEN }}" \
|
||||
-H "Authorization: Bearer ${{ steps.run.outputs.github_token }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
${GITHUB_API_URL:-https://api.github.com}/installation/token
|
||||
|
||||
@ -27,7 +27,6 @@ This is a GitHub Action that allows running Claude Code within GitHub workflows.
|
||||
### Key Design Patterns
|
||||
|
||||
- Uses Bun runtime for development and execution
|
||||
- Named pipes for IPC between prompt input and Claude process
|
||||
- JSON streaming output format for execution logs
|
||||
- Composite action pattern to orchestrate multiple steps
|
||||
- Provider-agnostic design supporting Anthropic API, AWS Bedrock, and Google Vertex AI
|
||||
@ -50,11 +49,10 @@ This is a GitHub Action that allows running Claude Code within GitHub workflows.
|
||||
|
||||
- Unit tests for configuration logic
|
||||
- Integration tests for prompt preparation
|
||||
- Full workflow tests in `.github/workflows/test-action.yml`
|
||||
- Full workflow tests in `.github/workflows/test-base-action.yml`
|
||||
|
||||
## Important Technical Details
|
||||
|
||||
- Uses `mkfifo` to create named pipes for prompt input
|
||||
- Outputs execution logs as JSON to `/tmp/claude-execution-output.json`
|
||||
- Timeout enforcement via `timeout` command wrapper
|
||||
- Strict TypeScript configuration with Bun-specific settings
|
||||
|
||||
@ -4,6 +4,14 @@ This GitHub Action allows you to run [Claude Code](https://www.anthropic.com/cla
|
||||
|
||||
For simply tagging @claude in issues and PRs out of the box, [check out the Claude Code action and GitHub app](https://github.com/anthropics/claude-code-action).
|
||||
|
||||
## Trust model
|
||||
|
||||
This action is a thin wrapper that installs and runs Claude Code with the inputs you provide. It does **not** enforce any trust boundaries on its own. Running this action in a directory is equivalent to running Claude Code in that directory — Claude reads project-level configuration (`.claude/`, `CLAUDE.md`, `.mcp.json`, etc.) from the working directory, and the action's own setup steps run from there as well.
|
||||
|
||||
**The caller is responsible for ensuring the working directory and prompt are trusted.** If your workflow processes untrusted input (issues, fork pull requests, external comments), use [`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action) instead — it provides actor permission checks, restores project configuration from the base ref in PR contexts, and is the supported path for those scenarios.
|
||||
|
||||
See [Claude Code's security documentation](https://docs.anthropic.com/en/docs/claude-code/security) and the [GitHub Actions guidance on `pull_request_target`](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/) for background.
|
||||
|
||||
## Usage
|
||||
|
||||
Add the following to your workflow file:
|
||||
@ -85,29 +93,32 @@ Add the following to your workflow file:
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
||||
| `prompt` | 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 | '' |
|
||||
| `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 | '' |
|
||||
| `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 | '' |
|
||||
| `system_prompt` | Override 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 | '' |
|
||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
||||
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
||||
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
||||
| `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' |
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
||||
| `prompt` | 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 | '' |
|
||||
| `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 | '' |
|
||||
| `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 | '' |
|
||||
| `system_prompt` | Override 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 | '' |
|
||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
||||
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
||||
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
||||
| `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' |
|
||||
| `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.
|
||||
|
||||
\*\*`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
|
||||
|
||||
| Output | Description |
|
||||
@ -336,7 +347,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
||||
@ -42,6 +42,10 @@ inputs:
|
||||
description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
default: "false"
|
||||
use_foundry:
|
||||
description: "Use Microsoft Foundry with OIDC authentication instead of direct Anthropic API"
|
||||
required: false
|
||||
default: "false"
|
||||
|
||||
use_node_cache:
|
||||
description: "Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files)"
|
||||
@ -55,6 +59,18 @@ 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."
|
||||
required: false
|
||||
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: ""
|
||||
|
||||
outputs:
|
||||
conclusion:
|
||||
@ -63,6 +79,12 @@ outputs:
|
||||
execution_file:
|
||||
description: "Path to the JSON file containing Claude Code execution log"
|
||||
value: ${{ steps.run_claude.outputs.execution_file }}
|
||||
structured_output:
|
||||
description: "JSON string containing all structured output fields when --json-schema is provided in claude_args (use fromJSON() or jq to parse)"
|
||||
value: ${{ steps.run_claude.outputs.structured_output }}
|
||||
session_id:
|
||||
description: "The Claude Code session ID that can be used with --resume to continue this conversation"
|
||||
value: ${{ steps.run_claude.outputs.session_id }}
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
@ -75,35 +97,55 @@ runs:
|
||||
|
||||
- name: Install Bun
|
||||
if: inputs.path_to_bun_executable == ''
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # https://github.com/oven-sh/setup-bun/releases/tag/v2.2.0
|
||||
with:
|
||||
bun-version: 1.2.11
|
||||
bun-version: 1.3.6
|
||||
|
||||
- name: Setup Custom Bun Path
|
||||
if: inputs.path_to_bun_executable != ''
|
||||
shell: bash
|
||||
env:
|
||||
PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||
run: |
|
||||
echo "Using custom Bun executable: ${{ inputs.path_to_bun_executable }}"
|
||||
echo "Using custom Bun executable: $PATH_TO_BUN_EXECUTABLE"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
BUN_DIR=$(dirname "${{ inputs.path_to_bun_executable }}")
|
||||
BUN_DIR=$(dirname "$PATH_TO_BUN_EXECUTABLE")
|
||||
echo "$BUN_DIR" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Install Dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
cd ${GITHUB_ACTION_PATH}
|
||||
bun install
|
||||
bun install --production
|
||||
|
||||
- name: Install Claude Code
|
||||
shell: bash
|
||||
env:
|
||||
PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||
run: |
|
||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||
echo "Installing Claude Code..."
|
||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.96
|
||||
if [ -z "$PATH_TO_CLAUDE_CODE_EXECUTABLE" ]; then
|
||||
CLAUDE_CODE_VERSION="2.1.123"
|
||||
echo "Installing Claude Code v${CLAUDE_CODE_VERSION}..."
|
||||
for attempt in 1 2 3; do
|
||||
echo "Installation attempt $attempt..."
|
||||
if command -v timeout &> /dev/null; then
|
||||
# Use --foreground to kill entire process group on timeout, --kill-after to send SIGKILL if SIGTERM fails
|
||||
timeout --foreground --kill-after=10 120 bash -c "curl -fsSL https://claude.ai/install.sh | bash -s -- $CLAUDE_CODE_VERSION" && break
|
||||
else
|
||||
curl -fsSL https://claude.ai/install.sh | bash -s -- "$CLAUDE_CODE_VERSION" && break
|
||||
fi
|
||||
if [ $attempt -eq 3 ]; then
|
||||
echo "Failed to install Claude Code after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "Installation failed, retrying..."
|
||||
sleep 5
|
||||
done
|
||||
echo "Claude Code installed successfully"
|
||||
else
|
||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||
echo "Using custom Claude Code executable: $PATH_TO_CLAUDE_CODE_EXECUTABLE"
|
||||
# Add the directory containing the custom executable to PATH
|
||||
CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}")
|
||||
CLAUDE_DIR=$(dirname "$PATH_TO_CLAUDE_CODE_EXECUTABLE")
|
||||
echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
@ -126,20 +168,26 @@ runs:
|
||||
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
|
||||
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
|
||||
|
||||
# Provider configuration
|
||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }}
|
||||
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
|
||||
CLAUDE_CODE_USE_BEDROCK: ${{ inputs.use_bedrock == 'true' && '1' || '' }}
|
||||
CLAUDE_CODE_USE_VERTEX: ${{ inputs.use_vertex == 'true' && '1' || '' }}
|
||||
CLAUDE_CODE_USE_FOUNDRY: ${{ inputs.use_foundry == 'true' && '1' || '' }}
|
||||
|
||||
# AWS configuration
|
||||
AWS_REGION: ${{ env.AWS_REGION }}
|
||||
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
|
||||
AWS_BEARER_TOKEN_BEDROCK: ${{ env.AWS_BEARER_TOKEN_BEDROCK }}
|
||||
ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL || (env.AWS_REGION && format('https://bedrock-runtime.{0}.amazonaws.com', env.AWS_REGION)) }}
|
||||
|
||||
# GCP configuration
|
||||
@ -147,3 +195,21 @@ runs:
|
||||
CLOUD_ML_REGION: ${{ env.CLOUD_ML_REGION }}
|
||||
GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||
ANTHROPIC_VERTEX_BASE_URL: ${{ env.ANTHROPIC_VERTEX_BASE_URL }}
|
||||
|
||||
# Microsoft Foundry configuration
|
||||
ANTHROPIC_FOUNDRY_RESOURCE: ${{ env.ANTHROPIC_FOUNDRY_RESOURCE }}
|
||||
ANTHROPIC_FOUNDRY_BASE_URL: ${{ env.ANTHROPIC_FOUNDRY_BASE_URL }}
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: ${{ env.ANTHROPIC_DEFAULT_SONNET_MODEL }}
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${{ env.ANTHROPIC_DEFAULT_HAIKU_MODEL }}
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: ${{ env.ANTHROPIC_DEFAULT_OPUS_MODEL }}
|
||||
|
||||
# Telemetry configuration
|
||||
CLAUDE_CODE_ENABLE_TELEMETRY: ${{ env.CLAUDE_CODE_ENABLE_TELEMETRY }}
|
||||
OTEL_METRICS_EXPORTER: ${{ env.OTEL_METRICS_EXPORTER }}
|
||||
OTEL_LOGS_EXPORTER: ${{ env.OTEL_LOGS_EXPORTER }}
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: ${{ env.OTEL_EXPORTER_OTLP_PROTOCOL }}
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: ${{ env.OTEL_EXPORTER_OTLP_ENDPOINT }}
|
||||
OTEL_EXPORTER_OTLP_HEADERS: ${{ env.OTEL_EXPORTER_OTLP_HEADERS }}
|
||||
OTEL_METRIC_EXPORT_INTERVAL: ${{ env.OTEL_METRIC_EXPORT_INTERVAL }}
|
||||
OTEL_LOGS_EXPORT_INTERVAL: ${{ env.OTEL_LOGS_EXPORT_INTERVAL }}
|
||||
OTEL_RESOURCE_ATTRIBUTES: ${{ env.OTEL_RESOURCE_ATTRIBUTES }}
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@anthropic-ai/claude-code-base-action",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.123",
|
||||
"shell-quote": "^1.8.3",
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -25,8 +27,34 @@
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.123", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.123" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-a4TysYoR9DBdkM9Uwh4J5ub7TwKmRPe5hFiWh4En+IKC+qkk5UFkxFM22c//cZjYZKynHX0ah2t6LUqb+najYA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.123", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tYAXCjlXZQklsUs0J//gip3fZQRzhlH5OCgvNXV70qe7A1iiwHqO2KPGvEHV1L+deEKQoMZmTaCOrQpN6zju3w=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.123", "", { "os": "darwin", "cpu": "x64" }, "sha512-AcUC6sTon6z6HculP87KsAOeTMRLBwpovdhcXUTjXUpo/8nplJ7lBEzWjZCHt8FF1KuN/WBy1Z4bDg/59TQDmA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-7+GnbcF3/aZ8RJ1WmU/ogtPsOpknBAoUPer90MvZuFYBLPT9iI/U7f24gjrOHuYdcbDA5n7jFlhcfIO26F5DJQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-bYgRiaf2q+yVbGAoUluuhqrEW1zexL34+3HDmK9DneKXa2K2EJpw4M6Sq4XoBD/JezGaemoAP78Xv/M/QUS1OQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-Xi+Rwk8uP5vWEnawJOlsk179fr0ATLl5J90MlbLj+puKaX5svEq8ljS+P3zq6zHTJeKh9GKLzPf7bc5YJKwcew=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-IX95lFKhmmndY/YPfWPsVV+C3rLYJmuuq5wCS53p6jYIkCMxH1iGfhBGF1EUWcXO4Uc8yqXFmQ3aaxMzOOPrwA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.123", "", { "os": "win32", "cpu": "arm64" }, "sha512-WDZmAQG1rOiqNLZlSXaCjSWmqJvLk2io+vFQWWqSy2b5HCk9pa3PadLiaLztiihyk81wPhH9Q/44kOxdyfEGMw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.123", "", { "os": "win32", "cpu": "x64" }, "sha512-588xrd1i6d4kXQ6FqwL+cgBiN4evRQSi5DCtPa02CZ3VEbuVQBeFlyPlD8tfWtNNeGZ4NM8kjPNNzZz5omezPA=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.9", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw=="],
|
||||
@ -35,20 +63,202 @@
|
||||
|
||||
"@types/shell-quote": ["@types/shell-quote@1.7.5", "", {}, "sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||
|
||||
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
|
||||
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
|
||||
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
|
||||
|
||||
"cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
|
||||
|
||||
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
|
||||
|
||||
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
|
||||
|
||||
"eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="],
|
||||
|
||||
"eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="],
|
||||
|
||||
"express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="],
|
||||
|
||||
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
|
||||
|
||||
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
|
||||
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
|
||||
|
||||
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||
|
||||
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
|
||||
|
||||
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
|
||||
|
||||
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="],
|
||||
|
||||
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
|
||||
|
||||
"prettier": ["prettier@3.5.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw=="],
|
||||
|
||||
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
|
||||
|
||||
"qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
|
||||
|
||||
"raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="],
|
||||
|
||||
"serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="],
|
||||
|
||||
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
|
||||
|
||||
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
|
||||
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
|
||||
|
||||
"undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||
|
||||
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
|
||||
|
||||
"zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ jobs:
|
||||
"--rm",
|
||||
"-e",
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN",
|
||||
"ghcr.io/github/github-mcp-server:sha-7aced2b"
|
||||
"ghcr.io/github/github-mcp-server:sha-23fa0dd"
|
||||
],
|
||||
"env": {
|
||||
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
196
base-action/package-lock.json
generated
Normal file
196
base-action/package-lock.json
generated
Normal file
@ -0,0 +1,196 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.123",
|
||||
"shell-quote": "^1.8.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@ -5,22 +5,38 @@ import { preparePrompt } from "./prepare-prompt";
|
||||
import { runClaude } from "./run-claude";
|
||||
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
|
||||
import { validateEnvironmentVariables } from "./validate-env";
|
||||
import { installPlugins } from "./install-plugins";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
validateEnvironmentVariables();
|
||||
|
||||
// The composite action's "Install Claude Code" step writes the binary to
|
||||
// ~/.local/bin/claude. Pass that path explicitly so the Agent SDK doesn't
|
||||
// fall back to its bundled platform package, which bun may resolve to the
|
||||
// wrong libc variant on Linux.
|
||||
const claudeExecutable =
|
||||
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE ||
|
||||
`${process.env.HOME}/.local/bin/claude`;
|
||||
|
||||
await setupClaudeCodeSettings(
|
||||
process.env.INPUT_SETTINGS,
|
||||
undefined, // homeDir
|
||||
);
|
||||
|
||||
// Install Claude Code plugins if specified
|
||||
await installPlugins(
|
||||
process.env.INPUT_PLUGIN_MARKETPLACES,
|
||||
process.env.INPUT_PLUGINS,
|
||||
claudeExecutable,
|
||||
);
|
||||
|
||||
const promptConfig = await preparePrompt({
|
||||
prompt: process.env.INPUT_PROMPT || "",
|
||||
promptFile: process.env.INPUT_PROMPT_FILE || "",
|
||||
});
|
||||
|
||||
await runClaude(promptConfig.path, {
|
||||
const result = await runClaude(promptConfig.path, {
|
||||
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
|
||||
allowedTools: process.env.INPUT_ALLOWED_TOOLS,
|
||||
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
|
||||
@ -28,12 +44,23 @@ async function run() {
|
||||
mcpConfig: process.env.INPUT_MCP_CONFIG,
|
||||
systemPrompt: process.env.INPUT_SYSTEM_PROMPT,
|
||||
appendSystemPrompt: process.env.INPUT_APPEND_SYSTEM_PROMPT,
|
||||
claudeEnv: process.env.INPUT_CLAUDE_ENV,
|
||||
fallbackModel: process.env.INPUT_FALLBACK_MODEL,
|
||||
model: process.env.ANTHROPIC_MODEL,
|
||||
pathToClaudeCodeExecutable:
|
||||
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
|
||||
pathToClaudeCodeExecutable: claudeExecutable,
|
||||
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
|
||||
});
|
||||
|
||||
// Set outputs for the standalone base-action
|
||||
core.setOutput("conclusion", result.conclusion);
|
||||
if (result.executionFile) {
|
||||
core.setOutput("execution_file", result.executionFile);
|
||||
}
|
||||
if (result.sessionId) {
|
||||
core.setOutput("session_id", result.sessionId);
|
||||
}
|
||||
if (result.structuredOutput) {
|
||||
core.setOutput("structured_output", result.structuredOutput);
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(`Action failed with error: ${error}`);
|
||||
core.setOutput("conclusion", "failure");
|
||||
|
||||
243
base-action/src/install-plugins.ts
Normal file
243
base-action/src/install-plugins.ts
Normal file
@ -0,0 +1,243 @@
|
||||
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$/;
|
||||
|
||||
/**
|
||||
* Checks if a marketplace input is a local path (not a URL)
|
||||
* @param input - The marketplace input to check
|
||||
* @returns true if the input is a local path, false if it's a URL
|
||||
*/
|
||||
function isLocalPath(input: string): boolean {
|
||||
// Local paths start with ./, ../, /, or a drive letter (Windows)
|
||||
return (
|
||||
input.startsWith("./") ||
|
||||
input.startsWith("../") ||
|
||||
input.startsWith("/") ||
|
||||
/^[a-zA-Z]:[\\\/]/.test(input)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a marketplace URL or local path
|
||||
* @param input - The marketplace URL or local path to validate
|
||||
* @throws {Error} If the input is invalid
|
||||
*/
|
||||
function validateMarketplaceInput(input: string): void {
|
||||
const normalized = input.trim();
|
||||
|
||||
if (!normalized) {
|
||||
throw new Error("Marketplace URL or path cannot be empty");
|
||||
}
|
||||
|
||||
// Local paths are passed directly to Claude Code which handles them
|
||||
if (isLocalPath(normalized)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate as URL
|
||||
if (!MARKETPLACE_URL_REGEX.test(normalized)) {
|
||||
throw new Error(`Invalid marketplace URL format: ${input}`);
|
||||
}
|
||||
|
||||
// Additional check for valid URL structure
|
||||
try {
|
||||
new URL(normalized);
|
||||
} catch {
|
||||
throw new Error(`Invalid marketplace URL: ${input}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 or local paths and return an array of validated entries
|
||||
* @param marketplaces - Newline-separated list of marketplace Git URLs or local paths
|
||||
* @returns Array of validated marketplace URLs or paths (empty array if none provided)
|
||||
*/
|
||||
function parseMarketplaces(marketplaces?: string): string[] {
|
||||
const trimmed = marketplaces?.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Split by newline and process each entry
|
||||
return trimmed
|
||||
.split("\n")
|
||||
.map((entry) => entry.trim())
|
||||
.filter((entry) => {
|
||||
if (entry.length === 0) return false;
|
||||
|
||||
validateMarketplaceInput(entry);
|
||||
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 marketplace - The marketplace Git URL or local path 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,
|
||||
marketplace: string,
|
||||
): Promise<void> {
|
||||
console.log(`Adding marketplace: ${marketplace}`);
|
||||
|
||||
return executeClaudeCommand(
|
||||
claudeExecutable,
|
||||
["plugin", "marketplace", "add", marketplace],
|
||||
`Failed to add marketplace '${marketplace}'`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs Claude Code plugins from a newline-separated list
|
||||
* @param marketplacesInput - Newline-separated list of marketplace Git URLs or local paths
|
||||
* @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");
|
||||
}
|
||||
}
|
||||
291
base-action/src/parse-sdk-options.ts
Normal file
291
base-action/src/parse-sdk-options.ts
Normal file
@ -0,0 +1,291 @@
|
||||
import { parse as parseShellArgs } from "shell-quote";
|
||||
import type { ClaudeOptions } from "./run-claude";
|
||||
import type { Options as SdkOptions } from "@anthropic-ai/claude-agent-sdk";
|
||||
|
||||
/**
|
||||
* Result of parsing ClaudeOptions for SDK usage
|
||||
*/
|
||||
export type ParsedSdkOptions = {
|
||||
sdkOptions: SdkOptions;
|
||||
showFullOutput: boolean;
|
||||
hasJsonSchema: boolean;
|
||||
};
|
||||
|
||||
// Flags that should accumulate multiple values instead of overwriting
|
||||
// Include both camelCase and hyphenated variants for CLI compatibility
|
||||
const ACCUMULATING_FLAGS = new Set([
|
||||
"allowedTools",
|
||||
"allowed-tools",
|
||||
"disallowedTools",
|
||||
"disallowed-tools",
|
||||
"mcp-config",
|
||||
]);
|
||||
|
||||
// Delimiter used to join accumulated flag values
|
||||
const ACCUMULATE_DELIMITER = "\x00";
|
||||
|
||||
type McpConfig = {
|
||||
mcpServers?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Merge multiple MCP config values into a single config.
|
||||
* Each config can be a JSON string or a file path.
|
||||
* For JSON strings, mcpServers objects are merged.
|
||||
* For file paths, they are kept as-is (user's file takes precedence and is used last).
|
||||
*/
|
||||
function mergeMcpConfigs(configValues: string[]): string {
|
||||
const merged: McpConfig = { mcpServers: {} };
|
||||
let lastFilePath: string | null = null;
|
||||
|
||||
for (const config of configValues) {
|
||||
const trimmed = config.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
// Check if it's a JSON string (starts with {) or a file path
|
||||
if (trimmed.startsWith("{")) {
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as McpConfig;
|
||||
if (parsed.mcpServers) {
|
||||
Object.assign(merged.mcpServers!, parsed.mcpServers);
|
||||
}
|
||||
} catch {
|
||||
// If JSON parsing fails, treat as file path
|
||||
lastFilePath = trimmed;
|
||||
}
|
||||
} else {
|
||||
// It's a file path - store it to handle separately
|
||||
lastFilePath = trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have file paths, we need to keep the merged JSON and let the file
|
||||
// be handled separately. Since we can only return one value, merge what we can.
|
||||
// If there's a file path, we need a different approach - read the file at runtime.
|
||||
// For now, if there's a file path, we'll stringify the merged config.
|
||||
// The action prepends its config as JSON, so we can safely merge inline JSON configs.
|
||||
|
||||
// If no inline configs were found (all file paths), return the last file path
|
||||
if (Object.keys(merged.mcpServers!).length === 0 && lastFilePath) {
|
||||
return lastFilePath;
|
||||
}
|
||||
|
||||
// Note: If user passes a file path, we cannot merge it at parse time since
|
||||
// we don't have access to the file system here. The action's built-in MCP
|
||||
// servers are always passed as inline JSON, so they will be merged.
|
||||
// If user also passes inline JSON, it will be merged.
|
||||
// If user passes a file path, they should ensure it includes all needed servers.
|
||||
|
||||
return JSON.stringify(merged);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip comment lines from a shell argument string.
|
||||
* Lines whose first non-whitespace character is `#` are removed entirely.
|
||||
* Inline `#` within a line (e.g. inside a quoted value) is left untouched
|
||||
* because shell-quote handles quoting — we only need to remove full comment lines
|
||||
* before shell-quote sees them.
|
||||
*/
|
||||
function stripShellComments(input: string): string {
|
||||
return input
|
||||
.split("\n")
|
||||
.filter((line) => !line.trim().startsWith("#"))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse claudeArgs string into extraArgs record for SDK pass-through
|
||||
* The SDK/CLI will handle --mcp-config, --json-schema, etc.
|
||||
* For allowedTools and disallowedTools, multiple occurrences are accumulated (null-char joined).
|
||||
* Accumulating flags also consume all consecutive non-flag values
|
||||
* (e.g., --allowed-tools "Tool1" "Tool2" "Tool3" captures all three).
|
||||
*/
|
||||
function parseClaudeArgsToExtraArgs(
|
||||
claudeArgs?: string,
|
||||
): Record<string, string | null> {
|
||||
if (!claudeArgs?.trim()) return {};
|
||||
|
||||
const result: Record<string, string | null> = {};
|
||||
const args = parseShellArgs(stripShellComments(claudeArgs)).filter(
|
||||
(arg): arg is string => typeof arg === "string",
|
||||
);
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (arg?.startsWith("--")) {
|
||||
const flag = arg.slice(2);
|
||||
const nextArg = args[i + 1];
|
||||
|
||||
// Check if next arg is a value (not another flag)
|
||||
if (nextArg && !nextArg.startsWith("--")) {
|
||||
// For accumulating flags, consume all consecutive non-flag values
|
||||
// This handles: --allowed-tools "Tool1" "Tool2" "Tool3"
|
||||
if (ACCUMULATING_FLAGS.has(flag)) {
|
||||
const values: string[] = [];
|
||||
while (i + 1 < args.length && !args[i + 1]?.startsWith("--")) {
|
||||
i++;
|
||||
values.push(args[i]!);
|
||||
}
|
||||
const joinedValues = values.join(ACCUMULATE_DELIMITER);
|
||||
if (result[flag]) {
|
||||
result[flag] =
|
||||
`${result[flag]}${ACCUMULATE_DELIMITER}${joinedValues}`;
|
||||
} else {
|
||||
result[flag] = joinedValues;
|
||||
}
|
||||
} else {
|
||||
result[flag] = nextArg;
|
||||
i++; // Skip the value
|
||||
}
|
||||
} else {
|
||||
result[flag] = null; // Boolean flag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ClaudeOptions into SDK-compatible options
|
||||
* Uses extraArgs for CLI pass-through instead of duplicating option parsing
|
||||
*/
|
||||
export function parseSdkOptions(options: ClaudeOptions): ParsedSdkOptions {
|
||||
// Determine output verbosity
|
||||
const isDebugMode = process.env.ACTIONS_STEP_DEBUG === "true";
|
||||
const showFullOutput = options.showFullOutput === "true" || isDebugMode;
|
||||
|
||||
// Parse claudeArgs into extraArgs for CLI pass-through
|
||||
const extraArgs = parseClaudeArgsToExtraArgs(options.claudeArgs);
|
||||
|
||||
// Detect if --json-schema is present (for hasJsonSchema flag)
|
||||
const hasJsonSchema = "json-schema" in extraArgs;
|
||||
|
||||
// Extract and merge allowedTools from all sources:
|
||||
// 1. From extraArgs (parsed from claudeArgs - contains tag mode's tools)
|
||||
// - Check both camelCase (--allowedTools) and hyphenated (--allowed-tools) variants
|
||||
// 2. From options.allowedTools (direct input - may be undefined)
|
||||
// This prevents duplicate flags being overwritten when claudeArgs contains --allowedTools
|
||||
const allowedToolsValues = [
|
||||
extraArgs["allowedTools"],
|
||||
extraArgs["allowed-tools"],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(ACCUMULATE_DELIMITER);
|
||||
const extraArgsAllowedTools = allowedToolsValues
|
||||
? allowedToolsValues
|
||||
.split(ACCUMULATE_DELIMITER)
|
||||
.flatMap((v) => v.split(","))
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const directAllowedTools = options.allowedTools
|
||||
? options.allowedTools.split(",").map((t) => t.trim())
|
||||
: [];
|
||||
const mergedAllowedTools = [
|
||||
...new Set([...extraArgsAllowedTools, ...directAllowedTools]),
|
||||
];
|
||||
delete extraArgs["allowedTools"];
|
||||
delete extraArgs["allowed-tools"];
|
||||
|
||||
// Same for disallowedTools - check both camelCase and hyphenated variants
|
||||
const disallowedToolsValues = [
|
||||
extraArgs["disallowedTools"],
|
||||
extraArgs["disallowed-tools"],
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(ACCUMULATE_DELIMITER);
|
||||
const extraArgsDisallowedTools = disallowedToolsValues
|
||||
? disallowedToolsValues
|
||||
.split(ACCUMULATE_DELIMITER)
|
||||
.flatMap((v) => v.split(","))
|
||||
.map((t) => t.trim())
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
const directDisallowedTools = options.disallowedTools
|
||||
? options.disallowedTools.split(",").map((t) => t.trim())
|
||||
: [];
|
||||
const mergedDisallowedTools = [
|
||||
...new Set([...extraArgsDisallowedTools, ...directDisallowedTools]),
|
||||
];
|
||||
delete extraArgs["disallowedTools"];
|
||||
delete extraArgs["disallowed-tools"];
|
||||
|
||||
// Merge multiple --mcp-config values by combining their mcpServers objects
|
||||
// The action prepends its config (github_comment, github_ci, etc.) as inline JSON,
|
||||
// and users may provide their own config as inline JSON or file path
|
||||
if (extraArgs["mcp-config"]) {
|
||||
const mcpConfigValues = extraArgs["mcp-config"].split(ACCUMULATE_DELIMITER);
|
||||
if (mcpConfigValues.length > 1) {
|
||||
extraArgs["mcp-config"] = mergeMcpConfigs(mcpConfigValues);
|
||||
}
|
||||
}
|
||||
|
||||
// Build custom environment
|
||||
const env: Record<string, string | undefined> = { ...process.env };
|
||||
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
||||
env.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
||||
}
|
||||
// Set the entrypoint for Claude Code to identify this as the GitHub Action
|
||||
env.CLAUDE_CODE_ENTRYPOINT = "claude-code-github-action";
|
||||
|
||||
// Remove OIDC token request variables so Claude cannot mint new tokens.
|
||||
// These are only needed by the action itself (via @actions/core.getIDToken()),
|
||||
// not by the Claude session.
|
||||
delete env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
delete env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
|
||||
// Build system prompt option - default to claude_code preset
|
||||
let systemPrompt: SdkOptions["systemPrompt"];
|
||||
if (options.systemPrompt) {
|
||||
systemPrompt = options.systemPrompt;
|
||||
} else if (options.appendSystemPrompt) {
|
||||
systemPrompt = {
|
||||
type: "preset",
|
||||
preset: "claude_code",
|
||||
append: options.appendSystemPrompt,
|
||||
};
|
||||
} else {
|
||||
// Default to claude_code preset when no custom prompt is specified
|
||||
systemPrompt = {
|
||||
type: "preset",
|
||||
preset: "claude_code",
|
||||
};
|
||||
}
|
||||
|
||||
// Build SDK options - use merged tools from both direct options and claudeArgs
|
||||
const sdkOptions: SdkOptions = {
|
||||
// Direct options from ClaudeOptions inputs
|
||||
model: options.model,
|
||||
maxTurns: options.maxTurns ? parseInt(options.maxTurns, 10) : undefined,
|
||||
allowedTools:
|
||||
mergedAllowedTools.length > 0 ? mergedAllowedTools : undefined,
|
||||
disallowedTools:
|
||||
mergedDisallowedTools.length > 0 ? mergedDisallowedTools : undefined,
|
||||
systemPrompt,
|
||||
fallbackModel: options.fallbackModel,
|
||||
pathToClaudeCodeExecutable: options.pathToClaudeCodeExecutable,
|
||||
|
||||
// Pass through claudeArgs as extraArgs - CLI handles --mcp-config, --json-schema, etc.
|
||||
// Note: allowedTools and disallowedTools have been removed from extraArgs to prevent duplicates
|
||||
extraArgs,
|
||||
env,
|
||||
|
||||
// Load settings from sources - prefer user's --setting-sources if provided, otherwise use all sources
|
||||
// This ensures users can override the default behavior (e.g., --setting-sources user to avoid in-repo configs)
|
||||
settingSources: extraArgs["setting-sources"]
|
||||
? (extraArgs["setting-sources"].split(
|
||||
",",
|
||||
) as SdkOptions["settingSources"])
|
||||
: ["user", "project", "local"],
|
||||
};
|
||||
|
||||
// Remove setting-sources from extraArgs to avoid passing it twice
|
||||
delete extraArgs["setting-sources"];
|
||||
|
||||
return {
|
||||
sdkOptions,
|
||||
showFullOutput,
|
||||
hasJsonSchema,
|
||||
};
|
||||
}
|
||||
244
base-action/src/run-claude-sdk.ts
Normal file
244
base-action/src/run-claude-sdk.ts
Normal file
@ -0,0 +1,244 @@
|
||||
import * as core from "@actions/core";
|
||||
import { readFile, writeFile, access } from "fs/promises";
|
||||
import { dirname, join } from "path";
|
||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||
import type {
|
||||
SDKMessage,
|
||||
SDKResultMessage,
|
||||
SDKUserMessage,
|
||||
} from "@anthropic-ai/claude-agent-sdk";
|
||||
import type { ParsedSdkOptions } from "./parse-sdk-options";
|
||||
|
||||
export type ClaudeRunResult = {
|
||||
executionFile?: string;
|
||||
sessionId?: string;
|
||||
conclusion: "success" | "failure";
|
||||
structuredOutput?: string;
|
||||
};
|
||||
|
||||
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
||||
|
||||
/** Filename for the user request file, written by prompt generation */
|
||||
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
||||
|
||||
/**
|
||||
* Check if a file exists
|
||||
*/
|
||||
async function fileExists(path: string): Promise<boolean> {
|
||||
try {
|
||||
await access(path);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a prompt configuration for the SDK.
|
||||
* If a user request file exists alongside the prompt file, returns a multi-block
|
||||
* SDKUserMessage that enables slash command processing in the CLI.
|
||||
* Otherwise, returns the prompt as a simple string.
|
||||
*/
|
||||
async function createPromptConfig(
|
||||
promptPath: string,
|
||||
showFullOutput: boolean,
|
||||
): Promise<string | AsyncIterable<SDKUserMessage>> {
|
||||
const promptContent = await readFile(promptPath, "utf-8");
|
||||
|
||||
// Check for user request file in the same directory
|
||||
const userRequestPath = join(dirname(promptPath), USER_REQUEST_FILENAME);
|
||||
const hasUserRequest = await fileExists(userRequestPath);
|
||||
|
||||
if (!hasUserRequest) {
|
||||
// No user request file - use simple string prompt
|
||||
return promptContent;
|
||||
}
|
||||
|
||||
// User request file exists - create multi-block message
|
||||
const userRequest = await readFile(userRequestPath, "utf-8");
|
||||
if (showFullOutput) {
|
||||
console.log("Using multi-block message with user request:", userRequest);
|
||||
} else {
|
||||
console.log("Using multi-block message with user request (content hidden)");
|
||||
}
|
||||
|
||||
// Create an async generator that yields a single multi-block message
|
||||
// The context/instructions go first, then the user's actual request last
|
||||
// This allows the CLI to detect and process slash commands in the user request
|
||||
async function* createMultiBlockMessage(): AsyncGenerator<SDKUserMessage> {
|
||||
yield {
|
||||
type: "user",
|
||||
session_id: "",
|
||||
message: {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: promptContent }, // Instructions + GitHub context
|
||||
{ type: "text", text: userRequest }, // User's request (may be a slash command)
|
||||
],
|
||||
},
|
||||
parent_tool_use_id: null,
|
||||
};
|
||||
}
|
||||
|
||||
return createMultiBlockMessage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes SDK output to match CLI sanitization behavior
|
||||
*/
|
||||
function sanitizeSdkOutput(
|
||||
message: SDKMessage,
|
||||
showFullOutput: boolean,
|
||||
): string | null {
|
||||
if (showFullOutput) {
|
||||
return JSON.stringify(message, null, 2);
|
||||
}
|
||||
|
||||
// System initialization - safe to show
|
||||
if (message.type === "system" && message.subtype === "init") {
|
||||
return JSON.stringify(
|
||||
{
|
||||
type: "system",
|
||||
subtype: "init",
|
||||
message: "Claude Code initialized",
|
||||
model: "model" in message ? message.model : "unknown",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
// Result messages - show sanitized summary
|
||||
if (message.type === "result") {
|
||||
const resultMsg = message as SDKResultMessage;
|
||||
return JSON.stringify(
|
||||
{
|
||||
type: "result",
|
||||
subtype: resultMsg.subtype,
|
||||
is_error: resultMsg.is_error,
|
||||
duration_ms: resultMsg.duration_ms,
|
||||
num_turns: resultMsg.num_turns,
|
||||
total_cost_usd: resultMsg.total_cost_usd,
|
||||
permission_denials_count: resultMsg.permission_denials?.length ?? 0,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
// Suppress other message types in non-full-output mode
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run Claude using the Agent SDK
|
||||
*/
|
||||
export async function runClaudeWithSdk(
|
||||
promptPath: string,
|
||||
{ sdkOptions, showFullOutput, hasJsonSchema }: ParsedSdkOptions,
|
||||
): Promise<ClaudeRunResult> {
|
||||
// Create prompt configuration - may be a string or multi-block message
|
||||
const prompt = await createPromptConfig(promptPath, showFullOutput);
|
||||
|
||||
if (!showFullOutput) {
|
||||
console.log(
|
||||
"Running Claude Code via SDK (full output hidden for security)...",
|
||||
);
|
||||
console.log(
|
||||
"Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.",
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Running Claude with prompt from file: ${promptPath}`);
|
||||
// Log SDK options without env (which could contain sensitive data)
|
||||
const { env, extraArgs, ...optionsToLog } = sdkOptions;
|
||||
console.log("SDK options:", JSON.stringify(optionsToLog, null, 2));
|
||||
|
||||
const messages: SDKMessage[] = [];
|
||||
let resultMessage: SDKResultMessage | undefined;
|
||||
|
||||
try {
|
||||
for await (const message of query({ prompt, options: sdkOptions })) {
|
||||
messages.push(message);
|
||||
|
||||
const sanitized = sanitizeSdkOutput(message, showFullOutput);
|
||||
if (sanitized) {
|
||||
console.log(sanitized);
|
||||
}
|
||||
|
||||
if (message.type === "result") {
|
||||
resultMessage = message as SDKResultMessage;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("SDK execution error:", error);
|
||||
throw new Error(`SDK execution error: ${error}`);
|
||||
}
|
||||
|
||||
const result: ClaudeRunResult = {
|
||||
conclusion: "failure",
|
||||
};
|
||||
|
||||
// Write execution file
|
||||
try {
|
||||
await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2));
|
||||
console.log(`Log saved to ${EXECUTION_FILE}`);
|
||||
result.executionFile = EXECUTION_FILE;
|
||||
} catch (error) {
|
||||
core.warning(`Failed to write execution file: ${error}`);
|
||||
}
|
||||
|
||||
// Extract session_id from system.init message
|
||||
const initMessage = messages.find(
|
||||
(m) => m.type === "system" && "subtype" in m && m.subtype === "init",
|
||||
);
|
||||
if (initMessage && "session_id" in initMessage && initMessage.session_id) {
|
||||
result.sessionId = initMessage.session_id as string;
|
||||
core.info(`Set session_id: ${result.sessionId}`);
|
||||
}
|
||||
|
||||
if (!resultMessage) {
|
||||
core.error("No result message received from Claude");
|
||||
throw new Error("No result message received from Claude");
|
||||
}
|
||||
|
||||
const isSuccess = resultMessage.subtype === "success";
|
||||
result.conclusion = isSuccess ? "success" : "failure";
|
||||
|
||||
// Handle structured output
|
||||
if (hasJsonSchema) {
|
||||
if (
|
||||
isSuccess &&
|
||||
"structured_output" in resultMessage &&
|
||||
resultMessage.structured_output
|
||||
) {
|
||||
result.structuredOutput = JSON.stringify(resultMessage.structured_output);
|
||||
core.info(
|
||||
`Set structured_output with ${Object.keys(resultMessage.structured_output as object).length} field(s)`,
|
||||
);
|
||||
} else {
|
||||
core.setFailed(
|
||||
`--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`,
|
||||
);
|
||||
result.conclusion = "failure";
|
||||
throw new Error(
|
||||
`--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
if ("errors" in resultMessage && resultMessage.errors) {
|
||||
core.error(`Execution failed: ${resultMessage.errors.join(", ")}`);
|
||||
}
|
||||
throw new Error(
|
||||
`Claude execution failed: ${
|
||||
"errors" in resultMessage && resultMessage.errors
|
||||
? resultMessage.errors.join(", ")
|
||||
: "unknown error"
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@ -1,16 +1,6 @@
|
||||
import * as core from "@actions/core";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { unlink, writeFile, stat } from "fs/promises";
|
||||
import { createWriteStream } from "fs";
|
||||
import { spawn } from "child_process";
|
||||
import { parse as parseShellArgs } from "shell-quote";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`;
|
||||
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
||||
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
|
||||
import { runClaudeWithSdk } from "./run-claude-sdk";
|
||||
import type { ClaudeRunResult } from "./run-claude-sdk";
|
||||
import { parseSdkOptions } from "./parse-sdk-options";
|
||||
|
||||
export type ClaudeOptions = {
|
||||
claudeArgs?: string;
|
||||
@ -22,236 +12,14 @@ export type ClaudeOptions = {
|
||||
mcpConfig?: string;
|
||||
systemPrompt?: string;
|
||||
appendSystemPrompt?: string;
|
||||
claudeEnv?: string;
|
||||
fallbackModel?: string;
|
||||
showFullOutput?: string;
|
||||
};
|
||||
|
||||
type PreparedConfig = {
|
||||
claudeArgs: string[];
|
||||
promptPath: string;
|
||||
env: Record<string, string>;
|
||||
};
|
||||
|
||||
export function prepareRunConfig(
|
||||
export async function runClaude(
|
||||
promptPath: string,
|
||||
options: ClaudeOptions,
|
||||
): PreparedConfig {
|
||||
// Build Claude CLI arguments:
|
||||
// 1. Prompt flag (always first)
|
||||
// 2. User's claudeArgs (full control)
|
||||
// 3. BASE_ARGS (always last, cannot be overridden)
|
||||
|
||||
const claudeArgs = ["-p"];
|
||||
|
||||
// Parse and add user's custom Claude arguments
|
||||
if (options.claudeArgs?.trim()) {
|
||||
const parsed = parseShellArgs(options.claudeArgs);
|
||||
const customArgs = parsed.filter(
|
||||
(arg): arg is string => typeof arg === "string",
|
||||
);
|
||||
claudeArgs.push(...customArgs);
|
||||
}
|
||||
|
||||
// BASE_ARGS are always appended last (cannot be overridden)
|
||||
claudeArgs.push(...BASE_ARGS);
|
||||
|
||||
const customEnv: Record<string, string> = {};
|
||||
|
||||
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
||||
customEnv.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
||||
}
|
||||
|
||||
return {
|
||||
claudeArgs,
|
||||
promptPath,
|
||||
env: customEnv,
|
||||
};
|
||||
}
|
||||
|
||||
export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
||||
const config = prepareRunConfig(promptPath, options);
|
||||
|
||||
// Create a named pipe
|
||||
try {
|
||||
await unlink(PIPE_PATH);
|
||||
} catch (e) {
|
||||
// Ignore if file doesn't exist
|
||||
}
|
||||
|
||||
// Create the named pipe
|
||||
await execAsync(`mkfifo "${PIPE_PATH}"`);
|
||||
|
||||
// Log prompt file size
|
||||
let promptSize = "unknown";
|
||||
try {
|
||||
const stats = await stat(config.promptPath);
|
||||
promptSize = stats.size.toString();
|
||||
} catch (e) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
console.log(`Prompt file size: ${promptSize} bytes`);
|
||||
|
||||
// Log custom environment variables if any
|
||||
const customEnvKeys = Object.keys(config.env).filter(
|
||||
(key) => key !== "CLAUDE_ACTION_INPUTS_PRESENT",
|
||||
);
|
||||
if (customEnvKeys.length > 0) {
|
||||
console.log(`Custom environment variables: ${customEnvKeys.join(", ")}`);
|
||||
}
|
||||
|
||||
// Log custom arguments if any
|
||||
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
|
||||
console.log(`Custom Claude arguments: ${options.claudeArgs}`);
|
||||
}
|
||||
|
||||
// Output to console
|
||||
console.log(`Running Claude with prompt from file: ${config.promptPath}`);
|
||||
console.log(`Full command: claude ${config.claudeArgs.join(" ")}`);
|
||||
|
||||
// Start sending prompt to pipe in background
|
||||
const catProcess = spawn("cat", [config.promptPath], {
|
||||
stdio: ["ignore", "pipe", "inherit"],
|
||||
});
|
||||
const pipeStream = createWriteStream(PIPE_PATH);
|
||||
catProcess.stdout.pipe(pipeStream);
|
||||
|
||||
catProcess.on("error", (error) => {
|
||||
console.error("Error reading prompt file:", error);
|
||||
pipeStream.destroy();
|
||||
});
|
||||
|
||||
// Use custom executable path if provided, otherwise default to "claude"
|
||||
const claudeExecutable = options.pathToClaudeCodeExecutable || "claude";
|
||||
|
||||
const claudeProcess = spawn(claudeExecutable, config.claudeArgs, {
|
||||
stdio: ["pipe", "pipe", "inherit"],
|
||||
env: {
|
||||
...process.env,
|
||||
...config.env,
|
||||
},
|
||||
});
|
||||
|
||||
// Handle Claude process errors
|
||||
claudeProcess.on("error", (error) => {
|
||||
console.error("Error spawning Claude process:", error);
|
||||
pipeStream.destroy();
|
||||
});
|
||||
|
||||
// Capture output for parsing execution metrics
|
||||
let output = "";
|
||||
claudeProcess.stdout.on("data", (data) => {
|
||||
const text = data.toString();
|
||||
|
||||
// Try to parse as JSON and pretty print if it's on a single line
|
||||
const lines = text.split("\n");
|
||||
lines.forEach((line: string, index: number) => {
|
||||
if (line.trim() === "") return;
|
||||
|
||||
try {
|
||||
// Check if this line is a JSON object
|
||||
const parsed = JSON.parse(line);
|
||||
const prettyJson = JSON.stringify(parsed, null, 2);
|
||||
process.stdout.write(prettyJson);
|
||||
if (index < lines.length - 1 || text.endsWith("\n")) {
|
||||
process.stdout.write("\n");
|
||||
}
|
||||
} catch (e) {
|
||||
// Not a JSON object, print as is
|
||||
process.stdout.write(line);
|
||||
if (index < lines.length - 1 || text.endsWith("\n")) {
|
||||
process.stdout.write("\n");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
output += text;
|
||||
});
|
||||
|
||||
// Handle stdout errors
|
||||
claudeProcess.stdout.on("error", (error) => {
|
||||
console.error("Error reading Claude stdout:", error);
|
||||
});
|
||||
|
||||
// Pipe from named pipe to Claude
|
||||
const pipeProcess = spawn("cat", [PIPE_PATH]);
|
||||
pipeProcess.stdout.pipe(claudeProcess.stdin);
|
||||
|
||||
// Handle pipe process errors
|
||||
pipeProcess.on("error", (error) => {
|
||||
console.error("Error reading from named pipe:", error);
|
||||
claudeProcess.kill("SIGTERM");
|
||||
});
|
||||
|
||||
// Wait for Claude to finish
|
||||
const exitCode = await new Promise<number>((resolve) => {
|
||||
claudeProcess.on("close", (code) => {
|
||||
resolve(code || 0);
|
||||
});
|
||||
|
||||
claudeProcess.on("error", (error) => {
|
||||
console.error("Claude process error:", error);
|
||||
resolve(1);
|
||||
});
|
||||
});
|
||||
|
||||
// Clean up processes
|
||||
try {
|
||||
catProcess.kill("SIGTERM");
|
||||
} catch (e) {
|
||||
// Process may already be dead
|
||||
}
|
||||
try {
|
||||
pipeProcess.kill("SIGTERM");
|
||||
} catch (e) {
|
||||
// Process may already be dead
|
||||
}
|
||||
|
||||
// Clean up pipe file
|
||||
try {
|
||||
await unlink(PIPE_PATH);
|
||||
} catch (e) {
|
||||
// Ignore errors during cleanup
|
||||
}
|
||||
|
||||
// Set conclusion based on exit code
|
||||
if (exitCode === 0) {
|
||||
// Try to process the output and save execution metrics
|
||||
try {
|
||||
await writeFile("output.txt", output);
|
||||
|
||||
// Process output.txt into JSON and save to execution file
|
||||
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
|
||||
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
await writeFile(EXECUTION_FILE, jsonOutput);
|
||||
|
||||
console.log(`Log saved to ${EXECUTION_FILE}`);
|
||||
} catch (e) {
|
||||
core.warning(`Failed to process output for execution metrics: ${e}`);
|
||||
}
|
||||
|
||||
core.setOutput("conclusion", "success");
|
||||
core.setOutput("execution_file", EXECUTION_FILE);
|
||||
} else {
|
||||
core.setOutput("conclusion", "failure");
|
||||
|
||||
// Still try to save execution file if we have output
|
||||
if (output) {
|
||||
try {
|
||||
await writeFile("output.txt", output);
|
||||
// Increase maxBuffer from Node.js default of 1MB to 10MB to handle large Claude outputs
|
||||
const { stdout: jsonOutput } = await execAsync("jq -s '.' output.txt", {
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
await writeFile(EXECUTION_FILE, jsonOutput);
|
||||
core.setOutput("execution_file", EXECUTION_FILE);
|
||||
} catch (e) {
|
||||
// Ignore errors when processing output during failure
|
||||
}
|
||||
}
|
||||
|
||||
process.exit(exitCode);
|
||||
}
|
||||
): Promise<ClaudeRunResult> {
|
||||
const parsedOptions = parseSdkOptions(options);
|
||||
return runClaudeWithSdk(promptPath, parsedOptions);
|
||||
}
|
||||
|
||||
@ -1,39 +1,50 @@
|
||||
/**
|
||||
* Validates the environment variables required for running Claude Code
|
||||
* based on the selected provider (Anthropic API, AWS Bedrock, or Google Vertex AI)
|
||||
* based on the selected provider (Anthropic API, AWS Bedrock, Google Vertex AI, or Microsoft Foundry)
|
||||
*/
|
||||
export function validateEnvironmentVariables() {
|
||||
const useBedrock = process.env.CLAUDE_CODE_USE_BEDROCK === "1";
|
||||
const useVertex = process.env.CLAUDE_CODE_USE_VERTEX === "1";
|
||||
const useFoundry = process.env.CLAUDE_CODE_USE_FOUNDRY === "1";
|
||||
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
||||
const claudeCodeOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
if (useBedrock && useVertex) {
|
||||
// Check for mutual exclusivity between providers
|
||||
const activeProviders = [useBedrock, useVertex, useFoundry].filter(Boolean);
|
||||
if (activeProviders.length > 1) {
|
||||
errors.push(
|
||||
"Cannot use both Bedrock and Vertex AI simultaneously. Please set only one provider.",
|
||||
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
||||
);
|
||||
}
|
||||
|
||||
if (!useBedrock && !useVertex) {
|
||||
if (!useBedrock && !useVertex && !useFoundry) {
|
||||
if (!anthropicApiKey && !claudeCodeOAuthToken) {
|
||||
errors.push(
|
||||
"Either ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN is required when using direct Anthropic API.",
|
||||
);
|
||||
}
|
||||
} else if (useBedrock) {
|
||||
const requiredBedrockVars = {
|
||||
AWS_REGION: process.env.AWS_REGION,
|
||||
AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID,
|
||||
AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY,
|
||||
};
|
||||
const awsRegion = process.env.AWS_REGION;
|
||||
const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
|
||||
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
|
||||
const awsBearerToken = process.env.AWS_BEARER_TOKEN_BEDROCK;
|
||||
|
||||
Object.entries(requiredBedrockVars).forEach(([key, value]) => {
|
||||
if (!value) {
|
||||
errors.push(`${key} is required when using AWS Bedrock.`);
|
||||
}
|
||||
});
|
||||
// AWS_REGION is always required for Bedrock
|
||||
if (!awsRegion) {
|
||||
errors.push("AWS_REGION is required when using AWS Bedrock.");
|
||||
}
|
||||
|
||||
// Either bearer token OR access key credentials must be provided
|
||||
const hasAccessKeyCredentials = awsAccessKeyId && awsSecretAccessKey;
|
||||
const hasBearerToken = awsBearerToken;
|
||||
|
||||
if (!hasAccessKeyCredentials && !hasBearerToken) {
|
||||
errors.push(
|
||||
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
||||
);
|
||||
}
|
||||
} else if (useVertex) {
|
||||
const requiredVertexVars = {
|
||||
ANTHROPIC_VERTEX_PROJECT_ID: process.env.ANTHROPIC_VERTEX_PROJECT_ID,
|
||||
@ -45,6 +56,16 @@ export function validateEnvironmentVariables() {
|
||||
errors.push(`${key} is required when using Google Vertex AI.`);
|
||||
}
|
||||
});
|
||||
} else if (useFoundry) {
|
||||
const foundryResource = process.env.ANTHROPIC_FOUNDRY_RESOURCE;
|
||||
const foundryBaseUrl = process.env.ANTHROPIC_FOUNDRY_BASE_URL;
|
||||
|
||||
// Either resource name or base URL is required
|
||||
if (!foundryResource && !foundryBaseUrl) {
|
||||
errors.push(
|
||||
"Either ANTHROPIC_FOUNDRY_RESOURCE or ANTHROPIC_FOUNDRY_BASE_URL is required when using Microsoft Foundry.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
|
||||
@ -9,4 +9,4 @@ fi
|
||||
# Run the test workflow locally
|
||||
# You'll need to provide your ANTHROPIC_API_KEY
|
||||
echo "Running action locally with act..."
|
||||
act push --secret ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" -W .github/workflows/test-action.yml --container-architecture linux/amd64
|
||||
act push --secret ANTHROPIC_API_KEY="$ANTHROPIC_API_KEY" -W .github/workflows/test-base-action.yml --container-architecture linux/amd64
|
||||
706
base-action/test/install-plugins.test.ts
Normal file
706
base-action/test/install-plugins.test.ts
Normal file
@ -0,0 +1,706 @@
|
||||
#!/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" },
|
||||
);
|
||||
});
|
||||
|
||||
// Local marketplace path tests
|
||||
test("should accept local marketplace path with ./", async () => {
|
||||
const spy = createMockSpawn();
|
||||
await installPlugins("./my-local-marketplace", "test-plugin");
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "./my-local-marketplace"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"claude",
|
||||
["plugin", "install", "test-plugin"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
});
|
||||
|
||||
test("should accept local marketplace path with absolute Unix path", async () => {
|
||||
const spy = createMockSpawn();
|
||||
await installPlugins("/home/user/my-marketplace", "test-plugin");
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "/home/user/my-marketplace"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
});
|
||||
|
||||
test("should accept local marketplace path with Windows absolute path", async () => {
|
||||
const spy = createMockSpawn();
|
||||
await installPlugins("C:\\Users\\user\\marketplace", "test-plugin");
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "C:\\Users\\user\\marketplace"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
});
|
||||
|
||||
test("should accept mixed local and remote marketplaces", async () => {
|
||||
const spy = createMockSpawn();
|
||||
await installPlugins(
|
||||
"./local-marketplace\nhttps://github.com/user/remote.git",
|
||||
"test-plugin",
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "./local-marketplace"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "https://github.com/user/remote.git"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
});
|
||||
|
||||
test("should accept local path with ../ (parent directory)", async () => {
|
||||
const spy = createMockSpawn();
|
||||
await installPlugins("../shared-plugins/marketplace", "test-plugin");
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "../shared-plugins/marketplace"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
});
|
||||
|
||||
test("should accept local path with nested directories", async () => {
|
||||
const spy = createMockSpawn();
|
||||
await installPlugins("./plugins/my-org/my-marketplace", "test-plugin");
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "./plugins/my-org/my-marketplace"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
});
|
||||
|
||||
test("should accept local path with dots in directory name", async () => {
|
||||
const spy = createMockSpawn();
|
||||
await installPlugins("./my.plugin.marketplace", "test-plugin");
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"claude",
|
||||
["plugin", "marketplace", "add", "./my.plugin.marketplace"],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -2,6 +2,6 @@
|
||||
"name": "mcp-test",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.11.0"
|
||||
"@modelcontextprotocol/sdk": "^1.24.0"
|
||||
}
|
||||
}
|
||||
|
||||
425
base-action/test/parse-sdk-options.test.ts
Normal file
425
base-action/test/parse-sdk-options.test.ts
Normal file
@ -0,0 +1,425 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { parseSdkOptions } from "../src/parse-sdk-options";
|
||||
import type { ClaudeOptions } from "../src/run-claude";
|
||||
|
||||
describe("parseSdkOptions", () => {
|
||||
describe("allowedTools merging", () => {
|
||||
test("should extract allowedTools from claudeArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read,Write"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
||||
expect(result.sdkOptions.extraArgs?.["allowedTools"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should extract allowedTools from claudeArgs with MCP tools", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs:
|
||||
'--allowedTools "Edit,Read,mcp__github_comment__update_claude_comment"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Edit",
|
||||
"Read",
|
||||
"mcp__github_comment__update_claude_comment",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should accumulate multiple --allowedTools flags from claudeArgs", () => {
|
||||
// This simulates tag mode adding its tools, then user adding their own
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs:
|
||||
'--allowedTools "Edit,Read,mcp__github_comment__update_claude_comment" --model "claude-3" --allowedTools "Bash(npm install),mcp__github__get_issue"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Edit",
|
||||
"Read",
|
||||
"mcp__github_comment__update_claude_comment",
|
||||
"Bash(npm install)",
|
||||
"mcp__github__get_issue",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should merge allowedTools from both claudeArgs and direct options", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read"',
|
||||
allowedTools: "Write,Glob",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Edit",
|
||||
"Read",
|
||||
"Write",
|
||||
"Glob",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should deduplicate allowedTools when merging", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read"',
|
||||
allowedTools: "Edit,Write",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
||||
});
|
||||
|
||||
test("should use only direct options when claudeArgs has no allowedTools", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--model "claude-3-5-sonnet"',
|
||||
allowedTools: "Edit,Read",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read"]);
|
||||
});
|
||||
|
||||
test("should return undefined allowedTools when neither source has it", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--model "claude-3-5-sonnet"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should remove allowedTools from extraArgs after extraction", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read" --model "claude-3-5-sonnet"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["allowedTools"]).toBeUndefined();
|
||||
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-3-5-sonnet");
|
||||
});
|
||||
|
||||
test("should handle hyphenated --allowed-tools flag", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowed-tools "Edit,Read,Write"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit", "Read", "Write"]);
|
||||
expect(result.sdkOptions.extraArgs?.["allowed-tools"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should accumulate multiple --allowed-tools flags (hyphenated)", () => {
|
||||
// This is the exact scenario from issue #746
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs:
|
||||
'--allowed-tools "Bash(git log:*)" "Bash(git diff:*)" "Bash(git fetch:*)" "Bash(gh pr:*)"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.allowedTools).toEqual([
|
||||
"Bash(git log:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git fetch:*)",
|
||||
"Bash(gh pr:*)",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle mixed camelCase and hyphenated allowedTools flags", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--allowedTools "Edit,Read" --allowed-tools "Write,Glob"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
// Both should be merged - note: order depends on which key is found first
|
||||
expect(result.sdkOptions.allowedTools).toContain("Edit");
|
||||
expect(result.sdkOptions.allowedTools).toContain("Read");
|
||||
expect(result.sdkOptions.allowedTools).toContain("Write");
|
||||
expect(result.sdkOptions.allowedTools).toContain("Glob");
|
||||
});
|
||||
});
|
||||
|
||||
describe("disallowedTools merging", () => {
|
||||
test("should extract disallowedTools from claudeArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--disallowedTools "Bash,Write"',
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.disallowedTools).toEqual(["Bash", "Write"]);
|
||||
expect(result.sdkOptions.extraArgs?.["disallowedTools"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("should merge disallowedTools from both sources", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--disallowedTools "Bash"',
|
||||
disallowedTools: "Write",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.disallowedTools).toEqual(["Bash", "Write"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("mcp-config merging", () => {
|
||||
test("should pass through single mcp-config in extraArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config '{"mcpServers":{"server1":{"command":"cmd1"}}}'`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["mcp-config"]).toBe(
|
||||
'{"mcpServers":{"server1":{"command":"cmd1"}}}',
|
||||
);
|
||||
});
|
||||
|
||||
test("should merge multiple mcp-config flags with inline JSON", () => {
|
||||
// Simulates action prepending its config, then user providing their own
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config '{"mcpServers":{"github_comment":{"command":"node","args":["server.js"]}}}' --mcp-config '{"mcpServers":{"user_server":{"command":"custom","args":["run"]}}}'`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
const mcpConfig = JSON.parse(
|
||||
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
||||
);
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("github_comment");
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("user_server");
|
||||
expect(mcpConfig.mcpServers.github_comment.command).toBe("node");
|
||||
expect(mcpConfig.mcpServers.user_server.command).toBe("custom");
|
||||
});
|
||||
|
||||
test("should merge three mcp-config flags", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config '{"mcpServers":{"server1":{"command":"cmd1"}}}' --mcp-config '{"mcpServers":{"server2":{"command":"cmd2"}}}' --mcp-config '{"mcpServers":{"server3":{"command":"cmd3"}}}'`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
const mcpConfig = JSON.parse(
|
||||
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
||||
);
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("server1");
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("server2");
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("server3");
|
||||
});
|
||||
|
||||
test("should handle mcp-config file path when no inline JSON exists", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config /tmp/user-mcp-config.json`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["mcp-config"]).toBe(
|
||||
"/tmp/user-mcp-config.json",
|
||||
);
|
||||
});
|
||||
|
||||
test("should merge inline JSON configs when file path is also present", () => {
|
||||
// When action provides inline JSON and user provides a file path,
|
||||
// the inline JSON configs should be merged (file paths cannot be merged at parse time)
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config '{"mcpServers":{"github_comment":{"command":"node"}}}' --mcp-config '{"mcpServers":{"github_ci":{"command":"node"}}}' --mcp-config /tmp/user-config.json`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
// The inline JSON configs should be merged
|
||||
const mcpConfig = JSON.parse(
|
||||
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
||||
);
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("github_comment");
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("github_ci");
|
||||
});
|
||||
|
||||
test("should handle mcp-config with other flags", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config '{"mcpServers":{"server1":{}}}' --model claude-3-5-sonnet --mcp-config '{"mcpServers":{"server2":{}}}'`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
const mcpConfig = JSON.parse(
|
||||
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
||||
);
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("server1");
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("server2");
|
||||
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-3-5-sonnet");
|
||||
});
|
||||
|
||||
test("should handle real-world scenario: action config + user config", () => {
|
||||
// This is the exact scenario from the bug report
|
||||
const actionConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
github_comment: {
|
||||
command: "node",
|
||||
args: ["github-comment-server.js"],
|
||||
},
|
||||
github_ci: { command: "node", args: ["github-ci-server.js"] },
|
||||
},
|
||||
});
|
||||
const userConfig = JSON.stringify({
|
||||
mcpServers: {
|
||||
my_custom_server: { command: "python", args: ["server.py"] },
|
||||
},
|
||||
});
|
||||
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--mcp-config '${actionConfig}' --mcp-config '${userConfig}'`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
const mcpConfig = JSON.parse(
|
||||
result.sdkOptions.extraArgs?.["mcp-config"] as string,
|
||||
);
|
||||
// All servers should be present
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("github_comment");
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("github_ci");
|
||||
expect(mcpConfig.mcpServers).toHaveProperty("my_custom_server");
|
||||
});
|
||||
});
|
||||
|
||||
describe("other extraArgs passthrough", () => {
|
||||
test("should pass through json-schema in extraArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: `--json-schema '{"type":"object"}'`,
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["json-schema"]).toBe(
|
||||
'{"type":"object"}',
|
||||
);
|
||||
expect(result.hasJsonSchema).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("shell comment stripping", () => {
|
||||
test("should parse flags before and after a comment line", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--model 'claude-haiku'\n# comment\n--allowed-tools 'Edit'",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku");
|
||||
expect(result.sdkOptions.allowedTools).toEqual(["Edit"]);
|
||||
});
|
||||
|
||||
test("should parse flags correctly when no comments are present", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--model 'claude-haiku'",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku");
|
||||
});
|
||||
|
||||
test("should not strip inline # that appears inside a quoted value", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--model 'claude-haiku' --prompt 'use color #ff0000'",
|
||||
};
|
||||
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.extraArgs?.["model"]).toBe("claude-haiku");
|
||||
expect(result.sdkOptions.extraArgs?.["prompt"]).toBe("use color #ff0000");
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment variables passthrough", () => {
|
||||
test("should include OTEL environment variables in sdkOptions.env", () => {
|
||||
// Set up test environment variables
|
||||
const originalEnv = { ...process.env };
|
||||
process.env.CLAUDE_CODE_ENABLE_TELEMETRY = "1";
|
||||
process.env.OTEL_METRICS_EXPORTER = "otlp";
|
||||
process.env.OTEL_LOGS_EXPORTER = "otlp";
|
||||
process.env.OTEL_EXPORTER_OTLP_PROTOCOL = "http/json";
|
||||
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = "https://example.com";
|
||||
process.env.OTEL_EXPORTER_OTLP_HEADERS =
|
||||
"Authorization=Bearer test-token";
|
||||
process.env.OTEL_METRIC_EXPORT_INTERVAL = "10000";
|
||||
process.env.OTEL_LOGS_EXPORT_INTERVAL = "5000";
|
||||
process.env.OTEL_RESOURCE_ATTRIBUTES = "department=test";
|
||||
|
||||
try {
|
||||
const options: ClaudeOptions = {};
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
// Verify OTEL env vars are passed through to sdkOptions.env
|
||||
expect(result.sdkOptions.env?.CLAUDE_CODE_ENABLE_TELEMETRY).toBe("1");
|
||||
expect(result.sdkOptions.env?.OTEL_METRICS_EXPORTER).toBe("otlp");
|
||||
expect(result.sdkOptions.env?.OTEL_LOGS_EXPORTER).toBe("otlp");
|
||||
expect(result.sdkOptions.env?.OTEL_EXPORTER_OTLP_PROTOCOL).toBe(
|
||||
"http/json",
|
||||
);
|
||||
expect(result.sdkOptions.env?.OTEL_EXPORTER_OTLP_ENDPOINT).toBe(
|
||||
"https://example.com",
|
||||
);
|
||||
expect(result.sdkOptions.env?.OTEL_EXPORTER_OTLP_HEADERS).toBe(
|
||||
"Authorization=Bearer test-token",
|
||||
);
|
||||
expect(result.sdkOptions.env?.OTEL_METRIC_EXPORT_INTERVAL).toBe(
|
||||
"10000",
|
||||
);
|
||||
expect(result.sdkOptions.env?.OTEL_LOGS_EXPORT_INTERVAL).toBe("5000");
|
||||
expect(result.sdkOptions.env?.OTEL_RESOURCE_ATTRIBUTES).toBe(
|
||||
"department=test",
|
||||
);
|
||||
} finally {
|
||||
// Restore original environment
|
||||
process.env = originalEnv;
|
||||
}
|
||||
});
|
||||
|
||||
test("should set CLAUDE_CODE_ENTRYPOINT in sdkOptions.env", () => {
|
||||
const options: ClaudeOptions = {};
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(result.sdkOptions.env?.CLAUDE_CODE_ENTRYPOINT).toBe(
|
||||
"claude-code-github-action",
|
||||
);
|
||||
});
|
||||
|
||||
test("should strip ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN from env", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_URL =
|
||||
"https://token.actions.githubusercontent.com";
|
||||
process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN = "secret-token-value";
|
||||
|
||||
try {
|
||||
const options: ClaudeOptions = {};
|
||||
const result = parseSdkOptions(options);
|
||||
|
||||
expect(
|
||||
result.sdkOptions.env?.ACTIONS_ID_TOKEN_REQUEST_URL,
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
result.sdkOptions.env?.ACTIONS_ID_TOKEN_REQUEST_TOKEN,
|
||||
).toBeUndefined();
|
||||
} finally {
|
||||
process.env = originalEnv;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { prepareRunConfig, type ClaudeOptions } from "../src/run-claude";
|
||||
|
||||
describe("prepareRunConfig", () => {
|
||||
test("should prepare config with basic arguments", () => {
|
||||
const options: ClaudeOptions = {};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should include promptPath", () => {
|
||||
const options: ClaudeOptions = {};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.promptPath).toBe("/tmp/test-prompt.txt");
|
||||
});
|
||||
|
||||
test("should use provided prompt path", () => {
|
||||
const options: ClaudeOptions = {};
|
||||
const prepared = prepareRunConfig("/custom/prompt/path.txt", options);
|
||||
|
||||
expect(prepared.promptPath).toBe("/custom/prompt/path.txt");
|
||||
});
|
||||
|
||||
describe("claudeArgs handling", () => {
|
||||
test("should parse and include custom claude arguments", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--max-turns 10 --model claude-3-opus-20240229",
|
||||
};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--max-turns",
|
||||
"10",
|
||||
"--model",
|
||||
"claude-3-opus-20240229",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle empty claudeArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "",
|
||||
};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle claudeArgs with quoted strings", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--system-prompt "You are a helpful assistant"',
|
||||
};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--system-prompt",
|
||||
"You are a helpful assistant",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -13,15 +13,19 @@ describe("validateEnvironmentVariables", () => {
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
delete process.env.CLAUDE_CODE_USE_BEDROCK;
|
||||
delete process.env.CLAUDE_CODE_USE_VERTEX;
|
||||
delete process.env.CLAUDE_CODE_USE_FOUNDRY;
|
||||
delete process.env.AWS_REGION;
|
||||
delete process.env.AWS_ACCESS_KEY_ID;
|
||||
delete process.env.AWS_SECRET_ACCESS_KEY;
|
||||
delete process.env.AWS_SESSION_TOKEN;
|
||||
delete process.env.AWS_BEARER_TOKEN_BEDROCK;
|
||||
delete process.env.ANTHROPIC_BEDROCK_BASE_URL;
|
||||
delete process.env.ANTHROPIC_VERTEX_PROJECT_ID;
|
||||
delete process.env.CLOUD_ML_REGION;
|
||||
delete process.env.GOOGLE_APPLICATION_CREDENTIALS;
|
||||
delete process.env.ANTHROPIC_VERTEX_BASE_URL;
|
||||
delete process.env.ANTHROPIC_FOUNDRY_RESOURCE;
|
||||
delete process.env.ANTHROPIC_FOUNDRY_BASE_URL;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -92,31 +96,58 @@ describe("validateEnvironmentVariables", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("should fail when AWS_ACCESS_KEY_ID is missing", () => {
|
||||
test("should fail when only AWS_SECRET_ACCESS_KEY is provided without bearer token", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.AWS_REGION = "us-east-1";
|
||||
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"AWS_ACCESS_KEY_ID is required when using AWS Bedrock.",
|
||||
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
||||
);
|
||||
});
|
||||
|
||||
test("should fail when AWS_SECRET_ACCESS_KEY is missing", () => {
|
||||
test("should fail when only AWS_ACCESS_KEY_ID is provided without bearer token", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.AWS_REGION = "us-east-1";
|
||||
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"AWS_SECRET_ACCESS_KEY is required when using AWS Bedrock.",
|
||||
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
||||
);
|
||||
});
|
||||
|
||||
test("should report all missing Bedrock variables", () => {
|
||||
test("should pass when AWS_BEARER_TOKEN_BEDROCK is provided instead of access keys", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.AWS_REGION = "us-east-1";
|
||||
process.env.AWS_BEARER_TOKEN_BEDROCK = "test-bearer-token";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).not.toThrow();
|
||||
});
|
||||
|
||||
test("should pass when both bearer token and access keys are provided", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.AWS_REGION = "us-east-1";
|
||||
process.env.AWS_BEARER_TOKEN_BEDROCK = "test-bearer-token";
|
||||
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
||||
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).not.toThrow();
|
||||
});
|
||||
|
||||
test("should fail when no authentication method is provided", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.AWS_REGION = "us-east-1";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
||||
);
|
||||
});
|
||||
|
||||
test("should report missing region and authentication", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
/AWS_REGION is required when using AWS Bedrock.*AWS_ACCESS_KEY_ID is required when using AWS Bedrock.*AWS_SECRET_ACCESS_KEY is required when using AWS Bedrock/s,
|
||||
/AWS_REGION is required when using AWS Bedrock.*Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock/s,
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -167,6 +198,56 @@ describe("validateEnvironmentVariables", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Microsoft Foundry", () => {
|
||||
test("should pass when ANTHROPIC_FOUNDRY_RESOURCE is provided", () => {
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).not.toThrow();
|
||||
});
|
||||
|
||||
test("should pass when ANTHROPIC_FOUNDRY_BASE_URL is provided", () => {
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
process.env.ANTHROPIC_FOUNDRY_BASE_URL =
|
||||
"https://test-resource.services.ai.azure.com";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).not.toThrow();
|
||||
});
|
||||
|
||||
test("should pass when both resource and base URL are provided", () => {
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
||||
process.env.ANTHROPIC_FOUNDRY_BASE_URL =
|
||||
"https://custom.services.ai.azure.com";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).not.toThrow();
|
||||
});
|
||||
|
||||
test("should construct Foundry base URL from resource name when ANTHROPIC_FOUNDRY_BASE_URL is not provided", () => {
|
||||
// This test verifies our action.yml change, which constructs:
|
||||
// ANTHROPIC_FOUNDRY_BASE_URL: ${{ env.ANTHROPIC_FOUNDRY_BASE_URL || (env.ANTHROPIC_FOUNDRY_RESOURCE && format('https://{0}.services.ai.azure.com', env.ANTHROPIC_FOUNDRY_RESOURCE)) }}
|
||||
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "my-foundry-resource";
|
||||
// ANTHROPIC_FOUNDRY_BASE_URL is intentionally not set
|
||||
|
||||
// The actual URL construction happens in the composite action in action.yml
|
||||
// This test is a placeholder to document the behavior
|
||||
expect(() => validateEnvironmentVariables()).not.toThrow();
|
||||
|
||||
// In the actual action, ANTHROPIC_FOUNDRY_BASE_URL would be:
|
||||
// https://my-foundry-resource.services.ai.azure.com
|
||||
});
|
||||
|
||||
test("should fail when neither ANTHROPIC_FOUNDRY_RESOURCE nor ANTHROPIC_FOUNDRY_BASE_URL is provided", () => {
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"Either ANTHROPIC_FOUNDRY_RESOURCE or ANTHROPIC_FOUNDRY_BASE_URL is required when using Microsoft Foundry.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Multiple providers", () => {
|
||||
test("should fail when both Bedrock and Vertex are enabled", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
@ -179,7 +260,51 @@ describe("validateEnvironmentVariables", () => {
|
||||
process.env.CLOUD_ML_REGION = "us-central1";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"Cannot use both Bedrock and Vertex AI simultaneously. Please set only one provider.",
|
||||
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
||||
);
|
||||
});
|
||||
|
||||
test("should fail when both Bedrock and Foundry are enabled", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
// Provide all required vars to isolate the mutual exclusion error
|
||||
process.env.AWS_REGION = "us-east-1";
|
||||
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
||||
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
||||
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
||||
);
|
||||
});
|
||||
|
||||
test("should fail when both Vertex and Foundry are enabled", () => {
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
// Provide all required vars to isolate the mutual exclusion error
|
||||
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
||||
process.env.CLOUD_ML_REGION = "us-central1";
|
||||
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
||||
);
|
||||
});
|
||||
|
||||
test("should fail when all three providers are enabled", () => {
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = "1";
|
||||
process.env.CLAUDE_CODE_USE_VERTEX = "1";
|
||||
process.env.CLAUDE_CODE_USE_FOUNDRY = "1";
|
||||
// Provide all required vars to isolate the mutual exclusion error
|
||||
process.env.AWS_REGION = "us-east-1";
|
||||
process.env.AWS_ACCESS_KEY_ID = "test-access-key";
|
||||
process.env.AWS_SECRET_ACCESS_KEY = "test-secret-key";
|
||||
process.env.ANTHROPIC_VERTEX_PROJECT_ID = "test-project";
|
||||
process.env.CLOUD_ML_REGION = "us-central1";
|
||||
process.env.ANTHROPIC_FOUNDRY_RESOURCE = "test-resource";
|
||||
|
||||
expect(() => validateEnvironmentVariables()).toThrow(
|
||||
"Cannot use multiple providers simultaneously. Please set only one of: CLAUDE_CODE_USE_BEDROCK, CLAUDE_CODE_USE_VERTEX, or CLAUDE_CODE_USE_FOUNDRY.",
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -204,10 +329,7 @@ describe("validateEnvironmentVariables", () => {
|
||||
" - AWS_REGION is required when using AWS Bedrock.",
|
||||
);
|
||||
expect(error!.message).toContain(
|
||||
" - AWS_ACCESS_KEY_ID is required when using AWS Bedrock.",
|
||||
);
|
||||
expect(error!.message).toContain(
|
||||
" - AWS_SECRET_ACCESS_KEY is required when using AWS Bedrock.",
|
||||
" - Either AWS_BEARER_TOKEN_BEDROCK or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are required when using AWS Bedrock.",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
80
bun.lock
80
bun.lock
@ -1,11 +1,13 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "@anthropic-ai/claude-code-action",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.123",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
@ -35,8 +37,32 @@
|
||||
|
||||
"@actions/io": ["@actions/io@1.1.3", "", {}, "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.2.123", "", { "dependencies": { "@anthropic-ai/sdk": "^0.81.0", "@modelcontextprotocol/sdk": "^1.29.0" }, "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.2.123", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.2.123", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.2.123" }, "peerDependencies": { "zod": "^4.0.0" } }, "sha512-a4TysYoR9DBdkM9Uwh4J5ub7TwKmRPe5hFiWh4En+IKC+qkk5UFkxFM22c//cZjYZKynHX0ah2t6LUqb+najYA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.2.123", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tYAXCjlXZQklsUs0J//gip3fZQRzhlH5OCgvNXV70qe7A1iiwHqO2KPGvEHV1L+deEKQoMZmTaCOrQpN6zju3w=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.2.123", "", { "os": "darwin", "cpu": "x64" }, "sha512-AcUC6sTon6z6HculP87KsAOeTMRLBwpovdhcXUTjXUpo/8nplJ7lBEzWjZCHt8FF1KuN/WBy1Z4bDg/59TQDmA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-7+GnbcF3/aZ8RJ1WmU/ogtPsOpknBAoUPer90MvZuFYBLPT9iI/U7f24gjrOHuYdcbDA5n7jFlhcfIO26F5DJQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.2.123", "", { "os": "linux", "cpu": "arm64" }, "sha512-bYgRiaf2q+yVbGAoUluuhqrEW1zexL34+3HDmK9DneKXa2K2EJpw4M6Sq4XoBD/JezGaemoAP78Xv/M/QUS1OQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-Xi+Rwk8uP5vWEnawJOlsk179fr0ATLl5J90MlbLj+puKaX5svEq8ljS+P3zq6zHTJeKh9GKLzPf7bc5YJKwcew=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.2.123", "", { "os": "linux", "cpu": "x64" }, "sha512-IX95lFKhmmndY/YPfWPsVV+C3rLYJmuuq5wCS53p6jYIkCMxH1iGfhBGF1EUWcXO4Uc8yqXFmQ3aaxMzOOPrwA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.2.123", "", { "os": "win32", "cpu": "arm64" }, "sha512-WDZmAQG1rOiqNLZlSXaCjSWmqJvLk2io+vFQWWqSy2b5HCk9pa3PadLiaLztiihyk81wPhH9Q/44kOxdyfEGMw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.2.123", "", { "os": "win32", "cpu": "x64" }, "sha512-588xrd1i6d4kXQ6FqwL+cgBiN4evRQSi5DCtPa02CZ3VEbuVQBeFlyPlD8tfWtNNeGZ4NM8kjPNNzZz5omezPA=="],
|
||||
|
||||
"@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.81.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-D4K5PvEV6wPiRtVlVsJHIUhHAmOZ6IT/I9rKlTf84gR7GyyAurPJK7z9BOf/AZqC5d1DhYQGJNKRmV+q8dGhgw=="],
|
||||
|
||||
"@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
|
||||
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.16.0", "", { "dependencies": { "ajv": "^6.12.6", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.0.1", "express-rate-limit": "^7.5.0", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.23.8", "zod-to-json-schema": "^3.24.1" } }, "sha512-8ofX7gkZcLj9H9rSd50mCgm3SSF8C7XoclxJuLoV0Cz3rEQ1tv9MZRYYvJtm9n1BiEQQMzSmE/w2AEkNacLYfg=="],
|
||||
|
||||
"@octokit/auth-token": ["@octokit/auth-token@4.0.0", "", {}, "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA=="],
|
||||
@ -77,6 +103,8 @@
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
|
||||
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||
|
||||
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
|
||||
|
||||
"before-after-hook": ["before-after-hook@2.2.3", "", {}, "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ=="],
|
||||
@ -147,6 +175,8 @@
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||
|
||||
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
|
||||
|
||||
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
|
||||
@ -173,20 +203,30 @@
|
||||
|
||||
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hono": ["hono@4.12.9", "", {}, "sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA=="],
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="],
|
||||
|
||||
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
|
||||
|
||||
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
||||
|
||||
"json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||
|
||||
"json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
|
||||
@ -233,6 +273,8 @@
|
||||
|
||||
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||
@ -263,6 +305,8 @@
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="],
|
||||
|
||||
"tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="],
|
||||
|
||||
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
|
||||
@ -291,6 +335,8 @@
|
||||
|
||||
"zod-to-json-schema": ["zod-to-json-schema@3.24.6", "", { "peerDependencies": { "zod": "^3.24.1" } }, "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
||||
|
||||
"@octokit/core/@octokit/graphql": ["@octokit/graphql@7.1.1", "", { "dependencies": { "@octokit/request": "^8.4.1", "@octokit/types": "^13.0.0", "universal-user-agent": "^6.0.0" } }, "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g=="],
|
||||
|
||||
"@octokit/core/@octokit/types": ["@octokit/types@13.10.0", "", { "dependencies": { "@octokit/openapi-types": "^24.2.0" } }, "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA=="],
|
||||
@ -323,12 +369,24 @@
|
||||
|
||||
"accepts/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"ajv-formats/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"send/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"type-is/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express-rate-limit": ["express-rate-limit@8.3.1", "", { "dependencies": { "ip-address": "10.1.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="],
|
||||
|
||||
"@octokit/core/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/endpoint/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
@ -367,12 +425,24 @@
|
||||
|
||||
"accepts/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"send/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"type-is/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body/http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"@octokit/plugin-request-log/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
|
||||
|
||||
"@octokit/rest/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@10.1.4", "", { "dependencies": { "@octokit/types": "^14.0.0", "universal-user-agent": "^7.0.2" } }, "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA=="],
|
||||
@ -380,5 +450,15 @@
|
||||
"@octokit/rest/@octokit/plugin-paginate-rest/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@octokit/rest/@octokit/plugin-rest-endpoint-methods/@octokit/types/@octokit/openapi-types": ["@octokit/openapi-types@24.2.0", "", {}, "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser/iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/body-parser/qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/express/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
|
||||
|
||||
"@anthropic-ai/claude-agent-sdk/@modelcontextprotocol/sdk/raw-body/http-errors/statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
|
||||
}
|
||||
}
|
||||
|
||||
2
bunfig.toml
Normal file
2
bunfig.toml
Normal file
@ -0,0 +1,2 @@
|
||||
# Intentionally minimal. action.yml pins --config to this file so bun resolves
|
||||
# its runtime config from the action directory rather than the workspace.
|
||||
@ -1,16 +1,17 @@
|
||||
# Cloud Providers
|
||||
|
||||
You can authenticate with Claude using any of these three methods:
|
||||
You can authenticate with Claude using any of these four methods:
|
||||
|
||||
1. Direct Anthropic API (default)
|
||||
2. Amazon Bedrock with OIDC authentication
|
||||
3. Google Vertex AI with OIDC authentication
|
||||
4. Microsoft Foundry with OIDC authentication
|
||||
|
||||
For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/github-actions#using-with-aws-bedrock-%26-google-vertex-ai).
|
||||
For detailed setup instructions for AWS Bedrock and Google Vertex AI, see the [official documentation](https://code.claude.com/docs/en/github-actions#for-aws-bedrock:).
|
||||
|
||||
**Note**:
|
||||
|
||||
- Bedrock and Vertex use OIDC authentication exclusively
|
||||
- Bedrock, Vertex, and Microsoft Foundry use OIDC authentication exclusively
|
||||
- AWS Bedrock automatically uses cross-region inference profiles for certain models
|
||||
- For cross-region inference profile models, you need to request and be granted access to the Claude models in all regions that the inference profile uses
|
||||
|
||||
@ -40,11 +41,19 @@ Use provider-specific model names based on your chosen provider:
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet@20250805
|
||||
# ... other inputs
|
||||
|
||||
# For Microsoft Foundry with OIDC
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_foundry: "true"
|
||||
claude_args: |
|
||||
--model claude-sonnet-4-5
|
||||
# ... other inputs
|
||||
```
|
||||
|
||||
## OIDC Authentication for Bedrock and Vertex
|
||||
## OIDC Authentication for Cloud Providers
|
||||
|
||||
Both AWS Bedrock and GCP Vertex AI require OIDC authentication.
|
||||
AWS Bedrock, GCP Vertex AI, and Microsoft Foundry all support OIDC authentication.
|
||||
|
||||
```yaml
|
||||
# For AWS Bedrock with OIDC
|
||||
@ -97,3 +106,36 @@ Both AWS Bedrock and GCP Vertex AI require OIDC authentication.
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
```
|
||||
|
||||
```yaml
|
||||
# For Microsoft Foundry with OIDC
|
||||
- name: Authenticate to Azure
|
||||
uses: azure/login@v2
|
||||
with:
|
||||
client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: Generate GitHub App token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@v2
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_foundry: "true"
|
||||
claude_args: |
|
||||
--model claude-sonnet-4-5
|
||||
# ... other inputs
|
||||
env:
|
||||
ANTHROPIC_FOUNDRY_BASE_URL: https://my-resource.services.ai.azure.com
|
||||
|
||||
permissions:
|
||||
id-token: write # Required for OIDC
|
||||
```
|
||||
|
||||
## Microsoft Foundry Setup
|
||||
|
||||
For detailed setup instructions for Microsoft Foundry, see the [official documentation](https://docs.anthropic.com/en/docs/claude-code/microsoft-foundry).
|
||||
|
||||
@ -130,7 +130,7 @@ To allow Claude to view workflow run results, job logs, and CI status:
|
||||
2. **Configure the action with additional permissions**:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
additional_permissions: |
|
||||
@ -162,7 +162,7 @@ jobs:
|
||||
claude-ci-helper:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
additional_permissions: |
|
||||
@ -172,9 +172,14 @@ jobs:
|
||||
|
||||
**Important Notes**:
|
||||
|
||||
- The GitHub token must have the `actions: read` permission in your workflow
|
||||
- The GitHub token must have the corresponding permission in your workflow
|
||||
- If the permission is missing, Claude will warn you and suggest adding it
|
||||
- Currently, only `actions: read` is supported, but the format allows for future extensions
|
||||
- The following additional permissions can be requested beyond the defaults:
|
||||
- `actions: read`
|
||||
- `checks: read`
|
||||
- `discussions: read` or `discussions: write`
|
||||
- `workflows: read` or `workflows: write`
|
||||
- Standard permissions (`contents: write`, `pull_requests: write`, `issues: write`) are always included and do not need to be specified
|
||||
|
||||
## Custom Environment Variables
|
||||
|
||||
@ -343,3 +348,31 @@ Many individual input parameters have been consolidated into `claude_args` or `s
|
||||
| `mcp_config` | Use `claude_args: "--mcp-config '{...}'"` |
|
||||
| `direct_prompt` | Use `prompt` input instead |
|
||||
| `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.
|
||||
|
||||
744
docs/create-app.html
Normal file
744
docs/create-app.html
Normal file
@ -0,0 +1,744 @@
|
||||
<!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>
|
||||
@ -2,17 +2,26 @@
|
||||
|
||||
These examples show how to configure Claude to act automatically based on GitHub events. When you provide a `prompt` input, the action automatically runs in agent mode without requiring manual @mentions. Without a `prompt`, it runs in interactive mode, responding to @claude mentions.
|
||||
|
||||
## Mode Detection & Tracking Comments
|
||||
|
||||
The action automatically detects which mode to use based on your configuration:
|
||||
|
||||
- **Interactive Mode** (no `prompt` input): Responds to @claude mentions, creates tracking comments with progress indicators
|
||||
- **Automation Mode** (with `prompt` input): Executes immediately, **does not create tracking comments**
|
||||
|
||||
> **Note**: In v1, automation mode intentionally does not create tracking comments by default to reduce noise in automated workflows. If you need progress tracking, use the `track_progress: true` input parameter.
|
||||
|
||||
## Supported GitHub Events
|
||||
|
||||
This action supports the following GitHub events ([learn more GitHub event triggers](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows)):
|
||||
|
||||
- `pull_request` - When PRs are opened or synchronized
|
||||
- `pull_request` or `pull_request_target` - When PRs are opened or synchronized
|
||||
- `issue_comment` - When comments are created on issues or PRs
|
||||
- `pull_request_comment` - When comments are made on PR diffs
|
||||
- `issues` - When issues are opened or assigned
|
||||
- `pull_request_review` - When PR reviews are submitted
|
||||
- `pull_request_review_comment` - When comments are made on PR reviews
|
||||
- `repository_dispatch` - Custom events triggered via API (coming soon)
|
||||
- `repository_dispatch` - Custom events triggered via API
|
||||
- `workflow_dispatch` - Manual workflow triggers (coming soon)
|
||||
|
||||
## Automated Documentation Updates
|
||||
|
||||
@ -61,68 +61,3 @@ For specialized use cases, you can fine-tune behavior using `claude_args`:
|
||||
--system-prompt "You are a code review specialist"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
## Network Restrictions
|
||||
|
||||
For enhanced security, you can restrict Claude's network access to specific domains only. This feature is particularly useful for:
|
||||
|
||||
- Enterprise environments with strict security policies
|
||||
- Preventing access to external services
|
||||
- Limiting Claude to only your internal APIs and services
|
||||
|
||||
When `experimental_allowed_domains` is set, Claude can only access the domains you explicitly list. You'll need to include the appropriate provider domains based on your authentication method.
|
||||
|
||||
### Provider-Specific Examples
|
||||
|
||||
#### If using Anthropic API or subscription
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Or: claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
experimental_allowed_domains: |
|
||||
.anthropic.com
|
||||
```
|
||||
|
||||
#### If using AWS Bedrock
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_bedrock: "true"
|
||||
experimental_allowed_domains: |
|
||||
bedrock.*.amazonaws.com
|
||||
bedrock-runtime.*.amazonaws.com
|
||||
```
|
||||
|
||||
#### If using Google Vertex AI
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
use_vertex: "true"
|
||||
experimental_allowed_domains: |
|
||||
*.googleapis.com
|
||||
vertexai.googleapis.com
|
||||
```
|
||||
|
||||
### Common GitHub Domains
|
||||
|
||||
In addition to your provider domains, you may need to include GitHub-related domains. For GitHub.com users, common domains include:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
experimental_allowed_domains: |
|
||||
.anthropic.com # For Anthropic API
|
||||
.github.com
|
||||
.githubusercontent.com
|
||||
ghcr.io
|
||||
.blob.core.windows.net
|
||||
```
|
||||
|
||||
For GitHub Enterprise users, replace the GitHub.com domains above with your enterprise domains (e.g., `.github.company.com`, `packages.company.com`, etc.).
|
||||
|
||||
To determine which domains your workflow needs, you can temporarily run without restrictions and monitor the network requests, or check your GitHub Enterprise configuration for the specific services you use.
|
||||
|
||||
67
docs/faq.md
67
docs/faq.md
@ -28,6 +28,33 @@ permissions:
|
||||
|
||||
The OIDC token is required in order for the Claude GitHub app to function. If you wish to not use the GitHub app, you can instead provide a `github_token` input to the action for Claude to operate with. See the [Claude Code permissions documentation][perms] for more.
|
||||
|
||||
### Why am I getting '403 Resource not accessible by integration' errors?
|
||||
|
||||
This error occurs when the action tries to fetch the authenticated user information using a GitHub App installation token. GitHub App tokens have limited access and cannot access the `/user` endpoint, which causes this 403 error.
|
||||
|
||||
**Solution**: The action now includes `bot_id` and `bot_name` inputs that default to Claude's bot credentials. This avoids the need to fetch user information from the API.
|
||||
|
||||
For the default claude[bot]:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# bot_id and bot_name have sensible defaults, no need to specify
|
||||
```
|
||||
|
||||
For custom bots, specify both:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_id: "12345678" # Your bot's GitHub user ID
|
||||
bot_name: "my-bot" # Your bot's username
|
||||
```
|
||||
|
||||
This issue typically only affects agent/automation mode workflows. Interactive workflows (with @claude mentions) don't encounter this issue as they use the comment author's information.
|
||||
|
||||
## Claude's Capabilities and Limitations
|
||||
|
||||
### Why won't Claude update workflow files when I ask it to?
|
||||
@ -100,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.
|
||||
|
||||
```
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
depth: 0 # will fetch full repo history
|
||||
```
|
||||
|
||||
@ -186,6 +213,44 @@ 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`.
|
||||
|
||||
### 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
|
||||
|
||||
1. **Always specify permissions explicitly** in your workflow file
|
||||
|
||||
@ -14,18 +14,19 @@ 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:
|
||||
|
||||
| Deprecated Input | Replacement | Notes |
|
||||
| --------------------- | -------------------------------- | --------------------------------------------- |
|
||||
| `mode` | Auto-detected | Action automatically chooses based on context |
|
||||
| `direct_prompt` | `prompt` | Direct drop-in replacement |
|
||||
| `override_prompt` | `prompt` | Use GitHub context variables instead |
|
||||
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
|
||||
| `max_turns` | `claude_args: --max-turns` | Use CLI format |
|
||||
| `model` | `claude_args: --model` | Specify via CLI |
|
||||
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
|
||||
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
|
||||
| `claude_env` | `settings` with env object | Use settings JSON |
|
||||
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
|
||||
| Deprecated Input | Replacement | Notes |
|
||||
| --------------------- | ------------------------------------ | --------------------------------------------- |
|
||||
| `mode` | Auto-detected | Action automatically chooses based on context |
|
||||
| `direct_prompt` | `prompt` | Direct drop-in replacement |
|
||||
| `override_prompt` | `prompt` | Use GitHub context variables instead |
|
||||
| `custom_instructions` | `claude_args: --system-prompt` | Move to CLI arguments |
|
||||
| `max_turns` | `claude_args: --max-turns` | Use CLI format |
|
||||
| `model` | `claude_args: --model` | Specify via CLI |
|
||||
| `allowed_tools` | `claude_args: --allowedTools` | Use CLI format |
|
||||
| `disallowed_tools` | `claude_args: --disallowedTools` | Use CLI format |
|
||||
| `claude_env` | `settings` with env object | Use settings JSON |
|
||||
| `mcp_config` | `claude_args: --mcp-config` | Pass MCP config via CLI arguments |
|
||||
| `timeout_minutes` | Use GitHub Actions `timeout-minutes` | Configure at job level instead of input level |
|
||||
|
||||
## Migration Examples
|
||||
|
||||
@ -74,13 +75,75 @@ The following inputs have been deprecated and replaced:
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: "Review this PR for security issues"
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this PR for security issues
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet-20250805
|
||||
--allowedTools Edit,Read,Write
|
||||
```
|
||||
|
||||
> **⚠️ Important**: For PR reviews, always include the repository and PR context in your prompt. This ensures Claude knows which PR to review.
|
||||
|
||||
### Automation with Progress Tracking (New in v1.0)
|
||||
|
||||
**Missing the tracking comments from v0.x agent mode?** The new `track_progress` input brings them back!
|
||||
|
||||
In v1.0, automation mode (with `prompt` input) doesn't create tracking comments by default to reduce noise. However, if you need progress visibility, you can use the `track_progress` feature:
|
||||
|
||||
**Before (v0.x with tracking):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
with:
|
||||
mode: "agent"
|
||||
direct_prompt: "Review this PR for security issues"
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
**After (v1.0 with tracking):**
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
track_progress: true # Forces tag mode with tracking comments
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this PR for security issues
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
```
|
||||
|
||||
#### Benefits of `track_progress`
|
||||
|
||||
1. **Preserves GitHub Context**: Automatically includes all PR/issue details, comments, and attachments
|
||||
2. **Brings Back Tracking Comments**: Creates progress indicators just like v0.x agent mode
|
||||
3. **Works with Custom Prompts**: Your `prompt` is injected as custom instructions while maintaining context
|
||||
|
||||
#### Supported Events for `track_progress`
|
||||
|
||||
The `track_progress` input only works with these GitHub events:
|
||||
|
||||
**Pull Request Events:**
|
||||
|
||||
- `opened` - New PR created
|
||||
- `synchronize` - PR updated with new commits
|
||||
- `ready_for_review` - Draft PR marked as ready
|
||||
- `reopened` - Previously closed PR reopened
|
||||
|
||||
**Issue Events:**
|
||||
|
||||
- `opened` - New issue created
|
||||
- `edited` - Issue title or body modified
|
||||
- `labeled` - Label added to issue
|
||||
- `assigned` - Issue assigned to user
|
||||
|
||||
> **Note**: Using `track_progress: true` with unsupported events will cause an error.
|
||||
|
||||
### Custom Template with Variables
|
||||
|
||||
**Before (v0.x):**
|
||||
@ -100,10 +163,16 @@ The following inputs have been deprecated and replaced:
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Analyze PR #${{ github.event.pull_request.number }} in ${{ github.repository }}
|
||||
Focus on security vulnerabilities in the changed files
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Analyze this pull request focusing on security vulnerabilities in the changed files.
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
```
|
||||
|
||||
> **💡 Tip**: While you can access GitHub context variables in your prompt, it's recommended to use the standard `REPO:` and `PR NUMBER:` format for consistency.
|
||||
|
||||
### Environment Variables
|
||||
|
||||
**Before (v0.x):**
|
||||
@ -130,6 +199,30 @@ The following inputs have been deprecated and replaced:
|
||||
}
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
The action now automatically detects the appropriate mode:
|
||||
@ -244,6 +337,8 @@ You can also pass MCP configuration from a file:
|
||||
- [ ] Convert `disallowed_tools` to `claude_args` with `--disallowedTools`
|
||||
- [ ] Move `claude_env` to `settings` JSON format
|
||||
- [ ] 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
|
||||
- [ ] Test workflow in a non-production environment
|
||||
|
||||
## Getting Help
|
||||
|
||||
167
docs/security.md
167
docs/security.md
@ -4,21 +4,150 @@
|
||||
|
||||
- **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
|
||||
- **⚠️ Allowed bots are not checked for repository permissions.** A bot that matches an entry does **not** need to be installed on your repository or have write access. On a **public repository**, external parties — including GitHub Apps created by anyone — may be able to trigger workflow events such as opening issues, commenting, or reviewing pull requests. If your workflow listens on those events and `allowed_bots` is set to `'*'`, any such App can invoke this action with a prompt it controls.
|
||||
- Prefer an explicit list over `'*'`
|
||||
- Only list App names you trust
|
||||
- If you need `'*'`, scope workflow `permissions:` to the minimum required
|
||||
- **⚠️ 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
|
||||
- When set, Claude does a best-effort scrub of Anthropic, cloud, and GitHub Actions secrets from subprocess environments. On Linux runners with bubblewrap available, subprocesses additionally run with PID-namespace isolation. This reduces but does not eliminate prompt injection risk — keep workflow permissions minimal and validate all outputs. Set `CLAUDE_CODE_SUBPROCESS_ENV_SCRUB: 0` in your workflow or job `env:` block to opt out.
|
||||
- Optionally set `CLAUDE_CODE_SCRIPT_CAPS` in your workflow `env:` block to limit how many times Claude can call specific scripts per run. Value is JSON: `{"script-name.sh": maxCalls}`. Example: `CLAUDE_CODE_SCRIPT_CAPS: '{"edit-issue-labels.sh":2}'` allows at most 2 calls to `edit-issue-labels.sh`. Useful for write-capable helper scripts.
|
||||
- When using `allowed_non_write_users`, always pass `github_token: ${{ secrets.GITHUB_TOKEN }}`. The auto-generated workflow token is scoped to the job's declared permissions and expires when the job completes. **Do not use a personal access token** — a static token does not rotate between runs and could be partially or fully recovered over time via prompt injection. Restricting allowed tools via `claude_args` reduces the rate of recovery but may not eliminate the risk. We recommend restricting allowed tools (e.g. `claude_args: '--allowedTools "Bash(gh issue view:*)"'`) to the minimum required when using `allowed_non_write_users`.
|
||||
- **Token Permissions**: The GitHub app receives only a short-lived token scoped specifically to the repository it's operating in
|
||||
- **No Cross-Repository Access**: Each action invocation is limited to the repository where it was triggered
|
||||
- **Limited Scope**: The token cannot access other repositories or perform actions beyond the configured permissions
|
||||
|
||||
## Using this action with `pull_request_target` or `workflow_run`
|
||||
|
||||
`pull_request_target` and `workflow_run` execute with the **base repository's secrets**. If your workflow checks out the PR head (`ref: ${{ github.event.pull_request.head.sha }}` for `pull_request_target`, `ref: ${{ github.event.workflow_run.head_sha }}` for `workflow_run`) into `$GITHUB_WORKSPACE` before this action, the action and Claude run with that checkout as the working directory.
|
||||
|
||||
**Do not check out an untrusted ref into the workspace root before this action.** Use one of these patterns instead:
|
||||
|
||||
```yaml
|
||||
# Preferred — check out the base ref (default).
|
||||
- uses: actions/checkout@v6 # no `ref:` → base branch
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
```
|
||||
|
||||
```yaml
|
||||
# If you need the PR's files locally — check out the base ref at the workspace
|
||||
# root (this action expects a git repo there), then check out the head ref into
|
||||
# a subdirectory and pass it via --add-dir.
|
||||
- uses: actions/checkout@v6 # no `ref:` → base branch at workspace root
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
# For workflow_run use: ${{ github.event.workflow_run.head_sha }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: pr-head
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_args: "--add-dir pr-head"
|
||||
```
|
||||
|
||||
This is general guidance for these event types — see [GitHub's documentation](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/).
|
||||
|
||||
### `claude-code-action` vs `claude-code-base-action`
|
||||
|
||||
`claude-code-base-action` is a lower-level building block that installs and runs Claude Code with the inputs you provide. It does not perform actor permission checks or restore project configuration from the base ref. If you need those behaviors, use this action (`claude-code-action`). See the [base-action README](../base-action/README.md#trust-model) for details.
|
||||
|
||||
## Pull Request Creation
|
||||
|
||||
In its default configuration, **Claude does not create pull requests automatically** when responding to `@claude` mentions. Instead:
|
||||
|
||||
- Claude commits code changes to a new branch
|
||||
- Claude provides a **link to the GitHub PR creation page** in its response
|
||||
- **The user must click the link and create the PR themselves**, ensuring human oversight before any code is proposed for merging
|
||||
|
||||
This design ensures that users retain full control over what pull requests are created and can review the changes before initiating the PR workflow.
|
||||
|
||||
## ⚠️ 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.
|
||||
|
||||
On public repos, you can also use `include_comments_by_actor` to allowlist which users' comments are passed to Claude, reducing exposure to untrusted input. Use `exclude_comments_by_actor` to filter out noisy bot comments (e.g., `dependabot[bot]`, `renovate[bot]`). If an actor matches both lists, exclusion takes priority. See [Usage](./usage.md) for details.
|
||||
|
||||
## GitHub App Permissions
|
||||
|
||||
The [Claude Code GitHub app](https://github.com/apps/claude) requires these permissions:
|
||||
The [Claude Code GitHub app](https://github.com/apps/claude) requests the following permissions:
|
||||
|
||||
- **Pull Requests**: Read and write to create PRs and push changes
|
||||
- **Issues**: Read and write to respond to issues
|
||||
- **Contents**: Read and write to modify repository files
|
||||
### Currently Used Permissions
|
||||
|
||||
- **Contents** (Read & Write): For reading repository files and creating branches
|
||||
- **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
|
||||
|
||||
All commits made by Claude through this action are automatically signed with commit signatures. This ensures the authenticity and integrity of commits, providing a verifiable trail of changes made by the action.
|
||||
By default, commits made by Claude are unsigned. You can enable commit signing using one of two methods:
|
||||
|
||||
### Option 1: GitHub API Commit Signing (use_commit_signing)
|
||||
|
||||
This uses GitHub's API to create commits, which automatically signs them as verified from the GitHub App:
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@main
|
||||
with:
|
||||
use_commit_signing: true
|
||||
```
|
||||
|
||||
This is the simplest option and requires no additional setup. However, because it uses the GitHub API instead of git CLI, it cannot perform complex git operations like rebasing, cherry-picking, or interactive history manipulation.
|
||||
|
||||
### Option 2: SSH Signing Key (ssh_signing_key)
|
||||
|
||||
This uses an SSH key to sign commits via git CLI. Use this option when you need both signed commits AND standard git operations (rebasing, cherry-picking, etc.):
|
||||
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@main
|
||||
with:
|
||||
ssh_signing_key: ${{ secrets.SSH_SIGNING_KEY }}
|
||||
bot_id: "YOUR_GITHUB_USER_ID"
|
||||
bot_name: "YOUR_GITHUB_USERNAME"
|
||||
```
|
||||
|
||||
Commits will show as verified and attributed to the GitHub account that owns the signing key.
|
||||
|
||||
**Setup steps:**
|
||||
|
||||
1. Generate an SSH key pair for signing:
|
||||
|
||||
```bash
|
||||
ssh-keygen -t ed25519 -f ~/.ssh/signing_key -N "" -C "commit signing key"
|
||||
```
|
||||
|
||||
2. Add the **public key** to your GitHub account:
|
||||
|
||||
- Go to GitHub → Settings → SSH and GPG keys
|
||||
- Click "New SSH key"
|
||||
- Select **Key type: Signing Key** (important)
|
||||
- Paste the contents of `~/.ssh/signing_key.pub`
|
||||
|
||||
3. Add the **private key** to your repository secrets:
|
||||
|
||||
- Go to your repo → Settings → Secrets and variables → Actions
|
||||
- Create a new secret named `SSH_SIGNING_KEY`
|
||||
- Paste the contents of `~/.ssh/signing_key`
|
||||
|
||||
4. Get your GitHub user ID:
|
||||
|
||||
```bash
|
||||
gh api users/YOUR_USERNAME --jq '.id'
|
||||
```
|
||||
|
||||
5. Update your workflow with `bot_id` and `bot_name` matching the account where you added the signing key.
|
||||
|
||||
**Note:** If both `ssh_signing_key` and `use_commit_signing` are provided, `ssh_signing_key` takes precedence.
|
||||
|
||||
## ⚠️ Authentication Protection
|
||||
|
||||
@ -36,3 +165,31 @@ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
anthropic_api_key: "sk-ant-api03-..." # 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.
|
||||
|
||||
@ -20,7 +20,48 @@ If you prefer not to install the official Claude app, you can create your own Gi
|
||||
- Organization policies prevent installing third-party apps
|
||||
- You're using AWS Bedrock or Google Vertex AI
|
||||
|
||||
**Steps to create and use a custom GitHub App:**
|
||||
### Option 1: Quick Setup with App Manifest (Recommended)
|
||||
|
||||
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:**
|
||||
|
||||
@ -76,7 +117,7 @@ If you prefer not to install the official Claude app, you can create your own Gi
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
# Use Claude with your custom app's token
|
||||
- uses: anthropics/claude-code-action@beta
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
599
docs/solutions.md
Normal file
599
docs/solutions.md
Normal file
@ -0,0 +1,599 @@
|
||||
# Solutions & Use Cases
|
||||
|
||||
This guide provides complete, ready-to-use solutions for common automation scenarios with Claude Code Action. Each solution includes working examples, configuration details, and expected outcomes.
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Automatic PR Code Review](#automatic-pr-code-review)
|
||||
- [Review Only Specific File Paths](#review-only-specific-file-paths)
|
||||
- [Review PRs from External Contributors](#review-prs-from-external-contributors)
|
||||
- [Custom PR Review Checklist](#custom-pr-review-checklist)
|
||||
- [Scheduled Repository Maintenance](#scheduled-repository-maintenance)
|
||||
- [Issue Auto-Triage and Labeling](#issue-auto-triage-and-labeling)
|
||||
- [Documentation Sync on API Changes](#documentation-sync-on-api-changes)
|
||||
- [Security-Focused PR Reviews](#security-focused-pr-reviews)
|
||||
|
||||
---
|
||||
|
||||
## Automatic PR Code Review
|
||||
|
||||
**When to use:** Automatically review every PR opened or updated in your repository.
|
||||
|
||||
### Basic Example (No Tracking)
|
||||
|
||||
```yaml
|
||||
name: Claude Auto Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please review this pull request with a focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Use `gh pr comment` for top-level feedback.
|
||||
Use `mcp__github_inline_comment__create_inline_comment` (with `confirmed: true`) to highlight specific code issues.
|
||||
Only post GitHub comments - don't submit review text as messages.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Triggers on `opened` and `synchronize` (new commits)
|
||||
- Always include `REPO` and `PR NUMBER` for context
|
||||
- Specify tools for commenting and reviewing
|
||||
- PR branch is pre-checked out
|
||||
|
||||
**Expected Output:** Claude posts review comments directly to the PR with inline annotations where appropriate.
|
||||
|
||||
### Enhanced Example (With Progress Tracking)
|
||||
|
||||
Want visual progress tracking for PR reviews? Use `track_progress: true` to get tracking comments like in v0.x:
|
||||
|
||||
```yaml
|
||||
name: Claude Auto Review with Tracking
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
|
||||
jobs:
|
||||
review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
track_progress: true # ✨ Enables tracking comments
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please review this pull request with a focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Security implications
|
||||
- Performance considerations
|
||||
|
||||
Provide detailed feedback using inline comments for specific issues.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
```
|
||||
|
||||
**Benefits of Progress Tracking:**
|
||||
|
||||
- **Visual Progress Indicators**: Shows "In progress" status with checkboxes
|
||||
- **Preserves Full Context**: Automatically includes all PR details, comments, and attachments
|
||||
- **Migration-Friendly**: Perfect for teams moving from v0.x who miss tracking comments
|
||||
- **Works with Custom Prompts**: Your prompt becomes custom instructions while maintaining GitHub context
|
||||
|
||||
**Expected Output:**
|
||||
|
||||
1. Claude creates a tracking comment: "Claude Code is reviewing this pull request..."
|
||||
2. Updates the comment with progress checkboxes as it works
|
||||
3. Posts detailed review feedback with inline annotations
|
||||
4. Updates tracking comment to "Completed" when done
|
||||
|
||||
---
|
||||
|
||||
## Review Only Specific File Paths
|
||||
|
||||
**When to use:** Review PRs only when specific critical files change.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Review Critical Files
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "src/auth/**"
|
||||
- "src/api/**"
|
||||
- "config/security.yml"
|
||||
|
||||
jobs:
|
||||
security-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
This PR modifies critical authentication or API files.
|
||||
|
||||
Please provide a security-focused review with emphasis on:
|
||||
- Authentication and authorization flows
|
||||
- Input validation and sanitization
|
||||
- SQL injection or XSS vulnerabilities
|
||||
- API security best practices
|
||||
|
||||
Note: The PR branch is already checked out.
|
||||
|
||||
Post detailed security findings as PR comments.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- `paths:` filter triggers only for specific file changes
|
||||
- Custom prompt emphasizes security for sensitive areas
|
||||
- Useful for compliance or security reviews
|
||||
|
||||
**Expected Output:** Security-focused review when critical files are modified.
|
||||
|
||||
---
|
||||
|
||||
## Review PRs from External Contributors
|
||||
|
||||
**When to use:** Apply stricter review criteria for external or new contributors.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: External Contributor Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
external-review:
|
||||
if: github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
CONTRIBUTOR: ${{ github.event.pull_request.user.login }}
|
||||
|
||||
This is a first-time contribution from @${{ github.event.pull_request.user.login }}.
|
||||
|
||||
Please provide a comprehensive review focusing on:
|
||||
- Compliance with project coding standards
|
||||
- Proper test coverage (unit and integration)
|
||||
- Documentation for new features
|
||||
- Potential breaking changes
|
||||
- License header requirements
|
||||
|
||||
Be welcoming but thorough in your review. Use inline comments for code-specific feedback.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr view:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- `if:` condition targets specific contributor types
|
||||
- Includes contributor username in context
|
||||
- Emphasis on onboarding and standards
|
||||
|
||||
**Expected Output:** Detailed review helping new contributors understand project standards.
|
||||
|
||||
---
|
||||
|
||||
## Custom PR Review Checklist
|
||||
|
||||
**When to use:** Enforce specific review criteria for your team's workflow.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: PR Review Checklist
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
checklist-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Review this PR against our team checklist:
|
||||
|
||||
## Code Quality
|
||||
- [ ] Code follows our style guide
|
||||
- [ ] No commented-out code
|
||||
- [ ] Meaningful variable names
|
||||
- [ ] DRY principle followed
|
||||
|
||||
## Testing
|
||||
- [ ] Unit tests for new functions
|
||||
- [ ] Integration tests for new endpoints
|
||||
- [ ] Edge cases covered
|
||||
- [ ] Test coverage > 80%
|
||||
|
||||
## Documentation
|
||||
- [ ] README updated if needed
|
||||
- [ ] API docs updated
|
||||
- [ ] Inline comments for complex logic
|
||||
- [ ] CHANGELOG.md updated
|
||||
|
||||
## Security
|
||||
- [ ] No hardcoded credentials
|
||||
- [ ] Input validation implemented
|
||||
- [ ] Proper error handling
|
||||
- [ ] No sensitive data in logs
|
||||
|
||||
For each item, check if it's satisfied and comment on any that need attention.
|
||||
Post a summary comment with checklist results.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Structured checklist in prompt
|
||||
- Systematic review approach
|
||||
- Team-specific criteria
|
||||
|
||||
**Expected Output:** Systematic review with checklist results and specific feedback.
|
||||
|
||||
---
|
||||
|
||||
## Scheduled Repository Maintenance
|
||||
|
||||
**When to use:** Regular automated maintenance tasks.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Weekly Maintenance
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Every Sunday at midnight
|
||||
workflow_dispatch: # Manual trigger option
|
||||
|
||||
jobs:
|
||||
maintenance:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
|
||||
Perform weekly repository maintenance:
|
||||
|
||||
1. Check for outdated dependencies in package.json
|
||||
2. Scan for security vulnerabilities using `npm audit`
|
||||
3. Review open issues older than 90 days
|
||||
4. Check for TODO comments in recent commits
|
||||
5. Verify README.md examples still work
|
||||
|
||||
Create a single issue summarizing any findings.
|
||||
If critical security issues are found, also comment on open PRs.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Read,Bash(npm:*),Bash(gh issue:*),Bash(git:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- `schedule:` for automated runs
|
||||
- `workflow_dispatch:` for manual triggering
|
||||
- Comprehensive tool permissions for analysis
|
||||
|
||||
**Expected Output:** Weekly maintenance report as GitHub issue.
|
||||
|
||||
---
|
||||
|
||||
## Issue Auto-Triage and Labeling
|
||||
|
||||
**When to use:** Automatically categorize and prioritize new issues.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Issue Triage
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE NUMBER: ${{ github.event.issue.number }}
|
||||
TITLE: ${{ github.event.issue.title }}
|
||||
BODY: ${{ github.event.issue.body }}
|
||||
AUTHOR: ${{ github.event.issue.user.login }}
|
||||
|
||||
Analyze this new issue and:
|
||||
1. Determine if it's a bug report, feature request, or question
|
||||
2. Assess priority (critical, high, medium, low)
|
||||
3. Suggest appropriate labels
|
||||
4. Check if it duplicates existing issues
|
||||
|
||||
Use ./scripts/gh.sh to interact with GitHub:
|
||||
- `./scripts/gh.sh issue view [number]` to view the issue
|
||||
- `./scripts/gh.sh search issues "query"` to find similar issues
|
||||
- `./scripts/gh.sh label list` to see available labels
|
||||
|
||||
Based on your analysis, add the appropriate labels using:
|
||||
`./scripts/edit-issue-labels.sh --add-label "label1" --add-label "label2"`
|
||||
(the issue number is read automatically from the workflow event)
|
||||
|
||||
If it appears to be a duplicate, post a comment mentioning the original issue.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Bash(./scripts/gh.sh:*),Bash(./scripts/edit-issue-labels.sh:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Triggered on new issues
|
||||
- Issue context in prompt
|
||||
- Label management capabilities
|
||||
- Requires `scripts/gh.sh` and `scripts/edit-issue-labels.sh` in your repo (see this repo's `scripts/` directory for examples)
|
||||
|
||||
**Expected Output:** Automatically labeled and categorized issues.
|
||||
|
||||
---
|
||||
|
||||
## Documentation Sync on API Changes
|
||||
|
||||
**When to use:** Keep docs up-to-date when API code changes.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Sync API Documentation
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
paths:
|
||||
- "src/api/**/*.ts"
|
||||
- "src/routes/**/*.ts"
|
||||
|
||||
jobs:
|
||||
doc-sync:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
This PR modifies API endpoints. Please:
|
||||
|
||||
1. Review the API changes in src/api and src/routes
|
||||
2. Update API.md to document any new or changed endpoints
|
||||
3. Ensure OpenAPI spec is updated if needed
|
||||
4. Update example requests/responses
|
||||
|
||||
Use standard REST API documentation format.
|
||||
Commit any documentation updates to this PR branch.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "Read,Write,Edit,Bash(git:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Path-specific trigger
|
||||
- Write permissions for doc updates
|
||||
- Git tools for committing
|
||||
|
||||
**Expected Output:** API documentation automatically updated with code changes.
|
||||
|
||||
---
|
||||
|
||||
## Security-Focused PR Reviews
|
||||
|
||||
**When to use:** Deep security analysis for sensitive repositories.
|
||||
|
||||
**Complete Example:**
|
||||
|
||||
```yaml
|
||||
name: Security Review
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
security-events: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Optional: Add track_progress: true for visual progress tracking during security reviews
|
||||
# track_progress: true
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Perform a comprehensive security review:
|
||||
|
||||
## OWASP Top 10 Analysis
|
||||
- SQL Injection vulnerabilities
|
||||
- Cross-Site Scripting (XSS)
|
||||
- Broken Authentication
|
||||
- Sensitive Data Exposure
|
||||
- XML External Entities (XXE)
|
||||
- Broken Access Control
|
||||
- Security Misconfiguration
|
||||
- Cross-Site Request Forgery (CSRF)
|
||||
- Using Components with Known Vulnerabilities
|
||||
- Insufficient Logging & Monitoring
|
||||
|
||||
## Additional Security Checks
|
||||
- Hardcoded secrets or credentials
|
||||
- Insecure cryptographic practices
|
||||
- Unsafe deserialization
|
||||
- Server-Side Request Forgery (SSRF)
|
||||
- Race conditions or TOCTOU issues
|
||||
|
||||
Rate severity as: CRITICAL, HIGH, MEDIUM, LOW, or NONE.
|
||||
Post detailed findings with recommendations.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*)"
|
||||
```
|
||||
|
||||
**Key Configuration:**
|
||||
|
||||
- Security-focused prompt structure
|
||||
- OWASP alignment
|
||||
- Severity rating system
|
||||
|
||||
**Expected Output:** Detailed security analysis with prioritized findings.
|
||||
|
||||
---
|
||||
|
||||
## Tips for All Solutions
|
||||
|
||||
### Always Include GitHub Context
|
||||
|
||||
```yaml
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
[Your specific instructions]
|
||||
```
|
||||
|
||||
### Common Tool Permissions
|
||||
|
||||
- **PR Comments**: `Bash(gh pr comment:*)`
|
||||
- **Inline Comments**: `mcp__github_inline_comment__create_inline_comment` — pass `confirmed: true` to post immediately. When omitted, the comment is buffered and classified after the session ends (real review comments post, test/probe comments are filtered). This prevents subagent test comments from reaching PRs. To disable classification entirely, set `classify_inline_comments: 'false'` on the action.
|
||||
- **File Operations**: `Read,Write,Edit`
|
||||
- **Git Operations**: `Bash(git:*)`
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Be specific in your prompts
|
||||
- Include expected output format
|
||||
- Set clear success criteria
|
||||
- Provide context about the repository
|
||||
- Use inline comments for code-specific feedback
|
||||
133
docs/usage.md
133
docs/usage.md
@ -32,6 +32,11 @@ jobs:
|
||||
# --max-turns 10
|
||||
# --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)
|
||||
# trigger_phrase: "/claude"
|
||||
# Optional: add assignee trigger for issues
|
||||
@ -47,27 +52,38 @@ jobs:
|
||||
|
||||
## Inputs
|
||||
|
||||
| Input | Description | Required | Default |
|
||||
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------- | -------- | --------- |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
||||
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
|
||||
| `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" |
|
||||
| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - |
|
||||
| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` |
|
||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `mcp_config` | Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers | No | "" |
|
||||
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
||||
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
|
||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
|
||||
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
|
||||
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
||||
| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" |
|
||||
| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` |
|
||||
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" |
|
||||
| Input | Description | Required | Default |
|
||||
| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- |
|
||||
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
|
||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - |
|
||||
| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - |
|
||||
| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` |
|
||||
| `include_fix_links` | Include 'Fix this' links in PR code review feedback that open Claude Code with context to fix the identified issue | No | `true` |
|
||||
| `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 | "" |
|
||||
| `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` |
|
||||
| `classify_inline_comments` | Buffer inline comments without `confirmed: true` and classify them (real review vs test/probe) via Haiku before posting after the session ends. Prevents subagent test comments. Set `'false'` to post all inline comments immediately | No | `true` |
|
||||
| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
|
||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
|
||||
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - |
|
||||
| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - |
|
||||
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
|
||||
| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` |
|
||||
| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" |
|
||||
| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" |
|
||||
| `use_commit_signing` | Enable commit signing using GitHub's API. Simple but cannot perform complex git operations like rebasing. See [Security](./security.md#commit-signing) | No | `false` |
|
||||
| `ssh_signing_key` | SSH private key for signing commits. Enables signed commits with full git CLI support (rebasing, etc.). See [Security](./security.md#commit-signing) | No | "" |
|
||||
| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID). Required with `ssh_signing_key` for verified commits | No | `41898282` |
|
||||
| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name). Required with `ssh_signing_key` for verified commits | No | `claude[bot]` |
|
||||
| `include_comments_by_actor` | Comma-separated list of actor usernames to INCLUDE in comments. Supports the `*[bot]` wildcard to match all bot accounts. Empty (default) includes all actors | No | "" |
|
||||
| `exclude_comments_by_actor` | Comma-separated list of actor usernames to EXCLUDE from comments. Supports the `*[bot]` wildcard to match all bot accounts. If an actor matches both lists, exclusion takes priority | No | "" |
|
||||
| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots. **⚠️ On public repos with `'*'`, external Apps may be able to invoke this action.** See [Security](./security.md) | No | "" |
|
||||
| `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 | "" |
|
||||
| `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 | "" |
|
||||
|
||||
### Deprecated Inputs
|
||||
|
||||
@ -84,6 +100,7 @@ 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` |
|
||||
| `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"` |
|
||||
| `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 |
|
||||
|
||||
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
|
||||
@ -139,7 +156,11 @@ For a comprehensive guide on migrating from v0.x to v1.0, including step-by-step
|
||||
```yaml
|
||||
- uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: "Update the API documentation"
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Update the API documentation to reflect changes in this PR
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--model claude-4-0-sonnet-20250805
|
||||
@ -168,6 +189,74 @@ For a comprehensive guide on migrating from v0.x to v1.0, including step-by-step
|
||||
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)
|
||||
claude_args: |
|
||||
--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 via `--json-schema` flag in `claude_args`
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
name: Auto Fix CI Failures (Signed Commits)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
actions: read
|
||||
issues: write
|
||||
id-token: write # Required for OIDC token exchange
|
||||
|
||||
jobs:
|
||||
auto-fix-signed:
|
||||
if: |
|
||||
github.event.workflow_run.conclusion == 'failure' &&
|
||||
github.event.workflow_run.pull_requests[0] &&
|
||||
!startsWith(github.event.workflow_run.head_branch, 'claude-auto-fix-ci-signed-')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
fetch-depth: 0
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate fix branch name
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME="claude-auto-fix-ci-signed-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
# Don't create branch locally - MCP tools will create it via API
|
||||
echo "Generated branch name: $BRANCH_NAME (will be created by MCP tools)"
|
||||
|
||||
- name: Get CI failure details
|
||||
id: failure_details
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const run = await github.rest.actions.getWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const jobs = await github.rest.actions.listJobsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: ${{ github.event.workflow_run.id }}
|
||||
});
|
||||
|
||||
const failedJobs = jobs.data.jobs.filter(job => job.conclusion === 'failure');
|
||||
|
||||
let errorLogs = [];
|
||||
for (const job of failedJobs) {
|
||||
const logs = await github.rest.actions.downloadJobLogsForWorkflowRun({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
job_id: job.id
|
||||
});
|
||||
errorLogs.push({
|
||||
jobName: job.name,
|
||||
logs: logs.data
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
runUrl: run.data.html_url,
|
||||
failedJobs: failedJobs.map(j => j.name),
|
||||
errorLogs: errorLogs
|
||||
};
|
||||
|
||||
- name: Fix CI failures with Claude (Signed Commits)
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
env:
|
||||
CLAUDE_BRANCH: ${{ steps.branch.outputs.branch_name }}
|
||||
BASE_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
with:
|
||||
prompt: |
|
||||
/fix-ci-signed
|
||||
Failed CI Run: ${{ fromJSON(steps.failure_details.outputs.result).runUrl }}
|
||||
Failed Jobs: ${{ join(fromJSON(steps.failure_details.outputs.result).failedJobs, ', ') }}
|
||||
PR Number: ${{ github.event.workflow_run.pull_requests[0].number }}
|
||||
Branch Name: ${{ steps.branch.outputs.branch_name }}
|
||||
Base Branch: ${{ github.event.workflow_run.head_branch }}
|
||||
Repository: ${{ github.repository }}
|
||||
|
||||
Error logs:
|
||||
${{ toJSON(fromJSON(steps.failure_details.outputs.result).errorLogs) }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
use_commit_signing: true
|
||||
claude_args: "--allowedTools 'Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*),mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files'"
|
||||
@ -1,148 +0,0 @@
|
||||
---
|
||||
description: Analyze and fix CI failures with signed commits using MCP tools
|
||||
allowed_tools: Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*),mcp__github_file_ops__commit_files,mcp__github_file_ops__delete_files
|
||||
---
|
||||
|
||||
# Fix CI Failures with Signed Commits
|
||||
|
||||
You are tasked with analyzing CI failure logs and fixing the issues using MCP tools for signed commits. Follow these steps:
|
||||
|
||||
## Context Provided
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Important Context Information
|
||||
|
||||
Look for these key pieces of information in the arguments:
|
||||
|
||||
- **Failed CI Run URL**: Link to the failed CI run
|
||||
- **Failed Jobs**: List of jobs that failed
|
||||
- **PR Number**: The PR number to comment on
|
||||
- **Branch Name**: The fix branch you're working on
|
||||
- **Base Branch**: The original PR branch
|
||||
- **Error logs**: Detailed logs from failed jobs
|
||||
|
||||
## CRITICAL: Use MCP Tools for Git Operations
|
||||
|
||||
**IMPORTANT**: You MUST use MCP tools for all git operations to ensure commits are properly signed. DO NOT use `git` commands directly via Bash.
|
||||
|
||||
- Use `mcp__github_file_ops__commit_files` to commit and push changes
|
||||
- Use `mcp__github_file_ops__delete_files` to delete files
|
||||
|
||||
## Step 1: Analyze the Failure
|
||||
|
||||
Parse the provided CI failure information to understand:
|
||||
|
||||
- Which jobs failed and why
|
||||
- The specific error messages and stack traces
|
||||
- Whether failures are test-related, build-related, or linting issues
|
||||
|
||||
## Step 2: Search and Understand the Codebase
|
||||
|
||||
Use MCP search tools to locate the failing code:
|
||||
|
||||
- Use `mcp_github_file_ops_server__search_files` or `mcp_github_file_ops_server__file_search` to find failing test names or functions
|
||||
- Use `mcp_github_file_ops_server__read_file` to read source files mentioned in error messages
|
||||
- Review related configuration files (package.json, tsconfig.json, etc.)
|
||||
|
||||
## Step 3: Apply Targeted Fixes
|
||||
|
||||
Make minimal, focused changes:
|
||||
|
||||
- **For test failures**: Determine if the test or implementation needs fixing
|
||||
- **For type errors**: Fix type definitions or correct the code logic
|
||||
- **For linting issues**: Apply formatting using the project's tools
|
||||
- **For build errors**: Resolve dependency or configuration issues
|
||||
- **For missing imports**: Add the necessary imports or install packages
|
||||
|
||||
Requirements:
|
||||
|
||||
- Only fix the actual CI failures, avoid unrelated changes
|
||||
- Follow existing code patterns and conventions
|
||||
- Ensure changes are production-ready, not temporary hacks
|
||||
- Preserve existing functionality while fixing issues
|
||||
|
||||
## Step 4: Verify Fixes Locally
|
||||
|
||||
Run available verification commands using Bash:
|
||||
|
||||
- Execute the failing tests locally to confirm they pass
|
||||
- Run the project's lint command (check package.json for scripts)
|
||||
- Run type checking if available
|
||||
- Execute any build commands to ensure compilation succeeds
|
||||
|
||||
## Step 5: Commit and Push Changes Using MCP
|
||||
|
||||
**CRITICAL**: You MUST use MCP tools for committing and pushing:
|
||||
|
||||
1. Prepare all your file changes (using Edit/MultiEdit/Write tools as needed)
|
||||
2. **Use `mcp__github_file_ops__commit_files` to commit and push all changes**
|
||||
- Pass the file paths you've edited in the `files` array
|
||||
- Set `message` to describe the specific fixes (e.g., "Fix CI failures: remove syntax errors and format code")
|
||||
- The MCP tool will automatically create the branch specified in "Branch Name:" from the context and push signed commits
|
||||
|
||||
**IMPORTANT**: The MCP tool will create the branch from the context automatically. The branch name from "Branch Name:" in the context will be used.
|
||||
|
||||
Example usage:
|
||||
|
||||
```
|
||||
mcp__github_file_ops__commit_files with:
|
||||
- files: ["src/utils/retry.ts", "src/other/file.ts"] // List of file paths you edited
|
||||
- message: "Fix CI failures: [describe specific fixes]"
|
||||
```
|
||||
|
||||
Note: The branch will be created from the Base Branch specified in the context.
|
||||
|
||||
## Step 6: Create PR Comment (REQUIRED - DO NOT SKIP)
|
||||
|
||||
**CRITICAL: You MUST create a PR comment after pushing. This step is MANDATORY.**
|
||||
|
||||
After successfully pushing the fixes, you MUST create a comment on the original PR to notify about the auto-fix. DO NOT end the task without completing this step.
|
||||
|
||||
1. Extract the PR number from the context provided in arguments (look for "PR Number:" in the context)
|
||||
2. **MANDATORY**: Execute the gh CLI command below to create the comment
|
||||
3. Verify the comment was created successfully
|
||||
|
||||
**YOU MUST RUN THIS COMMAND** (replace placeholders with actual values from context):
|
||||
|
||||
```bash
|
||||
gh pr comment PR_NUMBER --body "## 🤖 CI Auto-Fix Available (Signed Commits)
|
||||
|
||||
Claude has analyzed the CI failures and prepared fixes with signed commits.
|
||||
|
||||
[**→ Create pull request to fix CI**](https://github.com/OWNER/REPO/compare/BASE_BRANCH...FIX_BRANCH?quick_pull=1)
|
||||
|
||||
_This fix was generated automatically based on the [failed CI run](FAILED_CI_RUN_URL)._"
|
||||
```
|
||||
|
||||
**IMPORTANT REPLACEMENTS YOU MUST MAKE:**
|
||||
|
||||
- Replace `PR_NUMBER` with the actual PR number from "PR Number:" in context
|
||||
- Replace `OWNER/REPO` with the repository from "Repository:" in context
|
||||
- Replace `BASE_BRANCH` with the branch from "Base Branch:" in context
|
||||
- Replace `FIX_BRANCH` with the branch from "Branch Name:" in context
|
||||
- Replace `FAILED_CI_RUN_URL` with the URL from "Failed CI Run:" in context
|
||||
|
||||
**DO NOT SKIP THIS STEP. The task is NOT complete until the PR comment is created.**
|
||||
|
||||
## Step 7: Final Verification
|
||||
|
||||
**BEFORE CONSIDERING THE TASK COMPLETE**, verify you have:
|
||||
|
||||
1. ✅ Fixed all CI failures
|
||||
2. ✅ Committed the changes using `mcp_github_file_ops_server__push_files`
|
||||
3. ✅ Verified the branch was pushed successfully
|
||||
4. ✅ **CREATED THE PR COMMENT using `gh pr comment` command from Step 6**
|
||||
|
||||
If you have NOT created the PR comment, go back to Step 6 and execute the command.
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- Always use MCP tools for git operations to ensure proper commit signing
|
||||
- Focus exclusively on fixing the reported CI failures
|
||||
- Maintain code quality and follow the project's established patterns
|
||||
- If a fix requires significant refactoring, document why it's necessary
|
||||
- When multiple solutions exist, choose the simplest one that maintains code quality
|
||||
- **THE TASK IS NOT COMPLETE WITHOUT THE PR COMMENT**
|
||||
|
||||
Begin by analyzing the failure details provided above.
|
||||
@ -1,127 +0,0 @@
|
||||
---
|
||||
description: Analyze and fix CI failures by examining logs and making targeted fixes
|
||||
allowed_tools: Edit,MultiEdit,Write,Read,Glob,Grep,LS,Bash(git:*),Bash(bun:*),Bash(npm:*),Bash(npx:*),Bash(gh:*)
|
||||
---
|
||||
|
||||
# Fix CI Failures
|
||||
|
||||
You are tasked with analyzing CI failure logs and fixing the issues. Follow these steps:
|
||||
|
||||
## Context Provided
|
||||
|
||||
$ARGUMENTS
|
||||
|
||||
## Important Context Information
|
||||
|
||||
Look for these key pieces of information in the arguments:
|
||||
|
||||
- **Failed CI Run URL**: Link to the failed CI run
|
||||
- **Failed Jobs**: List of jobs that failed
|
||||
- **PR Number**: The PR number to comment on
|
||||
- **Branch Name**: The fix branch you're working on
|
||||
- **Base Branch**: The original PR branch
|
||||
- **Error logs**: Detailed logs from failed jobs
|
||||
|
||||
## Step 1: Analyze the Failure
|
||||
|
||||
Parse the provided CI failure information to understand:
|
||||
|
||||
- Which jobs failed and why
|
||||
- The specific error messages and stack traces
|
||||
- Whether failures are test-related, build-related, or linting issues
|
||||
|
||||
## Step 2: Search and Understand the Codebase
|
||||
|
||||
Use search tools to locate the failing code:
|
||||
|
||||
- Search for the failing test names or functions
|
||||
- Find the source files mentioned in error messages
|
||||
- Review related configuration files (package.json, tsconfig.json, etc.)
|
||||
|
||||
## Step 3: Apply Targeted Fixes
|
||||
|
||||
Make minimal, focused changes:
|
||||
|
||||
- **For test failures**: Determine if the test or implementation needs fixing
|
||||
- **For type errors**: Fix type definitions or correct the code logic
|
||||
- **For linting issues**: Apply formatting using the project's tools
|
||||
- **For build errors**: Resolve dependency or configuration issues
|
||||
- **For missing imports**: Add the necessary imports or install packages
|
||||
|
||||
Requirements:
|
||||
|
||||
- Only fix the actual CI failures, avoid unrelated changes
|
||||
- Follow existing code patterns and conventions
|
||||
- Ensure changes are production-ready, not temporary hacks
|
||||
- Preserve existing functionality while fixing issues
|
||||
|
||||
## Step 4: Verify Fixes Locally
|
||||
|
||||
Run available verification commands:
|
||||
|
||||
- Execute the failing tests locally to confirm they pass
|
||||
- Run the project's lint command (check package.json for scripts)
|
||||
- Run type checking if available
|
||||
- Execute any build commands to ensure compilation succeeds
|
||||
|
||||
## Step 5: Commit and Push Changes
|
||||
|
||||
After applying ALL fixes:
|
||||
|
||||
1. Stage all modified files with `git add -A`
|
||||
2. Commit with: `git commit -m "Fix CI failures: [describe specific fixes]"`
|
||||
3. Document which CI jobs/tests were addressed
|
||||
4. **CRITICAL**: Push the branch with `git push origin HEAD` - You MUST push the branch after committing
|
||||
|
||||
## Step 6: Create PR Comment (REQUIRED - DO NOT SKIP)
|
||||
|
||||
**CRITICAL: You MUST create a PR comment after pushing. This step is MANDATORY.**
|
||||
|
||||
After successfully pushing the fixes, you MUST create a comment on the original PR to notify about the auto-fix. DO NOT end the task without completing this step.
|
||||
|
||||
1. Extract the PR number from the context provided in arguments (look for "PR Number:" in the context)
|
||||
2. **MANDATORY**: Execute the gh CLI command below to create the comment
|
||||
3. Verify the comment was created successfully
|
||||
|
||||
**YOU MUST RUN THIS COMMAND** (replace placeholders with actual values from context):
|
||||
|
||||
```bash
|
||||
gh pr comment PR_NUMBER --body "## 🤖 CI Auto-Fix Available
|
||||
|
||||
Claude has analyzed the CI failures and prepared fixes.
|
||||
|
||||
[**→ Create pull request to fix CI**](https://github.com/OWNER/REPO/compare/BASE_BRANCH...FIX_BRANCH?quick_pull=1)
|
||||
|
||||
_This fix was generated automatically based on the [failed CI run](FAILED_CI_RUN_URL)._"
|
||||
```
|
||||
|
||||
**IMPORTANT REPLACEMENTS YOU MUST MAKE:**
|
||||
|
||||
- Replace `PR_NUMBER` with the actual PR number from "PR Number:" in context
|
||||
- Replace `OWNER/REPO` with the repository from "Repository:" in context
|
||||
- Replace `BASE_BRANCH` with the branch from "Base Branch:" in context
|
||||
- Replace `FIX_BRANCH` with the branch from "Branch Name:" in context
|
||||
- Replace `FAILED_CI_RUN_URL` with the URL from "Failed CI Run:" in context
|
||||
|
||||
**DO NOT SKIP THIS STEP. The task is NOT complete until the PR comment is created.**
|
||||
|
||||
## Step 7: Final Verification
|
||||
|
||||
**BEFORE CONSIDERING THE TASK COMPLETE**, verify you have:
|
||||
|
||||
1. ✅ Fixed all CI failures
|
||||
2. ✅ Committed the changes
|
||||
3. ✅ Pushed the branch with `git push origin HEAD`
|
||||
4. ✅ **CREATED THE PR COMMENT using `gh pr comment` command from Step 6**
|
||||
|
||||
If you have NOT created the PR comment, go back to Step 6 and execute the command.
|
||||
|
||||
## Important Guidelines
|
||||
|
||||
- Focus exclusively on fixing the reported CI failures
|
||||
- Maintain code quality and follow the project's established patterns
|
||||
- If a fix requires significant refactoring, document why it's necessary
|
||||
- When multiple solutions exist, choose the simplest one that maintains code quality
|
||||
- **THE TASK IS NOT COMPLETE WITHOUT THE PR COMMENT**
|
||||
|
||||
Begin by analyzing the failure details provided above.
|
||||
@ -1,5 +1,21 @@
|
||||
name: Auto Fix CI Failures
|
||||
|
||||
# ⚠️ SECURITY NOTE
|
||||
#
|
||||
# This workflow checks out the PR branch and runs build/test commands
|
||||
# (npm, bun, etc.) against it with elevated permissions (contents:write,
|
||||
# id-token:write). This means code from the PR branch executes in a
|
||||
# trusted context with access to secrets and the ability to push to the
|
||||
# repository.
|
||||
#
|
||||
# Only use this workflow in repositories where everyone with write access
|
||||
# is fully trusted with these permissions. Do not use this in repositories
|
||||
# that accept contributions from untrusted or semi-trusted collaborators.
|
||||
#
|
||||
# The pull_requests[0] check below limits this to same-repo PRs (fork PRs
|
||||
# are excluded), but anyone who can push a branch to this repository can
|
||||
# control what code runs here.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["CI"]
|
||||
@ -22,7 +38,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.workflow_run.head_branch }}
|
||||
fetch-depth: 0
|
||||
@ -35,10 +51,14 @@ jobs:
|
||||
|
||||
- name: Create fix branch
|
||||
id: branch
|
||||
env:
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
RUN_ID: ${{ github.run_id }}
|
||||
run: |
|
||||
BRANCH_NAME="claude-auto-fix-ci-${{ github.event.workflow_run.head_branch }}-${{ github.run_id }}"
|
||||
SAFE_BRANCH=$(printf '%s' "$HEAD_BRANCH" | tr -cd 'a-zA-Z0-9/_.-')
|
||||
BRANCH_NAME="claude-auto-fix-ci-${SAFE_BRANCH}-${RUN_ID}"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Get CI failure details
|
||||
id: failure_details
|
||||
@ -80,7 +100,7 @@ jobs:
|
||||
|
||||
- name: Fix CI failures with Claude
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
/fix-ci
|
||||
@ -1,30 +0,0 @@
|
||||
name: Claude Args Example
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
prompt:
|
||||
description: "Prompt for Claude"
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
claude-with-custom-args:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Run Claude with custom arguments
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
with:
|
||||
prompt: ${{ github.event.inputs.prompt }}
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
# claude_args provides direct CLI argument control
|
||||
# This allows full customization of Claude's behavior
|
||||
claude_args: |
|
||||
--max-turns 15
|
||||
--model claude-opus-4-1-20250805
|
||||
--allowedTools Edit,Read,Write,Bash
|
||||
--disallowedTools WebSearch
|
||||
--system-prompt "You are a senior engineer focused on code quality"
|
||||
@ -1,48 +0,0 @@
|
||||
name: Claude PR Auto Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
auto-review:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Automatic PR Review
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Please review this pull request.
|
||||
|
||||
Note: The PR branch is already checked out in the current working directory.
|
||||
|
||||
Focus on:
|
||||
- Code quality and best practices
|
||||
- Potential bugs or issues
|
||||
- Performance considerations
|
||||
- Security implications
|
||||
- Test coverage
|
||||
- Documentation updates if needed
|
||||
- Verify that README.md and docs are updated for any new features or config changes
|
||||
|
||||
Provide constructive feedback with specific suggestions for improvement.
|
||||
Use `gh pr comment:*` for top-level comments.
|
||||
Use `mcp__github_inline_comment__create_inline_comment` to highlight specific areas of concern.
|
||||
Only your GitHub comments that you post will be seen, so don't submit your review as a normal message, just as comments.
|
||||
If the PR has already been reviewed, or there are no noteworthy changes, don't post anything.
|
||||
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*), Bash(gh pr diff:*), Bash(gh pr view:*)"
|
||||
@ -1,54 +0,0 @@
|
||||
name: Claude Automatic Mode Detection Examples
|
||||
|
||||
on:
|
||||
# Events for interactive mode (responds to @claude mentions)
|
||||
issue_comment:
|
||||
types: [created]
|
||||
issues:
|
||||
types: [opened, labeled]
|
||||
pull_request:
|
||||
types: [opened]
|
||||
# Events for automation mode (runs with explicit prompt)
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 0 * * 0" # Weekly on Sunday
|
||||
|
||||
jobs:
|
||||
# Interactive Mode - Activated automatically when no prompt is provided
|
||||
interactive-mode-example:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@v1-dev
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
# Interactive mode (auto-detected when no prompt):
|
||||
# - Scans for @claude mentions in comments, issues, and PRs
|
||||
# - Only acts when trigger phrase is found
|
||||
# - Creates tracking comments with progress checkboxes
|
||||
# - Perfect for: Interactive Q&A, on-demand code changes
|
||||
|
||||
# Automation Mode - Activated automatically when prompt is provided
|
||||
automation-mode-scheduled-task:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: anthropics/claude-code-action@v1-dev
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
Check for outdated dependencies and security vulnerabilities.
|
||||
Create an issue if any critical problems are found.
|
||||
# Automation mode (auto-detected when prompt provided):
|
||||
# - Works with any GitHub event
|
||||
# - Executes immediately without waiting for @claude mentions
|
||||
# - No tracking comments created
|
||||
# - Perfect for: scheduled maintenance, automated reviews, CI/CD tasks
|
||||
@ -26,20 +26,16 @@ jobs:
|
||||
actions: read # Required for Claude to read CI results on PRs
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
id: claude
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
# Optional: Customize the trigger phrase (default: @claude)
|
||||
# trigger_phrase: "/claude"
|
||||
|
||||
|
||||
@ -15,12 +15,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Check for duplicate issues
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
Analyze this new issue and check if it's a duplicate of existing issues in the repository.
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
name: Issue Triage
|
||||
name: Claude Issue Triage
|
||||
description: Run Claude Code for issue triage in GitHub Actions
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
@ -13,63 +14,16 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Triage issue with Claude
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
- name: Run Claude Code for Issue Triage
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
prompt: |
|
||||
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
|
||||
|
||||
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
|
||||
|
||||
Issue Information:
|
||||
- REPO: ${{ github.repository }}
|
||||
- ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
|
||||
TASK OVERVIEW:
|
||||
|
||||
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||
|
||||
2. Next, use the GitHub tools to get context about the issue:
|
||||
- You have access to these tools:
|
||||
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
|
||||
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
|
||||
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
|
||||
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
|
||||
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
|
||||
- Start by using mcp__github__get_issue to get the issue details
|
||||
|
||||
3. Analyze the issue content, considering:
|
||||
- The issue title and description
|
||||
- The type of issue (bug report, feature request, question, etc.)
|
||||
- Technical areas mentioned
|
||||
- Severity or priority indicators
|
||||
- User impact
|
||||
- Components affected
|
||||
|
||||
4. Select appropriate labels from the available labels list provided above:
|
||||
- Choose labels that accurately reflect the issue's nature
|
||||
- Be specific but comprehensive
|
||||
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
|
||||
- Consider platform labels (android, ios) if applicable
|
||||
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
|
||||
|
||||
5. Apply the selected labels:
|
||||
- Use mcp__github__update_issue to apply your selected labels
|
||||
- DO NOT post any comments explaining your decision
|
||||
- DO NOT communicate directly with users
|
||||
- If no labels are clearly applicable, do not apply any labels
|
||||
|
||||
IMPORTANT GUIDELINES:
|
||||
- Be thorough in your analysis
|
||||
- Only select labels from the provided list above
|
||||
- DO NOT post any comments to the issue
|
||||
- Your ONLY action should be to apply labels using mcp__github__update_issue
|
||||
- It's okay to not add any labels if none are clearly applicable
|
||||
# NOTE: /label-issue requires .claude/commands/label-issue.md and scripts/edit-issue-labels.sh in your repo (see this repo for examples)
|
||||
prompt: "/label-issue REPO: ${{ github.repository }} ISSUE_NUMBER: ${{ github.event.issue.number }}"
|
||||
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
claude_args: |
|
||||
--allowedTools "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
|
||||
allowed_non_write_users: "*" # Required for issue triage workflow, if users without repo write access create issues
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@ -23,15 +23,18 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 2 # Need at least 2 commits to analyze the latest
|
||||
|
||||
- name: Run Claude Analysis
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
BRANCH: ${{ github.ref_name }}
|
||||
|
||||
Analyze the latest commit in this repository.
|
||||
|
||||
${{ github.event.inputs.analysis_type == 'summarize-commit' && 'Task: Provide a clear, concise summary of what changed in the latest commit. Include the commit message, files changed, and the purpose of the changes.' || '' }}
|
||||
74
examples/pr-review-comprehensive.yml
Normal file
74
examples/pr-review-comprehensive.yml
Normal file
@ -0,0 +1,74 @@
|
||||
name: PR Review with Progress Tracking
|
||||
|
||||
# This example demonstrates how to use the track_progress feature to get
|
||||
# visual progress tracking for PR reviews, similar to v0.x agent mode.
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, ready_for_review, reopened]
|
||||
|
||||
jobs:
|
||||
review-with-tracking:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: PR Review with Progress Tracking
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
|
||||
# Enable progress tracking
|
||||
track_progress: true
|
||||
|
||||
# Your custom review instructions
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
PR NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
Perform a comprehensive code review with the following focus areas:
|
||||
|
||||
1. **Code Quality**
|
||||
- Clean code principles and best practices
|
||||
- Proper error handling and edge cases
|
||||
- Code readability and maintainability
|
||||
|
||||
2. **Security**
|
||||
- Check for potential security vulnerabilities
|
||||
- Validate input sanitization
|
||||
- Review authentication/authorization logic
|
||||
|
||||
3. **Performance**
|
||||
- Identify potential performance bottlenecks
|
||||
- Review database queries for efficiency
|
||||
- Check for memory leaks or resource issues
|
||||
|
||||
4. **Testing**
|
||||
- Verify adequate test coverage
|
||||
- Review test quality and edge cases
|
||||
- Check for missing test scenarios
|
||||
|
||||
5. **Documentation**
|
||||
- Ensure code is properly documented
|
||||
- Verify README updates for new features
|
||||
- Check API documentation accuracy
|
||||
|
||||
Provide detailed feedback using inline comments for specific issues.
|
||||
Use top-level comments for general observations or praise.
|
||||
|
||||
# Tools for comprehensive PR review
|
||||
claude_args: |
|
||||
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"
|
||||
|
||||
# When track_progress is enabled:
|
||||
# - Creates a tracking comment with progress checkboxes
|
||||
# - Includes all PR context (comments, attachments, images)
|
||||
# - Updates progress as the review proceeds
|
||||
# - Marks as completed when done
|
||||
@ -18,12 +18,12 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Review PR from Specific Author
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
@ -19,12 +19,12 @@ jobs:
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Claude Code Review
|
||||
uses: anthropics/claude-code-action@v1-dev
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
prompt: |
|
||||
116
examples/test-failure-analysis.yml
Normal file
116
examples/test-failure-analysis.yml
Normal file
@ -0,0 +1,116 @@
|
||||
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
|
||||
claude_args: |
|
||||
--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 }}
|
||||
WORKFLOW_NAME: ${{ github.event.workflow_run.name }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
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 "$WORKFLOW_NAME" --ref "$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 }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
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 "$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
|
||||
27
github-app-manifest.json
Normal file
27
github-app-manifest.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@ -12,6 +12,7 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.1",
|
||||
"@actions/github": "^6.0.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.123",
|
||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||
"@octokit/graphql": "^8.2.2",
|
||||
"@octokit/rest": "^21.1.1",
|
||||
|
||||
83
scripts/edit-issue-labels.sh
Executable file
83
scripts/edit-issue-labels.sh
Executable file
@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Edits labels on a GitHub issue.
|
||||
# Usage: ./scripts/edit-issue-labels.sh --add-label bug --add-label needs-triage --remove-label untriaged
|
||||
#
|
||||
# The issue number is read from the workflow event payload.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Read from event payload so the issue number is bound to the triggering event
|
||||
ISSUE=$(jq -r '.issue.number // empty' "${GITHUB_EVENT_PATH:?GITHUB_EVENT_PATH not set}")
|
||||
if ! [[ "$ISSUE" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: no issue number in event payload" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ADD_LABELS=()
|
||||
REMOVE_LABELS=()
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--add-label)
|
||||
ADD_LABELS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--remove-label)
|
||||
REMOVE_LABELS+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
echo "Error: unknown argument (only --add-label and --remove-label are accepted)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ${#ADD_LABELS[@]} -eq 0 && ${#REMOVE_LABELS[@]} -eq 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fetch valid labels from the repo
|
||||
VALID_LABELS=$(gh label list --limit 500 --json name --jq '.[].name')
|
||||
|
||||
# Filter to only labels that exist in the repo
|
||||
FILTERED_ADD=()
|
||||
for label in "${ADD_LABELS[@]}"; do
|
||||
if echo "$VALID_LABELS" | grep -qxF "$label"; then
|
||||
FILTERED_ADD+=("$label")
|
||||
fi
|
||||
done
|
||||
|
||||
FILTERED_REMOVE=()
|
||||
for label in "${REMOVE_LABELS[@]}"; do
|
||||
if echo "$VALID_LABELS" | grep -qxF "$label"; then
|
||||
FILTERED_REMOVE+=("$label")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ ${#FILTERED_ADD[@]} -eq 0 && ${#FILTERED_REMOVE[@]} -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Build gh command arguments
|
||||
GH_ARGS=("issue" "edit" "$ISSUE")
|
||||
|
||||
for label in "${FILTERED_ADD[@]}"; do
|
||||
GH_ARGS+=("--add-label" "$label")
|
||||
done
|
||||
|
||||
for label in "${FILTERED_REMOVE[@]}"; do
|
||||
GH_ARGS+=("--remove-label" "$label")
|
||||
done
|
||||
|
||||
gh "${GH_ARGS[@]}"
|
||||
|
||||
if [[ ${#FILTERED_ADD[@]} -gt 0 ]]; then
|
||||
echo "Added: ${FILTERED_ADD[*]}"
|
||||
fi
|
||||
if [[ ${#FILTERED_REMOVE[@]} -gt 0 ]]; then
|
||||
echo "Removed: ${FILTERED_REMOVE[*]}"
|
||||
fi
|
||||
96
scripts/gh.sh
Executable file
96
scripts/gh.sh
Executable file
@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Wrapper around gh CLI that only allows specific subcommands and flags.
|
||||
# All commands are scoped to the current repository via GH_REPO or GITHUB_REPOSITORY.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/gh.sh issue view 123
|
||||
# ./scripts/gh.sh issue view 123 --comments
|
||||
# ./scripts/gh.sh issue list --state open --limit 20
|
||||
# ./scripts/gh.sh search issues "search query" --limit 10
|
||||
# ./scripts/gh.sh label list --limit 100
|
||||
|
||||
export GH_HOST=github.com
|
||||
|
||||
REPO="${GH_REPO:-${GITHUB_REPOSITORY:-}}"
|
||||
if [[ -z "$REPO" || "$REPO" == */*/* || "$REPO" != */* ]]; then
|
||||
echo "Error: GH_REPO or GITHUB_REPOSITORY must be set to owner/repo format (e.g., GITHUB_REPOSITORY=anthropics/claude-code)" >&2
|
||||
exit 1
|
||||
fi
|
||||
export GH_REPO="$REPO"
|
||||
|
||||
ALLOWED_FLAGS=(--comments --state --limit --label)
|
||||
FLAGS_WITH_VALUES=(--state --limit --label)
|
||||
|
||||
SUB1="${1:-}"
|
||||
SUB2="${2:-}"
|
||||
CMD="$SUB1 $SUB2"
|
||||
case "$CMD" in
|
||||
"issue view"|"issue list"|"search issues"|"label list")
|
||||
;;
|
||||
*)
|
||||
echo "Error: only 'issue view', 'issue list', 'search issues', 'label list' are allowed (e.g., ./scripts/gh.sh issue view 123)" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
shift 2
|
||||
|
||||
# Separate flags from positional arguments
|
||||
POSITIONAL=()
|
||||
FLAGS=()
|
||||
skip_next=false
|
||||
for arg in "$@"; do
|
||||
if [[ "$skip_next" == true ]]; then
|
||||
FLAGS+=("$arg")
|
||||
skip_next=false
|
||||
elif [[ "$arg" == -* ]]; then
|
||||
flag="${arg%%=*}"
|
||||
matched=false
|
||||
for allowed in "${ALLOWED_FLAGS[@]}"; do
|
||||
if [[ "$flag" == "$allowed" ]]; then
|
||||
matched=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ "$matched" == false ]]; then
|
||||
echo "Error: only --comments, --state, --limit, --label flags are allowed (e.g., ./scripts/gh.sh issue list --state open --limit 20)" >&2
|
||||
exit 1
|
||||
fi
|
||||
FLAGS+=("$arg")
|
||||
# If flag expects a value and isn't using = syntax, skip next arg
|
||||
if [[ "$arg" != *=* ]]; then
|
||||
for vflag in "${FLAGS_WITH_VALUES[@]}"; do
|
||||
if [[ "$flag" == "$vflag" ]]; then
|
||||
skip_next=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
POSITIONAL+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$CMD" == "search issues" ]]; then
|
||||
QUERY="${POSITIONAL[0]:-}"
|
||||
QUERY_LOWER=$(echo "$QUERY" | tr '[:upper:]' '[:lower:]')
|
||||
if [[ "$QUERY_LOWER" == *"repo:"* || "$QUERY_LOWER" == *"org:"* || "$QUERY_LOWER" == *"user:"* ]]; then
|
||||
echo "Error: search query must not contain repo:, org:, or user: qualifiers (e.g., ./scripts/gh.sh search issues \"bug report\" --limit 10)" >&2
|
||||
exit 1
|
||||
fi
|
||||
gh "$SUB1" "$SUB2" "$QUERY" --repo "$REPO" "${FLAGS[@]}"
|
||||
elif [[ "$CMD" == "issue view" ]]; then
|
||||
if [[ ${#POSITIONAL[@]} -ne 1 ]] || ! [[ "${POSITIONAL[0]}" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: issue view requires exactly one numeric issue number (e.g., ./scripts/gh.sh issue view 123)" >&2
|
||||
exit 1
|
||||
fi
|
||||
gh "$SUB1" "$SUB2" "${POSITIONAL[0]}" "${FLAGS[@]}"
|
||||
else
|
||||
if [[ ${#POSITIONAL[@]} -ne 0 ]]; then
|
||||
echo "Error: issue list and label list do not accept positional arguments (e.g., ./scripts/gh.sh issue list --state open, ./scripts/gh.sh label list --limit 100)" >&2
|
||||
exit 1
|
||||
fi
|
||||
gh "$SUB1" "$SUB2" "${FLAGS[@]}"
|
||||
fi
|
||||
36
scripts/git-push.sh
Executable file
36
scripts/git-push.sh
Executable file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Wrapper around `git push` that only allows `origin <ref>` with no flags.
|
||||
# Defends against --receive-pack / --exec RCE and arbitrary-remote exfiltration
|
||||
# (H1 #3556799). `git push:*` in allowedTools permits `git push --receive-pack='sh -c ...' ext::sh`
|
||||
# which runs arbitrary shell on the Actions runner. This wrapper closes that.
|
||||
#
|
||||
# Usage:
|
||||
# git-push.sh origin HEAD
|
||||
# git-push.sh origin claude/issue-123-20260304
|
||||
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "Error: exactly two arguments required: origin <ref>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for arg in "$@"; do
|
||||
if [[ "$arg" == -* ]]; then
|
||||
echo "Error: flags are not allowed (got: $arg)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ "$1" != "origin" ]]; then
|
||||
echo "Error: remote must be 'origin' (got: $1)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
REF="$2"
|
||||
if [[ "$REF" != "HEAD" ]] && ! git check-ref-format --branch "$REF" >/dev/null 2>&1; then
|
||||
echo "Error: invalid ref: $REF" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec git push origin "$REF"
|
||||
@ -1,123 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Setup Network Restrictions with Squid Proxy
|
||||
# This script sets up a Squid proxy to restrict network access to whitelisted domains only.
|
||||
|
||||
set -e
|
||||
|
||||
# Check if experimental_allowed_domains is provided
|
||||
if [ -z "$EXPERIMENTAL_ALLOWED_DOMAINS" ]; then
|
||||
echo "ERROR: EXPERIMENTAL_ALLOWED_DOMAINS environment variable is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check required environment variables
|
||||
if [ -z "$RUNNER_TEMP" ]; then
|
||||
echo "ERROR: RUNNER_TEMP environment variable is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$GITHUB_ENV" ]; then
|
||||
echo "ERROR: GITHUB_ENV environment variable is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Setting up network restrictions with Squid proxy..."
|
||||
|
||||
SQUID_START_TIME=$(date +%s.%N)
|
||||
|
||||
# Create whitelist file
|
||||
echo "$EXPERIMENTAL_ALLOWED_DOMAINS" > $RUNNER_TEMP/whitelist.txt
|
||||
|
||||
# Ensure each domain has proper format
|
||||
# If domain doesn't start with a dot and isn't an IP, add the dot for subdomain matching
|
||||
mv $RUNNER_TEMP/whitelist.txt $RUNNER_TEMP/whitelist.txt.orig
|
||||
while IFS= read -r domain; do
|
||||
if [ -n "$domain" ]; then
|
||||
# Trim whitespace
|
||||
domain=$(echo "$domain" | xargs)
|
||||
# If it's not empty and doesn't start with a dot, add one
|
||||
if [[ "$domain" != .* ]] && [[ ! "$domain" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo ".$domain" >> $RUNNER_TEMP/whitelist.txt
|
||||
else
|
||||
echo "$domain" >> $RUNNER_TEMP/whitelist.txt
|
||||
fi
|
||||
fi
|
||||
done < $RUNNER_TEMP/whitelist.txt.orig
|
||||
|
||||
# Create Squid config with whitelist
|
||||
echo "http_port 3128" > $RUNNER_TEMP/squid.conf
|
||||
echo "" >> $RUNNER_TEMP/squid.conf
|
||||
echo "# Define ACLs" >> $RUNNER_TEMP/squid.conf
|
||||
echo "acl whitelist dstdomain \"/etc/squid/whitelist.txt\"" >> $RUNNER_TEMP/squid.conf
|
||||
echo "acl localnet src 127.0.0.1/32" >> $RUNNER_TEMP/squid.conf
|
||||
echo "acl localnet src 172.17.0.0/16" >> $RUNNER_TEMP/squid.conf
|
||||
echo "acl SSL_ports port 443" >> $RUNNER_TEMP/squid.conf
|
||||
echo "acl Safe_ports port 80" >> $RUNNER_TEMP/squid.conf
|
||||
echo "acl Safe_ports port 443" >> $RUNNER_TEMP/squid.conf
|
||||
echo "acl CONNECT method CONNECT" >> $RUNNER_TEMP/squid.conf
|
||||
echo "" >> $RUNNER_TEMP/squid.conf
|
||||
echo "# Deny requests to certain unsafe ports" >> $RUNNER_TEMP/squid.conf
|
||||
echo "http_access deny !Safe_ports" >> $RUNNER_TEMP/squid.conf
|
||||
echo "" >> $RUNNER_TEMP/squid.conf
|
||||
echo "# Only allow CONNECT to SSL ports" >> $RUNNER_TEMP/squid.conf
|
||||
echo "http_access deny CONNECT !SSL_ports" >> $RUNNER_TEMP/squid.conf
|
||||
echo "" >> $RUNNER_TEMP/squid.conf
|
||||
echo "# Allow localhost" >> $RUNNER_TEMP/squid.conf
|
||||
echo "http_access allow localhost" >> $RUNNER_TEMP/squid.conf
|
||||
echo "" >> $RUNNER_TEMP/squid.conf
|
||||
echo "# Allow localnet access to whitelisted domains" >> $RUNNER_TEMP/squid.conf
|
||||
echo "http_access allow localnet whitelist" >> $RUNNER_TEMP/squid.conf
|
||||
echo "" >> $RUNNER_TEMP/squid.conf
|
||||
echo "# Deny everything else" >> $RUNNER_TEMP/squid.conf
|
||||
echo "http_access deny all" >> $RUNNER_TEMP/squid.conf
|
||||
|
||||
echo "Starting Squid proxy..."
|
||||
# First, remove any existing container
|
||||
sudo docker rm -f squid-proxy 2>/dev/null || true
|
||||
|
||||
# Ensure whitelist file is not empty (Squid fails with empty files)
|
||||
if [ ! -s "$RUNNER_TEMP/whitelist.txt" ]; then
|
||||
echo "WARNING: Whitelist file is empty, adding a dummy entry"
|
||||
echo ".example.com" >> $RUNNER_TEMP/whitelist.txt
|
||||
fi
|
||||
|
||||
# Use sudo to prevent Claude from stopping the container
|
||||
CONTAINER_ID=$(sudo docker run -d \
|
||||
--name squid-proxy \
|
||||
-p 127.0.0.1:3128:3128 \
|
||||
-v $RUNNER_TEMP/squid.conf:/etc/squid/squid.conf:ro \
|
||||
-v $RUNNER_TEMP/whitelist.txt:/etc/squid/whitelist.txt:ro \
|
||||
ubuntu/squid:latest 2>&1) || {
|
||||
echo "ERROR: Failed to start Squid container"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Wait for proxy to be ready (usually < 1 second)
|
||||
READY=false
|
||||
for i in {1..30}; do
|
||||
if nc -z 127.0.0.1 3128 2>/dev/null; then
|
||||
TOTAL_TIME=$(echo "scale=3; $(date +%s.%N) - $SQUID_START_TIME" | bc)
|
||||
echo "Squid proxy ready in ${TOTAL_TIME}s"
|
||||
READY=true
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
if [ "$READY" != "true" ]; then
|
||||
echo "ERROR: Squid proxy failed to start within 3 seconds"
|
||||
echo "Container logs:"
|
||||
sudo docker logs squid-proxy 2>&1 || true
|
||||
echo "Container status:"
|
||||
sudo docker ps -a | grep squid-proxy || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Set proxy environment variables
|
||||
echo "http_proxy=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
echo "https_proxy=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
echo "HTTP_PROXY=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
echo "HTTPS_PROXY=http://127.0.0.1:3128" >> $GITHUB_ENV
|
||||
|
||||
echo "Network restrictions setup completed successfully"
|
||||
@ -20,19 +20,18 @@ import {
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
import type { CommonFields, PreparedContext, EventData } from "./types";
|
||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import type { Mode, ModeContext } from "../modes/types";
|
||||
import { extractUserRequest } from "../utils/extract-user-request";
|
||||
export type { CommonFields, PreparedContext } from "./types";
|
||||
|
||||
// Tag mode defaults - these tools are needed for tag mode to function
|
||||
const BASE_ALLOWED_TOOLS = [
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
];
|
||||
const GIT_PUSH_WRAPPER = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`;
|
||||
|
||||
/** Filename for the user request file, read by the SDK runner */
|
||||
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
||||
|
||||
// Tag mode defaults - these tools are needed for tag mode to function.
|
||||
// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode
|
||||
// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside it.
|
||||
const BASE_ALLOWED_TOOLS = ["Glob", "Grep", "LS", "Read"];
|
||||
|
||||
export function buildAllowedToolsString(
|
||||
customAllowedTools?: string[],
|
||||
@ -56,10 +55,7 @@ export function buildAllowedToolsString(
|
||||
baseTools.push(
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
`Bash(${GIT_PUSH_WRAPPER}:*)`,
|
||||
"Bash(git rm:*)",
|
||||
);
|
||||
}
|
||||
@ -192,11 +188,6 @@ export function prepareContext(
|
||||
if (!isPR) {
|
||||
throw new Error("IS_PR must be true for pull_request_review event");
|
||||
}
|
||||
if (!commentBody) {
|
||||
throw new Error(
|
||||
"COMMENT_BODY is required for pull_request_review event",
|
||||
);
|
||||
}
|
||||
eventData = {
|
||||
eventName: "pull_request_review",
|
||||
isPR: true,
|
||||
@ -335,6 +326,7 @@ export function prepareContext(
|
||||
return {
|
||||
...commonFields,
|
||||
eventData,
|
||||
githubContext: context,
|
||||
};
|
||||
}
|
||||
|
||||
@ -383,6 +375,7 @@ export function getEventTypeAndContext(envVars: PreparedContext): {
|
||||
};
|
||||
|
||||
case "pull_request":
|
||||
case "pull_request_target":
|
||||
return {
|
||||
eventType: "PULL_REQUEST",
|
||||
triggerContext: eventData.eventAction
|
||||
@ -434,7 +427,7 @@ function getCommitInstructions(
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin HEAD)`;
|
||||
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin HEAD)`;
|
||||
} else {
|
||||
const branchName = eventData.claudeBranch || eventData.baseBranch;
|
||||
return `
|
||||
@ -448,7 +441,7 @@ function getCommitInstructions(
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin ${branchName})`;
|
||||
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin ${branchName})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -457,9 +450,148 @@ export function generatePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean,
|
||||
mode: Mode,
|
||||
modeName: "tag" | "agent",
|
||||
): string {
|
||||
return mode.generatePrompt(context, githubData, useCommitSigning);
|
||||
if (modeName === "agent") {
|
||||
return context.prompt || `Repository: ${context.repository}`;
|
||||
}
|
||||
|
||||
// Tag mode
|
||||
const defaultPrompt = generateDefaultPrompt(
|
||||
context,
|
||||
githubData,
|
||||
useCommitSigning,
|
||||
);
|
||||
|
||||
if (context.githubContext?.inputs?.prompt) {
|
||||
return (
|
||||
defaultPrompt +
|
||||
`
|
||||
|
||||
<custom_instructions>
|
||||
${context.githubContext.inputs.prompt}
|
||||
</custom_instructions>`
|
||||
);
|
||||
}
|
||||
|
||||
return defaultPrompt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a simplified prompt for tag mode (opt-in via USE_SIMPLE_PROMPT env var)
|
||||
* @internal
|
||||
*/
|
||||
function generateSimplePrompt(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean = false,
|
||||
): string {
|
||||
const {
|
||||
contextData,
|
||||
comments,
|
||||
changedFilesWithSHA,
|
||||
reviewData,
|
||||
imageUrlMap,
|
||||
} = githubData;
|
||||
const { eventData } = context;
|
||||
|
||||
const { triggerContext } = getEventTypeAndContext(context);
|
||||
|
||||
const formattedContext = formatContext(contextData, eventData.isPR);
|
||||
const formattedComments = formatComments(comments, imageUrlMap);
|
||||
const formattedReviewComments = eventData.isPR
|
||||
? formatReviewComments(reviewData, imageUrlMap)
|
||||
: "";
|
||||
const formattedChangedFiles = eventData.isPR
|
||||
? formatChangedFilesWithSHA(changedFilesWithSHA)
|
||||
: "";
|
||||
|
||||
const hasImages = imageUrlMap && imageUrlMap.size > 0;
|
||||
const imagesInfo = hasImages
|
||||
? `\n\n<images_info>
|
||||
Images from comments have been saved to disk. Paths are in the formatted content above. Use Read tool to view them.
|
||||
</images_info>`
|
||||
: "";
|
||||
|
||||
const formattedBody = contextData?.body
|
||||
? formatBody(contextData.body, imageUrlMap)
|
||||
: "No description provided";
|
||||
|
||||
const entityType = eventData.isPR ? "pull request" : "issue";
|
||||
const jobUrl = `${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
|
||||
let promptContent = `You were tagged on a GitHub ${entityType} via "${context.triggerPhrase}". Read the request and decide how to help.
|
||||
|
||||
<context>
|
||||
${formattedContext}
|
||||
</context>
|
||||
|
||||
<${eventData.isPR ? "pr" : "issue"}_body>
|
||||
${formattedBody}
|
||||
</${eventData.isPR ? "pr" : "issue"}_body>
|
||||
|
||||
<comments>
|
||||
${formattedComments || "No comments"}
|
||||
</comments>
|
||||
${
|
||||
eventData.isPR
|
||||
? `
|
||||
<review_comments>
|
||||
${formattedReviewComments || "No review comments"}
|
||||
</review_comments>
|
||||
|
||||
<changed_files>
|
||||
${formattedChangedFiles || "No files changed"}
|
||||
</changed_files>`
|
||||
: ""
|
||||
}${imagesInfo}
|
||||
|
||||
<metadata>
|
||||
repository: ${context.repository}
|
||||
${eventData.isPR && eventData.prNumber ? `pr_number: ${eventData.prNumber}` : ""}
|
||||
${!eventData.isPR && eventData.issueNumber ? `issue_number: ${eventData.issueNumber}` : ""}
|
||||
trigger: ${triggerContext}
|
||||
triggered_by: ${context.triggerUsername ?? "Unknown"}
|
||||
claude_comment_id: ${context.claudeCommentId}
|
||||
</metadata>
|
||||
${
|
||||
(eventData.eventName === "issue_comment" ||
|
||||
eventData.eventName === "pull_request_review_comment" ||
|
||||
eventData.eventName === "pull_request_review") &&
|
||||
eventData.commentBody
|
||||
? `
|
||||
<trigger_comment>
|
||||
${sanitizeContent(eventData.commentBody)}
|
||||
</trigger_comment>`
|
||||
: ""
|
||||
}
|
||||
|
||||
Your request is in <trigger_comment> above${eventData.eventName === "issues" ? ` (or the ${entityType} body for assigned/labeled events)` : ""}.
|
||||
|
||||
Decide what's being asked:
|
||||
1. **Question or code review** - Answer directly or provide feedback
|
||||
2. **Code change** - Implement the change, commit, and push
|
||||
|
||||
Communication:
|
||||
- Your ONLY visible output is your GitHub comment - update it with progress and results
|
||||
- Use mcp__github_comment__update_claude_comment to update (only "body" param needed)
|
||||
- Use checklist format for tasks: - [ ] incomplete, - [x] complete
|
||||
- Use ### headers (not #)
|
||||
${getCommitInstructions(eventData, githubData, context, useCommitSigning)}
|
||||
${
|
||||
eventData.claudeBranch
|
||||
? `
|
||||
When done with changes, provide a PR link:
|
||||
[Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...${eventData.claudeBranch}?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
|
||||
Use THREE dots (...) between branches. URL-encode all parameters.`
|
||||
: ""
|
||||
}
|
||||
|
||||
Always include at the bottom:
|
||||
- Job link: [View job run](${jobUrl})
|
||||
- Follow the repo's CLAUDE.md file for project-specific guidelines`;
|
||||
|
||||
return promptContent;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -471,6 +603,10 @@ export function generateDefaultPrompt(
|
||||
githubData: FetchDataResult,
|
||||
useCommitSigning: boolean = false,
|
||||
): string {
|
||||
// Use simplified prompt if opted in
|
||||
if (process.env.USE_SIMPLE_PROMPT === "true") {
|
||||
return generateSimplePrompt(context, githubData, useCommitSigning);
|
||||
}
|
||||
const {
|
||||
contextData,
|
||||
comments,
|
||||
@ -616,7 +752,13 @@ ${eventData.eventName === "issue_comment" || eventData.eventName === "pull_reque
|
||||
- Reference specific code sections with file paths and line numbers${eventData.isPR ? `\n - AFTER reading files and analyzing code, you MUST call mcp__github_comment__update_claude_comment to post your review` : ""}
|
||||
- Formulate a concise, technical, and helpful response based on the context.
|
||||
- Reference specific code with inline formatting or code blocks.
|
||||
- Include relevant file paths and line numbers when applicable.
|
||||
- Include relevant file paths and line numbers when applicable.${
|
||||
eventData.isPR && context.githubContext?.inputs.includeFixLinks
|
||||
? `
|
||||
- When identifying issues that could be fixed, include an inline link: [Fix this →](https://claude.ai/code?q=<URI_ENCODED_INSTRUCTIONS>&repo=${context.repository})
|
||||
The query should be URI-encoded and include enough context for Claude Code to understand and fix the issue (file path, line numbers, branch name, what needs to change).`
|
||||
: ""
|
||||
}
|
||||
- ${eventData.isPR ? `IMPORTANT: Submit your review feedback by updating the Claude comment using mcp__github_comment__update_claude_comment. This will be displayed as your PR review.` : `Remember that this feedback must be posted to the GitHub comment using mcp__github_comment__update_claude_comment.`}
|
||||
|
||||
B. For Straightforward Changes:
|
||||
@ -674,7 +816,7 @@ ${
|
||||
: `- Use git commands via the Bash tool for version control (remember that you have access to these git commands):
|
||||
- Stage files: Bash(git add <files>)
|
||||
- Commit changes: Bash(git commit -m "<message>")
|
||||
- Push to remote: Bash(git push origin <branch>) (NEVER force push)
|
||||
- Push to remote: Bash(${GIT_PUSH_WRAPPER} origin <branch>)
|
||||
- Delete files: Bash(git rm <files>) followed by commit and push
|
||||
- Check status: Bash(git status)
|
||||
- View diff: Bash(git diff)${eventData.isPR && eventData.baseBranch ? `\n - IMPORTANT: For PR diffs, use: Bash(git diff origin/${eventData.baseBranch}...HEAD)` : ""}`
|
||||
@ -682,7 +824,7 @@ ${
|
||||
- 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.
|
||||
- Use h3 headers (###) for section titles in your comments, not h1 headers (#).
|
||||
- Your comment must always include the job run link (and branch link if there is one) at the bottom.
|
||||
- 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).
|
||||
|
||||
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.
|
||||
@ -707,7 +849,7 @@ What You CANNOT Do:
|
||||
- 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:
|
||||
"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)."
|
||||
"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)."
|
||||
|
||||
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.
|
||||
|
||||
@ -723,29 +865,70 @@ f. If you are unable to complete certain steps, such as running a linter or test
|
||||
return promptContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the user's request from the prepared context and GitHub data.
|
||||
*
|
||||
* This is used to send the user's actual command/request as a separate
|
||||
* content block, enabling slash command processing in the CLI.
|
||||
*
|
||||
* @param context - The prepared context containing event data and trigger phrase
|
||||
* @param githubData - The fetched GitHub data containing issue/PR body content
|
||||
* @returns The extracted user request text (e.g., "/review-pr" or "fix this bug"),
|
||||
* or null for assigned/labeled events without an explicit trigger in the body
|
||||
*
|
||||
* @example
|
||||
* // Comment event: "@claude /review-pr" -> returns "/review-pr"
|
||||
* // Issue body with "@claude fix this" -> returns "fix this"
|
||||
* // Issue assigned without @claude in body -> returns null
|
||||
*/
|
||||
function extractUserRequestFromContext(
|
||||
context: PreparedContext,
|
||||
githubData: FetchDataResult,
|
||||
): string | null {
|
||||
const { eventData, triggerPhrase } = context;
|
||||
|
||||
// For comment events, extract from comment body
|
||||
if (
|
||||
"commentBody" in eventData &&
|
||||
eventData.commentBody &&
|
||||
(eventData.eventName === "issue_comment" ||
|
||||
eventData.eventName === "pull_request_review_comment" ||
|
||||
eventData.eventName === "pull_request_review")
|
||||
) {
|
||||
return extractUserRequest(eventData.commentBody, triggerPhrase);
|
||||
}
|
||||
|
||||
// For issue/PR events triggered by body content, extract from the body
|
||||
if (githubData.contextData?.body) {
|
||||
const request = extractUserRequest(
|
||||
githubData.contextData.body,
|
||||
triggerPhrase,
|
||||
);
|
||||
if (request) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
// For assigned/labeled events without explicit trigger in body,
|
||||
// return null to indicate the full context should be used
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function createPrompt(
|
||||
mode: Mode,
|
||||
modeContext: ModeContext,
|
||||
commentId: number,
|
||||
baseBranch: string | undefined,
|
||||
claudeBranch: string | undefined,
|
||||
githubData: FetchDataResult,
|
||||
context: ParsedGitHubContext,
|
||||
) {
|
||||
try {
|
||||
// Prepare the context for prompt generation
|
||||
let claudeCommentId: string = "";
|
||||
if (mode.name === "tag") {
|
||||
if (!modeContext.commentId) {
|
||||
throw new Error(
|
||||
`${mode.name} mode requires a comment ID for prompt generation`,
|
||||
);
|
||||
}
|
||||
claudeCommentId = modeContext.commentId.toString();
|
||||
}
|
||||
const claudeCommentId = commentId.toString();
|
||||
|
||||
const preparedContext = prepareContext(
|
||||
context,
|
||||
claudeCommentId,
|
||||
modeContext.baseBranch,
|
||||
modeContext.claudeBranch,
|
||||
baseBranch,
|
||||
claudeBranch,
|
||||
);
|
||||
|
||||
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||
@ -757,7 +940,7 @@ export async function createPrompt(
|
||||
preparedContext,
|
||||
githubData,
|
||||
context.inputs.useCommitSigning,
|
||||
mode,
|
||||
"tag",
|
||||
);
|
||||
|
||||
// Log the final prompt to console
|
||||
@ -771,22 +954,33 @@ export async function createPrompt(
|
||||
promptContent,
|
||||
);
|
||||
|
||||
// Set allowed tools
|
||||
// Extract and write the user request separately for SDK multi-block messaging
|
||||
// This allows the CLI to process slash commands (e.g., "@claude /review-pr")
|
||||
const userRequest = extractUserRequestFromContext(
|
||||
preparedContext,
|
||||
githubData,
|
||||
);
|
||||
if (userRequest) {
|
||||
await writeFile(
|
||||
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/${USER_REQUEST_FILENAME}`,
|
||||
userRequest,
|
||||
);
|
||||
console.log("===== USER REQUEST =====");
|
||||
console.log(userRequest);
|
||||
console.log("========================");
|
||||
}
|
||||
|
||||
// NOTE: these env var exports are dead — nothing reads ALLOWED_TOOLS / DISALLOWED_TOOLS.
|
||||
// The live path is modes/tag/index.ts which builds --allowedTools into claudeArgs directly.
|
||||
// Kept only so the H1 report's pointed-to file stays in sync with the live fix.
|
||||
const hasActionsReadPermission = false;
|
||||
|
||||
// Get mode-specific tools
|
||||
const modeAllowedTools = mode.getAllowedTools();
|
||||
const modeDisallowedTools = mode.getDisallowedTools();
|
||||
|
||||
const allAllowedTools = buildAllowedToolsString(
|
||||
modeAllowedTools,
|
||||
[],
|
||||
hasActionsReadPermission,
|
||||
context.inputs.useCommitSigning,
|
||||
);
|
||||
const allDisallowedTools = buildDisallowedToolsString(
|
||||
modeDisallowedTools,
|
||||
modeAllowedTools,
|
||||
);
|
||||
const allDisallowedTools = buildDisallowedToolsString([], []);
|
||||
|
||||
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);
|
||||
core.exportVariable("DISALLOWED_TOOLS", allDisallowedTools);
|
||||
|
||||
@ -23,7 +23,7 @@ type PullRequestReviewEvent = {
|
||||
eventName: "pull_request_review";
|
||||
isPR: true;
|
||||
prNumber: string;
|
||||
commentBody: string;
|
||||
commentBody?: string; // May be absent for approvals without comments
|
||||
claudeBranch?: string;
|
||||
baseBranch?: string;
|
||||
};
|
||||
@ -78,8 +78,7 @@ type IssueLabeledEvent = {
|
||||
labelTrigger: string;
|
||||
};
|
||||
|
||||
type PullRequestEvent = {
|
||||
eventName: "pull_request";
|
||||
type PullRequestBaseEvent = {
|
||||
eventAction?: string; // opened, synchronize, etc.
|
||||
isPR: true;
|
||||
prNumber: string;
|
||||
@ -87,6 +86,14 @@ type PullRequestEvent = {
|
||||
baseBranch?: string;
|
||||
};
|
||||
|
||||
type PullRequestEvent = PullRequestBaseEvent & {
|
||||
eventName: "pull_request";
|
||||
};
|
||||
|
||||
type PullRequestTargetEvent = PullRequestBaseEvent & {
|
||||
eventName: "pull_request_target";
|
||||
};
|
||||
|
||||
// Union type for all possible event types
|
||||
export type EventData =
|
||||
| PullRequestReviewCommentEvent
|
||||
@ -96,7 +103,8 @@ export type EventData =
|
||||
| IssueOpenedEvent
|
||||
| IssueAssignedEvent
|
||||
| IssueLabeledEvent
|
||||
| PullRequestEvent;
|
||||
| PullRequestEvent
|
||||
| PullRequestTargetEvent;
|
||||
|
||||
// Combined type with separate eventData field
|
||||
export type PreparedContext = CommonFields & {
|
||||
|
||||
21
src/entrypoints/cleanup-ssh-signing.ts
Normal file
21
src/entrypoints/cleanup-ssh-signing.ts
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Cleanup SSH signing key after action completes
|
||||
* This is run as a post step for security purposes
|
||||
*/
|
||||
|
||||
import { cleanupSshSigning } from "../github/operations/git-config";
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
await cleanupSshSigning();
|
||||
} catch (error) {
|
||||
// Don't fail the action if cleanup fails, just log it
|
||||
console.error("Failed to cleanup SSH signing key:", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
run();
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
import * as core from "@actions/core";
|
||||
|
||||
export function collectActionInputsPresence(): void {
|
||||
export function collectActionInputsPresence(): string {
|
||||
const inputDefaults: Record<string, string> = {
|
||||
trigger_phrase: "@claude",
|
||||
assignee_trigger: "",
|
||||
@ -25,15 +23,15 @@ export function collectActionInputsPresence(): void {
|
||||
github_token: "",
|
||||
max_turns: "",
|
||||
use_sticky_comment: "false",
|
||||
classify_inline_comments: "true",
|
||||
use_commit_signing: "false",
|
||||
experimental_allowed_domains: "",
|
||||
ssh_signing_key: "",
|
||||
};
|
||||
|
||||
const allInputsJson = process.env.ALL_INPUTS;
|
||||
if (!allInputsJson) {
|
||||
console.log("ALL_INPUTS environment variable not found");
|
||||
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||
return;
|
||||
return JSON.stringify({});
|
||||
}
|
||||
|
||||
let allInputs: Record<string, string>;
|
||||
@ -41,8 +39,7 @@ export function collectActionInputsPresence(): void {
|
||||
allInputs = JSON.parse(allInputsJson);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse ALL_INPUTS JSON:", e);
|
||||
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||
return;
|
||||
return JSON.stringify({});
|
||||
}
|
||||
|
||||
const presentInputs: Record<string, boolean> = {};
|
||||
@ -54,5 +51,5 @@ export function collectActionInputsPresence(): void {
|
||||
presentInputs[name] = isSet;
|
||||
}
|
||||
|
||||
core.setOutput("action_inputs_present", JSON.stringify(presentInputs));
|
||||
return JSON.stringify(presentInputs);
|
||||
}
|
||||
|
||||
233
src/entrypoints/post-buffered-inline-comments.ts
Normal file
233
src/entrypoints/post-buffered-inline-comments.ts
Normal file
@ -0,0 +1,233 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Reads buffered inline-comment calls from /tmp/inline-comments-buffer.jsonl,
|
||||
* classifies each as "real review" vs "test/probe" using Haiku, and posts
|
||||
* only the real ones. Calls with confirmed=false are never posted.
|
||||
*
|
||||
* If the Anthropic API is unavailable (Bedrock/Vertex users without a direct
|
||||
* key), falls back to posting everything with confirmed !== false. This
|
||||
* preserves backward compatibility — before this change, all unconfirmed
|
||||
* calls posted immediately.
|
||||
*/
|
||||
import { readFileSync } from "fs";
|
||||
import { createOctokit } from "../github/api/client";
|
||||
|
||||
const BUFFER_PATH = "/tmp/inline-comments-buffer.jsonl";
|
||||
|
||||
type BufferedComment = {
|
||||
ts: string;
|
||||
path: string;
|
||||
line?: number;
|
||||
startLine?: number;
|
||||
side?: "LEFT" | "RIGHT";
|
||||
commit_id?: string;
|
||||
body: string;
|
||||
confirmed?: boolean;
|
||||
};
|
||||
|
||||
const CLASSIFICATION_PROMPT = `You are classifying PR inline comments as either REAL code review feedback or TEST/PROBE calls.
|
||||
|
||||
A TEST/PROBE call is when an automated agent is checking whether a commenting tool works. These typically:
|
||||
- Start with phrases like "Test comment", "Testing if", "Can I", "Does this work", "Checking if"
|
||||
- Have generic/placeholder content not specific to any code
|
||||
- Exist to verify tool functionality, not to provide review feedback
|
||||
|
||||
A REAL review comment:
|
||||
- Discusses specific code, logic, bugs, or style
|
||||
- Provides actionable feedback for the PR author
|
||||
- References concrete aspects of the change
|
||||
|
||||
For each numbered comment body below, respond with ONLY a JSON array of booleans where true = REAL review comment, false = test/probe. No other text.
|
||||
|
||||
Comments:
|
||||
`;
|
||||
|
||||
async function classifyComments(bodies: string[]): Promise<boolean[] | null> {
|
||||
const apiKey = process.env.ANTHROPIC_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.log(
|
||||
"ANTHROPIC_API_KEY not set — skipping classification, posting all unconfirmed comments",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const prompt =
|
||||
CLASSIFICATION_PROMPT +
|
||||
bodies.map((b, i) => `${i + 1}. ${JSON.stringify(b)}`).join("\n");
|
||||
|
||||
try {
|
||||
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: "claude-haiku-4-5",
|
||||
max_tokens: 1024,
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.log(
|
||||
`Classification API returned ${res.status} — posting all unconfirmed comments`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
content: { type: string; text: string }[];
|
||||
};
|
||||
const text = data.content.find((c) => c.type === "text")?.text ?? "";
|
||||
const match = text.match(/\[[\s\S]*\]/);
|
||||
if (!match) {
|
||||
console.log(
|
||||
"Could not parse classification response — posting all unconfirmed comments",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const parsed = JSON.parse(match[0]);
|
||||
if (
|
||||
!Array.isArray(parsed) ||
|
||||
parsed.length !== bodies.length ||
|
||||
!parsed.every((v) => typeof v === "boolean")
|
||||
) {
|
||||
console.log(
|
||||
"Classification response shape mismatch — posting all unconfirmed comments",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
console.log(
|
||||
`Classification failed (${e instanceof Error ? e.message : String(e)}) — posting all unconfirmed comments`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function postComment(
|
||||
octokit: ReturnType<typeof createOctokit>["rest"],
|
||||
owner: string,
|
||||
repo: string,
|
||||
pull_number: number,
|
||||
headSha: string,
|
||||
c: BufferedComment,
|
||||
): Promise<boolean> {
|
||||
const params: Parameters<typeof octokit.rest.pulls.createReviewComment>[0] = {
|
||||
owner,
|
||||
repo,
|
||||
pull_number,
|
||||
body: c.body,
|
||||
path: c.path,
|
||||
side: c.side || "RIGHT",
|
||||
commit_id: c.commit_id || headSha,
|
||||
};
|
||||
if (c.startLine) {
|
||||
params.start_line = c.startLine;
|
||||
params.start_side = c.side || "RIGHT";
|
||||
params.line = c.line;
|
||||
} else {
|
||||
params.line = c.line;
|
||||
}
|
||||
try {
|
||||
await octokit.rest.pulls.createReviewComment(params);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(
|
||||
` failed ${c.path}:${c.line}: ${e instanceof Error ? e.message : String(e)}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let raw: string;
|
||||
try {
|
||||
raw = readFileSync(BUFFER_PATH, "utf8");
|
||||
} catch {
|
||||
console.log("No buffered inline comments");
|
||||
return;
|
||||
}
|
||||
|
||||
const comments: BufferedComment[] = raw
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line));
|
||||
|
||||
if (comments.length === 0) {
|
||||
console.log("No buffered inline comments");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${comments.length} buffered inline comment(s)`);
|
||||
|
||||
const githubToken = process.env.GITHUB_TOKEN;
|
||||
const owner = process.env.REPO_OWNER;
|
||||
const repo = process.env.REPO_NAME;
|
||||
const prNumber = process.env.PR_NUMBER;
|
||||
|
||||
if (!githubToken || !owner || !repo || !prNumber) {
|
||||
console.log(
|
||||
"::warning::Missing GITHUB_TOKEN/REPO_OWNER/REPO_NAME/PR_NUMBER — cannot post buffered comments",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Partition: confirmed=false are never posted; the rest are candidates
|
||||
const neverPost = comments.filter((c) => c.confirmed === false);
|
||||
const candidates = comments.filter((c) => c.confirmed !== false);
|
||||
|
||||
if (neverPost.length > 0) {
|
||||
console.log(` ${neverPost.length} with confirmed=false — not posting`);
|
||||
}
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Classify candidates
|
||||
const verdicts = await classifyComments(candidates.map((c) => c.body));
|
||||
const toPost =
|
||||
verdicts === null
|
||||
? candidates
|
||||
: candidates.filter((_, i) => verdicts[i] === true);
|
||||
const filtered =
|
||||
verdicts === null ? [] : candidates.filter((_, i) => verdicts[i] === false);
|
||||
|
||||
if (filtered.length > 0) {
|
||||
console.log(
|
||||
`::warning::${filtered.length} buffered comment(s) classified as test/probe — NOT posted:`,
|
||||
);
|
||||
for (const c of filtered) {
|
||||
console.log(` [${c.path}:${c.line}] ${c.body.slice(0, 120)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (toPost.length === 0) {
|
||||
console.log("No real comments to post");
|
||||
return;
|
||||
}
|
||||
|
||||
const octokit = createOctokit(githubToken).rest;
|
||||
const pull_number = parseInt(prNumber, 10);
|
||||
const pr = await octokit.pulls.get({ owner, repo, pull_number });
|
||||
const headSha = pr.data.head.sha;
|
||||
|
||||
console.log(`Posting ${toPost.length} classified-as-real comment(s)`);
|
||||
let posted = 0;
|
||||
for (const c of toPost) {
|
||||
if (await postComment(octokit, owner, repo, pull_number, headSha, c)) {
|
||||
console.log(` posted ${c.path}:${c.line}`);
|
||||
posted++;
|
||||
}
|
||||
}
|
||||
console.log(`Posted ${posted}/${toPost.length}`);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error("post-buffered-inline-comments failed:", e);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -10,8 +10,10 @@ import { setupGitHubToken } from "../github/token";
|
||||
import { checkWritePermissions } from "../github/validation/permissions";
|
||||
import { createOctokit } from "../github/api/client";
|
||||
import { parseGitHubContext, isEntityContext } from "../github/context";
|
||||
import { getMode } from "../modes/registry";
|
||||
import { prepare } from "../prepare";
|
||||
import { detectMode } from "../modes/detector";
|
||||
import { prepareTagMode } from "../modes/tag";
|
||||
import { prepareAgentMode } from "../modes/agent";
|
||||
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||
import { collectActionInputsPresence } from "./collect-inputs";
|
||||
|
||||
async function run() {
|
||||
@ -22,7 +24,10 @@ async function run() {
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// Auto-detect mode based on context
|
||||
const mode = getMode(context);
|
||||
const modeName = detectMode(context);
|
||||
console.log(
|
||||
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
|
||||
// Setup GitHub token
|
||||
const githubToken = await setupGitHubToken();
|
||||
@ -30,9 +35,13 @@ async function run() {
|
||||
|
||||
// Step 3: Check write permissions (only for entity contexts)
|
||||
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(
|
||||
octokit.rest,
|
||||
context,
|
||||
context.inputs.allowedNonWriteUsers,
|
||||
githubTokenProvided,
|
||||
);
|
||||
if (!hasWritePermissions) {
|
||||
throw new Error(
|
||||
@ -42,10 +51,13 @@ async function run() {
|
||||
}
|
||||
|
||||
// Check trigger conditions
|
||||
const containsTrigger = mode.shouldTrigger(context);
|
||||
const containsTrigger =
|
||||
modeName === "tag"
|
||||
? isEntityContext(context) && checkContainsTrigger(context)
|
||||
: !!context.inputs?.prompt;
|
||||
|
||||
// Debug logging
|
||||
console.log(`Mode: ${mode.name}`);
|
||||
console.log(`Mode: ${modeName}`);
|
||||
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
||||
console.log(`Trigger result: ${containsTrigger}`);
|
||||
|
||||
@ -59,31 +71,20 @@ async function run() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 5: Use the new modular prepare function
|
||||
const result = await prepare({
|
||||
context,
|
||||
octokit,
|
||||
mode,
|
||||
githubToken,
|
||||
});
|
||||
// Run prepare
|
||||
console.log(
|
||||
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
if (modeName === "tag") {
|
||||
await prepareTagMode({ context, octokit, githubToken });
|
||||
} else {
|
||||
await prepareAgentMode({ context, octokit, githubToken });
|
||||
}
|
||||
|
||||
// MCP config is handled by individual modes (tag/agent) and included in their claude_args output
|
||||
|
||||
// Expose the GitHub token (Claude App token) as an output
|
||||
core.setOutput("github_token", githubToken);
|
||||
|
||||
// Step 6: Get system prompt from mode if available
|
||||
if (mode.getSystemPrompt) {
|
||||
const modeContext = mode.prepareContext(context, {
|
||||
commentId: result.commentId,
|
||||
baseBranch: result.branchInfo.baseBranch,
|
||||
claudeBranch: result.branchInfo.claudeBranch,
|
||||
});
|
||||
const systemPrompt = mode.getSystemPrompt(modeContext);
|
||||
if (systemPrompt) {
|
||||
core.exportVariable("APPEND_SYSTEM_PROMPT", systemPrompt);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
||||
|
||||
353
src/entrypoints/run.ts
Normal file
353
src/entrypoints/run.ts
Normal file
@ -0,0 +1,353 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
/**
|
||||
* Unified entrypoint for the Claude Code Action.
|
||||
* Merges all previously separate action.yml steps (prepare, install, run, cleanup)
|
||||
* into a single TypeScript orchestrator.
|
||||
*/
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { dirname } from "path";
|
||||
import { spawn } from "child_process";
|
||||
import { appendFile } from "fs/promises";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { setupGitHubToken, WorkflowValidationSkipError } from "../github/token";
|
||||
import { checkWritePermissions } from "../github/validation/permissions";
|
||||
import { createOctokit } from "../github/api/client";
|
||||
import type { Octokits } from "../github/api/client";
|
||||
import {
|
||||
parseGitHubContext,
|
||||
isEntityContext,
|
||||
isPullRequestEvent,
|
||||
isPullRequestReviewEvent,
|
||||
isPullRequestReviewCommentEvent,
|
||||
} from "../github/context";
|
||||
import type { GitHubContext } from "../github/context";
|
||||
import { detectMode } from "../modes/detector";
|
||||
import { prepareTagMode } from "../modes/tag";
|
||||
import { prepareAgentMode } from "../modes/agent";
|
||||
import { checkContainsTrigger } from "../github/validation/trigger";
|
||||
import { restoreConfigFromBase } from "../github/operations/restore-config";
|
||||
import { validateBranchName } from "../github/operations/branch";
|
||||
import { collectActionInputsPresence } from "./collect-inputs";
|
||||
import { updateCommentLink } from "./update-comment-link";
|
||||
import { formatTurnsFromData } from "./format-turns";
|
||||
import type { Turn } from "./format-turns";
|
||||
// Base-action imports (used directly instead of subprocess)
|
||||
import { validateEnvironmentVariables } from "../../base-action/src/validate-env";
|
||||
import { setupClaudeCodeSettings } from "../../base-action/src/setup-claude-code-settings";
|
||||
import { installPlugins } from "../../base-action/src/install-plugins";
|
||||
import { preparePrompt } from "../../base-action/src/prepare-prompt";
|
||||
import { runClaude } from "../../base-action/src/run-claude";
|
||||
import type { ClaudeRunResult } from "../../base-action/src/run-claude-sdk";
|
||||
|
||||
/**
|
||||
* Install Claude Code CLI, handling retry logic and custom executable paths.
|
||||
* Returns the absolute path to the claude executable.
|
||||
*/
|
||||
async function installClaudeCode(): Promise<string> {
|
||||
const customExecutable = process.env.PATH_TO_CLAUDE_CODE_EXECUTABLE;
|
||||
if (customExecutable) {
|
||||
if (/[\x00-\x1f\x7f]/.test(customExecutable)) {
|
||||
throw new Error(
|
||||
"PATH_TO_CLAUDE_CODE_EXECUTABLE contains control characters (e.g. newlines), which is not allowed",
|
||||
);
|
||||
}
|
||||
console.log(`Using custom Claude Code executable: ${customExecutable}`);
|
||||
const claudeDir = dirname(customExecutable);
|
||||
// Add to PATH by appending to GITHUB_PATH
|
||||
const githubPath = process.env.GITHUB_PATH;
|
||||
if (githubPath) {
|
||||
await appendFile(githubPath, `${claudeDir}\n`);
|
||||
}
|
||||
// Also add to current process PATH
|
||||
process.env.PATH = `${claudeDir}:${process.env.PATH}`;
|
||||
return customExecutable;
|
||||
}
|
||||
|
||||
const claudeCodeVersion = "2.1.123";
|
||||
console.log(`Installing Claude Code v${claudeCodeVersion}...`);
|
||||
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
console.log(`Installation attempt ${attempt}...`);
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const child = spawn(
|
||||
"bash",
|
||||
[
|
||||
"-c",
|
||||
`curl -fsSL https://claude.ai/install.sh | bash -s -- ${claudeCodeVersion}`,
|
||||
],
|
||||
{ stdio: "inherit" },
|
||||
);
|
||||
child.on("close", (code) => {
|
||||
if (code === 0) resolve();
|
||||
else reject(new Error(`Install failed with exit code ${code}`));
|
||||
});
|
||||
child.on("error", reject);
|
||||
});
|
||||
console.log("Claude Code installed successfully");
|
||||
// Add to PATH
|
||||
const homeBin = `${process.env.HOME}/.local/bin`;
|
||||
const githubPath = process.env.GITHUB_PATH;
|
||||
if (githubPath) {
|
||||
await appendFile(githubPath, `${homeBin}\n`);
|
||||
}
|
||||
process.env.PATH = `${homeBin}:${process.env.PATH}`;
|
||||
return `${homeBin}/claude`;
|
||||
} catch (error) {
|
||||
if (attempt === 3) {
|
||||
throw new Error(
|
||||
`Failed to install Claude Code after 3 attempts: ${error}`,
|
||||
);
|
||||
}
|
||||
console.log("Installation failed, retrying...");
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
throw new Error("unreachable");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the step summary from Claude's execution output file.
|
||||
*/
|
||||
async function writeStepSummary(executionFile: string): Promise<void> {
|
||||
const summaryFile = process.env.GITHUB_STEP_SUMMARY;
|
||||
if (!summaryFile) return;
|
||||
|
||||
try {
|
||||
const fileContent = readFileSync(executionFile, "utf-8");
|
||||
const data: Turn[] = JSON.parse(fileContent);
|
||||
const markdown = formatTurnsFromData(data);
|
||||
await appendFile(summaryFile, markdown);
|
||||
console.log("Successfully formatted Claude Code report");
|
||||
} catch (error) {
|
||||
console.error(`Failed to format output: ${error}`);
|
||||
// Fall back to raw JSON
|
||||
try {
|
||||
let fallback = "## Claude Code Report (Raw Output)\n\n";
|
||||
fallback +=
|
||||
"Failed to format output (please report). Here's the raw JSON:\n\n";
|
||||
fallback += "```json\n";
|
||||
fallback += readFileSync(executionFile, "utf-8");
|
||||
fallback += "\n```\n";
|
||||
await appendFile(summaryFile, fallback);
|
||||
} catch {
|
||||
console.error("Failed to write raw output to step summary");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
let githubToken: string | undefined;
|
||||
let commentId: number | undefined;
|
||||
let claudeBranch: string | undefined;
|
||||
let baseBranch: string | undefined;
|
||||
let executionFile: string | undefined;
|
||||
let claudeSuccess = false;
|
||||
let prepareSuccess = true;
|
||||
let prepareError: string | undefined;
|
||||
let context: GitHubContext | undefined;
|
||||
let octokit: Octokits | undefined;
|
||||
// Track whether we've completed prepare phase, so we can attribute errors correctly
|
||||
let prepareCompleted = false;
|
||||
try {
|
||||
// Phase 1: Prepare
|
||||
const actionInputsPresent = collectActionInputsPresence();
|
||||
context = parseGitHubContext();
|
||||
const modeName = detectMode(context);
|
||||
console.log(
|
||||
`Auto-detected mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
|
||||
try {
|
||||
githubToken = await setupGitHubToken();
|
||||
} catch (error) {
|
||||
if (error instanceof WorkflowValidationSkipError) {
|
||||
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
|
||||
console.log("Exiting due to workflow validation skip");
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
octokit = createOctokit(githubToken);
|
||||
|
||||
// Set GITHUB_TOKEN and GH_TOKEN in process env for downstream usage
|
||||
process.env.GITHUB_TOKEN = githubToken;
|
||||
process.env.GH_TOKEN = githubToken;
|
||||
|
||||
// Check write permissions (only for entity contexts)
|
||||
if (isEntityContext(context)) {
|
||||
const hasWritePermissions = await checkWritePermissions(
|
||||
octokit.rest,
|
||||
context,
|
||||
context.inputs.allowedNonWriteUsers,
|
||||
!!process.env.OVERRIDE_GITHUB_TOKEN,
|
||||
);
|
||||
if (!hasWritePermissions) {
|
||||
throw new Error(
|
||||
"Actor does not have write permissions to the repository",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check trigger conditions
|
||||
const containsTrigger =
|
||||
modeName === "tag"
|
||||
? isEntityContext(context) && checkContainsTrigger(context)
|
||||
: !!context.inputs?.prompt;
|
||||
console.log(`Mode: ${modeName}`);
|
||||
console.log(`Context prompt: ${context.inputs?.prompt || "NO PROMPT"}`);
|
||||
console.log(`Trigger result: ${containsTrigger}`);
|
||||
|
||||
if (!containsTrigger) {
|
||||
console.log("No trigger found, skipping remaining steps");
|
||||
core.setOutput("github_token", githubToken);
|
||||
return;
|
||||
}
|
||||
|
||||
// Run prepare
|
||||
console.log(
|
||||
`Preparing with mode: ${modeName} for event: ${context.eventName}`,
|
||||
);
|
||||
const prepareResult =
|
||||
modeName === "tag"
|
||||
? await prepareTagMode({ context, octokit, githubToken })
|
||||
: await prepareAgentMode({ context, octokit, githubToken });
|
||||
|
||||
commentId = prepareResult.commentId;
|
||||
claudeBranch = prepareResult.branchInfo.claudeBranch;
|
||||
baseBranch = prepareResult.branchInfo.baseBranch;
|
||||
prepareCompleted = true;
|
||||
|
||||
// Phase 2: Install Claude Code CLI
|
||||
const claudeExecutable = await installClaudeCode();
|
||||
|
||||
// Phase 3: Run Claude (import base-action directly)
|
||||
// Set env vars needed by the base-action code
|
||||
process.env.INPUT_ACTION_INPUTS_PRESENT = actionInputsPresent;
|
||||
process.env.CLAUDE_CODE_ACTION = "1";
|
||||
process.env.DETAILED_PERMISSION_MESSAGES = "1";
|
||||
|
||||
validateEnvironmentVariables();
|
||||
|
||||
// On PRs, .claude/ and .mcp.json in the checkout are attacker-controlled.
|
||||
// Restore them from the base branch before the CLI reads them.
|
||||
//
|
||||
// We read pull_request.base.ref from the payload directly because agent
|
||||
// mode's branchInfo.baseBranch defaults to the repo's default branch rather
|
||||
// than the PR's actual target (agent/index.ts). For issue_comment on a PR the payload
|
||||
// lacks base.ref, so we fall back to the mode-provided value — tag mode
|
||||
// fetches it from GraphQL; agent mode on issue_comment is an edge case
|
||||
// that at worst restores from the wrong trusted branch (still secure).
|
||||
if (isEntityContext(context) && context.isPR) {
|
||||
let restoreBase = baseBranch;
|
||||
if (
|
||||
isPullRequestEvent(context) ||
|
||||
isPullRequestReviewEvent(context) ||
|
||||
isPullRequestReviewCommentEvent(context)
|
||||
) {
|
||||
restoreBase = context.payload.pull_request.base.ref;
|
||||
validateBranchName(restoreBase);
|
||||
}
|
||||
if (restoreBase) {
|
||||
restoreConfigFromBase(restoreBase);
|
||||
}
|
||||
}
|
||||
|
||||
await setupClaudeCodeSettings(process.env.INPUT_SETTINGS);
|
||||
|
||||
await installPlugins(
|
||||
process.env.INPUT_PLUGIN_MARKETPLACES,
|
||||
process.env.INPUT_PLUGINS,
|
||||
claudeExecutable,
|
||||
);
|
||||
|
||||
const promptFile =
|
||||
process.env.INPUT_PROMPT_FILE ||
|
||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`;
|
||||
const promptConfig = await preparePrompt({
|
||||
prompt: "",
|
||||
promptFile,
|
||||
});
|
||||
|
||||
const claudeResult: ClaudeRunResult = await runClaude(promptConfig.path, {
|
||||
claudeArgs: prepareResult.claudeArgs,
|
||||
appendSystemPrompt: process.env.APPEND_SYSTEM_PROMPT,
|
||||
model: process.env.ANTHROPIC_MODEL,
|
||||
pathToClaudeCodeExecutable: claudeExecutable,
|
||||
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
|
||||
});
|
||||
|
||||
claudeSuccess = claudeResult.conclusion === "success";
|
||||
executionFile = claudeResult.executionFile;
|
||||
|
||||
// Set action-level outputs
|
||||
if (claudeResult.executionFile) {
|
||||
core.setOutput("execution_file", claudeResult.executionFile);
|
||||
}
|
||||
if (claudeResult.sessionId) {
|
||||
core.setOutput("session_id", claudeResult.sessionId);
|
||||
}
|
||||
if (claudeResult.structuredOutput) {
|
||||
core.setOutput("structured_output", claudeResult.structuredOutput);
|
||||
}
|
||||
core.setOutput("conclusion", claudeResult.conclusion);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
// Only mark as prepare failure if we haven't completed the prepare phase
|
||||
if (!prepareCompleted) {
|
||||
prepareSuccess = false;
|
||||
prepareError = errorMessage;
|
||||
}
|
||||
core.setFailed(`Action failed with error: ${errorMessage}`);
|
||||
} finally {
|
||||
// Phase 4: Cleanup (always runs)
|
||||
|
||||
// Update tracking comment
|
||||
if (
|
||||
commentId &&
|
||||
context &&
|
||||
isEntityContext(context) &&
|
||||
githubToken &&
|
||||
octokit
|
||||
) {
|
||||
try {
|
||||
await updateCommentLink({
|
||||
commentId,
|
||||
githubToken,
|
||||
claudeBranch,
|
||||
baseBranch: baseBranch || context.repository.default_branch || "main",
|
||||
triggerUsername: context.actor,
|
||||
context,
|
||||
octokit,
|
||||
claudeSuccess,
|
||||
outputFile: executionFile,
|
||||
prepareSuccess,
|
||||
prepareError,
|
||||
useCommitSigning: context.inputs.useCommitSigning,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error updating comment with job link:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Write step summary (unless display_report is set to false)
|
||||
if (
|
||||
executionFile &&
|
||||
existsSync(executionFile) &&
|
||||
process.env.DISPLAY_REPORT !== "false"
|
||||
) {
|
||||
await writeStepSummary(executionFile);
|
||||
}
|
||||
|
||||
// Set remaining action-level outputs
|
||||
core.setOutput("branch_name", claudeBranch);
|
||||
core.setOutput("github_token", githubToken);
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
run();
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import { createOctokit } from "../github/api/client";
|
||||
import type { Octokits } from "../github/api/client";
|
||||
import * as fs from "fs/promises";
|
||||
import {
|
||||
updateCommentBody,
|
||||
@ -11,229 +12,258 @@ import {
|
||||
isPullRequestReviewCommentEvent,
|
||||
isEntityContext,
|
||||
} from "../github/context";
|
||||
import type { ParsedGitHubContext } from "../github/context";
|
||||
import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup";
|
||||
import { updateClaudeComment } from "../github/operations/comments/update-claude-comment";
|
||||
|
||||
export type UpdateCommentLinkParams = {
|
||||
commentId: number;
|
||||
githubToken: string;
|
||||
claudeBranch?: string;
|
||||
baseBranch: string;
|
||||
triggerUsername?: string;
|
||||
context: ParsedGitHubContext;
|
||||
octokit: Octokits;
|
||||
claudeSuccess: boolean;
|
||||
outputFile?: string;
|
||||
prepareSuccess: boolean;
|
||||
prepareError?: string;
|
||||
useCommitSigning: boolean;
|
||||
};
|
||||
|
||||
export async function updateCommentLink(
|
||||
params: UpdateCommentLinkParams,
|
||||
): Promise<void> {
|
||||
const {
|
||||
commentId,
|
||||
claudeBranch,
|
||||
baseBranch,
|
||||
triggerUsername,
|
||||
context,
|
||||
octokit,
|
||||
useCommitSigning,
|
||||
} = params;
|
||||
|
||||
const { owner, repo } = context.repository;
|
||||
|
||||
const serverUrl = GITHUB_SERVER_URL;
|
||||
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
|
||||
let comment;
|
||||
let isPRReviewComment = false;
|
||||
|
||||
try {
|
||||
// GitHub has separate ID namespaces for review comments and issue comments
|
||||
// We need to use the correct API based on the event type
|
||||
if (isPullRequestReviewCommentEvent(context)) {
|
||||
// For PR review comments, use the pulls API
|
||||
console.log(`Fetching PR review comment ${commentId}`);
|
||||
const { data: prComment } = await octokit.rest.pulls.getReviewComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
});
|
||||
comment = prComment;
|
||||
isPRReviewComment = true;
|
||||
console.log("Successfully fetched as PR review comment");
|
||||
}
|
||||
|
||||
// For all other event types, use the issues API
|
||||
if (!comment) {
|
||||
console.log(`Fetching issue comment ${commentId}`);
|
||||
const { data: issueComment } = await octokit.rest.issues.getComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
});
|
||||
comment = issueComment;
|
||||
isPRReviewComment = false;
|
||||
console.log("Successfully fetched as issue comment");
|
||||
}
|
||||
} catch (finalError) {
|
||||
// If all attempts fail, try to determine more information about the comment
|
||||
console.error("Failed to fetch comment. Debug info:");
|
||||
console.error(`Comment ID: ${commentId}`);
|
||||
console.error(`Event name: ${context.eventName}`);
|
||||
console.error(`Entity number: ${context.entityNumber}`);
|
||||
console.error(`Repository: ${context.repository.full_name}`);
|
||||
|
||||
// Try to get the PR info to understand the comment structure
|
||||
try {
|
||||
const { data: pr } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: context.entityNumber,
|
||||
});
|
||||
console.log(`PR state: ${pr.state}`);
|
||||
console.log(`PR comments count: ${pr.comments}`);
|
||||
console.log(`PR review comments count: ${pr.review_comments}`);
|
||||
} catch {
|
||||
console.error("Could not fetch PR info for debugging");
|
||||
}
|
||||
|
||||
throw finalError;
|
||||
}
|
||||
|
||||
const currentBody = comment.body ?? "";
|
||||
|
||||
// Check if we need to add branch link for new branches
|
||||
const { shouldDeleteBranch, branchLink } = await checkAndCommitOrDeleteBranch(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
claudeBranch,
|
||||
baseBranch,
|
||||
useCommitSigning,
|
||||
);
|
||||
|
||||
// Check if we need to add PR URL when we have a new branch
|
||||
let prLink = "";
|
||||
// If claudeBranch is set, it means we created a new branch (for issues or closed/merged PRs)
|
||||
if (claudeBranch && !shouldDeleteBranch) {
|
||||
// Check if comment already contains a PR URL
|
||||
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const prUrlPattern = new RegExp(
|
||||
`${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
|
||||
);
|
||||
const containsPRUrl = currentBody.match(prUrlPattern);
|
||||
|
||||
if (!containsPRUrl) {
|
||||
// Check if there are changes to the branch compared to the default branch
|
||||
try {
|
||||
const { data: comparison } =
|
||||
await octokit.rest.repos.compareCommitsWithBasehead({
|
||||
owner,
|
||||
repo,
|
||||
basehead: `${baseBranch}...${claudeBranch}`,
|
||||
});
|
||||
|
||||
// If there are changes (commits or file changes), add the PR URL
|
||||
if (
|
||||
comparison.total_commits > 0 ||
|
||||
(comparison.files && comparison.files.length > 0)
|
||||
) {
|
||||
const entityType = context.isPR ? "PR" : "Issue";
|
||||
const prTitle = encodeURIComponent(
|
||||
`${entityType} #${context.entityNumber}: Changes from Claude`,
|
||||
);
|
||||
const prBody = encodeURIComponent(
|
||||
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
|
||||
);
|
||||
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
|
||||
prLink = `\n[Create a PR](${prUrl})`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking for changes in branch:", error);
|
||||
// Don't fail the entire update if we can't check for changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if action failed and read output file for execution details
|
||||
let executionDetails: {
|
||||
total_cost_usd?: number;
|
||||
duration_ms?: number;
|
||||
duration_api_ms?: number;
|
||||
} | null = null;
|
||||
let actionFailed = false;
|
||||
let errorDetails: string | undefined;
|
||||
|
||||
if (!params.prepareSuccess && params.prepareError) {
|
||||
actionFailed = true;
|
||||
errorDetails = params.prepareError;
|
||||
} else {
|
||||
// Check for existence of output file and parse it if available
|
||||
try {
|
||||
if (params.outputFile) {
|
||||
const fileContent = await fs.readFile(params.outputFile, "utf8");
|
||||
const outputData = JSON.parse(fileContent);
|
||||
|
||||
// Output file is an array, get the last element which contains execution details
|
||||
if (Array.isArray(outputData) && outputData.length > 0) {
|
||||
const lastElement = outputData[outputData.length - 1];
|
||||
if (
|
||||
lastElement.type === "result" &&
|
||||
"total_cost_usd" in lastElement &&
|
||||
"duration_ms" in lastElement
|
||||
) {
|
||||
executionDetails = {
|
||||
total_cost_usd: lastElement.total_cost_usd,
|
||||
duration_ms: lastElement.duration_ms,
|
||||
duration_api_ms: lastElement.duration_api_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
actionFailed = !params.claudeSuccess;
|
||||
} catch (error) {
|
||||
console.error("Error reading output file:", error);
|
||||
actionFailed = !params.claudeSuccess;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare input for updateCommentBody function
|
||||
const commentInput: CommentUpdateInput = {
|
||||
currentBody,
|
||||
actionFailed,
|
||||
executionDetails,
|
||||
jobUrl,
|
||||
branchLink,
|
||||
prLink,
|
||||
branchName: shouldDeleteBranch || !branchLink ? undefined : claudeBranch,
|
||||
triggerUsername,
|
||||
errorDetails,
|
||||
};
|
||||
|
||||
const updatedBody = updateCommentBody(commentInput);
|
||||
|
||||
try {
|
||||
await updateClaudeComment(octokit.rest, {
|
||||
owner,
|
||||
repo,
|
||||
commentId,
|
||||
body: updatedBody,
|
||||
isPullRequestReviewComment: isPRReviewComment,
|
||||
});
|
||||
console.log(
|
||||
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
console.error(
|
||||
`Failed to update ${isPRReviewComment ? "PR review" : "issue"} comment:`,
|
||||
updateError,
|
||||
);
|
||||
throw updateError;
|
||||
}
|
||||
}
|
||||
|
||||
async function run() {
|
||||
try {
|
||||
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
|
||||
const githubToken = process.env.GITHUB_TOKEN!;
|
||||
const claudeBranch = process.env.CLAUDE_BRANCH;
|
||||
const baseBranch = process.env.BASE_BRANCH || "main";
|
||||
const triggerUsername = process.env.TRIGGER_USERNAME;
|
||||
|
||||
const context = parseGitHubContext();
|
||||
|
||||
// This script is only called for entity-based events
|
||||
if (!isEntityContext(context)) {
|
||||
throw new Error("update-comment-link requires an entity context");
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repository;
|
||||
|
||||
const githubToken = process.env.GITHUB_TOKEN!;
|
||||
const octokit = createOctokit(githubToken);
|
||||
|
||||
const serverUrl = GITHUB_SERVER_URL;
|
||||
const jobUrl = `${serverUrl}/${owner}/${repo}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
||||
|
||||
let comment;
|
||||
let isPRReviewComment = false;
|
||||
|
||||
try {
|
||||
// GitHub has separate ID namespaces for review comments and issue comments
|
||||
// We need to use the correct API based on the event type
|
||||
if (isPullRequestReviewCommentEvent(context)) {
|
||||
// For PR review comments, use the pulls API
|
||||
console.log(`Fetching PR review comment ${commentId}`);
|
||||
const { data: prComment } = await octokit.rest.pulls.getReviewComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
});
|
||||
comment = prComment;
|
||||
isPRReviewComment = true;
|
||||
console.log("Successfully fetched as PR review comment");
|
||||
}
|
||||
|
||||
// For all other event types, use the issues API
|
||||
if (!comment) {
|
||||
console.log(`Fetching issue comment ${commentId}`);
|
||||
const { data: issueComment } = await octokit.rest.issues.getComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: commentId,
|
||||
});
|
||||
comment = issueComment;
|
||||
isPRReviewComment = false;
|
||||
console.log("Successfully fetched as issue comment");
|
||||
}
|
||||
} catch (finalError) {
|
||||
// If all attempts fail, try to determine more information about the comment
|
||||
console.error("Failed to fetch comment. Debug info:");
|
||||
console.error(`Comment ID: ${commentId}`);
|
||||
console.error(`Event name: ${context.eventName}`);
|
||||
console.error(`Entity number: ${context.entityNumber}`);
|
||||
console.error(`Repository: ${context.repository.full_name}`);
|
||||
|
||||
// Try to get the PR info to understand the comment structure
|
||||
try {
|
||||
const { data: pr } = await octokit.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: context.entityNumber,
|
||||
});
|
||||
console.log(`PR state: ${pr.state}`);
|
||||
console.log(`PR comments count: ${pr.comments}`);
|
||||
console.log(`PR review comments count: ${pr.review_comments}`);
|
||||
} catch {
|
||||
console.error("Could not fetch PR info for debugging");
|
||||
}
|
||||
|
||||
throw finalError;
|
||||
}
|
||||
|
||||
const currentBody = comment.body ?? "";
|
||||
|
||||
// Check if we need to add branch link for new branches
|
||||
const useCommitSigning = process.env.USE_COMMIT_SIGNING === "true";
|
||||
const { shouldDeleteBranch, branchLink } =
|
||||
await checkAndCommitOrDeleteBranch(
|
||||
octokit,
|
||||
owner,
|
||||
repo,
|
||||
claudeBranch,
|
||||
baseBranch,
|
||||
useCommitSigning,
|
||||
);
|
||||
|
||||
// Check if we need to add PR URL when we have a new branch
|
||||
let prLink = "";
|
||||
// If claudeBranch is set, it means we created a new branch (for issues or closed/merged PRs)
|
||||
if (claudeBranch && !shouldDeleteBranch) {
|
||||
// Check if comment already contains a PR URL
|
||||
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const prUrlPattern = new RegExp(
|
||||
`${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
|
||||
);
|
||||
const containsPRUrl = currentBody.match(prUrlPattern);
|
||||
|
||||
if (!containsPRUrl) {
|
||||
// Check if there are changes to the branch compared to the default branch
|
||||
try {
|
||||
const { data: comparison } =
|
||||
await octokit.rest.repos.compareCommitsWithBasehead({
|
||||
owner,
|
||||
repo,
|
||||
basehead: `${baseBranch}...${claudeBranch}`,
|
||||
});
|
||||
|
||||
// If there are changes (commits or file changes), add the PR URL
|
||||
if (
|
||||
comparison.total_commits > 0 ||
|
||||
(comparison.files && comparison.files.length > 0)
|
||||
) {
|
||||
const entityType = context.isPR ? "PR" : "Issue";
|
||||
const prTitle = encodeURIComponent(
|
||||
`${entityType} #${context.entityNumber}: Changes from Claude`,
|
||||
);
|
||||
const prBody = encodeURIComponent(
|
||||
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
|
||||
);
|
||||
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
|
||||
prLink = `\n[Create a PR](${prUrl})`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking for changes in branch:", error);
|
||||
// Don't fail the entire update if we can't check for changes
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if action failed and read output file for execution details
|
||||
let executionDetails: {
|
||||
cost_usd?: number;
|
||||
duration_ms?: number;
|
||||
duration_api_ms?: number;
|
||||
} | null = null;
|
||||
let actionFailed = false;
|
||||
let errorDetails: string | undefined;
|
||||
|
||||
// First check if prepare step failed
|
||||
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||
const prepareError = process.env.PREPARE_ERROR;
|
||||
|
||||
if (!prepareSuccess && prepareError) {
|
||||
actionFailed = true;
|
||||
errorDetails = prepareError;
|
||||
} else {
|
||||
// Check for existence of output file and parse it if available
|
||||
try {
|
||||
const outputFile = process.env.OUTPUT_FILE;
|
||||
if (outputFile) {
|
||||
const fileContent = await fs.readFile(outputFile, "utf8");
|
||||
const outputData = JSON.parse(fileContent);
|
||||
|
||||
// Output file is an array, get the last element which contains execution details
|
||||
if (Array.isArray(outputData) && outputData.length > 0) {
|
||||
const lastElement = outputData[outputData.length - 1];
|
||||
if (
|
||||
lastElement.type === "result" &&
|
||||
"cost_usd" in lastElement &&
|
||||
"duration_ms" in lastElement
|
||||
) {
|
||||
executionDetails = {
|
||||
cost_usd: lastElement.cost_usd,
|
||||
duration_ms: lastElement.duration_ms,
|
||||
duration_api_ms: lastElement.duration_api_ms,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the Claude action failed
|
||||
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
||||
actionFailed = !claudeSuccess;
|
||||
} catch (error) {
|
||||
console.error("Error reading output file:", error);
|
||||
// If we can't read the file, check for any failure markers
|
||||
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare input for updateCommentBody function
|
||||
const commentInput: CommentUpdateInput = {
|
||||
currentBody,
|
||||
actionFailed,
|
||||
executionDetails,
|
||||
jobUrl,
|
||||
branchLink,
|
||||
prLink,
|
||||
branchName: shouldDeleteBranch || !branchLink ? undefined : claudeBranch,
|
||||
triggerUsername,
|
||||
errorDetails,
|
||||
};
|
||||
|
||||
const updatedBody = updateCommentBody(commentInput);
|
||||
|
||||
try {
|
||||
await updateClaudeComment(octokit.rest, {
|
||||
owner,
|
||||
repo,
|
||||
commentId,
|
||||
body: updatedBody,
|
||||
isPullRequestReviewComment: isPRReviewComment,
|
||||
});
|
||||
console.log(
|
||||
`✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`,
|
||||
);
|
||||
} catch (updateError) {
|
||||
console.error(
|
||||
`Failed to update ${isPRReviewComment ? "PR review" : "issue"} comment:`,
|
||||
updateError,
|
||||
);
|
||||
throw updateError;
|
||||
}
|
||||
await updateCommentLink({
|
||||
commentId: parseInt(process.env.CLAUDE_COMMENT_ID!),
|
||||
githubToken,
|
||||
claudeBranch: process.env.CLAUDE_BRANCH,
|
||||
baseBranch:
|
||||
process.env.BASE_BRANCH || context.repository.default_branch || "main",
|
||||
triggerUsername: process.env.TRIGGER_USERNAME,
|
||||
context,
|
||||
octokit,
|
||||
claudeSuccess: process.env.CLAUDE_SUCCESS !== "false",
|
||||
outputFile: process.env.OUTPUT_FILE,
|
||||
prepareSuccess: process.env.PREPARE_SUCCESS !== "false",
|
||||
prepareError: process.env.PREPARE_ERROR,
|
||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||
});
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
@ -242,4 +272,6 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
if (import.meta.main) {
|
||||
run();
|
||||
}
|
||||
|
||||
@ -12,10 +12,24 @@ export const PR_QUERY = `
|
||||
baseRefName
|
||||
headRefName
|
||||
headRefOid
|
||||
isCrossRepository
|
||||
headRepository {
|
||||
owner {
|
||||
login
|
||||
}
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
lastEditedAt
|
||||
additions
|
||||
deletions
|
||||
state
|
||||
labels(first: 1) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
commits(first: 100) {
|
||||
totalCount
|
||||
nodes {
|
||||
@ -46,6 +60,8 @@ export const PR_QUERY = `
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
lastEditedAt
|
||||
isMinimized
|
||||
}
|
||||
}
|
||||
@ -59,6 +75,8 @@ export const PR_QUERY = `
|
||||
body
|
||||
state
|
||||
submittedAt
|
||||
updatedAt
|
||||
lastEditedAt
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
@ -70,6 +88,8 @@ export const PR_QUERY = `
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
lastEditedAt
|
||||
isMinimized
|
||||
}
|
||||
}
|
||||
@ -90,7 +110,14 @@ export const ISSUE_QUERY = `
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
lastEditedAt
|
||||
state
|
||||
labels(first: 1) {
|
||||
nodes {
|
||||
name
|
||||
}
|
||||
}
|
||||
comments(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
@ -100,6 +127,8 @@ export const ISSUE_QUERY = `
|
||||
login
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
lastEditedAt
|
||||
isMinimized
|
||||
}
|
||||
}
|
||||
|
||||
13
src/github/constants.ts
Normal file
13
src/github/constants.ts
Normal file
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* GitHub-related constants used throughout the application
|
||||
*/
|
||||
|
||||
/**
|
||||
* Claude App bot user ID
|
||||
*/
|
||||
export const CLAUDE_APP_BOT_ID = 41898282;
|
||||
|
||||
/**
|
||||
* Claude bot username
|
||||
*/
|
||||
export const CLAUDE_BOT_LOGIN = "claude[bot]";
|
||||
@ -8,6 +8,7 @@ import type {
|
||||
PullRequestReviewCommentEvent,
|
||||
WorkflowRunEvent,
|
||||
} from "@octokit/webhooks-types";
|
||||
import { CLAUDE_APP_BOT_ID, CLAUDE_BOT_LOGIN } from "./constants";
|
||||
// Custom types for GitHub Actions events that aren't webhooks
|
||||
export type WorkflowDispatchEvent = {
|
||||
action?: never;
|
||||
@ -25,6 +26,20 @@ export type WorkflowDispatchEvent = {
|
||||
workflow: string;
|
||||
};
|
||||
|
||||
export type RepositoryDispatchEvent = {
|
||||
action: string;
|
||||
client_payload?: Record<string, any>;
|
||||
repository: {
|
||||
name: string;
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
sender: {
|
||||
login: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ScheduleEvent = {
|
||||
action?: never;
|
||||
schedule?: string;
|
||||
@ -47,6 +62,7 @@ const ENTITY_EVENT_NAMES = [
|
||||
|
||||
const AUTOMATION_EVENT_NAMES = [
|
||||
"workflow_dispatch",
|
||||
"repository_dispatch",
|
||||
"schedule",
|
||||
"workflow_run",
|
||||
] as const;
|
||||
@ -63,6 +79,7 @@ type BaseContext = {
|
||||
owner: string;
|
||||
repo: string;
|
||||
full_name: string;
|
||||
default_branch?: string;
|
||||
};
|
||||
actor: string;
|
||||
inputs: {
|
||||
@ -72,10 +89,19 @@ type BaseContext = {
|
||||
labelTrigger: string;
|
||||
baseBranch?: string;
|
||||
branchPrefix: string;
|
||||
branchNameTemplate?: string;
|
||||
useStickyComment: boolean;
|
||||
classifyInlineComments: boolean;
|
||||
useCommitSigning: boolean;
|
||||
sshSigningKey: string;
|
||||
botId: string;
|
||||
botName: string;
|
||||
allowedBots: string;
|
||||
allowedNonWriteUsers: string;
|
||||
trackProgress: boolean;
|
||||
includeFixLinks: boolean;
|
||||
includeCommentsByActor: string;
|
||||
excludeCommentsByActor: string;
|
||||
};
|
||||
};
|
||||
|
||||
@ -92,10 +118,14 @@ export type ParsedGitHubContext = BaseContext & {
|
||||
isPR: boolean;
|
||||
};
|
||||
|
||||
// Context for automation events (workflow_dispatch, schedule, workflow_run)
|
||||
// Context for automation events (workflow_dispatch, repository_dispatch, schedule, workflow_run)
|
||||
export type AutomationContext = BaseContext & {
|
||||
eventName: AutomationEventName;
|
||||
payload: WorkflowDispatchEvent | ScheduleEvent | WorkflowRunEvent;
|
||||
payload:
|
||||
| WorkflowDispatchEvent
|
||||
| RepositoryDispatchEvent
|
||||
| ScheduleEvent
|
||||
| WorkflowRunEvent;
|
||||
};
|
||||
|
||||
// Union type for all contexts
|
||||
@ -111,6 +141,7 @@ export function parseGitHubContext(): GitHubContext {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
full_name: `${context.repo.owner}/${context.repo.repo}`,
|
||||
default_branch: context.payload.repository?.default_branch,
|
||||
},
|
||||
actor: context.actor,
|
||||
inputs: {
|
||||
@ -120,10 +151,19 @@ export function parseGitHubContext(): GitHubContext {
|
||||
labelTrigger: process.env.LABEL_TRIGGER ?? "",
|
||||
baseBranch: process.env.BASE_BRANCH,
|
||||
branchPrefix: process.env.BRANCH_PREFIX ?? "claude/",
|
||||
branchNameTemplate: process.env.BRANCH_NAME_TEMPLATE,
|
||||
useStickyComment: process.env.USE_STICKY_COMMENT === "true",
|
||||
classifyInlineComments: process.env.CLASSIFY_INLINE_COMMENTS !== "false",
|
||||
useCommitSigning: process.env.USE_COMMIT_SIGNING === "true",
|
||||
sshSigningKey: process.env.SSH_SIGNING_KEY || "",
|
||||
botId: process.env.BOT_ID ?? String(CLAUDE_APP_BOT_ID),
|
||||
botName: process.env.BOT_NAME ?? CLAUDE_BOT_LOGIN,
|
||||
allowedBots: process.env.ALLOWED_BOTS ?? "",
|
||||
allowedNonWriteUsers: process.env.ALLOWED_NON_WRITE_USERS ?? "",
|
||||
trackProgress: process.env.TRACK_PROGRESS === "true",
|
||||
includeFixLinks: process.env.INCLUDE_FIX_LINKS === "true",
|
||||
includeCommentsByActor: process.env.INCLUDE_COMMENTS_BY_ACTOR ?? "",
|
||||
excludeCommentsByActor: process.env.EXCLUDE_COMMENTS_BY_ACTOR ?? "",
|
||||
},
|
||||
};
|
||||
|
||||
@ -148,7 +188,8 @@ export function parseGitHubContext(): GitHubContext {
|
||||
isPR: Boolean(payload.issue.pull_request),
|
||||
};
|
||||
}
|
||||
case "pull_request": {
|
||||
case "pull_request":
|
||||
case "pull_request_target": {
|
||||
const payload = context.payload as PullRequestEvent;
|
||||
return {
|
||||
...commonFields,
|
||||
@ -185,6 +226,13 @@ export function parseGitHubContext(): GitHubContext {
|
||||
payload: context.payload as unknown as WorkflowDispatchEvent,
|
||||
};
|
||||
}
|
||||
case "repository_dispatch": {
|
||||
return {
|
||||
...commonFields,
|
||||
eventName: "repository_dispatch",
|
||||
payload: context.payload as unknown as RepositoryDispatchEvent,
|
||||
};
|
||||
}
|
||||
case "schedule": {
|
||||
return {
|
||||
...commonFields,
|
||||
|
||||
@ -1,6 +1,14 @@
|
||||
import { execFileSync } from "child_process";
|
||||
import type { Octokits } from "../api/client";
|
||||
import { ISSUE_QUERY, PR_QUERY, USER_QUERY } from "../api/queries/github";
|
||||
import {
|
||||
isIssueCommentEvent,
|
||||
isIssuesEvent,
|
||||
isPullRequestEvent,
|
||||
isPullRequestReviewEvent,
|
||||
isPullRequestReviewCommentEvent,
|
||||
type ParsedGitHubContext,
|
||||
} from "../context";
|
||||
import type {
|
||||
GitHubComment,
|
||||
GitHubFile,
|
||||
@ -12,6 +20,211 @@ import type {
|
||||
} from "../types";
|
||||
import type { CommentWithImages } from "../utils/image-downloader";
|
||||
import { downloadCommentImages } from "../utils/image-downloader";
|
||||
import {
|
||||
parseActorFilter,
|
||||
shouldIncludeCommentByActor,
|
||||
} from "../utils/actor-filter";
|
||||
|
||||
/**
|
||||
* Extracts the trigger timestamp from the GitHub webhook payload.
|
||||
* This timestamp represents when the triggering comment/review/event was created.
|
||||
*
|
||||
* @param context - Parsed GitHub context from webhook
|
||||
* @returns ISO timestamp string or undefined if not available
|
||||
*/
|
||||
export function extractTriggerTimestamp(
|
||||
context: ParsedGitHubContext,
|
||||
): string | undefined {
|
||||
if (isIssueCommentEvent(context)) {
|
||||
return context.payload.comment.created_at || undefined;
|
||||
} else if (isPullRequestReviewEvent(context)) {
|
||||
return context.payload.review.submitted_at || undefined;
|
||||
} else if (isPullRequestReviewCommentEvent(context)) {
|
||||
return context.payload.comment.created_at || undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the original title from the GitHub webhook payload.
|
||||
* This is the title as it existed when the trigger event occurred.
|
||||
*
|
||||
* @param context - Parsed GitHub context from webhook
|
||||
* @returns The original title string or undefined if not available
|
||||
*/
|
||||
export function extractOriginalTitle(
|
||||
context: ParsedGitHubContext,
|
||||
): string | undefined {
|
||||
if (isIssueCommentEvent(context)) {
|
||||
return context.payload.issue?.title;
|
||||
} else if (isPullRequestEvent(context)) {
|
||||
return context.payload.pull_request?.title;
|
||||
} else if (isPullRequestReviewEvent(context)) {
|
||||
return context.payload.pull_request?.title;
|
||||
} else if (isPullRequestReviewCommentEvent(context)) {
|
||||
return context.payload.pull_request?.title;
|
||||
} else if (isIssuesEvent(context)) {
|
||||
return context.payload.issue?.title;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the original body from the GitHub webhook payload.
|
||||
* This is the body as it existed when the trigger event occurred,
|
||||
* preventing TOCTOU attacks where an attacker edits the body after
|
||||
* the trigger but before the action reads it.
|
||||
*
|
||||
* @param context - Parsed GitHub context from webhook
|
||||
* @returns The original body string, null (no body), or undefined (not available)
|
||||
*/
|
||||
export function extractOriginalBody(
|
||||
context: ParsedGitHubContext,
|
||||
): string | null | undefined {
|
||||
if (isIssueCommentEvent(context)) {
|
||||
return context.payload.issue?.body;
|
||||
} else if (isPullRequestEvent(context)) {
|
||||
return context.payload.pull_request?.body;
|
||||
} else if (isPullRequestReviewEvent(context)) {
|
||||
return context.payload.pull_request?.body;
|
||||
} else if (isPullRequestReviewCommentEvent(context)) {
|
||||
return context.payload.pull_request?.body;
|
||||
} else if (isIssuesEvent(context)) {
|
||||
return context.payload.issue?.body;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters comments to only include those that existed in their final state before the trigger time.
|
||||
* This prevents malicious actors from editing comments after the trigger to inject harmful content.
|
||||
*
|
||||
* @param comments - Array of GitHub comments to filter
|
||||
* @param triggerTime - ISO timestamp of when the trigger comment was created
|
||||
* @returns Filtered array of comments that were created and last edited before trigger time
|
||||
*/
|
||||
export function filterCommentsToTriggerTime<
|
||||
T extends { createdAt: string; updatedAt?: string; lastEditedAt?: string },
|
||||
>(comments: T[], triggerTime: string | undefined): T[] {
|
||||
if (!triggerTime) return comments;
|
||||
|
||||
const triggerTimestamp = new Date(triggerTime).getTime();
|
||||
|
||||
return comments.filter((comment) => {
|
||||
// Comment must have been created before trigger (not at or after)
|
||||
const createdTimestamp = new Date(comment.createdAt).getTime();
|
||||
if (createdTimestamp >= triggerTimestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If comment has been edited, the most recent edit must have occurred before trigger
|
||||
// Use lastEditedAt if available, otherwise fall back to updatedAt
|
||||
const lastEditTime = comment.lastEditedAt || comment.updatedAt;
|
||||
if (lastEditTime) {
|
||||
const lastEditTimestamp = new Date(lastEditTime).getTime();
|
||||
if (lastEditTimestamp >= triggerTimestamp) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters reviews to only include those that existed in their final state before the trigger time.
|
||||
* Similar to filterCommentsToTriggerTime but for GitHubReview objects which use submittedAt instead of createdAt.
|
||||
*/
|
||||
export function filterReviewsToTriggerTime<
|
||||
T extends { submittedAt: string; updatedAt?: string; lastEditedAt?: string },
|
||||
>(reviews: T[], triggerTime: string | undefined): T[] {
|
||||
if (!triggerTime) return reviews;
|
||||
|
||||
const triggerTimestamp = new Date(triggerTime).getTime();
|
||||
|
||||
return reviews.filter((review) => {
|
||||
// Review must have been submitted before trigger (not at or after)
|
||||
const submittedTimestamp = new Date(review.submittedAt).getTime();
|
||||
if (submittedTimestamp >= triggerTimestamp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If review has been edited, the most recent edit must have occurred before trigger
|
||||
const lastEditTime = review.lastEditedAt || review.updatedAt;
|
||||
if (lastEditTime) {
|
||||
const lastEditTimestamp = new Date(lastEditTime).getTime();
|
||||
if (lastEditTimestamp >= triggerTimestamp) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the issue/PR body was edited after the trigger time.
|
||||
* This prevents a race condition where an attacker could edit the issue/PR body
|
||||
* between when an authorized user triggered Claude and when Claude processes the request.
|
||||
*
|
||||
* @param contextData - The PR or issue data containing body and edit timestamps
|
||||
* @param triggerTime - ISO timestamp of when the trigger event occurred
|
||||
* @returns true if the body is safe to use, false if it was edited after trigger
|
||||
*/
|
||||
export function isBodySafeToUse(
|
||||
contextData: { createdAt: string; updatedAt?: string; lastEditedAt?: string },
|
||||
triggerTime: string | undefined,
|
||||
): boolean {
|
||||
// If no trigger time is available, we can't validate - allow the body
|
||||
// This maintains backwards compatibility for triggers that don't have timestamps
|
||||
if (!triggerTime) return true;
|
||||
|
||||
const triggerTimestamp = new Date(triggerTime).getTime();
|
||||
|
||||
// Check if the body was edited after the trigger
|
||||
// Use lastEditedAt if available (more accurate for body edits), otherwise fall back to updatedAt
|
||||
const lastEditTime = contextData.lastEditedAt || contextData.updatedAt;
|
||||
if (lastEditTime) {
|
||||
const lastEditTimestamp = new Date(lastEditTime).getTime();
|
||||
if (lastEditTimestamp >= triggerTimestamp) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters comments by actor username based on include/exclude patterns
|
||||
* @param comments - Array of comments to filter
|
||||
* @param includeActors - Comma-separated actors to include
|
||||
* @param excludeActors - Comma-separated actors to exclude
|
||||
* @returns Filtered array of comments
|
||||
*/
|
||||
export function filterCommentsByActor<T extends { author: { login: string } }>(
|
||||
comments: T[],
|
||||
includeActors: string = "",
|
||||
excludeActors: string = "",
|
||||
): T[] {
|
||||
const includeParsed = parseActorFilter(includeActors);
|
||||
const excludeParsed = parseActorFilter(excludeActors);
|
||||
|
||||
// No filters = return all
|
||||
if (includeParsed.length === 0 && excludeParsed.length === 0) {
|
||||
return comments;
|
||||
}
|
||||
|
||||
return comments.filter((comment) =>
|
||||
shouldIncludeCommentByActor(
|
||||
comment.author.login,
|
||||
includeParsed,
|
||||
excludeParsed,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
type FetchDataParams = {
|
||||
octokits: Octokits;
|
||||
@ -19,6 +232,11 @@ type FetchDataParams = {
|
||||
prNumber: string;
|
||||
isPR: boolean;
|
||||
triggerUsername?: string;
|
||||
triggerTime?: string;
|
||||
originalTitle?: string;
|
||||
originalBody?: string | null;
|
||||
includeCommentsByActor?: string;
|
||||
excludeCommentsByActor?: string;
|
||||
};
|
||||
|
||||
export type GitHubFileWithSHA = GitHubFile & {
|
||||
@ -41,6 +259,11 @@ export async function fetchGitHubData({
|
||||
prNumber,
|
||||
isPR,
|
||||
triggerUsername,
|
||||
triggerTime,
|
||||
originalTitle,
|
||||
originalBody,
|
||||
includeCommentsByActor,
|
||||
excludeCommentsByActor,
|
||||
}: FetchDataParams): Promise<FetchDataResult> {
|
||||
const [owner, repo] = repository.split("/");
|
||||
if (!owner || !repo) {
|
||||
@ -68,8 +291,15 @@ export async function fetchGitHubData({
|
||||
const pullRequest = prResult.repository.pullRequest;
|
||||
contextData = pullRequest;
|
||||
changedFiles = pullRequest.files.nodes || [];
|
||||
comments = pullRequest.comments?.nodes || [];
|
||||
reviewData = pullRequest.reviews || [];
|
||||
comments = filterCommentsByActor(
|
||||
filterCommentsToTriggerTime(
|
||||
pullRequest.comments?.nodes || [],
|
||||
triggerTime,
|
||||
),
|
||||
includeCommentsByActor,
|
||||
excludeCommentsByActor,
|
||||
);
|
||||
reviewData = pullRequest.reviews || { nodes: [] };
|
||||
|
||||
console.log(`Successfully fetched PR #${prNumber} data`);
|
||||
} else {
|
||||
@ -88,7 +318,14 @@ export async function fetchGitHubData({
|
||||
|
||||
if (issueResult.repository.issue) {
|
||||
contextData = issueResult.repository.issue;
|
||||
comments = contextData?.comments?.nodes || [];
|
||||
comments = filterCommentsByActor(
|
||||
filterCommentsToTriggerTime(
|
||||
contextData?.comments?.nodes || [],
|
||||
triggerTime,
|
||||
),
|
||||
includeCommentsByActor,
|
||||
excludeCommentsByActor,
|
||||
);
|
||||
|
||||
console.log(`Successfully fetched issue #${prNumber} data`);
|
||||
} else {
|
||||
@ -141,29 +378,72 @@ export async function fetchGitHubData({
|
||||
body: c.body,
|
||||
}));
|
||||
|
||||
const reviewBodies: CommentWithImages[] =
|
||||
reviewData?.nodes
|
||||
?.filter((r) => r.body)
|
||||
.map((r) => ({
|
||||
type: "review_body" as const,
|
||||
id: r.databaseId,
|
||||
pullNumber: prNumber,
|
||||
body: r.body,
|
||||
})) ?? [];
|
||||
// Filter review bodies to trigger time
|
||||
const filteredReviewBodies = reviewData?.nodes
|
||||
? filterReviewsToTriggerTime(reviewData.nodes, triggerTime).filter(
|
||||
(r) => r.body,
|
||||
)
|
||||
: [];
|
||||
|
||||
const reviewComments: CommentWithImages[] =
|
||||
reviewData?.nodes
|
||||
?.flatMap((r) => r.comments?.nodes ?? [])
|
||||
.filter((c) => c.body && !c.isMinimized)
|
||||
.map((c) => ({
|
||||
type: "review_comment" as const,
|
||||
id: c.databaseId,
|
||||
body: c.body,
|
||||
})) ?? [];
|
||||
const reviewBodies: CommentWithImages[] = filteredReviewBodies.map((r) => ({
|
||||
type: "review_body" as const,
|
||||
id: r.databaseId,
|
||||
pullNumber: prNumber,
|
||||
body: r.body,
|
||||
}));
|
||||
|
||||
// Add the main issue/PR body if it has content
|
||||
const mainBody: CommentWithImages[] = contextData.body
|
||||
? [
|
||||
// Filter review comments to trigger time and by actor
|
||||
if (reviewData && reviewData.nodes) {
|
||||
// Filter reviews by actor
|
||||
reviewData.nodes = filterCommentsByActor(
|
||||
reviewData.nodes,
|
||||
includeCommentsByActor,
|
||||
excludeCommentsByActor,
|
||||
);
|
||||
|
||||
// Also filter inline review comments within each review
|
||||
reviewData.nodes.forEach((review) => {
|
||||
if (review.comments?.nodes) {
|
||||
review.comments.nodes = filterCommentsByActor(
|
||||
review.comments.nodes,
|
||||
includeCommentsByActor,
|
||||
excludeCommentsByActor,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const allReviewComments =
|
||||
reviewData?.nodes?.flatMap((r) => r.comments?.nodes ?? []) ?? [];
|
||||
const filteredReviewComments = filterCommentsToTriggerTime(
|
||||
allReviewComments,
|
||||
triggerTime,
|
||||
);
|
||||
|
||||
const reviewComments: CommentWithImages[] = filteredReviewComments
|
||||
.filter((c) => c.body && !c.isMinimized)
|
||||
.map((c) => ({
|
||||
type: "review_comment" as const,
|
||||
id: c.databaseId,
|
||||
body: c.body,
|
||||
}));
|
||||
|
||||
// Use the original body from the webhook payload if provided (TOCTOU protection).
|
||||
// The webhook payload captures the body at event time, before any attacker edits.
|
||||
if (originalBody !== undefined) {
|
||||
contextData.body = originalBody ?? "";
|
||||
}
|
||||
|
||||
// Add the main issue/PR body if it has content and wasn't edited after trigger.
|
||||
// When originalBody is provided, the body is already safe (from webhook payload).
|
||||
// Otherwise, fall back to timestamp-based validation.
|
||||
let mainBody: CommentWithImages[] = [];
|
||||
if (contextData.body) {
|
||||
if (
|
||||
originalBody !== undefined ||
|
||||
isBodySafeToUse(contextData, triggerTime)
|
||||
) {
|
||||
mainBody = [
|
||||
{
|
||||
...(isPR
|
||||
? {
|
||||
@ -177,8 +457,14 @@ export async function fetchGitHubData({
|
||||
body: contextData.body,
|
||||
}),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
];
|
||||
} else {
|
||||
console.warn(
|
||||
`Security: ${isPR ? "PR" : "Issue"} #${prNumber} body was edited after the trigger event. ` +
|
||||
`Excluding body content to prevent potential injection attacks.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const allComments = [
|
||||
...mainBody,
|
||||
@ -200,6 +486,11 @@ export async function fetchGitHubData({
|
||||
triggerDisplayName = await fetchUserDisplayName(octokits, triggerUsername);
|
||||
}
|
||||
|
||||
// Use the original title from the webhook payload if provided
|
||||
if (originalTitle !== undefined) {
|
||||
contextData.title = originalTitle;
|
||||
}
|
||||
|
||||
return {
|
||||
contextData,
|
||||
comments,
|
||||
|
||||
@ -14,7 +14,8 @@ export function formatContext(
|
||||
): string {
|
||||
if (isPR) {
|
||||
const prData = contextData as GitHubPullRequest;
|
||||
return `PR Title: ${prData.title}
|
||||
const sanitizedTitle = sanitizeContent(prData.title);
|
||||
return `PR Title: ${sanitizedTitle}
|
||||
PR Author: ${prData.author.login}
|
||||
PR Branch: ${prData.headRefName} -> ${prData.baseRefName}
|
||||
PR State: ${prData.state}
|
||||
@ -24,7 +25,8 @@ Total Commits: ${prData.commits.totalCount}
|
||||
Changed Files: ${prData.files.nodes.length} files`;
|
||||
} else {
|
||||
const issueData = contextData as GitHubIssue;
|
||||
return `Issue Title: ${issueData.title}
|
||||
const sanitizedTitle = sanitizeContent(issueData.title);
|
||||
return `Issue Title: ${sanitizedTitle}
|
||||
Issue Author: ${issueData.author.login}
|
||||
Issue State: ${issueData.state}`;
|
||||
}
|
||||
|
||||
@ -7,11 +7,123 @@
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
import * as core from "@actions/core";
|
||||
import { execFileSync } from "child_process";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import type { GitHubPullRequest } from "../types";
|
||||
import type { Octokits } from "../api/client";
|
||||
import type { FetchDataResult } from "../data/fetcher";
|
||||
import { generateBranchName } from "../../utils/branch-template";
|
||||
|
||||
/**
|
||||
* Extracts the first label from GitHub data, or returns undefined if no labels exist
|
||||
*/
|
||||
function extractFirstLabel(githubData: FetchDataResult): string | undefined {
|
||||
const labels = githubData.contextData.labels?.nodes;
|
||||
return labels && labels.length > 0 ? labels[0]?.name : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a git branch name against a strict whitelist pattern.
|
||||
* This prevents command injection by ensuring only safe characters are used.
|
||||
*
|
||||
* Valid branch names:
|
||||
* - Start with alphanumeric character (not dash, to prevent option injection)
|
||||
* - Contain only alphanumeric, forward slash, hyphen, underscore, period, or hash (#)
|
||||
* - Do not start or end with a period
|
||||
* - Do not end with a slash
|
||||
* - Do not contain '..' (path traversal)
|
||||
* - Do not contain '//' (consecutive slashes)
|
||||
* - Do not end with '.lock'
|
||||
* - Do not contain '@{'
|
||||
* - Do not contain control characters or special git characters (~^:?*[\])
|
||||
*/
|
||||
export function validateBranchName(branchName: string): void {
|
||||
// Check for empty or whitespace-only names
|
||||
if (!branchName || branchName.trim().length === 0) {
|
||||
throw new Error("Branch name cannot be empty");
|
||||
}
|
||||
|
||||
// Check for leading dash (prevents option injection like --help, -x)
|
||||
if (branchName.startsWith("-")) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot start with a dash.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for control characters and special git characters (~^:?*[\])
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (/[\x00-\x1F\x7F ~^:?*[\]\\]/.test(branchName)) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot contain control characters, spaces, or special git characters (~^:?*[\\]).`,
|
||||
);
|
||||
}
|
||||
|
||||
// Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period/hash/plus.
|
||||
// # is valid per git-check-ref-format and commonly used in branch names like "fix/#123-description".
|
||||
// + is valid per git-check-ref-format and generated by Claude Code's EnterWorktree tool when
|
||||
// converting worktree names containing "/" (e.g. "feat/foo" becomes "worktree-feat+foo").
|
||||
// All git calls use execFileSync (not shell interpolation), so neither # nor + carries injection risk.
|
||||
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.#+-]*$/;
|
||||
|
||||
if (!validPattern.test(branchName)) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, periods, hashes (#), or plus signs (+).`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for leading/trailing periods
|
||||
if (branchName.startsWith(".") || branchName.endsWith(".")) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot start or end with a period.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for trailing slash
|
||||
if (branchName.endsWith("/")) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot end with a slash.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check for consecutive slashes
|
||||
if (branchName.includes("//")) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot contain consecutive slashes.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Additional git-specific validations
|
||||
if (branchName.includes("..")) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot contain '..'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (branchName.endsWith(".lock")) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot end with '.lock'`,
|
||||
);
|
||||
}
|
||||
|
||||
if (branchName.includes("@{")) {
|
||||
throw new Error(
|
||||
`Invalid branch name: "${branchName}". Branch names cannot contain '@{'`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a git command safely using execFileSync to avoid shell interpolation.
|
||||
*
|
||||
* Security: execFileSync passes arguments directly to the git binary without
|
||||
* invoking a shell, preventing command injection attacks where malicious input
|
||||
* could be interpreted as shell commands (e.g., branch names containing `;`, `|`, `&&`).
|
||||
*
|
||||
* @param args - Git command arguments (e.g., ["checkout", "branch-name"])
|
||||
*/
|
||||
function execGit(args: string[]): void {
|
||||
execFileSync("git", args, { stdio: "inherit", env: process.env });
|
||||
}
|
||||
|
||||
export type BranchInfo = {
|
||||
baseBranch: string;
|
||||
@ -26,7 +138,7 @@ export async function setupBranch(
|
||||
): Promise<BranchInfo> {
|
||||
const { owner, repo } = context.repository;
|
||||
const entityNumber = context.entityNumber;
|
||||
const { baseBranch, branchPrefix } = context.inputs;
|
||||
const { baseBranch, branchPrefix, branchNameTemplate } = context.inputs;
|
||||
const isPR = context.isPR;
|
||||
|
||||
if (isPR) {
|
||||
@ -53,14 +165,33 @@ export async function setupBranch(
|
||||
`PR #${entityNumber}: ${commitCount} commits, using fetch depth ${fetchDepth}`,
|
||||
);
|
||||
|
||||
// Execute git commands to checkout PR branch (dynamic depth based on PR size)
|
||||
await $`git fetch origin --depth=${fetchDepth} ${branchName}`;
|
||||
await $`git checkout ${branchName} --`;
|
||||
// Validate branch names before use to prevent command injection
|
||||
validateBranchName(branchName);
|
||||
|
||||
// For cross-repository (fork) PRs, fetch via the pull ref since the
|
||||
// branch only exists on the fork's remote, not on origin.
|
||||
if (prData.isCrossRepository) {
|
||||
console.log(
|
||||
`PR #${entityNumber} is from a fork, fetching via refs/pull/${entityNumber}/head...`,
|
||||
);
|
||||
execGit([
|
||||
"fetch",
|
||||
"origin",
|
||||
`--depth=${fetchDepth}`,
|
||||
`pull/${entityNumber}/head:${branchName}`,
|
||||
]);
|
||||
} else {
|
||||
// Execute git commands to checkout PR branch (dynamic depth based on PR size)
|
||||
// Using execFileSync instead of shell template literals for security
|
||||
execGit(["fetch", "origin", `--depth=${fetchDepth}`, branchName]);
|
||||
}
|
||||
execGit(["checkout", branchName, "--"]);
|
||||
|
||||
console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
|
||||
|
||||
// For open PRs, we need to get the base branch of the PR
|
||||
const baseBranch = prData.baseRefName;
|
||||
validateBranchName(baseBranch);
|
||||
|
||||
return {
|
||||
baseBranch,
|
||||
@ -87,17 +218,8 @@ export async function setupBranch(
|
||||
// Generate branch name for either an issue or closed/merged PR
|
||||
const entityType = isPR ? "pr" : "issue";
|
||||
|
||||
// Create Kubernetes-compatible timestamp: lowercase, hyphens only, shorter format
|
||||
const now = new Date();
|
||||
const timestamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}-${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
// Ensure branch name is Kubernetes-compatible:
|
||||
// - Lowercase only
|
||||
// - Alphanumeric with hyphens
|
||||
// - No underscores
|
||||
// - Max 50 chars (to allow for prefixes)
|
||||
const branchName = `${branchPrefix}${entityType}-${entityNumber}-${timestamp}`;
|
||||
const newBranch = branchName.toLowerCase().substring(0, 50);
|
||||
// Get the SHA of the source branch to use in template
|
||||
let sourceSHA: string | undefined;
|
||||
|
||||
try {
|
||||
// Get the SHA of the source branch to verify it exists
|
||||
@ -107,8 +229,46 @@ export async function setupBranch(
|
||||
ref: `heads/${sourceBranch}`,
|
||||
});
|
||||
|
||||
const currentSHA = sourceBranchRef.data.object.sha;
|
||||
console.log(`Source branch SHA: ${currentSHA}`);
|
||||
sourceSHA = sourceBranchRef.data.object.sha;
|
||||
console.log(`Source branch SHA: ${sourceSHA}`);
|
||||
|
||||
// Extract first label from GitHub data
|
||||
const firstLabel = extractFirstLabel(githubData);
|
||||
|
||||
// Extract title from GitHub data
|
||||
const title = githubData.contextData.title;
|
||||
|
||||
// Generate branch name using template or default format
|
||||
let newBranch = generateBranchName(
|
||||
branchNameTemplate,
|
||||
branchPrefix,
|
||||
entityType,
|
||||
entityNumber,
|
||||
sourceSHA,
|
||||
firstLabel,
|
||||
title,
|
||||
);
|
||||
|
||||
// Check if generated branch already exists on remote
|
||||
try {
|
||||
await $`git ls-remote --exit-code origin refs/heads/${newBranch}`.quiet();
|
||||
|
||||
// If we get here, branch exists (exit code 0)
|
||||
console.log(
|
||||
`Branch '${newBranch}' already exists, falling back to default format`,
|
||||
);
|
||||
newBranch = generateBranchName(
|
||||
undefined, // Force default template
|
||||
branchPrefix,
|
||||
entityType,
|
||||
entityNumber,
|
||||
sourceSHA,
|
||||
firstLabel,
|
||||
title,
|
||||
);
|
||||
} catch {
|
||||
// Branch doesn't exist (non-zero exit code), continue with generated name
|
||||
}
|
||||
|
||||
// For commit signing, defer branch creation to the file ops server
|
||||
if (context.inputs.useCommitSigning) {
|
||||
@ -118,12 +278,10 @@ export async function setupBranch(
|
||||
|
||||
// Ensure we're on the source branch
|
||||
console.log(`Fetching and checking out source branch: ${sourceBranch}`);
|
||||
await $`git fetch origin ${sourceBranch} --depth=1`;
|
||||
await $`git checkout ${sourceBranch}`;
|
||||
validateBranchName(sourceBranch);
|
||||
execGit(["fetch", "origin", sourceBranch, "--depth=1"]);
|
||||
execGit(["checkout", sourceBranch, "--"]);
|
||||
|
||||
// Set outputs for GitHub Actions
|
||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
||||
core.setOutput("BASE_BRANCH", sourceBranch);
|
||||
return {
|
||||
baseBranch: sourceBranch,
|
||||
claudeBranch: newBranch,
|
||||
@ -138,19 +296,18 @@ export async function setupBranch(
|
||||
|
||||
// Fetch and checkout the source branch first to ensure we branch from the correct base
|
||||
console.log(`Fetching and checking out source branch: ${sourceBranch}`);
|
||||
await $`git fetch origin ${sourceBranch} --depth=1`;
|
||||
await $`git checkout ${sourceBranch}`;
|
||||
validateBranchName(sourceBranch);
|
||||
validateBranchName(newBranch);
|
||||
execGit(["fetch", "origin", sourceBranch, "--depth=1"]);
|
||||
execGit(["checkout", sourceBranch, "--"]);
|
||||
|
||||
// Create and checkout the new branch from the source branch
|
||||
await $`git checkout -b ${newBranch}`;
|
||||
execGit(["checkout", "-b", newBranch]);
|
||||
|
||||
console.log(
|
||||
`Successfully created and checked out local branch: ${newBranch}`,
|
||||
);
|
||||
|
||||
// Set outputs for GitHub Actions
|
||||
core.setOutput("CLAUDE_BRANCH", newBranch);
|
||||
core.setOutput("BASE_BRANCH", sourceBranch);
|
||||
return {
|
||||
baseBranch: sourceBranch,
|
||||
claudeBranch: newBranch,
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { GITHUB_SERVER_URL } from "../api/config";
|
||||
|
||||
export type ExecutionDetails = {
|
||||
cost_usd?: number;
|
||||
total_cost_usd?: number;
|
||||
duration_ms?: number;
|
||||
duration_api_ms?: number;
|
||||
};
|
||||
|
||||
@ -6,9 +6,14 @@
|
||||
*/
|
||||
|
||||
import { $ } from "bun";
|
||||
import { mkdir, writeFile, rm } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { homedir } from "os";
|
||||
import type { GitHubContext } from "../context";
|
||||
import { GITHUB_SERVER_URL } from "../api/config";
|
||||
|
||||
const SSH_SIGNING_KEY_PATH = join(homedir(), ".ssh", "claude_signing_key");
|
||||
|
||||
type GitUser = {
|
||||
login: string;
|
||||
id: number;
|
||||
@ -17,7 +22,7 @@ type GitUser = {
|
||||
export async function configureGitAuth(
|
||||
githubToken: string,
|
||||
context: GitHubContext,
|
||||
user: GitUser | null,
|
||||
user: GitUser,
|
||||
) {
|
||||
console.log("Configuring git authentication for non-signing mode");
|
||||
|
||||
@ -28,20 +33,14 @@ export async function configureGitAuth(
|
||||
? "users.noreply.github.com"
|
||||
: `users.noreply.${serverUrl.hostname}`;
|
||||
|
||||
// Configure git user based on the comment creator
|
||||
// Configure git user
|
||||
console.log("Configuring git user...");
|
||||
if (user) {
|
||||
const botName = user.login;
|
||||
const botId = user.id;
|
||||
console.log(`Setting git user as ${botName}...`);
|
||||
await $`git config user.name "${botName}"`;
|
||||
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`;
|
||||
console.log(`✓ Set git user as ${botName}`);
|
||||
} else {
|
||||
console.log("No user data in comment, using default bot user");
|
||||
await $`git config user.name "github-actions[bot]"`;
|
||||
await $`git config user.email "41898282+github-actions[bot]@${noreplyDomain}"`;
|
||||
}
|
||||
const botName = user.login;
|
||||
const botId = user.id;
|
||||
console.log(`Setting git user as ${botName}...`);
|
||||
await $`git config user.name "${botName}"`;
|
||||
await $`git config user.email "${botId}+${botName}@${noreplyDomain}"`;
|
||||
console.log(`✓ Set git user as ${botName}`);
|
||||
|
||||
// Remove the authorization header that actions/checkout sets
|
||||
console.log("Removing existing git authentication headers...");
|
||||
@ -52,11 +51,86 @@ export async function configureGitAuth(
|
||||
console.log("No existing authentication headers to remove");
|
||||
}
|
||||
|
||||
// Update the remote URL to include the token for authentication
|
||||
console.log("Updating remote URL with authentication...");
|
||||
const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
|
||||
await $`git remote set-url origin ${remoteUrl}`;
|
||||
console.log("✓ Updated remote URL with authentication token");
|
||||
if (process.env.ALLOWED_NON_WRITE_USERS) {
|
||||
// When processing content from non-write users, use a credential helper
|
||||
// instead of embedding the token in the remote URL. The helper script reads
|
||||
// from GH_TOKEN at auth time, so .git/config stays token-free. Written as a
|
||||
// file to avoid shell-escaping the helper body; placed under
|
||||
// GITHUB_ACTION_PATH so it sits alongside the action source.
|
||||
console.log("Configuring git credential helper...");
|
||||
process.env.GH_TOKEN = githubToken;
|
||||
const helperPath = join(
|
||||
process.env.GITHUB_ACTION_PATH || homedir(),
|
||||
".git-credential-gh-token",
|
||||
);
|
||||
await writeFile(
|
||||
helperPath,
|
||||
'#!/bin/sh\necho username=x-access-token\necho password="$GH_TOKEN"\n',
|
||||
{ mode: 0o700 },
|
||||
);
|
||||
const cleanUrl = `https://${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
|
||||
await $`git remote set-url origin ${cleanUrl}`;
|
||||
await $`git config credential.helper ${helperPath}`;
|
||||
console.log("✓ Configured credential helper");
|
||||
} else {
|
||||
// Update the remote URL to include the token for authentication
|
||||
console.log("Updating remote URL with authentication...");
|
||||
const remoteUrl = `https://x-access-token:${githubToken}@${serverUrl.host}/${context.repository.owner}/${context.repository.repo}.git`;
|
||||
await $`git remote set-url origin ${remoteUrl}`;
|
||||
console.log("✓ Updated remote URL with authentication token");
|
||||
}
|
||||
|
||||
console.log("Git authentication configured successfully");
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure git to use SSH signing for commits
|
||||
* This is an alternative to GitHub API-based commit signing (use_commit_signing)
|
||||
*/
|
||||
export async function setupSshSigning(sshSigningKey: string): Promise<void> {
|
||||
console.log("Configuring SSH signing for commits...");
|
||||
|
||||
// Validate SSH key format
|
||||
if (!sshSigningKey.trim()) {
|
||||
throw new Error("SSH signing key cannot be empty");
|
||||
}
|
||||
if (
|
||||
!sshSigningKey.includes("BEGIN") ||
|
||||
!sshSigningKey.includes("PRIVATE KEY")
|
||||
) {
|
||||
throw new Error("Invalid SSH private key format");
|
||||
}
|
||||
|
||||
// Create .ssh directory with secure permissions (700)
|
||||
const sshDir = join(homedir(), ".ssh");
|
||||
await mkdir(sshDir, { recursive: true, mode: 0o700 });
|
||||
|
||||
// Ensure key ends with newline (required for ssh-keygen to parse it)
|
||||
const normalizedKey = sshSigningKey.endsWith("\n")
|
||||
? sshSigningKey
|
||||
: sshSigningKey + "\n";
|
||||
|
||||
// Write the signing key atomically with secure permissions (600)
|
||||
await writeFile(SSH_SIGNING_KEY_PATH, normalizedKey, { mode: 0o600 });
|
||||
console.log(`✓ SSH signing key written to ${SSH_SIGNING_KEY_PATH}`);
|
||||
|
||||
// Configure git to use SSH signing
|
||||
await $`git config gpg.format ssh`;
|
||||
await $`git config user.signingkey ${SSH_SIGNING_KEY_PATH}`;
|
||||
await $`git config commit.gpgsign true`;
|
||||
|
||||
console.log("✓ Git configured to use SSH signing for commits");
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up the SSH signing key file
|
||||
* Should be called in the post step for security
|
||||
*/
|
||||
export async function cleanupSshSigning(): Promise<void> {
|
||||
try {
|
||||
await rm(SSH_SIGNING_KEY_PATH, { force: true });
|
||||
console.log("✓ SSH signing key cleaned up");
|
||||
} catch (error) {
|
||||
console.log("No SSH signing key to clean up");
|
||||
}
|
||||
}
|
||||
|
||||
109
src/github/operations/restore-config.ts
Normal file
109
src/github/operations/restore-config.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import { execFileSync } from "child_process";
|
||||
import { cpSync, existsSync, rmSync } from "fs";
|
||||
|
||||
// Paths that are both PR-controllable and read from cwd at CLI startup.
|
||||
//
|
||||
// Deliberately excluded from the CLI's broader auto-edit blocklist:
|
||||
// .git/ — not tracked by git; PR commits cannot place files there.
|
||||
// Restoring it would also undo the PR checkout entirely.
|
||||
// .gitconfig — git reads ~/.gitconfig and .git/config, never cwd/.gitconfig.
|
||||
// .bashrc etc. — shells source these from $HOME; checkout cannot reach $HOME.
|
||||
// .vscode/.idea— IDE config; nothing in the CLI's startup path reads them.
|
||||
const SENSITIVE_PATHS = [
|
||||
".claude",
|
||||
".mcp.json",
|
||||
".claude.json",
|
||||
".gitmodules",
|
||||
".ripgreprc",
|
||||
"CLAUDE.md",
|
||||
"CLAUDE.local.md",
|
||||
".husky",
|
||||
];
|
||||
|
||||
/**
|
||||
* Restores security-sensitive config paths from the PR base branch.
|
||||
*
|
||||
* The CLI's non-interactive mode trusts cwd: it reads `.mcp.json`,
|
||||
* `.claude/settings.json`, and `.claude/settings.local.json` from the working
|
||||
* directory and acts on them before any tool-permission gating — executing
|
||||
* hooks (including SessionStart), setting env vars (NODE_OPTIONS, LD_PRELOAD,
|
||||
* PATH), running apiKeyHelper/awsAuthRefresh shell commands, and auto-approving
|
||||
* MCP servers. When this action checks out a PR head, all of these are
|
||||
* attacker-controlled.
|
||||
*
|
||||
* Rather than enumerate every dangerous key, this replaces the entire `.claude/`
|
||||
* tree and `.mcp.json` with the versions from the PR base branch, which a
|
||||
* maintainer has reviewed and merged. Paths absent on base are deleted.
|
||||
*
|
||||
* Known limitation: if a PR legitimately modifies `.claude/` and the CLI later
|
||||
* commits with `git add -A`, the revert will be included in that commit. This
|
||||
* is a narrow UX tradeoff for closing the RCE surface.
|
||||
*
|
||||
* @param baseBranch - PR base branch name. Must be pre-validated (branch.ts
|
||||
* calls validateBranchName on it before returning).
|
||||
*/
|
||||
export function restoreConfigFromBase(baseBranch: string): void {
|
||||
console.log(
|
||||
`Restoring ${SENSITIVE_PATHS.join(", ")} from origin/${baseBranch} (PR head is untrusted)`,
|
||||
);
|
||||
|
||||
// Snapshot every PR-authored sensitive path into .claude-pr/ before deletion
|
||||
// so review agents can inspect what the PR changes without those files ever
|
||||
// being executed. Captured before the security delete so it reflects the
|
||||
// PR-authored version.
|
||||
rmSync(".claude-pr", { recursive: true, force: true });
|
||||
for (const p of SENSITIVE_PATHS) {
|
||||
if (existsSync(p)) {
|
||||
cpSync(p, `.claude-pr/${p}`, { recursive: true });
|
||||
}
|
||||
}
|
||||
if (existsSync(".claude-pr")) {
|
||||
console.log(
|
||||
"Preserved PR's sensitive paths → .claude-pr/ for review agents (not executed)",
|
||||
);
|
||||
}
|
||||
|
||||
// Delete PR-controlled versions BEFORE fetching so the attacker-controlled
|
||||
// .gitmodules is absent during the network operation. If git reads .gitmodules
|
||||
// during fetch (fetch.recurseSubmodules=on-demand, the git default), it will
|
||||
// attempt to fetch submodule objects and block on credential prompts in CI —
|
||||
// causing an indefinite hang. Deleting first closes that window.
|
||||
//
|
||||
// If the restore below fails for a given path, that path stays deleted —
|
||||
// the safe fallback (no attacker-controlled config). A bare `git checkout`
|
||||
// alone wouldn't remove files the PR added, so nuke first.
|
||||
for (const p of SENSITIVE_PATHS) {
|
||||
rmSync(p, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
// --no-recurse-submodules: explicitly suppress submodule fetching regardless of
|
||||
// fetch.recurseSubmodules config. Defense-in-depth alongside the delete above.
|
||||
execFileSync(
|
||||
"git",
|
||||
["fetch", "origin", baseBranch, "--depth=1", "--no-recurse-submodules"],
|
||||
{
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
},
|
||||
);
|
||||
|
||||
for (const p of SENSITIVE_PATHS) {
|
||||
try {
|
||||
execFileSync("git", ["checkout", `origin/${baseBranch}`, "--", p], {
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch {
|
||||
// Path doesn't exist on base — it stays deleted.
|
||||
}
|
||||
}
|
||||
|
||||
// `git checkout <ref> -- <path>` stages the restored files. Unstage so the
|
||||
// revert doesn't silently leak into commits the CLI makes later.
|
||||
try {
|
||||
execFileSync("git", ["reset", "--", ...SENSITIVE_PATHS], {
|
||||
stdio: "pipe",
|
||||
});
|
||||
} catch {
|
||||
// Nothing was staged, or paths don't exist on HEAD — either is fine.
|
||||
}
|
||||
}
|
||||
@ -3,6 +3,13 @@
|
||||
import * as core from "@actions/core";
|
||||
import { retryWithBackoff } from "../utils/retry";
|
||||
|
||||
export class WorkflowValidationSkipError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = "WorkflowValidationSkipError";
|
||||
}
|
||||
}
|
||||
|
||||
async function getOidcToken(): Promise<string> {
|
||||
try {
|
||||
const oidcToken = await core.getIDToken("claude-code-github-action");
|
||||
@ -16,15 +23,60 @@ async function getOidcToken(): Promise<string> {
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
||||
const DEFAULT_PERMISSIONS: Record<string, string> = {
|
||||
contents: "write",
|
||||
pull_requests: "write",
|
||||
issues: "write",
|
||||
};
|
||||
|
||||
export function parseAdditionalPermissions():
|
||||
| Record<string, string>
|
||||
| undefined {
|
||||
const raw = process.env.ADDITIONAL_PERMISSIONS;
|
||||
if (!raw || !raw.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const additional: Record<string, string> = {};
|
||||
for (const line of raw.split("\n")) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
const colonIndex = trimmed.indexOf(":");
|
||||
if (colonIndex === -1) continue;
|
||||
const key = trimmed.slice(0, colonIndex).trim();
|
||||
const value = trimmed.slice(colonIndex + 1).trim();
|
||||
if (key && value) {
|
||||
additional[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(additional).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return { ...DEFAULT_PERMISSIONS, ...additional };
|
||||
}
|
||||
|
||||
async function exchangeForAppToken(
|
||||
oidcToken: string,
|
||||
permissions?: Record<string, string>,
|
||||
): Promise<string> {
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${oidcToken}`,
|
||||
};
|
||||
const fetchOptions: RequestInit = {
|
||||
method: "POST",
|
||||
headers,
|
||||
};
|
||||
|
||||
if (permissions) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
fetchOptions.body = JSON.stringify({ permissions });
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.anthropic.com/api/github/github-app-token-exchange",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${oidcToken}`,
|
||||
},
|
||||
},
|
||||
fetchOptions,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
@ -51,8 +103,7 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
||||
console.log(
|
||||
"Action skipped due to workflow validation error. This is expected when adding Claude Code workflows to new repositories or on PRs with workflow changes. If you're seeing this, your workflow will begin working once you merge your PR.",
|
||||
);
|
||||
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
|
||||
process.exit(0);
|
||||
throw new WorkflowValidationSkipError(message);
|
||||
}
|
||||
|
||||
console.error(
|
||||
@ -75,34 +126,30 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
||||
}
|
||||
|
||||
export async function setupGitHubToken(): Promise<string> {
|
||||
try {
|
||||
// Check if GitHub token was provided as override
|
||||
const providedToken = process.env.OVERRIDE_GITHUB_TOKEN;
|
||||
// Check if GitHub token was provided as override
|
||||
const providedToken = process.env.OVERRIDE_GITHUB_TOKEN;
|
||||
|
||||
if (providedToken) {
|
||||
console.log("Using provided GITHUB_TOKEN for authentication");
|
||||
core.setOutput("GITHUB_TOKEN", providedToken);
|
||||
return providedToken;
|
||||
}
|
||||
|
||||
console.log("Requesting OIDC token...");
|
||||
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
||||
console.log("OIDC token successfully obtained");
|
||||
|
||||
console.log("Exchanging OIDC token for app token...");
|
||||
const appToken = await retryWithBackoff(() =>
|
||||
exchangeForAppToken(oidcToken),
|
||||
);
|
||||
console.log("App token successfully obtained");
|
||||
|
||||
console.log("Using GITHUB_TOKEN from OIDC");
|
||||
core.setOutput("GITHUB_TOKEN", appToken);
|
||||
return appToken;
|
||||
} catch (error) {
|
||||
// Only set failed if we get here - workflow validation errors will exit(0) before this
|
||||
core.setFailed(
|
||||
`Failed to setup GitHub token: ${error}\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
|
||||
);
|
||||
process.exit(1);
|
||||
if (providedToken) {
|
||||
console.log("Using provided GITHUB_TOKEN for authentication");
|
||||
return providedToken;
|
||||
}
|
||||
|
||||
console.log("Requesting OIDC token...");
|
||||
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
||||
console.log("OIDC token successfully obtained");
|
||||
|
||||
const permissions = parseAdditionalPermissions();
|
||||
|
||||
console.log("Exchanging OIDC token for app token...");
|
||||
const appToken = await retryWithBackoff(
|
||||
() => exchangeForAppToken(oidcToken, permissions),
|
||||
{
|
||||
shouldRetry: (error) => !(error instanceof WorkflowValidationSkipError),
|
||||
},
|
||||
);
|
||||
console.log("App token successfully obtained");
|
||||
core.setSecret(appToken);
|
||||
|
||||
console.log("Using GITHUB_TOKEN from OIDC");
|
||||
return appToken;
|
||||
}
|
||||
|
||||
@ -10,6 +10,8 @@ export type GitHubComment = {
|
||||
body: string;
|
||||
author: GitHubAuthor;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
lastEditedAt?: string;
|
||||
isMinimized?: boolean;
|
||||
};
|
||||
|
||||
@ -41,6 +43,8 @@ export type GitHubReview = {
|
||||
body: string;
|
||||
state: string;
|
||||
submittedAt: string;
|
||||
updatedAt?: string;
|
||||
lastEditedAt?: string;
|
||||
comments: {
|
||||
nodes: GitHubReviewComment[];
|
||||
};
|
||||
@ -53,10 +57,24 @@ export type GitHubPullRequest = {
|
||||
baseRefName: string;
|
||||
headRefName: string;
|
||||
headRefOid: string;
|
||||
isCrossRepository: boolean;
|
||||
headRepository: {
|
||||
owner: {
|
||||
login: string;
|
||||
};
|
||||
name: string;
|
||||
} | null;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
lastEditedAt?: string;
|
||||
additions: number;
|
||||
deletions: number;
|
||||
state: string;
|
||||
labels: {
|
||||
nodes: Array<{
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
commits: {
|
||||
totalCount: number;
|
||||
nodes: Array<{
|
||||
@ -79,7 +97,14 @@ export type GitHubIssue = {
|
||||
body: string;
|
||||
author: GitHubAuthor;
|
||||
createdAt: string;
|
||||
updatedAt?: string;
|
||||
lastEditedAt?: string;
|
||||
state: string;
|
||||
labels: {
|
||||
nodes: Array<{
|
||||
name: string;
|
||||
}>;
|
||||
};
|
||||
comments: {
|
||||
nodes: GitHubComment[];
|
||||
};
|
||||
|
||||
65
src/github/utils/actor-filter.ts
Normal file
65
src/github/utils/actor-filter.ts
Normal file
@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Parses actor filter string into array of patterns
|
||||
* @param filterString - Comma-separated actor names (e.g., "user1,user2,*[bot]")
|
||||
* @returns Array of actor patterns
|
||||
*/
|
||||
export function parseActorFilter(filterString: string): string[] {
|
||||
if (!filterString.trim()) return [];
|
||||
return filterString
|
||||
.split(",")
|
||||
.map((actor) => actor.trim())
|
||||
.filter((actor) => actor.length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an actor matches a pattern
|
||||
* Supports wildcards: "*[bot]" matches all bots, "dependabot[bot]" matches specific
|
||||
* @param actor - Actor username to check
|
||||
* @param pattern - Pattern to match against
|
||||
* @returns true if actor matches pattern
|
||||
*/
|
||||
export function actorMatchesPattern(actor: string, pattern: string): boolean {
|
||||
// Exact match
|
||||
if (actor === pattern) return true;
|
||||
|
||||
// Wildcard bot pattern: "*[bot]" matches any username ending with [bot]
|
||||
if (pattern === "*[bot]" && actor.endsWith("[bot]")) return true;
|
||||
|
||||
// No match
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a comment should be included based on actor filters
|
||||
* @param actor - Comment author username
|
||||
* @param includeActors - Array of actors to include (empty = include all)
|
||||
* @param excludeActors - Array of actors to exclude (empty = exclude none)
|
||||
* @returns true if comment should be included
|
||||
*/
|
||||
export function shouldIncludeCommentByActor(
|
||||
actor: string,
|
||||
includeActors: string[],
|
||||
excludeActors: string[],
|
||||
): boolean {
|
||||
// Check exclusion first (exclusion takes priority)
|
||||
if (excludeActors.length > 0) {
|
||||
for (const pattern of excludeActors) {
|
||||
if (actorMatchesPattern(actor, pattern)) {
|
||||
return false; // Excluded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check inclusion
|
||||
if (includeActors.length > 0) {
|
||||
for (const pattern of includeActors) {
|
||||
if (actorMatchesPattern(actor, pattern)) {
|
||||
return true; // Explicitly included
|
||||
}
|
||||
}
|
||||
return false; // Not in include list
|
||||
}
|
||||
|
||||
// No filters or passed all checks
|
||||
return true;
|
||||
}
|
||||
@ -6,11 +6,11 @@
|
||||
*/
|
||||
|
||||
import type { Octokit } from "@octokit/rest";
|
||||
import type { ParsedGitHubContext } from "../context";
|
||||
import type { GitHubContext } from "../context";
|
||||
|
||||
export async function checkHumanActor(
|
||||
octokit: Octokit,
|
||||
githubContext: ParsedGitHubContext,
|
||||
githubContext: GitHubContext,
|
||||
) {
|
||||
// Fetch user information from GitHub API
|
||||
const { data: userData } = await octokit.users.getByUsername({
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user