fix: write execution file when SDK throws (#1255)

This commit is contained in:
JerryLee 2026-05-15 08:37:28 +10:00 committed by GitHub
parent 0756f6ef2b
commit 1450f658d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 157 additions and 10 deletions

View 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;
}

View File

@ -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);

View File

@ -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

View 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();
}
});
});

View 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();
}
});
});

View File

@ -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;