mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 14:24:13 +08:00
* refactor: remove CLI path, use Agent SDK exclusively - Remove CLI-based Claude execution in favor of Agent SDK - Delete prepareRunConfig, parseAndSetSessionId, parseAndSetStructuredOutputs functions - Remove named pipe IPC and sanitizeJsonOutput helper - Remove test-agent-sdk job from test-base-action workflow (SDK is now default) - Delete run-claude.test.ts and structured-output.test.ts (testing removed CLI code) - Update CLAUDE.md to remove named pipe references Co-Authored-By: Claude <noreply@anthropic.com> Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 2 Claude-Permission-Prompts: 1 Claude-Escapes: 0 Claude-Plan: <claude-plan> # Plan: Remove Non-Agent SDK Code Path ## Overview Since `use_agent_sdk` defaults to `true`, remove the legacy CLI code path entirely from `base-action/src/run-claude.ts`. ## Files to Modify ### 1. `base-action/src/run-claude.ts` - Main Cleanup **Remove imports:** - `exec` from `child_process` - `promisify` from `util` - `unlink`, `writeFile`, `stat` from `fs/promises` (keep `readFile` - check if needed) - `createWriteStream` from `fs` - `spawn` from `child_process` - `parseShellArgs` from `shell-quote` (still used in `parse-sdk-options.ts`, keep package) **Remove constants:** - `execAsync` - `PIPE_PATH` - `EXECUTION_FILE` (defined in both files, keep in SDK file) - `BASE_ARGS` **Remove types:** - `PreparedConfig` type (lines 85-89) - only used by `prepareRunConfig()` **Remove functions:** - `sanitizeJsonOutput()` (lines 21-68) - `prepareRunConfig()` (lines 91-125) - also remove export - `parseAndSetSessionId()` (lines 131-155) - also remove export - `parseAndSetStructuredOutputs()` (lines 162-197) - also remove export **Simplify `runClaude()`:** - Remove `useAgentSdk` flag check and logging (lines 200-204) - Remove the `if (useAgentSdk)` block, make SDK call direct - Remove entire CLI path (lines 211-438) - Resulting function becomes just: ```typescript export async function runClaude(promptPath: string, options: ClaudeOptions) { const parsedOptions = parseSdkOptions(options); return runClaudeWithSdk(promptPath, parsedOptions); } ``` ### 2. Delete Test Files **`base-action/test/run-claude.test.ts`:** - Delete entire file (only tests `prepareRunConfig()`) **`base-action/test/structured-output.test.ts`:** - Delete entire file (only tests `parseAndSetStructuredOutputs()` and `parseAndSetSessionId()`) ### 3. Workflow Update **`.github/workflows/test-base-action.yml`:** - Remove `test-agent-sdk` job (lines 120-176) - redundant now ### 4. Documentation Update **`base-action/CLAUDE.md`:** - Line 30: Remove "- Named pipes for IPC between prompt input and Claude process" - Line 57: Remove "- Uses `mkfifo` to create named pipes for prompt input" ## Verification 1. Run `bun run typecheck` to ensure no type errors 2. Run `bun test` to ensure remaining tests pass 3. Run `bun run format` to fix any formatting issues </claude-plan> * fix: address PR review comments - Add session_id output handling in run-claude-sdk.ts (critical) - Remove unused claudeEnv parameter from ClaudeOptions and index.ts - Update stale CLI path comment in parse-sdk-options.ts Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%) Claude-Steers: 0 Claude-Permission-Prompts: 0 Claude-Escapes: 0 Claude-Plan: <claude-plan> # Plan: Remove Non-Agent SDK Code Path ## Overview Since `use_agent_sdk` defaults to `true`, remove the legacy CLI code path entirely from `base-action/src/run-claude.ts`. ## Files to Modify ### 1. `base-action/src/run-claude.ts` - Main Cleanup **Remove imports:** - `exec` from `child_process` - `promisify` from `util` - `unlink`, `writeFile`, `stat` from `fs/promises` (keep `readFile` - check if needed) - `createWriteStream` from `fs` - `spawn` from `child_process` - `parseShellArgs` from `shell-quote` (still used in `parse-sdk-options.ts`, keep package) **Remove constants:** - `execAsync` - `PIPE_PATH` - `EXECUTION_FILE` (defined in both files, keep in SDK file) - `BASE_ARGS` **Remove types:** - `PreparedConfig` type (lines 85-89) - only used by `prepareRunConfig()` **Remove functions:** - `sanitizeJsonOutput()` (lines 21-68) - `prepareRunConfig()` (lines 91-125) - also remove export - `parseAndSetSessionId()` (lines 131-155) - also remove export - `parseAndSetStructuredOutputs()` (lines 162-197) - also remove export **Simplify `runClaude()`:** - Remove `useAgentSdk` flag check and logging (lines 200-204) - Remove the `if (useAgentSdk)` block, make SDK call direct - Remove entire CLI path (lines 211-438) - Resulting function becomes just: ```typescript export async function runClaude(promptPath: string, options: ClaudeOptions) { const parsedOptions = parseSdkOptions(options); return runClaudeWithSdk(promptPath, parsedOptions); } ``` ### 2. Delete Test Files **`base-action/test/run-claude.test.ts`:** - Delete entire file (only tests `prepareRunConfig()`) **`base-action/test/structured-output.test.ts`:** - Delete entire file (only tests `parseAndSetStructuredOutputs()` and `parseAndSetSessionId()`) ### 3. Workflow Update **`.github/workflows/test-base-action.yml`:** - Remove `test-agent-sdk` job (lines 120-176) - redundant now ### 4. Documentation Update **`base-action/CLAUDE.md`:** - Line 30: Remove "- Named pipes for IPC between prompt input and Claude process" - Line 57: Remove "- Uses `mkfifo` to create named pipes for prompt input" ## Verification 1. Run `bun run typecheck` to ensure no type errors 2. Run `bun test` to ensure remaining tests pass 3. Run `bun run format` to fix any formatting issues </claude-plan>
229 lines
6.8 KiB
TypeScript
229 lines
6.8 KiB
TypeScript
import * as core from "@actions/core";
|
|
import { readFile, writeFile, access } from "fs/promises";
|
|
import { dirname, join } from "path";
|
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
import type {
|
|
SDKMessage,
|
|
SDKResultMessage,
|
|
SDKUserMessage,
|
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
import type { ParsedSdkOptions } from "./parse-sdk-options";
|
|
|
|
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
|
|
|
/** Filename for the user request file, written by prompt generation */
|
|
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
|
|
|
/**
|
|
* Check if a file exists
|
|
*/
|
|
async function fileExists(path: string): Promise<boolean> {
|
|
try {
|
|
await access(path);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a prompt configuration for the SDK.
|
|
* If a user request file exists alongside the prompt file, returns a multi-block
|
|
* SDKUserMessage that enables slash command processing in the CLI.
|
|
* Otherwise, returns the prompt as a simple string.
|
|
*/
|
|
async function createPromptConfig(
|
|
promptPath: string,
|
|
showFullOutput: boolean,
|
|
): Promise<string | AsyncIterable<SDKUserMessage>> {
|
|
const promptContent = await readFile(promptPath, "utf-8");
|
|
|
|
// Check for user request file in the same directory
|
|
const userRequestPath = join(dirname(promptPath), USER_REQUEST_FILENAME);
|
|
const hasUserRequest = await fileExists(userRequestPath);
|
|
|
|
if (!hasUserRequest) {
|
|
// No user request file - use simple string prompt
|
|
return promptContent;
|
|
}
|
|
|
|
// User request file exists - create multi-block message
|
|
const userRequest = await readFile(userRequestPath, "utf-8");
|
|
if (showFullOutput) {
|
|
console.log("Using multi-block message with user request:", userRequest);
|
|
} else {
|
|
console.log("Using multi-block message with user request (content hidden)");
|
|
}
|
|
|
|
// Create an async generator that yields a single multi-block message
|
|
// The context/instructions go first, then the user's actual request last
|
|
// This allows the CLI to detect and process slash commands in the user request
|
|
async function* createMultiBlockMessage(): AsyncGenerator<SDKUserMessage> {
|
|
yield {
|
|
type: "user",
|
|
session_id: "",
|
|
message: {
|
|
role: "user",
|
|
content: [
|
|
{ type: "text", text: promptContent }, // Instructions + GitHub context
|
|
{ type: "text", text: userRequest }, // User's request (may be a slash command)
|
|
],
|
|
},
|
|
parent_tool_use_id: null,
|
|
};
|
|
}
|
|
|
|
return createMultiBlockMessage();
|
|
}
|
|
|
|
/**
|
|
* Sanitizes SDK output to match CLI sanitization behavior
|
|
*/
|
|
function sanitizeSdkOutput(
|
|
message: SDKMessage,
|
|
showFullOutput: boolean,
|
|
): string | null {
|
|
if (showFullOutput) {
|
|
return JSON.stringify(message, null, 2);
|
|
}
|
|
|
|
// System initialization - safe to show
|
|
if (message.type === "system" && message.subtype === "init") {
|
|
return JSON.stringify(
|
|
{
|
|
type: "system",
|
|
subtype: "init",
|
|
message: "Claude Code initialized",
|
|
model: "model" in message ? message.model : "unknown",
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
|
|
// Result messages - show sanitized summary
|
|
if (message.type === "result") {
|
|
const resultMsg = message as SDKResultMessage;
|
|
return JSON.stringify(
|
|
{
|
|
type: "result",
|
|
subtype: resultMsg.subtype,
|
|
is_error: resultMsg.is_error,
|
|
duration_ms: resultMsg.duration_ms,
|
|
num_turns: resultMsg.num_turns,
|
|
total_cost_usd: resultMsg.total_cost_usd,
|
|
permission_denials: resultMsg.permission_denials,
|
|
},
|
|
null,
|
|
2,
|
|
);
|
|
}
|
|
|
|
// Suppress other message types in non-full-output mode
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Run Claude using the Agent SDK
|
|
*/
|
|
export async function runClaudeWithSdk(
|
|
promptPath: string,
|
|
{ sdkOptions, showFullOutput, hasJsonSchema }: ParsedSdkOptions,
|
|
): Promise<void> {
|
|
// Create prompt configuration - may be a string or multi-block message
|
|
const prompt = await createPromptConfig(promptPath, showFullOutput);
|
|
|
|
if (!showFullOutput) {
|
|
console.log(
|
|
"Running Claude Code via SDK (full output hidden for security)...",
|
|
);
|
|
console.log(
|
|
"Rerun in debug mode or enable `show_full_output: true` in your workflow file for full output.",
|
|
);
|
|
}
|
|
|
|
console.log(`Running Claude with prompt from file: ${promptPath}`);
|
|
// Log SDK options without env (which could contain sensitive data)
|
|
const { env, ...optionsToLog } = sdkOptions;
|
|
console.log("SDK options:", JSON.stringify(optionsToLog, null, 2));
|
|
|
|
const messages: SDKMessage[] = [];
|
|
let resultMessage: SDKResultMessage | undefined;
|
|
|
|
try {
|
|
for await (const message of query({ prompt, options: sdkOptions })) {
|
|
messages.push(message);
|
|
|
|
const sanitized = sanitizeSdkOutput(message, showFullOutput);
|
|
if (sanitized) {
|
|
console.log(sanitized);
|
|
}
|
|
|
|
if (message.type === "result") {
|
|
resultMessage = message as SDKResultMessage;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("SDK execution error:", error);
|
|
core.setOutput("conclusion", "failure");
|
|
process.exit(1);
|
|
}
|
|
|
|
// Write execution file
|
|
try {
|
|
await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2));
|
|
console.log(`Log saved to ${EXECUTION_FILE}`);
|
|
core.setOutput("execution_file", EXECUTION_FILE);
|
|
} catch (error) {
|
|
core.warning(`Failed to write execution file: ${error}`);
|
|
}
|
|
|
|
// Extract and set session_id from system.init message
|
|
const initMessage = messages.find(
|
|
(m) => m.type === "system" && "subtype" in m && m.subtype === "init",
|
|
);
|
|
if (initMessage && "session_id" in initMessage && initMessage.session_id) {
|
|
core.setOutput("session_id", initMessage.session_id);
|
|
core.info(`Set session_id: ${initMessage.session_id}`);
|
|
}
|
|
|
|
if (!resultMessage) {
|
|
core.setOutput("conclusion", "failure");
|
|
core.error("No result message received from Claude");
|
|
process.exit(1);
|
|
}
|
|
|
|
const isSuccess = resultMessage.subtype === "success";
|
|
core.setOutput("conclusion", isSuccess ? "success" : "failure");
|
|
|
|
// Handle structured output
|
|
if (hasJsonSchema) {
|
|
if (
|
|
isSuccess &&
|
|
"structured_output" in resultMessage &&
|
|
resultMessage.structured_output
|
|
) {
|
|
const structuredOutputJson = JSON.stringify(
|
|
resultMessage.structured_output,
|
|
);
|
|
core.setOutput("structured_output", structuredOutputJson);
|
|
core.info(
|
|
`Set structured_output with ${Object.keys(resultMessage.structured_output as object).length} field(s)`,
|
|
);
|
|
} else {
|
|
core.setFailed(
|
|
`--json-schema was provided but Claude did not return structured_output. Result subtype: ${resultMessage.subtype}`,
|
|
);
|
|
core.setOutput("conclusion", "failure");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
if (!isSuccess) {
|
|
if ("errors" in resultMessage && resultMessage.errors) {
|
|
core.error(`Execution failed: ${resultMessage.errors.join(", ")}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|