From 81181ca6582a1cc634aead16e6eae31c198ccaf1 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Wed, 28 May 2025 15:52:16 -0700 Subject: [PATCH] feat: display detailed error messages when prepare step fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Capture prepare step errors in action.yml (up to 2000 chars) - Add error details to comment update with collapsible section - Handle both prepare and Claude execution failures separately - Add test coverage for error detail display This helps users debug issues like git errors, permission problems, and branch creation failures more easily. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- action.yml | 23 ++++++++- src/entrypoints/update-comment-link.ts | 65 +++++++++++++++----------- src/github/operations/comment-logic.ts | 11 ++++- test/comment-logic.test.ts | 22 +++++++++ 4 files changed, 92 insertions(+), 29 deletions(-) diff --git a/action.yml b/action.yml index 1fbb0dc..3d60761 100644 --- a/action.yml +++ b/action.yml @@ -81,7 +81,26 @@ runs: id: prepare shell: bash run: | - bun run ${{ github.action_path }}/src/entrypoints/prepare.ts + set +e + # Create a temporary file to capture both stdout and stderr + TEMP_OUTPUT=$(mktemp) + bun run ${{ github.action_path }}/src/entrypoints/prepare.ts 2>&1 | tee "$TEMP_OUTPUT" + PREPARE_EXIT_CODE=${PIPESTATUS[0]} + + # If the command failed, save the error output + if [ $PREPARE_EXIT_CODE -ne 0 ]; then + # Extract last 50 lines of output as error details + ERROR_DETAILS=$(tail -n 50 "$TEMP_OUTPUT") + echo "prepare_error<> $GITHUB_OUTPUT + echo "$ERROR_DETAILS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + # Clean up temp file + rm -f "$TEMP_OUTPUT" + + # Exit with the original exit code + exit $PREPARE_EXIT_CODE env: TRIGGER_PHRASE: ${{ inputs.trigger_phrase }} ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }} @@ -147,6 +166,8 @@ runs: CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }} OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }} TRIGGER_USERNAME: ${{ github.event.comment.user.login || github.event.issue.user.login || github.event.pull_request.user.login || github.event.sender.login || github.triggering_actor || github.actor || '' }} + PREPARE_SUCCESS: ${{ steps.prepare.outcome == 'success' }} + PREPARE_ERROR: ${{ steps.prepare.outputs.prepare_error || '' }} - name: Display Claude Code Report if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != '' diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index daa2cec..d74bcd5 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -145,38 +145,48 @@ async function run() { duration_api_ms?: number; } | null = null; let actionFailed = false; + let errorDetails: string | undefined; - // Check for existence of output file and parse it if available - try { - const outputFile = process.env.OUTPUT_FILE; - if (outputFile) { - const fileContent = await fs.readFile(outputFile, "utf8"); - const outputData = JSON.parse(fileContent); + // First check if prepare step failed + const prepareSuccess = process.env.PREPARE_SUCCESS !== "false"; + const prepareError = process.env.PREPARE_ERROR; - // Output file is an array, get the last element which contains execution details - if (Array.isArray(outputData) && outputData.length > 0) { - const lastElement = outputData[outputData.length - 1]; - if ( - lastElement.role === "system" && - "cost_usd" in lastElement && - "duration_ms" in lastElement - ) { - executionDetails = { - cost_usd: lastElement.cost_usd, - duration_ms: lastElement.duration_ms, - duration_api_ms: lastElement.duration_api_ms, - }; + if (!prepareSuccess && prepareError) { + actionFailed = true; + errorDetails = prepareError; + } else { + // Check for existence of output file and parse it if available + try { + const outputFile = process.env.OUTPUT_FILE; + if (outputFile) { + const fileContent = await fs.readFile(outputFile, "utf8"); + const outputData = JSON.parse(fileContent); + + // Output file is an array, get the last element which contains execution details + if (Array.isArray(outputData) && outputData.length > 0) { + const lastElement = outputData[outputData.length - 1]; + if ( + lastElement.role === "system" && + "cost_usd" in lastElement && + "duration_ms" in lastElement + ) { + executionDetails = { + cost_usd: lastElement.cost_usd, + duration_ms: lastElement.duration_ms, + duration_api_ms: lastElement.duration_api_ms, + }; + } } } - } - // Check if the action failed by looking at the exit code or error marker - const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false"; - actionFailed = !claudeSuccess; - } catch (error) { - console.error("Error reading output file:", error); - // If we can't read the file, check for any failure markers - actionFailed = process.env.CLAUDE_SUCCESS === "false"; + // Check if the Claude action failed + const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false"; + actionFailed = !claudeSuccess; + } catch (error) { + console.error("Error reading output file:", error); + // If we can't read the file, check for any failure markers + actionFailed = process.env.CLAUDE_SUCCESS === "false"; + } } // Prepare input for updateCommentBody function @@ -189,6 +199,7 @@ async function run() { prLink, branchName: shouldDeleteBranch ? undefined : claudeBranch, triggerUsername, + errorDetails, }; const updatedBody = updateCommentBody(commentInput); diff --git a/src/github/operations/comment-logic.ts b/src/github/operations/comment-logic.ts index 6c2bad4..bc0253d 100644 --- a/src/github/operations/comment-logic.ts +++ b/src/github/operations/comment-logic.ts @@ -15,6 +15,7 @@ export type CommentUpdateInput = { prLink?: string; branchName?: string; triggerUsername?: string; + errorDetails?: string; }; export function ensureProperlyEncodedUrl(url: string): string | null { @@ -75,6 +76,7 @@ export function updateCommentBody(input: CommentUpdateInput): string { actionFailed, branchName, triggerUsername, + errorDetails, } = input; // Extract content from the original comment body @@ -177,7 +179,14 @@ export function updateCommentBody(input: CommentUpdateInput): string { } // Build the new body with blank line between header and separator - let newBody = `${header}${links}\n\n---\n`; + let newBody = `${header}${links}`; + + // Add error details if available + if (actionFailed && errorDetails) { + newBody += `\n\n
\nError details\n\n\`\`\`\n${errorDetails}\n\`\`\`\n\n
`; + } + + newBody += `\n\n---\n`; // Clean up the body content // Remove any existing View job run, branch links from the bottom diff --git a/test/comment-logic.test.ts b/test/comment-logic.test.ts index 3b86908..43f1d4b 100644 --- a/test/comment-logic.test.ts +++ b/test/comment-logic.test.ts @@ -39,6 +39,28 @@ describe("updateCommentBody", () => { expect(result).toContain("**Claude encountered an error after 45s**"); }); + it("includes error details when provided", () => { + const input = { + ...baseInput, + currentBody: "Claude Code is working...", + actionFailed: true, + executionDetails: { duration_ms: 45000 }, + errorDetails: + "fatal: not a git repository (or any of the parent directories): .git", + }; + + const result = updateCommentBody(input); + expect(result).toContain("**Claude encountered an error after 45s**"); + expect(result).toContain("[View job]"); + expect(result).toContain("
"); + expect(result).toContain("Error details"); + expect(result).toContain("fatal: not a git repository"); + // Ensure error details come after the header/links + const errorIndex = result.indexOf("
"); + const headerIndex = result.indexOf("**Claude encountered an error"); + expect(errorIndex).toBeGreaterThan(headerIndex); + }); + it("handles username extraction from content when not provided", () => { const input = { ...baseInput,