diff --git a/.changeset/patch-fail-claude-no-log-entries.md b/.changeset/patch-fail-claude-no-log-entries.md new file mode 100644 index 0000000000..589350451e --- /dev/null +++ b/.changeset/patch-fail-claude-no-log-entries.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fail Claude log parsing when no structured log entries appear so users see an execution failure instead of silent success. diff --git a/actions/setup/js/log_parser_bootstrap.cjs b/actions/setup/js/log_parser_bootstrap.cjs index 47753b8051..baeb49e42a 100644 --- a/actions/setup/js/log_parser_bootstrap.cjs +++ b/actions/setup/js/log_parser_bootstrap.cjs @@ -209,6 +209,12 @@ async function runLogParser(options) { core.error(`Failed to parse ${parserName} log`); } + // Claude-specific guardrail: if no structured log entries were parsed, treat as execution failure. + // This catches silent startup failures where Claude exits before producing JSON tool activity. + if (parserName === "Claude" && (!logEntries || logEntries.length === 0)) { + core.setFailed("Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution."); + } + // Handle MCP server failures if present if (mcpFailures && mcpFailures.length > 0) { const failedServers = mcpFailures.join(", "); diff --git a/actions/setup/js/log_parser_bootstrap.test.cjs b/actions/setup/js/log_parser_bootstrap.test.cjs index e564586332..730e094b30 100644 --- a/actions/setup/js/log_parser_bootstrap.test.cjs +++ b/actions/setup/js/log_parser_bootstrap.test.cjs @@ -62,6 +62,22 @@ describe("log_parser_bootstrap.cjs", () => { fs.unlinkSync(logFile), fs.rmdirSync(tmpDir)); }), + it("should fail Claude runs when no structured log entries are parsed", () => { + const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")); + const logFile = path.join(tmpDir, "test.log"); + try { + fs.writeFileSync(logFile, "unstructured log output"); + process.env.GH_AW_AGENT_OUTPUT = logFile; + const mockParseLog = vi.fn().mockReturnValue({ markdown: "## Result\n", mcpFailures: [], maxTurnsHit: false, logEntries: [] }); + runLogParser({ parseLog: mockParseLog, parserName: "Claude" }); + expect(mockCore.setFailed).toHaveBeenCalledWith( + "Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution." + ); + } finally { + fs.unlinkSync(logFile); + fs.rmdirSync(tmpDir); + } + }), it("should generate plain text summary when logEntries are available", () => { const tmpDir = fs.mkdtempSync(path.join(__dirname, "test-")), logFile = path.join(tmpDir, "test.log"); diff --git a/actions/setup/js/parse_claude_log.test.cjs b/actions/setup/js/parse_claude_log.test.cjs index 5eb2d04bc9..3c6a77c817 100644 --- a/actions/setup/js/parse_claude_log.test.cjs +++ b/actions/setup/js/parse_claude_log.test.cjs @@ -437,6 +437,11 @@ describe("parse_claude_log.cjs", () => { expect(mockCore.info).toHaveBeenCalledWith("No agent log file specified"); expect(mockCore.setFailed).not.toHaveBeenCalled(); }); + + it("should fail when Claude log has no structured entries", async () => { + await runScript("this is not structured Claude JSON output"); + expect(mockCore.setFailed).toHaveBeenCalledWith("Claude execution failed: no structured log entries were produced. This usually indicates a startup or configuration error before tool execution."); + }); }); describe("helper function tests", () => {