Compare commits

..

2 Commits

Author SHA1 Message Date
Wanghong Yuan MacBook
23e406ca24 Add plugins input to base-action/action.yml
Adds plugin installation support to the standalone base-action.

Changes:
- Added `plugins` input definition (comma-separated list)
- Added INPUT_PLUGINS environment variable to pass to src/index.ts

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 23:07:28 -07:00
Wanghong Yuan MacBook
f5f27d4716 Add plugins input to GitHub Action
This commit adds support for installing Claude Code plugins via a new `plugins` input parameter.

Changes:
- Added `plugins` input to action.yml (comma-separated list)
- Created `install-plugins.ts` with plugin installation logic
- Added comprehensive tests in `install-plugins.test.ts`
- Updated base-action index.ts to call plugin installation
- Plugins are installed after settings setup but before Claude execution

Usage example:
```yaml
- uses: anthropic-ai/claude-code-action@main
  with:
    plugins: "feature-dev,test-coverage-reviewer"
```

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 22:32:57 -07:00
7 changed files with 185 additions and 72 deletions

View File

@@ -1,57 +0,0 @@
name: Claude Code Review
on:
pull_request:
types: [opened, synchronize]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
# - "src/**/*.js"
# - "src/**/*.jsx"
jobs:
claude-review:
# Optional: Filter by PR author
# if: |
# github.event.pull_request.user.login == 'external-contributor' ||
# github.event.pull_request.user.login == 'new-developer' ||
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns
- Test coverage
Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
claude_args: '--allowed-tools "Bash(gh issue view:*),Bash(gh search:*),Bash(gh issue list:*),Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*),Bash(gh pr list:*)"'

View File

@@ -23,10 +23,9 @@ jobs:
pull-requests: read
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 1
@@ -35,16 +34,6 @@ jobs:
uses: anthropics/claude-code-action@v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Give a custom prompt to Claude. If this is not specified, Claude will perform the instructions specified in the comment that tagged it.
# prompt: 'Update the pull request description to include a summary of changes.'
# Optional: Add claude_args to customize behavior and configuration
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
# claude_args: '--allowed-tools Bash(gh pr:*)'
claude_args: |
--allowedTools "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
--model "claude-opus-4-1-20250805"

View File

@@ -41,6 +41,10 @@ inputs:
description: "Claude Code settings as JSON string or path to settings JSON file"
required: false
default: ""
plugins:
description: "Comma-separated list of Claude Code plugins to install (e.g., 'plugin-name1,plugin-name2')"
required: false
default: ""
# Auth configuration
anthropic_api_key:
@@ -208,6 +212,7 @@ runs:
CLAUDE_CODE_ACTION: "1"
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}

View File

@@ -18,6 +18,10 @@ inputs:
description: "Claude Code settings as JSON string or path to settings JSON file"
required: false
default: ""
plugins:
description: "Comma-separated list of Claude Code plugins to install (e.g., 'plugin-name1,plugin-name2')"
required: false
default: ""
# Action settings
claude_args:
@@ -123,6 +127,7 @@ runs:
INPUT_PROMPT: ${{ inputs.prompt }}
INPUT_PROMPT_FILE: ${{ inputs.prompt_file }}
INPUT_SETTINGS: ${{ inputs.settings }}
INPUT_PLUGINS: ${{ inputs.plugins }}
INPUT_CLAUDE_ARGS: ${{ inputs.claude_args }}
INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE: ${{ inputs.path_to_claude_code_executable }}
INPUT_PATH_TO_BUN_EXECUTABLE: ${{ inputs.path_to_bun_executable }}

View File

