mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
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>
This commit is contained in:
@@ -41,6 +41,10 @@ inputs:
|
|||||||
description: "Claude Code settings as JSON string or path to settings JSON file"
|
description: "Claude Code settings as JSON string or path to settings JSON file"
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ""
|
||||||
|
plugins:
|
||||||
|
description: "Comma-separated list of Claude Code plugins to install (e.g., 'plugin-name1,plugin-name2')"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
# Auth configuration
|
# Auth configuration
|
||||||
anthropic_api_key:
|
anthropic_api_key:
|
||||||
@@ -208,6 +212,7 @@ runs:
|
|||||||
CLAUDE_CODE_ACTION: "1"
|
CLAUDE_CODE_ACTION: "1"
|
||||||
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
INPUT_PROMPT_FILE: ${{ runner.temp }}/claude-prompts/claude-prompt.txt
|
||||||
INPUT_SETTINGS: ${{ inputs.settings }}
|
INPUT_SETTINGS: ${{ inputs.settings }}
|
||||||
|
INPUT_PLUGINS: ${{ inputs.plugins }}
|
||||||
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
|
INPUT_CLAUDE_ARGS: ${{ steps.prepare.outputs.claude_args }}
|
||||||
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
||||||
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
||||||
|
|||||||
@@ -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,12 @@ async function run() {
|
|||||||
undefined, // homeDir
|
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({
|
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 || "",
|
||||||
|
|||||||
80
base-action/src/install-plugins.ts
Normal file
80
base-action/src/install-plugins.ts
Normal 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");
|
||||||
|
}
|
||||||
84
base-action/test/install-plugins.test.ts
Normal file
84
base-action/test/install-plugins.test.ts
Normal 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"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user