fix(sdk): fail step when result has is_error:true despite success subtype (#1496)

Treat subtype success with is_error:true as a failed run so CI does not
show a misleading green check when the review never actually ran.

Fixes #1495

Co-authored-by: syf2211 <syf2211@users.noreply.github.com>
This commit is contained in:
石岳峰 2026-07-14 00:01:51 +08:00 committed by GitHub
parent e90deca476
commit 972a512078
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 4 deletions

View File

@ -208,7 +208,10 @@ export async function runClaudeWithSdk(
throw new Error("No result message received from Claude"); throw new Error("No result message received from Claude");
} }
const isSuccess = resultMessage.subtype === "success"; // subtype "success" with is_error:true means the run errored without producing
// a real result — treat it as failure so CI does not show a misleading green check.
const isSuccess =
resultMessage.subtype === "success" && !resultMessage.is_error;
result.conclusion = isSuccess ? "success" : "failure"; result.conclusion = isSuccess ? "success" : "failure";
// Handle structured output // Handle structured output
@ -234,12 +237,19 @@ export async function runClaudeWithSdk(
} }
if (!isSuccess) { if (!isSuccess) {
if (resultMessage.subtype === "success" && resultMessage.is_error) {
core.error(
"Claude result reported subtype success with is_error:true (run did not complete successfully)",
);
}
if ("errors" in resultMessage && resultMessage.errors) { if ("errors" in resultMessage && resultMessage.errors) {
core.error(`Execution failed: ${resultMessage.errors.join(", ")}`); core.error(`Execution failed: ${resultMessage.errors.join(", ")}`);
} }
throw new Error( throw new Error(
`Claude execution failed: ${ `Claude execution failed: ${
"errors" in resultMessage && resultMessage.errors resultMessage.subtype === "success" && resultMessage.is_error
? "result is_error:true"
: "errors" in resultMessage && resultMessage.errors
? resultMessage.errors.join(", ") ? resultMessage.errors.join(", ")
: "unknown error" : "unknown error"
}`, }`,

View File

@ -63,4 +63,69 @@ describe("runClaudeWithSdk", () => {
consoleLogSpy.mockRestore(); consoleLogSpy.mockRestore();
} }
}); });
test("fails when result subtype is success but is_error is true", async () => {
const consoleErrorSpy = spyOn(console, "error").mockImplementation(
() => {},
);
const consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
const coreErrorSpy = spyOn(
await import("@actions/core"),
"error",
).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-5",
};
const errorResultMessage = {
type: "result",
subtype: "success",
is_error: true,
duration_ms: 434,
num_turns: 1,
total_cost_usd: 0,
permission_denials: [],
};
mock.module("@anthropic-ai/claude-agent-sdk", () => ({
query: async function* () {
yield initMessage;
yield errorResultMessage;
},
}));
try {
const { runClaudeWithSdk } = await import("../src/run-claude-sdk");
await expect(
runClaudeWithSdk(promptPath, {
sdkOptions: {},
showFullOutput: false,
hasJsonSchema: false,
}),
).rejects.toThrow("result is_error:true");
const executionFile = join(tempDir, "claude-execution-output.json");
await expect(readFile(executionFile, "utf-8")).resolves.toBe(
JSON.stringify([initMessage, errorResultMessage], null, 2),
);
expect(coreErrorSpy).toHaveBeenCalledWith(
"Claude result reported subtype success with is_error:true (run did not complete successfully)",
);
} finally {
consoleErrorSpy.mockRestore();
consoleLogSpy.mockRestore();
coreErrorSpy.mockRestore();
}
});
}); });