mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
feat: add claudeArgs input for direct CLI argument passing
- Add claude_args input to action.yml for flexible CLI control - Parse arguments with industry-standard shell-quote library - Maintain proper argument order: -p [claudeArgs] [legacy] [BASE_ARGS] - Keep tag mode defaults (needed for functionality) - Agent mode has no defaults (full user control) - Add comprehensive tests for new functionality - Add example workflow showing usage
This commit is contained in:
@@ -10,7 +10,8 @@ const execAsync = promisify(exec);
|
||||
|
||||
const PIPE_PATH = `${process.env.RUNNER_TEMP}/claude_prompt_pipe`;
|
||||
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
||||
const BASE_ARGS = ["-p", "--verbose", "--output-format", "stream-json"];
|
||||
// These base args are always appended at the end
|
||||
const BASE_ARGS = ["--verbose", "--output-format", "stream-json"];
|
||||
|
||||
export type ClaudeOptions = {
|
||||
allowedTools?: string;
|
||||
@@ -68,10 +69,21 @@ export function prepareRunConfig(
|
||||
promptPath: string,
|
||||
options: ClaudeOptions,
|
||||
): PreparedConfig {
|
||||
// Start with base args
|
||||
const claudeArgs = [...BASE_ARGS];
|
||||
|
||||
// Add specific options first (these can be overridden by claudeArgs)
|
||||
// Build arguments in correct order:
|
||||
// 1. -p flag for prompt via pipe
|
||||
const claudeArgs = ["-p"];
|
||||
|
||||
// 2. User's custom arguments (can override defaults)
|
||||
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
|
||||
const parsed = parseShellArgs(options.claudeArgs);
|
||||
const customArgs = parsed.filter(
|
||||
(arg): arg is string => typeof arg === "string",
|
||||
);
|
||||
claudeArgs.push(...customArgs);
|
||||
}
|
||||
|
||||
// 3. Legacy specific options for backward compatibility
|
||||
// These will eventually be removed in favor of claudeArgs
|
||||
if (options.allowedTools) {
|
||||
claudeArgs.push("--allowedTools", options.allowedTools);
|
||||
}
|
||||
@@ -79,12 +91,6 @@ export function prepareRunConfig(
|
||||
claudeArgs.push("--disallowedTools", options.disallowedTools);
|
||||
}
|
||||
if (options.maxTurns) {
|
||||
const maxTurnsNum = parseInt(options.maxTurns, 10);
|
||||
if (isNaN(maxTurnsNum) || maxTurnsNum <= 0) {
|
||||
throw new Error(
|
||||
`maxTurns must be a positive number, got: ${options.maxTurns}`,
|
||||
);
|
||||
}
|
||||
claudeArgs.push("--max-turns", options.maxTurns);
|
||||
}
|
||||
if (options.mcpConfig) {
|
||||
@@ -102,6 +108,11 @@ export function prepareRunConfig(
|
||||
if (options.model) {
|
||||
claudeArgs.push("--model", options.model);
|
||||
}
|
||||
|
||||
// 4. Base args always at the end
|
||||
claudeArgs.push(...BASE_ARGS);
|
||||
|
||||
// Validate timeout if provided (affects process wrapper, not Claude)
|
||||
if (options.timeoutMinutes) {
|
||||
const timeoutMinutesNum = parseInt(options.timeoutMinutes, 10);
|
||||
if (isNaN(timeoutMinutesNum) || timeoutMinutesNum <= 0) {
|
||||
@@ -111,15 +122,6 @@ export function prepareRunConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and append custom arguments (these can override the above)
|
||||
if (options.claudeArgs && options.claudeArgs.trim() !== "") {
|
||||
const parsed = parseShellArgs(options.claudeArgs);
|
||||
const customArgs = parsed.filter(
|
||||
(arg): arg is string => typeof arg === "string",
|
||||
);
|
||||
claudeArgs.push(...customArgs);
|
||||
}
|
||||
|
||||
// Parse custom environment variables
|
||||
const customEnv = parseCustomEnvVars(options.claudeEnv);
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ describe("prepareRunConfig", () => {
|
||||
const options: ClaudeOptions = {};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs.slice(0, 4)).toEqual([
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
@@ -125,13 +125,13 @@ describe("prepareRunConfig", () => {
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--allowedTools",
|
||||
"Bash,Read",
|
||||
"--max-turns",
|
||||
"3",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -149,9 +149,6 @@ describe("prepareRunConfig", () => {
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--allowedTools",
|
||||
"Bash,Read",
|
||||
"--disallowedTools",
|
||||
@@ -166,10 +163,13 @@ describe("prepareRunConfig", () => {
|
||||
"Be concise",
|
||||
"--fallback-model",
|
||||
"claude-sonnet-4-20250514",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
describe("maxTurns validation", () => {
|
||||
describe("maxTurns handling", () => {
|
||||
test("should accept valid maxTurns value", () => {
|
||||
const options: ClaudeOptions = { maxTurns: "5" };
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
@@ -177,25 +177,28 @@ describe("prepareRunConfig", () => {
|
||||
expect(prepared.claudeArgs).toContain("5");
|
||||
});
|
||||
|
||||
test("should throw error for non-numeric maxTurns", () => {
|
||||
test("should pass through non-numeric maxTurns without validation (v1.0)", () => {
|
||||
const options: ClaudeOptions = { maxTurns: "abc" };
|
||||
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
|
||||
"maxTurns must be a positive number, got: abc",
|
||||
);
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
// v1.0: No validation - let Claude handle it
|
||||
expect(prepared.claudeArgs).toContain("--max-turns");
|
||||
expect(prepared.claudeArgs).toContain("abc");
|
||||
});
|
||||
|
||||
test("should throw error for negative maxTurns", () => {
|
||||
test("should pass through negative maxTurns without validation (v1.0)", () => {
|
||||
const options: ClaudeOptions = { maxTurns: "-1" };
|
||||
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
|
||||
"maxTurns must be a positive number, got: -1",
|
||||
);
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
// v1.0: No validation - let Claude handle it
|
||||
expect(prepared.claudeArgs).toContain("--max-turns");
|
||||
expect(prepared.claudeArgs).toContain("-1");
|
||||
});
|
||||
|
||||
test("should throw error for zero maxTurns", () => {
|
||||
test("should pass through zero maxTurns without validation (v1.0)", () => {
|
||||
const options: ClaudeOptions = { maxTurns: "0" };
|
||||
expect(() => prepareRunConfig("/tmp/test-prompt.txt", options)).toThrow(
|
||||
"maxTurns must be a positive number, got: 0",
|
||||
);
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
// v1.0: No validation - let Claude handle it
|
||||
expect(prepared.claudeArgs).toContain("--max-turns");
|
||||
expect(prepared.claudeArgs).toContain("0");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -229,6 +232,75 @@ describe("prepareRunConfig", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("claudeArgs handling", () => {
|
||||
test("should parse and include custom claude arguments", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--max-turns 10 --model claude-3-opus-20240229",
|
||||
};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--max-turns",
|
||||
"10",
|
||||
"--model",
|
||||
"claude-3-opus-20240229",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle claudeArgs with legacy options", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "--max-turns 10",
|
||||
allowedTools: "Bash,Read",
|
||||
};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--max-turns",
|
||||
"10",
|
||||
"--allowedTools",
|
||||
"Bash,Read",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle empty claudeArgs", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: "",
|
||||
};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
|
||||
test("should handle claudeArgs with quoted strings", () => {
|
||||
const options: ClaudeOptions = {
|
||||
claudeArgs: '--system-prompt "You are a helpful assistant"',
|
||||
};
|
||||
const prepared = prepareRunConfig("/tmp/test-prompt.txt", options);
|
||||
|
||||
expect(prepared.claudeArgs).toEqual([
|
||||
"-p",
|
||||
"--system-prompt",
|
||||
"You are a helpful assistant",
|
||||
"--verbose",
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom environment variables", () => {
|
||||
test("should parse empty claudeEnv correctly", () => {
|
||||
const options: ClaudeOptions = { claudeEnv: "" };
|
||||
|
||||
Reference in New Issue
Block a user