@@ -5,6 +5,7 @@ import { preparePrompt } from "./prepare-prompt";
import { runClaude } from "./run-claude";
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
import { validateEnvironmentVariables } from "./validate-env";
import { installPlugins } from "./install-plugins";
async function run() {
try {
@@ -15,6 +16,12 @@ async function run() {
undefined, // homeDir
);
// Install plugins if specified
await installPlugins(
process.env.INPUT_PLUGINS,
process.env.INPUT_PATH_TO_CLAUDE_CODE_EXECUTABLE || "claude",
);
const promptConfig = await preparePrompt({
prompt: process.env.INPUT_PROMPT || "",
promptFile: process.env.INPUT_PROMPT_FILE || "",

View File

@@ -0,0 +1,80 @@
#!/usr/bin/env bun
import { spawn } from "child_process";
// Declare console as global for TypeScript
declare const console: {
log: (message: string) => void;
error: (message: string) => void;
};
/**
* Parses a comma-separated list of plugin names and returns an array of trimmed plugin names
*/
export function parsePlugins(pluginsInput: string | undefined): string[] {
if (!pluginsInput || pluginsInput.trim() === "") {
return [];
}
return pluginsInput
.split(",")
.map((plugin) => plugin.trim())
.filter((plugin) => plugin.length > 0);
}
/**
* Installs a single Claude Code plugin
*/
export async function installPlugin(
pluginName: string,
claudeExecutable: string = "claude",
): Promise<void> {
return new Promise((resolve, reject) => {
const process = spawn(claudeExecutable, ["plugin", "install", pluginName], {
stdio: "inherit",
});
process.on("close", (code: number | null) => {
if (code === 0) {
resolve();
} else {
reject(
new Error(
`Failed to install plugin '${pluginName}' (exit code: ${code})`,
),
);
}
});
process.on("error", (err: Error) => {
reject(
new Error(`Failed to install plugin '${pluginName}': ${err.message}`),
);
});
});
}
/**
* Installs Claude Code plugins from a comma-separated list
*/
export async function installPlugins(
pluginsInput: string | undefined,
claudeExecutable: string = "claude",
): Promise<void> {
const plugins = parsePlugins(pluginsInput);
if (plugins.length === 0) {
console.log("No plugins to install");
return;
}
console.log(`Installing ${plugins.length} plugin(s)...`);
for (const plugin of plugins) {
console.log(`Installing plugin: ${plugin}`);
await installPlugin(plugin, claudeExecutable);
console.log(`✓ Successfully installed: ${plugin}`);
}
console.log("All plugins installed successfully");
}

View File

@@ -0,0 +1,84 @@
#!/usr/bin/env bun
import { describe, test, expect } from "bun:test";
import { parsePlugins } from "../src/install-plugins";
describe("parsePlugins", () => {
test("should return empty array for undefined input", () => {
expect(parsePlugins(undefined)).toEqual([]);
});
test("should return empty array for empty string", () => {
expect(parsePlugins("")).toEqual([]);
});
test("should return empty array for whitespace-only string", () => {
expect(parsePlugins(" \n\t ")).toEqual([]);
});
test("should parse single plugin", () => {
expect(parsePlugins("feature-dev")).toEqual(["feature-dev"]);
});
test("should parse multiple plugins", () => {
expect(parsePlugins("feature-dev,test-coverage-reviewer")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});
test("should trim whitespace around plugin names", () => {
expect(parsePlugins(" feature-dev , test-coverage-reviewer ")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});
test("should handle spaces between commas", () => {
expect(
parsePlugins(
"feature-dev, test-coverage-reviewer, code-quality-reviewer",
),
).toEqual([
"feature-dev",
"test-coverage-reviewer",
"code-quality-reviewer",
]);
});
test("should filter out empty values from consecutive commas", () => {
expect(parsePlugins("feature-dev,,test-coverage-reviewer")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});
test("should handle trailing comma", () => {
expect(parsePlugins("feature-dev,test-coverage-reviewer,")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});
test("should handle leading comma", () => {
expect(parsePlugins(",feature-dev,test-coverage-reviewer")).toEqual([
"feature-dev",
"test-coverage-reviewer",
]);
});
test("should handle plugins with special characters", () => {
expect(parsePlugins("@scope/plugin-name,plugin-name-2")).toEqual([
"@scope/plugin-name",
"plugin-name-2",
]);
});
test("should handle complex whitespace patterns", () => {
expect(
parsePlugins(
"\n feature-dev \n,\t test-coverage-reviewer\t, code-quality \n",
),
).toEqual(["feature-dev", "test-coverage-reviewer", "code-quality"]);
});
});