mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
* 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>
204 lines
6.1 KiB
TypeScript
204 lines
6.1 KiB
TypeScript
import { GITHUB_SERVER_URL } from "../api/config";
|
|
|
|
export type ExecutionDetails = {
|
|
cost_usd?: number;
|
|
duration_ms?: number;
|
|
duration_api_ms?: number;
|
|
};
|
|
|
|
export type CommentUpdateInput = {
|
|
currentBody: string;
|
|
actionFailed: boolean;
|
|
executionDetails: ExecutionDetails | null;
|
|
jobUrl: string;
|
|
branchLink?: string;
|
|
prLink?: string;
|
|
branchName?: string;
|
|
triggerUsername?: string;
|
|
errorDetails?: string;
|
|
};
|
|
|
|
export function ensureProperlyEncodedUrl(url: string): string | null {
|
|
try {
|
|
// First, try to parse the URL to see if it's already properly encoded
|
|
new URL(url);
|
|
if (url.includes(" ")) {
|
|
const [baseUrl, queryString] = url.split("?");
|
|
if (queryString) {
|
|
// Parse query parameters and re-encode them properly
|
|
const params = new URLSearchParams();
|
|
const pairs = queryString.split("&");
|
|
for (const pair of pairs) {
|
|
const [key, value = ""] = pair.split("=");
|
|
if (key) {
|
|
// Decode first in case it's partially encoded, then encode properly
|
|
params.set(key, decodeURIComponent(value));
|
|
}
|
|
}
|
|
return `${baseUrl}?${params.toString()}`;
|
|
}
|
|
// If no query string, just encode spaces
|
|
return url.replace(/ /g, "%20");
|
|
}
|
|
return url;
|
|
} catch (e) {
|
|
// If URL parsing fails, try basic fixes
|
|
try {
|
|
// Replace spaces with %20
|
|
let fixedUrl = url.replace(/ /g, "%20");
|
|
|
|
// Ensure colons in parameter values are encoded (but not in http:// or after domain)
|
|
const urlParts = fixedUrl.split("?");
|
|
if (urlParts.length > 1 && urlParts[1]) {
|
|
const [baseUrl, queryString] = urlParts;
|
|
// Encode colons in the query string that aren't already encoded
|
|
const fixedQuery = queryString.replace(/([^%]|^):(?!%2F%2F)/g, "$1%3A");
|
|
fixedUrl = `${baseUrl}?${fixedQuery}`;
|
|
}
|
|
|
|
// Try to validate the fixed URL
|
|
new URL(fixedUrl);
|
|
return fixedUrl;
|
|
} catch {
|
|
// If we still can't create a valid URL, return null
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
export function updateCommentBody(input: CommentUpdateInput): string {
|
|
const originalBody = input.currentBody;
|
|
const {
|
|
executionDetails,
|
|
jobUrl,
|
|
branchLink,
|
|
prLink,
|
|
actionFailed,
|
|
branchName,
|
|
triggerUsername,
|
|
errorDetails,
|
|
} = input;
|
|
|
|
// Extract content from the original comment body
|
|
// First, remove the "Claude Code is working…" or "Claude Code is working..." message
|
|
const workingPattern = /Claude Code is working[…\.]{1,3}(?:\s*<img[^>]*>)?/i;
|
|
let bodyContent = originalBody.replace(workingPattern, "").trim();
|
|
|
|
// Check if there's a PR link in the content
|
|
let prLinkFromContent = "";
|
|
|
|
// Match the entire markdown link structure
|
|
const prLinkPattern = /\[Create .* PR\]\((.*)\)$/m;
|
|
const prLinkMatch = bodyContent.match(prLinkPattern);
|
|
|
|
if (prLinkMatch && prLinkMatch[1]) {
|
|
const encodedUrl = ensureProperlyEncodedUrl(prLinkMatch[1]);
|
|
if (encodedUrl) {
|
|
prLinkFromContent = encodedUrl;
|
|
// Remove the PR link from the content
|
|
bodyContent = bodyContent.replace(prLinkMatch[0], "").trim();
|
|
}
|
|
}
|
|
|
|
// Calculate duration string if available
|
|
let durationStr = "";
|
|
if (executionDetails?.duration_ms !== undefined) {
|
|
const totalSeconds = Math.round(executionDetails.duration_ms / 1000);
|
|
const minutes = Math.floor(totalSeconds / 60);
|
|
const seconds = totalSeconds % 60;
|
|
durationStr = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
|
|
}
|
|
|
|
// Build the header
|
|
let header = "";
|
|
|
|
if (actionFailed) {
|
|
header = "**Claude encountered an error";
|
|
if (durationStr) {
|
|
header += ` after ${durationStr}`;
|
|
}
|
|
header += "**";
|
|
} else {
|
|
// Get the username from triggerUsername or extract from content
|
|
const usernameMatch = bodyContent.match(/@([a-zA-Z0-9-]+)/);
|
|
const username =
|
|
triggerUsername || (usernameMatch ? usernameMatch[1] : "user");
|
|
|
|
header = `**Claude finished @${username}'s task`;
|
|
if (durationStr) {
|
|
header += ` in ${durationStr}`;
|
|
}
|
|
header += "**";
|
|
}
|
|
|
|
// Add links section
|
|
let links = ` —— [View job](${jobUrl})`;
|
|
|
|
// Add branch name with link
|
|
if (branchName || branchLink) {
|
|
let finalBranchName = branchName;
|
|
let branchUrl = "";
|
|
|
|
if (branchLink) {
|
|
// Extract the branch URL from the link
|
|
const urlMatch = branchLink.match(/\((https:\/\/.*)\)/);
|
|
if (urlMatch && urlMatch[1]) {
|
|
branchUrl = urlMatch[1];
|
|
}
|
|
|
|
// Extract branch name from link if not provided
|
|
if (!finalBranchName) {
|
|
const branchNameMatch = branchLink.match(/tree\/([^"'\)]+)/);
|
|
if (branchNameMatch) {
|
|
finalBranchName = branchNameMatch[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we don't have a URL yet but have a branch name, construct it
|
|
if (!branchUrl && finalBranchName) {
|
|
// Extract owner/repo from jobUrl
|
|
const repoMatch = jobUrl.match(/github\.com\/([^\/]+)\/([^\/]+)\//);
|
|
if (repoMatch) {
|
|
branchUrl = `${GITHUB_SERVER_URL}/${repoMatch[1]}/${repoMatch[2]}/tree/${finalBranchName}`;
|
|
}
|
|
}
|
|
|
|
if (finalBranchName && branchUrl) {
|
|
links += ` • [\`${finalBranchName}\`](${branchUrl})`;
|
|
} else if (finalBranchName) {
|
|
links += ` • \`${finalBranchName}\``;
|
|
}
|
|
}
|
|
|
|
// Add PR link (either from content or provided)
|
|
const prUrl =
|
|
prLinkFromContent || (prLink ? prLink.match(/\(([^)]+)\)/)?.[1] : "");
|
|
if (prUrl) {
|
|
links += ` • [Create PR ➔](${prUrl})`;
|
|
}
|
|
|
|
// Build the new body with blank line between header and separator
|
|
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
|
|
// Remove any existing View job run, branch links from the bottom
|
|
bodyContent = bodyContent.replace(/\n?\[View job run\]\([^\)]+\)/g, "");
|
|
bodyContent = bodyContent.replace(/\n?\[View branch\]\([^\)]+\)/g, "");
|
|
|
|
// Remove any existing duration info at the bottom
|
|
bodyContent = bodyContent.replace(/\n*---\n*Duration: [0-9]+m? [0-9]+s/g, "");
|
|
|
|
// Add the cleaned body content
|
|
newBody += bodyContent;
|
|
|
|
return newBody.trim();
|
|
}
|