mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 15:04:13 +08:00
Compare commits
20 Commits
v1.0.14
...
claude/sla
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3ac1fb750 | ||
|
|
f2ceddc1a3 | ||
|
|
14ab4250bb | ||
|
|
c7fdd19642 | ||
|
|
92d173475f | ||
|
|
108e982900 | ||
|
|
7bb53ae6ee | ||
|
|
804b418b93 | ||
|
|
500439cb9b | ||
|
|
4cda0ef6d1 | ||
|
|
037b85d0d2 | ||
|
|
8a1c437175 | ||
|
|
56c8ae7d88 | ||
|
|
f4d737af0b | ||
|
|
29fe50368c | ||
|
|
8ad13bd20b | ||
|
|
7b914ae5c0 | ||
|
|
d4c09790f5 | ||
|
|
5033c581bb | ||
|
|
f8749bd14b |
@@ -17,7 +17,6 @@ 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.
|
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
|
||||||
|
|
||||||
2. Next, use gh commands to get context about the issue:
|
2. Next, use gh commands to get context about the issue:
|
||||||
|
|
||||||
- Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
|
- Use `gh issue view ${{ github.event.issue.number }}` to retrieve the current issue's details
|
||||||
- Use `gh search issues` to find similar issues that might provide context for proper categorization
|
- Use `gh search issues` to find similar issues that might provide context for proper categorization
|
||||||
- You have access to these Bash commands:
|
- You have access to these Bash commands:
|
||||||
@@ -27,7 +26,6 @@ TASK OVERVIEW:
|
|||||||
- Bash(gh search:\*) - to search for similar issues
|
- Bash(gh search:\*) - to search for similar issues
|
||||||
|
|
||||||
3. Analyze the issue content, considering:
|
3. Analyze the issue content, considering:
|
||||||
|
|
||||||
- The issue title and description
|
- The issue title and description
|
||||||
- The type of issue (bug report, feature request, question, etc.)
|
- The type of issue (bug report, feature request, question, etc.)
|
||||||
- Technical areas mentioned
|
- Technical areas mentioned
|
||||||
@@ -36,7 +34,6 @@ TASK OVERVIEW:
|
|||||||
- Components affected
|
- Components affected
|
||||||
|
|
||||||
4. Select appropriate labels from the available labels list provided above:
|
4. Select appropriate labels from the available labels list provided above:
|
||||||
|
|
||||||
- Choose labels that accurately reflect the issue's nature
|
- Choose labels that accurately reflect the issue's nature
|
||||||
- Be specific but comprehensive
|
- Be specific but comprehensive
|
||||||
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
|
- IMPORTANT: Add a priority label (P1, P2, or P3) based on the label descriptions from gh label list
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -47,6 +47,21 @@ Looking for specific automation patterns? Check our **[Solutions Guide](./docs/s
|
|||||||
|
|
||||||
Each solution includes complete working examples, configuration details, and expected outcomes.
|
Each solution includes complete working examples, configuration details, and expected outcomes.
|
||||||
|
|
||||||
|
## 📁 Examples
|
||||||
|
|
||||||
|
The [`examples/`](./examples/) folder contains ready-to-use GitHub Actions workflow files that demonstrate various automation patterns:
|
||||||
|
|
||||||
|
- **[`pr-review-comprehensive.yml`](./examples/pr-review-comprehensive.yml)** - Complete PR review automation with progress tracking
|
||||||
|
- **[`pr-review-filtered-paths.yml`](./examples/pr-review-filtered-paths.yml)** - Trigger reviews only for specific file paths
|
||||||
|
- **[`pr-review-filtered-authors.yml`](./examples/pr-review-filtered-authors.yml)** - Special handling for external contributors
|
||||||
|
- **[`issue-triage.yml`](./examples/issue-triage.yml)** - Automatic issue categorization and labeling
|
||||||
|
- **[`issue-deduplication.yml`](./examples/issue-deduplication.yml)** - Detect and handle duplicate issues
|
||||||
|
- **[`ci-failure-auto-fix.yml`](./examples/ci-failure-auto-fix.yml)** - Automatic CI failure analysis and fixes
|
||||||
|
- **[`manual-code-analysis.yml`](./examples/manual-code-analysis.yml)** - On-demand code analysis workflows
|
||||||
|
- **[`claude.yml`](./examples/claude.yml)** - Basic setup for @claude mentions
|
||||||
|
|
||||||
|
Copy these examples directly into your `.github/workflows/` directory and customize them for your specific needs. Each example includes detailed comments explaining the configuration and expected behavior.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **[Solutions Guide](./docs/solutions.md)** - **🎯 Ready-to-use automation patterns**
|
- **[Solutions Guide](./docs/solutions.md)** - **🎯 Ready-to-use automation patterns**
|
||||||
|
|||||||
17
action.yml
17
action.yml
@@ -101,6 +101,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."
|
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
show_full_output:
|
||||||
|
description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments."
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
plugins:
|
||||||
|
description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
plugin_marketplaces:
|
||||||
|
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
execution_file:
|
execution_file:
|
||||||
@@ -177,7 +189,7 @@ runs:
|
|||||||
# Install Claude Code if no custom executable is provided
|
# Install Claude Code if no custom executable is provided
|
||||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||||
echo "Installing Claude Code..."
|
echo "Installing Claude Code..."
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.25
|
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.37
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
else
|
else
|
||||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||||
@@ -213,6 +225,9 @@ runs:
|
|||||||
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
||||||
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||||
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||||
|
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
|
||||||
|
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||||
|
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
|
||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ Thank you for your interest in contributing to Claude Code Base Action! This doc
|
|||||||
```
|
```
|
||||||
|
|
||||||
This script:
|
This script:
|
||||||
|
|
||||||
- Installs `act` if not present (requires Homebrew on macOS)
|
- Installs `act` if not present (requires Homebrew on macOS)
|
||||||
- Runs the GitHub Action workflow locally using Docker
|
- Runs the GitHub Action workflow locally using Docker
|
||||||
- Requires your `ANTHROPIC_API_KEY` to be set
|
- Requires your `ANTHROPIC_API_KEY` to be set
|
||||||
|
|||||||
@@ -85,29 +85,32 @@ Add the following to your workflow file:
|
|||||||
|
|
||||||
## Inputs
|
## Inputs
|
||||||
|
|
||||||
| Input | Description | Required | Default |
|
| Input | Description | Required | Default |
|
||||||
| ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- |
|
||||||
| `prompt` | The prompt to send to Claude Code | No\* | '' |
|
| `prompt` | The prompt to send to Claude Code | No\* | '' |
|
||||||
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
|
| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' |
|
||||||
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
|
| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' |
|
||||||
| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' |
|
| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' |
|
||||||
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' |
|
| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' |
|
||||||
| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' |
|
| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' |
|
||||||
| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' |
|
| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' |
|
||||||
| `system_prompt` | Override system prompt | No | '' |
|
| `system_prompt` | Override system prompt | No | '' |
|
||||||
| `append_system_prompt` | Append to system prompt | No | '' |
|
| `append_system_prompt` | Append to system prompt | No | '' |
|
||||||
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
|
| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' |
|
||||||
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' |
|
||||||
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' |
|
||||||
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' |
|
||||||
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
|
| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' |
|
||||||
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
|
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' |
|
||||||
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
||||||
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' |
|
||||||
| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' |
|
| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' |
|
||||||
|
| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#️-full-output-security-warning)) | No | 'false'\*\* |
|
||||||
|
|
||||||
\*Either `prompt` or `prompt_file` must be provided, but not both.
|
\*Either `prompt` or `prompt_file` must be provided, but not both.
|
||||||
|
|
||||||
|
\*\*`show_full_output` is automatically enabled when GitHub Actions debug mode is active. See [security documentation](../docs/security.md#️-full-output-security-warning) for important security considerations.
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
||||||
| Output | Description |
|
| Output | Description |
|
||||||
@@ -487,7 +490,6 @@ This example shows how to use OIDC authentication with GCP Vertex AI:
|
|||||||
To securely use your Anthropic API key:
|
To securely use your Anthropic API key:
|
||||||
|
|
||||||
1. Add your API key as a repository secret:
|
1. Add your API key as a repository secret:
|
||||||
|
|
||||||
- Go to your repository's Settings
|
- Go to your repository's Settings
|
||||||
- Navigate to "Secrets and variables" → "Actions"
|
- Navigate to "Secrets and variables" → "Actions"
|
||||||
- Click "New repository secret"
|
- Click "New repository secret"
|
||||||
|
|||||||
@@ -55,6 +55,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."
|
description: "Optional path to a custom Bun executable. If provided, skips automatic Bun installation and uses this executable instead. WARNING: Using an incompatible version may cause problems if the action requires specific Bun features. This input is typically not needed unless you're debugging something specific or have unique needs in your environment."
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
show_full_output:
|
||||||
|
description: "Show full JSON output from Claude Code. WARNING: This outputs ALL Claude messages including tool execution results which may contain secrets, API keys, or other sensitive information. These logs are publicly visible in GitHub Actions. Only enable for debugging in non-sensitive environments."
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
plugins:
|
||||||
|
description: "Newline-separated list of Claude Code plugin names to install (e.g., 'code-review@claude-code-plugins\nfeature-dev@claude-code-plugins')"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
plugin_marketplaces:
|
||||||
|
description: "Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., 'https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git')"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
outputs:
|
outputs:
|
||||||
conclusion:
|
conclusion:
|
||||||
@@ -99,7 +111,7 @@ runs:
|
|||||||
run: |
|
run: |
|
||||||
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
|
||||||
echo "Installing Claude Code..."
|
echo "Installing Claude Code..."
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.25
|
curl -fsSL https://claude.ai/install.sh | bash -s 2.0.37
|
||||||
else
|
else
|
||||||
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
|
||||||
# Add the directory containing the custom executable to PATH
|
# Add the directory containing the custom executable to PATH
|
||||||
@@ -126,6 +138,9 @@ runs:
|
|||||||
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
|
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
|
||||||
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
|
||||||
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}
|
||||||
|
INPUT_SHOW_FULL_OUTPUT: ${{ inputs.show_full_output }}
|
||||||
|
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||||
|
INPUT_PLUGIN_MARKETPLACES: ${{ inputs.plugin_marketplaces }}
|
||||||
|
|
||||||
# Provider configuration
|
# Provider configuration
|
||||||
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||||
|
|||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { preparePrompt } from "./prepare-prompt";
|
|||||||
import { runClaude } from "./run-claude";
|
import { runClaude } from "./run-claude";
|
||||||
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
|
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
|
||||||
import { validateEnvironmentVariables } from "./validate-env";
|
import { validateEnvironmentVariables } from "./validate-env";
|
||||||
|
import { installPlugins } from "./install-plugins";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
@@ -15,6 +16,13 @@ async function run() {
|
|||||||
undefined, // homeDir
|
undefined, // homeDir
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Install Claude Code plugins if specified
|
||||||
|
await installPlugins(
|
||||||
|
process.env.INPUT_PLUGIN_MARKETPLACES,
|
||||||
|
process.env.INPUT_PLUGINS,
|
||||||
|
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
|
||||||
|
);
|
||||||
|
|
||||||
const promptConfig = await preparePrompt({
|
const promptConfig = await preparePrompt({
|
||||||
prompt: process.env.INPUT_PROMPT || "",
|
prompt: process.env.INPUT_PROMPT || "",
|
||||||
promptFile: process.env.INPUT_PROMPT_FILE || "",
|
promptFile: process.env.INPUT_PROMPT_FILE || "",
|
||||||
@@ -33,6 +41,7 @@ async function run() {
|
|||||||
model: process.env.ANTHROPIC_MODEL,
|
model: process.env.ANTHROPIC_MODEL,
|
||||||
pathToClaudeCodeExecutable:
|
pathToClaudeCodeExecutable:
|
||||||
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
|
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE,
|
||||||
|
showFullOutput: process.env.INPUT_SHOW_FULL_OUTPUT,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(`Action failed with error: ${error}`);
|
core.setFailed(`Action failed with error: ${error}`);
|
||||||
|
|||||||
222
base-action/src/install-plugins.ts
Normal file
222
base-action/src/install-plugins.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { spawn, ChildProcess } from "child_process";
|
||||||
|
|
||||||
|
const PLUGIN_NAME_REGEX = /^[@a-zA-Z0-9_\-\/\.]+$/;
|
||||||
|
const MAX_PLUGIN_NAME_LENGTH = 512;
|
||||||
|
const PATH_TRAVERSAL_REGEX =
|
||||||
|
/\.\.\/|\/\.\.|\.\/|\/\.|(?:^|\/)\.\.$|(?:^|\/)\.$|\.\.(?![0-9])/;
|
||||||
|
const MARKETPLACE_URL_REGEX =
|
||||||
|
/^https:\/\/[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=%]+\.git$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a marketplace URL for security issues
|
||||||
|
* @param url - The marketplace URL to validate
|
||||||
|
* @throws {Error} If the URL is invalid
|
||||||
|
*/
|
||||||
|
function validateMarketplaceUrl(url: string): void {
|
||||||
|
const normalized = url.trim();
|
||||||
|
|
||||||
|
if (!normalized) {
|
||||||
|
throw new Error("Marketplace URL cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MARKETPLACE_URL_REGEX.test(normalized)) {
|
||||||
|
throw new Error(`Invalid marketplace URL format: ${url}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional check for valid URL structure
|
||||||
|
try {
|
||||||
|
new URL(normalized);
|
||||||
|
} catch {
|
||||||
|
throw new Error(`Invalid marketplace URL: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a plugin name for security issues
|
||||||
|
* @param pluginName - The plugin name to validate
|
||||||
|
* @throws {Error} If the plugin name is invalid
|
||||||
|
*/
|
||||||
|
function validatePluginName(pluginName: string): void {
|
||||||
|
// Normalize Unicode to prevent homoglyph attacks (e.g., fullwidth dots, Unicode slashes)
|
||||||
|
const normalized = pluginName.normalize("NFC");
|
||||||
|
|
||||||
|
if (normalized.length > MAX_PLUGIN_NAME_LENGTH) {
|
||||||
|
throw new Error(`Plugin name too long: ${normalized.substring(0, 50)}...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PLUGIN_NAME_REGEX.test(normalized)) {
|
||||||
|
throw new Error(`Invalid plugin name format: ${pluginName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent path traversal attacks with single efficient regex check
|
||||||
|
if (PATH_TRAVERSAL_REGEX.test(normalized)) {
|
||||||
|
throw new Error(`Invalid plugin name format: ${pluginName}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a newline-separated list of marketplace URLs and return an array of validated URLs
|
||||||
|
* @param marketplaces - Newline-separated list of marketplace Git URLs
|
||||||
|
* @returns Array of validated marketplace URLs (empty array if none provided)
|
||||||
|
*/
|
||||||
|
function parseMarketplaces(marketplaces?: string): string[] {
|
||||||
|
const trimmed = marketplaces?.trim();
|
||||||
|
|
||||||
|
if (!trimmed) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by newline and process each URL
|
||||||
|
return trimmed
|
||||||
|
.split("\n")
|
||||||
|
.map((url) => url.trim())
|
||||||
|
.filter((url) => {
|
||||||
|
if (url.length === 0) return false;
|
||||||
|
|
||||||
|
validateMarketplaceUrl(url);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a newline-separated list of plugin names and return an array of trimmed, non-empty plugin names
|
||||||
|
* Validates plugin names to prevent command injection and path traversal attacks
|
||||||
|
* Allows: letters, numbers, @, -, _, /, . (common npm/scoped package characters)
|
||||||
|
* Disallows: path traversal (../, ./), shell metacharacters, and consecutive dots
|
||||||
|
* @param plugins - Newline-separated list of plugin names, or undefined/empty to return empty array
|
||||||
|
* @returns Array of validated plugin names (empty array if none provided)
|
||||||
|
* @throws {Error} If any plugin name fails validation
|
||||||
|
*/
|
||||||
|
function parsePlugins(plugins?: string): string[] {
|
||||||
|
const trimmedPlugins = plugins?.trim();
|
||||||
|
|
||||||
|
if (!trimmedPlugins) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by newline and process each plugin
|
||||||
|
return trimmedPlugins
|
||||||
|
.split("\n")
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter((p) => {
|
||||||
|
if (p.length === 0) return false;
|
||||||
|
|
||||||
|
validatePluginName(p);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a Claude Code CLI command with proper error handling
|
||||||
|
* @param claudeExecutable - Path to the Claude executable
|
||||||
|
* @param args - Command arguments to pass to the executable
|
||||||
|
* @param errorContext - Context string for error messages (e.g., "Failed to install plugin 'foo'")
|
||||||
|
* @returns Promise that resolves when the command completes successfully
|
||||||
|
* @throws {Error} If the command fails to execute
|
||||||
|
*/
|
||||||
|
async function executeClaudeCommand(
|
||||||
|
claudeExecutable: string,
|
||||||
|
args: string[],
|
||||||
|
errorContext: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const childProcess: ChildProcess = spawn(claudeExecutable, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on("close", (code: number | null) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve();
|
||||||
|
} else if (code === null) {
|
||||||
|
reject(new Error(`${errorContext}: process terminated by signal`));
|
||||||
|
} else {
|
||||||
|
reject(new Error(`${errorContext} (exit code: ${code})`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
childProcess.on("error", (err: Error) => {
|
||||||
|
reject(new Error(`${errorContext}: ${err.message}`));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a single Claude Code plugin
|
||||||
|
* @param pluginName - The name of the plugin to install
|
||||||
|
* @param claudeExecutable - Path to the Claude executable
|
||||||
|
* @returns Promise that resolves when the plugin is installed successfully
|
||||||
|
* @throws {Error} If the plugin installation fails
|
||||||
|
*/
|
||||||
|
async function installPlugin(
|
||||||
|
pluginName: string,
|
||||||
|
claudeExecutable: string,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`Installing plugin: ${pluginName}`);
|
||||||
|
|
||||||
|
return executeClaudeCommand(
|
||||||
|
claudeExecutable,
|
||||||
|
["plugin", "install", pluginName],
|
||||||
|
`Failed to install plugin '${pluginName}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a Claude Code plugin marketplace
|
||||||
|
* @param claudeExecutable - Path to the Claude executable
|
||||||
|
* @param marketplaceUrl - The marketplace Git URL to add
|
||||||
|
* @returns Promise that resolves when the marketplace add command completes
|
||||||
|
* @throws {Error} If the command fails to execute
|
||||||
|
*/
|
||||||
|
async function addMarketplace(
|
||||||
|
claudeExecutable: string,
|
||||||
|
marketplaceUrl: string,
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`Adding marketplace: ${marketplaceUrl}`);
|
||||||
|
|
||||||
|
return executeClaudeCommand(
|
||||||
|
claudeExecutable,
|
||||||
|
["plugin", "marketplace", "add", marketplaceUrl],
|
||||||
|
`Failed to add marketplace '${marketplaceUrl}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs Claude Code plugins from a newline-separated list
|
||||||
|
* @param marketplacesInput - Newline-separated list of marketplace Git URLs
|
||||||
|
* @param pluginsInput - Newline-separated list of plugin names
|
||||||
|
* @param claudeExecutable - Path to the Claude executable (defaults to "claude")
|
||||||
|
* @returns Promise that resolves when all plugins are installed
|
||||||
|
* @throws {Error} If any plugin fails validation or installation (stops on first error)
|
||||||
|
*/
|
||||||
|
export async function installPlugins(
|
||||||
|
marketplacesInput?: string,
|
||||||
|
pluginsInput?: string,
|
||||||
|
claudeExecutable?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
// Resolve executable path with explicit fallback
|
||||||
|
const resolvedExecutable = claudeExecutable || "claude";
|
||||||
|
|
||||||
|
// Parse and add all marketplaces before installing plugins
|
||||||
|
const marketplaces = parseMarketplaces(marketplacesInput);
|
||||||
|
|
||||||
|
if (marketplaces.length > 0) {
|
||||||
|
console.log(`Adding ${marketplaces.length} marketplace(s)...`);
|
||||||
|
for (const marketplace of marketplaces) {
|
||||||
|
await addMarketplace(resolvedExecutable, marketplace);
|
||||||
|
console.log(`✓ Successfully added marketplace: ${marketplace}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No marketplaces specified, skipping marketplace setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
const plugins = parsePlugins(pluginsInput);
|
||||||
|
if (plugins.length > 0) {
|
||||||
|
console.log(`Installing ${plugins.length} plugin(s)...`);
|
||||||
|
for (const plugin of plugins) {
|
||||||
|
await installPlugin(plugin, resolvedExecutable);
|
||||||
|
console.log(`✓ Successfully installed: ${plugin}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("No plugins specified, skipping plugins installation");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,59 @@ const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`;
|
|||||||
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
||||||
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
|
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitizes JSON output to remove sensitive information when full output is disabled
|
||||||
|
* Returns a safe summary message or null if the message should be completely suppressed
|
||||||
|
*/
|
||||||
|
function sanitizeJsonOutput(
|
||||||
|
jsonObj: any,
|
||||||
|
showFullOutput: boolean,
|
||||||
|
): string | null {
|
||||||
|
if (showFullOutput) {
|
||||||
|
// In full output mode, return the full JSON
|
||||||
|
return JSON.stringify(jsonObj, null, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In non-full-output mode, provide minimal safe output
|
||||||
|
const type = jsonObj.type;
|
||||||
|
const subtype = jsonObj.subtype;
|
||||||
|
|
||||||
|
// System initialization - safe to show
|
||||||
|
if (type === "system" && subtype === "init") {
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
type: "system",
|
||||||
|
subtype: "init",
|
||||||
|
message: "Claude Code initialized",
|
||||||
|
model: jsonObj.model || "unknown",
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Result messages - Always show the final result
|
||||||
|
if (type === "result") {
|
||||||
|
// These messages contain the final result and should always be visible
|
||||||
|
return JSON.stringify(
|
||||||
|
{
|
||||||
|
type: "result",
|
||||||
|
subtype: jsonObj.subtype,
|
||||||
|
is_error: jsonObj.is_error,
|
||||||
|
duration_ms: jsonObj.duration_ms,
|
||||||
|
num_turns: jsonObj.num_turns,
|
||||||
|
total_cost_usd: jsonObj.total_cost_usd,
|
||||||
|
permission_denials: jsonObj.permission_denials,
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For any other message types, suppress completely in non-full-output mode
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export type ClaudeOptions = {
|
export type ClaudeOptions = {
|
||||||
claudeArgs?: string;
|
claudeArgs?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
@@ -24,6 +77,7 @@ export type ClaudeOptions = {
|
|||||||
appendSystemPrompt?: string;
|
appendSystemPrompt?: string;
|
||||||
claudeEnv?: string;
|
claudeEnv?: string;
|
||||||
fallbackModel?: string;
|
fallbackModel?: string;
|
||||||
|
showFullOutput?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PreparedConfig = {
|
type PreparedConfig = {
|
||||||
@@ -138,12 +192,27 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
pipeStream.destroy();
|
pipeStream.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Determine if full output should be shown
|
||||||
|
// Show full output if explicitly set to "true" OR if GitHub Actions debug mode is enabled
|
||||||
|
const isDebugMode = process.env.ACTIONS_STEP_DEBUG === "true";
|
||||||
|
let showFullOutput = options.showFullOutput === "true" || isDebugMode;
|
||||||
|
|
||||||
|
if (isDebugMode && options.showFullOutput !== "false") {
|
||||||
|
console.log("Debug mode detected - showing full output");
|
||||||
|
showFullOutput = true;
|
||||||
|
} else if (!showFullOutput) {
|
||||||
|
console.log("Running Claude Code (full output hidden for security)...");
|
||||||
|
console.log(
|
||||||
|
"Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Capture output for parsing execution metrics
|
// Capture output for parsing execution metrics
|
||||||
let output = "";
|
let output = "";
|
||||||
claudeProcess.stdout.on("data", (data) => {
|
claudeProcess.stdout.on("data", (data) => {
|
||||||
const text = data.toString();
|
const text = data.toString();
|
||||||
|
|
||||||
// Try to parse as JSON and pretty print if it's on a single line
|
// Try to parse as JSON and handle based on verbose setting
|
||||||
const lines = text.split("\n");
|
const lines = text.split("\n");
|
||||||
lines.forEach((line: string, index: number) => {
|
lines.forEach((line: string, index: number) => {
|
||||||
if (line.trim() === "") return;
|
if (line.trim() === "") return;
|
||||||
@@ -151,17 +220,24 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
try {
|
try {
|
||||||
// Check if this line is a JSON object
|
// Check if this line is a JSON object
|
||||||
const parsed = JSON.parse(line);
|
const parsed = JSON.parse(line);
|
||||||
const prettyJson = JSON.stringify(parsed, null, 2);
|
const sanitizedOutput = sanitizeJsonOutput(parsed, showFullOutput);
|
||||||
process.stdout.write(prettyJson);
|
|
||||||
if (index < lines.length - 1 || text.endsWith("\n")) {
|
if (sanitizedOutput) {
|
||||||
process.stdout.write("\n");
|
process.stdout.write(sanitizedOutput);
|
||||||
|
if (index < lines.length - 1 || text.endsWith("\n")) {
|
||||||
|
process.stdout.write("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Not a JSON object, print as is
|
// Not a JSON object
|
||||||
process.stdout.write(line);
|
if (showFullOutput) {
|
||||||
if (index < lines.length - 1 || text.endsWith("\n")) {
|
// In full output mode, print as is
|
||||||
process.stdout.write("\n");
|
process.stdout.write(line);
|
||||||
|
if (index < lines.length - 1 || text.endsWith("\n")) {
|
||||||
|
process.stdout.write("\n");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// In non-full-output mode, suppress non-JSON output
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
599
base-action/test/install-plugins.test.ts
Normal file
599
base-action/test/install-plugins.test.ts
Normal file
@@ -0,0 +1,599 @@
|
|||||||
|
#!/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" },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -116,7 +116,6 @@ The `additional_permissions` input allows Claude to access GitHub Actions workfl
|
|||||||
To allow Claude to view workflow run results, job logs, and CI status:
|
To allow Claude to view workflow run results, job logs, and CI status:
|
||||||
|
|
||||||
1. **Grant the necessary permission to your GitHub token**:
|
1. **Grant the necessary permission to your GitHub token**:
|
||||||
|
|
||||||
- When using the default `GITHUB_TOKEN`, add the `actions: read` permission to your workflow:
|
- When using the default `GITHUB_TOKEN`, add the `actions: read` permission to your workflow:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
@@ -130,7 +129,7 @@ To allow Claude to view workflow run results, job logs, and CI status:
|
|||||||
2. **Configure the action with additional permissions**:
|
2. **Configure the action with additional permissions**:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- uses: anthropics/claude-code-action@beta
|
- uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
additional_permissions: |
|
additional_permissions: |
|
||||||
@@ -162,7 +161,7 @@ jobs:
|
|||||||
claude-ci-helper:
|
claude-ci-helper:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: anthropics/claude-code-action@beta
|
- uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
additional_permissions: |
|
additional_permissions: |
|
||||||
|
|||||||
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>
|
||||||
@@ -228,12 +228,10 @@ jobs:
|
|||||||
The action now automatically detects the appropriate mode:
|
The action now automatically detects the appropriate mode:
|
||||||
|
|
||||||
1. **If `prompt` is provided** → Runs in **automation mode**
|
1. **If `prompt` is provided** → Runs in **automation mode**
|
||||||
|
|
||||||
- Executes immediately without waiting for @claude mentions
|
- Executes immediately without waiting for @claude mentions
|
||||||
- Perfect for scheduled tasks, PR automation, etc.
|
- Perfect for scheduled tasks, PR automation, etc.
|
||||||
|
|
||||||
2. **If no `prompt` but @claude is mentioned** → Runs in **interactive mode**
|
2. **If no `prompt` but @claude is mentioned** → Runs in **interactive mode**
|
||||||
|
|
||||||
- Waits for and responds to @claude mentions
|
- Waits for and responds to @claude mentions
|
||||||
- Creates tracking comments with progress
|
- Creates tracking comments with progress
|
||||||
|
|
||||||
|
|||||||
@@ -56,3 +56,31 @@ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|||||||
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
|
anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable!
|
||||||
claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable!
|
claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable!
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## ⚠️ Full Output Security Warning
|
||||||
|
|
||||||
|
The `show_full_output` option is **disabled by default** for security reasons. When enabled, it outputs ALL Claude Code messages including:
|
||||||
|
|
||||||
|
- Full outputs from tool executions (e.g., `ps`, `env`, file reads)
|
||||||
|
- API responses that may contain tokens or credentials
|
||||||
|
- File contents that may include secrets
|
||||||
|
- Command outputs that may expose sensitive system information
|
||||||
|
|
||||||
|
**These logs are publicly visible in GitHub Actions for public repositories!**
|
||||||
|
|
||||||
|
### Automatic Enabling in Debug Mode
|
||||||
|
|
||||||
|
Full output is **automatically enabled** when GitHub Actions debug mode is active (when `ACTIONS_STEP_DEBUG` secret is set to `true`). This helps with debugging but carries the same security risks.
|
||||||
|
|
||||||
|
### When to Enable Full Output
|
||||||
|
|
||||||
|
Only enable `show_full_output: true` or GitHub Actions debug mode when:
|
||||||
|
|
||||||
|
- Working in a private repository with controlled access
|
||||||
|
- Debugging issues in a non-production environment
|
||||||
|
- You have verified no secrets will be exposed in the output
|
||||||
|
- You understand the security implications
|
||||||
|
|
||||||
|
### Recommended Practice
|
||||||
|
|
||||||
|
For debugging, prefer using `show_full_output: false` (the default) and rely on Claude Code's sanitized output, which shows only essential information like errors and completion status without exposing sensitive data.
|
||||||
|
|||||||
@@ -20,10 +20,46 @@ If you prefer not to install the official Claude app, you can create your own Gi
|
|||||||
- Organization policies prevent installing third-party apps
|
- Organization policies prevent installing third-party apps
|
||||||
- You're using AWS Bedrock or Google Vertex AI
|
- You're using AWS Bedrock or Google Vertex AI
|
||||||
|
|
||||||
**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:**
|
1. **Create a new GitHub App:**
|
||||||
|
|
||||||
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
|
- Go to https://github.com/settings/apps (for personal apps) or your organization's settings
|
||||||
- Click "New GitHub App"
|
- Click "New GitHub App"
|
||||||
- Configure the app with these minimum permissions:
|
- Configure the app with these minimum permissions:
|
||||||
@@ -36,19 +72,16 @@ If you prefer not to install the official Claude app, you can create your own Gi
|
|||||||
- Create the app
|
- Create the app
|
||||||
|
|
||||||
2. **Generate and download a private key:**
|
2. **Generate and download a private key:**
|
||||||
|
|
||||||
- After creating the app, scroll down to "Private keys"
|
- After creating the app, scroll down to "Private keys"
|
||||||
- Click "Generate a private key"
|
- Click "Generate a private key"
|
||||||
- Download the `.pem` file (keep this secure!)
|
- Download the `.pem` file (keep this secure!)
|
||||||
|
|
||||||
3. **Install the app on your repository:**
|
3. **Install the app on your repository:**
|
||||||
|
|
||||||
- Go to the app's settings page
|
- Go to the app's settings page
|
||||||
- Click "Install App"
|
- Click "Install App"
|
||||||
- Select the repositories where you want to use Claude
|
- Select the repositories where you want to use Claude
|
||||||
|
|
||||||
4. **Add the app credentials to your repository secrets:**
|
4. **Add the app credentials to your repository secrets:**
|
||||||
|
|
||||||
- Go to your repository's Settings → Secrets and variables → Actions
|
- Go to your repository's Settings → Secrets and variables → Actions
|
||||||
- Add these secrets:
|
- Add these secrets:
|
||||||
- `APP_ID`: Your GitHub App's ID (found in the app settings)
|
- `APP_ID`: Your GitHub App's ID (found in the app settings)
|
||||||
@@ -76,7 +109,7 @@ If you prefer not to install the official Claude app, you can create your own Gi
|
|||||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||||
|
|
||||||
# Use Claude with your custom app's token
|
# Use Claude with your custom app's token
|
||||||
- uses: anthropics/claude-code-action@beta
|
- uses: anthropics/claude-code-action@v1
|
||||||
with:
|
with:
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
github_token: ${{ steps.app-token.outputs.token }}
|
github_token: ${{ steps.app-token.outputs.token }}
|
||||||
@@ -97,7 +130,6 @@ For more information on creating GitHub Apps, see the [GitHub documentation](htt
|
|||||||
To securely use your Anthropic API key:
|
To securely use your Anthropic API key:
|
||||||
|
|
||||||
1. Add your API key as a repository secret:
|
1. Add your API key as a repository secret:
|
||||||
|
|
||||||
- Go to your repository's Settings
|
- Go to your repository's Settings
|
||||||
- Navigate to "Secrets and variables" → "Actions"
|
- Navigate to "Secrets and variables" → "Actions"
|
||||||
- Click "New repository secret"
|
- Click "New repository secret"
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ jobs:
|
|||||||
# --max-turns 10
|
# --max-turns 10
|
||||||
# --model claude-4-0-sonnet-20250805
|
# --model claude-4-0-sonnet-20250805
|
||||||
|
|
||||||
|
# Optional: add custom plugin marketplaces
|
||||||
|
# plugin_marketplaces: "https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git"
|
||||||
|
# Optional: install Claude Code plugins
|
||||||
|
# plugins: "code-review@claude-code-plugins\nfeature-dev@claude-code-plugins"
|
||||||
|
|
||||||
# Optional: add custom trigger phrase (default: @claude)
|
# Optional: add custom trigger phrase (default: @claude)
|
||||||
# trigger_phrase: "/claude"
|
# trigger_phrase: "/claude"
|
||||||
# Optional: add assignee trigger for issues
|
# Optional: add assignee trigger for issues
|
||||||
@@ -73,6 +78,8 @@ jobs:
|
|||||||
| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" |
|
| `allowed_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_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 | "" |
|
| `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
|
### Deprecated Inputs
|
||||||
|
|
||||||
|
|||||||
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"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user