mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-07-27 22:38:30 +08:00
fix: write execution file when SDK throws (#1255)
This commit is contained in:
parent
0756f6ef2b
commit
1450f658d3
42
base-action/src/execution-file.ts
Normal file
42
base-action/src/execution-file.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { writeFile } from "fs/promises";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
const EXECUTION_FILENAME = "claude-execution-output.json";
|
||||||
|
|
||||||
|
export function getExecutionFilePath(): string | undefined {
|
||||||
|
if (!process.env.RUNNER_TEMP) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return join(process.env.RUNNER_TEMP, EXECUTION_FILENAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function writeExecutionFile(
|
||||||
|
messages: unknown[],
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const executionFile = getExecutionFilePath();
|
||||||
|
if (!executionFile) {
|
||||||
|
core.warning("Failed to write execution file: RUNNER_TEMP is not set");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await writeFile(executionFile, JSON.stringify(messages, null, 2));
|
||||||
|
console.log(`Log saved to ${executionFile}`);
|
||||||
|
return executionFile;
|
||||||
|
} catch (error) {
|
||||||
|
core.warning(`Failed to write execution file: ${error}`);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setExecutionFileOutputIfPresent(): string | undefined {
|
||||||
|
const executionFile = getExecutionFilePath();
|
||||||
|
if (!executionFile || !existsSync(executionFile)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput("execution_file", executionFile);
|
||||||
|
return executionFile;
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ 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";
|
import { installPlugins } from "./install-plugins";
|
||||||
|
import { setExecutionFileOutputIfPresent } from "./execution-file";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
@ -62,6 +63,7 @@ async function run() {
|
|||||||
core.setOutput("structured_output", result.structuredOutput);
|
core.setOutput("structured_output", result.structuredOutput);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
setExecutionFileOutputIfPresent();
|
||||||
core.setFailed(`Action failed with error: ${error}`);
|
core.setFailed(`Action failed with error: ${error}`);
|
||||||
core.setOutput("conclusion", "failure");
|
core.setOutput("conclusion", "failure");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { readFile, writeFile, access } from "fs/promises";
|
import { readFile, access } from "fs/promises";
|
||||||
import { dirname, join } from "path";
|
import { dirname, join } from "path";
|
||||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import type {
|
import type {
|
||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
SDKUserMessage,
|
SDKUserMessage,
|
||||||
} from "@anthropic-ai/claude-agent-sdk";
|
} from "@anthropic-ai/claude-agent-sdk";
|
||||||
import type { ParsedSdkOptions } from "./parse-sdk-options";
|
import type { ParsedSdkOptions } from "./parse-sdk-options";
|
||||||
|
import { writeExecutionFile } from "./execution-file";
|
||||||
|
|
||||||
export type ClaudeRunResult = {
|
export type ClaudeRunResult = {
|
||||||
executionFile?: string;
|
executionFile?: string;
|
||||||
@ -16,8 +17,6 @@ export type ClaudeRunResult = {
|
|||||||
structuredOutput?: string;
|
structuredOutput?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const EXECUTION_FILE = `${process.env.RUNNER_TEMP}/claude-execution-output.json`;
|
|
||||||
|
|
||||||
/** Filename for the user request file, written by prompt generation */
|
/** Filename for the user request file, written by prompt generation */
|
||||||
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
||||||
|
|
||||||
@ -172,6 +171,7 @@ export async function runClaudeWithSdk(
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("SDK execution error:", error);
|
console.error("SDK execution error:", error);
|
||||||
|
await writeExecutionFile(messages);
|
||||||
throw new Error(`SDK execution error: ${error}`);
|
throw new Error(`SDK execution error: ${error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,13 +179,9 @@ export async function runClaudeWithSdk(
|
|||||||
conclusion: "failure",
|
conclusion: "failure",
|
||||||
};
|
};
|
||||||
|
|
||||||
// Write execution file
|
const executionFile = await writeExecutionFile(messages);
|
||||||
try {
|
if (executionFile) {
|
||||||
await writeFile(EXECUTION_FILE, JSON.stringify(messages, null, 2));
|
result.executionFile = executionFile;
|
||||||
console.log(`Log saved to ${EXECUTION_FILE}`);
|
|
||||||
result.executionFile = EXECUTION_FILE;
|
|
||||||
} catch (error) {
|
|
||||||
core.warning(`Failed to write execution file: ${error}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract session_id from system.init message
|
// Extract session_id from system.init message
|
||||||
|
|||||||
39
base-action/test/execution-file.test.ts
Normal file
39
base-action/test/execution-file.test.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { afterEach, describe, expect, spyOn, test } from "bun:test";
|
||||||
|
import { mkdtemp, rm, writeFile } from "fs/promises";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import { join } from "path";
|
||||||
|
import { setExecutionFileOutputIfPresent } from "../src/execution-file";
|
||||||
|
|
||||||
|
describe("execution file output", () => {
|
||||||
|
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
||||||
|
let tempDir: string | undefined;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (tempDir) {
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
tempDir = undefined;
|
||||||
|
}
|
||||||
|
process.env.RUNNER_TEMP = originalRunnerTemp;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sets execution_file output when the default execution file exists", async () => {
|
||||||
|
const setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
||||||
|
tempDir = await mkdtemp(join(tmpdir(), "claude-execution-file-"));
|
||||||
|
process.env.RUNNER_TEMP = tempDir;
|
||||||
|
const executionFile = join(tempDir, "claude-execution-output.json");
|
||||||
|
await writeFile(executionFile, "[]");
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(setExecutionFileOutputIfPresent()).toBe(executionFile);
|
||||||
|
expect(setOutputSpy).toHaveBeenCalledWith(
|
||||||
|
"execution_file",
|
||||||
|
executionFile,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
setOutputSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
66
base-action/test/run-claude-sdk.test.ts
Normal file
66
base-action/test/run-claude-sdk.test.ts
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
|
||||||
|
import { afterEach, describe, expect, mock, spyOn, test } from "bun:test";
|
||||||
|
import { mkdtemp, readFile, rm, writeFile } from "fs/promises";
|
||||||
|
import { tmpdir } from "os";
|
||||||
|
import { join } from "path";
|
||||||
|
|
||||||
|
describe("runClaudeWithSdk", () => {
|
||||||
|
const originalRunnerTemp = process.env.RUNNER_TEMP;
|
||||||
|
let tempDir: string | undefined;
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
if (tempDir) {
|
||||||
|
await rm(tempDir, { recursive: true, force: true });
|
||||||
|
tempDir = undefined;
|
||||||
|
}
|
||||||
|
process.env.RUNNER_TEMP = originalRunnerTemp;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("writes the execution file when the SDK throws after yielding messages", async () => {
|
||||||
|
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
|
||||||
|
() => {},
|
||||||
|
);
|
||||||
|
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
|
||||||
|
tempDir = await mkdtemp(join(tmpdir(), "claude-sdk-"));
|
||||||
|
process.env.RUNNER_TEMP = tempDir;
|
||||||
|
|
||||||
|
const promptPath = join(tempDir, "prompt.txt");
|
||||||
|
await writeFile(promptPath, "test prompt");
|
||||||
|
|
||||||
|
const initMessage = {
|
||||||
|
type: "system",
|
||||||
|
subtype: "init",
|
||||||
|
session_id: "session-123",
|
||||||
|
model: "claude-sonnet-4-6",
|
||||||
|
};
|
||||||
|
|
||||||
|
mock.module("@anthropic-ai/claude-agent-sdk", () => ({
|
||||||
|
query: async function* () {
|
||||||
|
yield initMessage;
|
||||||
|
throw new Error("Claude Code returned error_max_turns");
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { runClaudeWithSdk } = await import("../src/run-claude-sdk");
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
runClaudeWithSdk(promptPath, {
|
||||||
|
sdkOptions: {},
|
||||||
|
showFullOutput: false,
|
||||||
|
hasJsonSchema: false,
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("SDK execution error");
|
||||||
|
|
||||||
|
const executionFile = join(tempDir, "claude-execution-output.json");
|
||||||
|
await expect(readFile(executionFile, "utf-8")).resolves.toBe(
|
||||||
|
JSON.stringify([initMessage], null, 2),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
consoleErrorSpy.mockRestore();
|
||||||
|
consoleLogSpy.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -40,6 +40,7 @@ import { installPlugins } from "../../base-action/src/install-plugins";
|
|||||||
import { preparePrompt } from "../../base-action/src/prepare-prompt";
|
import { preparePrompt } from "../../base-action/src/prepare-prompt";
|
||||||
import { runClaude } from "../../base-action/src/run-claude";
|
import { runClaude } from "../../base-action/src/run-claude";
|
||||||
import type { ClaudeRunResult } from "../../base-action/src/run-claude-sdk";
|
import type { ClaudeRunResult } from "../../base-action/src/run-claude-sdk";
|
||||||
|
import { setExecutionFileOutputIfPresent } from "../../base-action/src/execution-file";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install Claude Code CLI, handling retry logic and custom executable paths.
|
* Install Claude Code CLI, handling retry logic and custom executable paths.
|
||||||
@ -296,6 +297,7 @@ async function run() {
|
|||||||
core.setOutput("conclusion", claudeResult.conclusion);
|
core.setOutput("conclusion", claudeResult.conclusion);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
executionFile ??= setExecutionFileOutputIfPresent();
|
||||||
// Only mark as prepare failure if we haven't completed the prepare phase
|
// Only mark as prepare failure if we haven't completed the prepare phase
|
||||||
if (!prepareCompleted) {
|
if (!prepareCompleted) {
|
||||||
prepareSuccess = false;
|
prepareSuccess = false;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user