feat: display detailed error messages when prepare step fails (#82)
* feat: display detailed error messages when prepare step fails - 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 <noreply@anthropic.com> * refactor: simplify error capture to show clean error messages only - Remove complex shell script that captured full output logs - Use core.setOutput in prepare.ts to pass clean error message directly - Avoid exposing potentially sensitive information from logs - Show only the actual error message (e.g. 'Failed to fetch issue data') This provides cleaner, more readable error messages without the risk of exposing sensitive information from debug logs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: simplify error display to show clean error messages only - Remove collapsible <details> section for error messages - Display errors in simple code blocks since messages are now clean and short - Makes error messages more direct and readable 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,8 @@ runs:
|
|||||||
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
CLAUDE_SUCCESS: ${{ steps.claude-code.outputs.conclusion == 'success' }}
|
||||||
OUTPUT_FILE: ${{ steps.claude-code.outputs.execution_file || '' }}
|
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 || '' }}
|
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
|
- name: Display Claude Code Report
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
if: steps.prepare.outputs.contains_trigger == 'true' && steps.claude-code.outputs.execution_file != ''
|
||||||
|
|||||||
@@ -92,7 +92,10 @@ async function run() {
|
|||||||
);
|
);
|
||||||
core.setOutput("mcp_config", mcpConfig);
|
core.setOutput("mcp_config", mcpConfig);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(`Prepare step failed with error: ${error}`);
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
core.setFailed(`Prepare step failed with error: ${errorMessage}`);
|
||||||
|
// Also output the clean error message for the action to capture
|
||||||
|
core.setOutput("prepare_error", errorMessage);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,38 +145,48 @@ async function run() {
|
|||||||
duration_api_ms?: number;
|
duration_api_ms?: number;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
let actionFailed = false;
|
let actionFailed = false;
|
||||||
|
let errorDetails: string | undefined;
|
||||||
|
|
||||||
// Check for existence of output file and parse it if available
|
// First check if prepare step failed
|
||||||
try {
|
const prepareSuccess = process.env.PREPARE_SUCCESS !== "false";
|
||||||
const outputFile = process.env.OUTPUT_FILE;
|
const prepareError = process.env.PREPARE_ERROR;
|
||||||
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 (!prepareSuccess && prepareError) {
|
||||||
if (Array.isArray(outputData) && outputData.length > 0) {
|
actionFailed = true;
|
||||||
const lastElement = outputData[outputData.length - 1];
|
errorDetails = prepareError;
|
||||||
if (
|
} else {
|
||||||
lastElement.role === "system" &&
|
// Check for existence of output file and parse it if available
|
||||||
"cost_usd" in lastElement &&
|
try {
|
||||||
"duration_ms" in lastElement
|
const outputFile = process.env.OUTPUT_FILE;
|
||||||
) {
|
if (outputFile) {
|
||||||
executionDetails = {
|
const fileContent = await fs.readFile(outputFile, "utf8");
|
||||||
cost_usd: lastElement.cost_usd,
|
const outputData = JSON.parse(fileContent);
|
||||||
duration_ms: lastElement.duration_ms,
|
|
||||||
duration_api_ms: lastElement.duration_api_ms,
|
// 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
|
// Check if the Claude action failed
|
||||||
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
const claudeSuccess = process.env.CLAUDE_SUCCESS !== "false";
|
||||||
actionFailed = !claudeSuccess;
|
actionFailed = !claudeSuccess;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error reading output file:", error);
|
console.error("Error reading output file:", error);
|
||||||
// If we can't read the file, check for any failure markers
|
// If we can't read the file, check for any failure markers
|
||||||
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
actionFailed = process.env.CLAUDE_SUCCESS === "false";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare input for updateCommentBody function
|
// Prepare input for updateCommentBody function
|
||||||
@@ -189,6 +199,7 @@ async function run() {
|
|||||||
prLink,
|
prLink,
|
||||||
branchName: shouldDeleteBranch ? undefined : claudeBranch,
|
branchName: shouldDeleteBranch ? undefined : claudeBranch,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
|
errorDetails,
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatedBody = updateCommentBody(commentInput);
|
const updatedBody = updateCommentBody(commentInput);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type CommentUpdateInput = {
|
|||||||
prLink?: string;
|
prLink?: string;
|
||||||
branchName?: string;
|
branchName?: string;
|
||||||
triggerUsername?: string;
|
triggerUsername?: string;
|
||||||
|
errorDetails?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ensureProperlyEncodedUrl(url: string): string | null {
|
export function ensureProperlyEncodedUrl(url: string): string | null {
|
||||||
@@ -75,6 +76,7 @@ export function updateCommentBody(input: CommentUpdateInput): string {
|
|||||||
actionFailed,
|
actionFailed,
|
||||||
branchName,
|
branchName,
|
||||||
triggerUsername,
|
triggerUsername,
|
||||||
|
errorDetails,
|
||||||
} = input;
|
} = input;
|
||||||
|
|
||||||
// Extract content from the original comment body
|
// 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
|
// 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\`\`\`\n${errorDetails}\n\`\`\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
newBody += `\n\n---\n`;
|
||||||
|
|
||||||
// Clean up the body content
|
// Clean up the body content
|
||||||
// Remove any existing View job run, branch links from the bottom
|
// Remove any existing View job run, branch links from the bottom
|
||||||
|
|||||||
@@ -39,6 +39,25 @@ describe("updateCommentBody", () => {
|
|||||||
expect(result).toContain("**Claude encountered an error after 45s**");
|
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: "Failed to fetch issue data",
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = updateCommentBody(input);
|
||||||
|
expect(result).toContain("**Claude encountered an error after 45s**");
|
||||||
|
expect(result).toContain("[View job]");
|
||||||
|
expect(result).toContain("```\nFailed to fetch issue data\n```");
|
||||||
|
// 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", () => {
|
it("handles username extraction from content when not provided", () => {
|
||||||
const input = {
|
const input = {
|
||||||
...baseInput,
|
...baseInput,
|
||||||
|
|||||||
Reference in New Issue
Block a user