* feat: enhance error reporting with specific error types from Claude execution - Extract error subtypes (error_during_execution, error_max_turns) from result object - Display specific error messages in comment header based on error type - Use total_cost_usd field from SDKResultMessage type - Prevent showing redundant error details when already displayed in header 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * chore: update claude-code-base-action to v0.0.19 * feat: use GitHub display name in Co-authored-by trailers (#163) * feat: use GitHub display name in Co-authored-by trailers - Add name field to GitHubAuthor type - Update GraphQL queries to fetch user display names - Add triggerDisplayName to CommonFields type - Extract display name from fetched GitHub data in prepareContext - Update Co-authored-by trailer generation to use display name when available This ensures consistency with GitHub's web interface behavior where Co-authored-by trailers use the user's display name rather than username. Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com> * fix: update GraphQL queries to handle Actor type correctly The name field is only available on the User subtype of Actor in GitHub's GraphQL API. This commit updates the queries to use inline fragments (... on User) to conditionally access the name field when the actor is a User type. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: clarify Co-authored-by instructions in prompt Replace interpolated values with clear references to XML tags and add explicit formatting instructions. This makes it clearer how to use the GitHub display name when available while maintaining the username for the email portion. Changes: - Use explicit references to <trigger_display_name> and <trigger_username> tags - Add clear formatting instructions and example - Explain fallback behavior when display name is not available 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * feat: fetch trigger user display name via dedicated GraphQL query Instead of trying to extract the display name from existing data (which was incomplete due to Actor type limitations), we now: - Add a dedicated USER_QUERY to fetch user display names - Pass the trigger username to fetchGitHubData - Fetch the display name during data collection phase - Simplify prepareContext to use the pre-fetched display name This ensures we always get the correct display name for Co-authored-by trailers, regardless of where the trigger came from. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com> * feat: use dynamic fetch depth based on PR commit count (#169) - Replace fixed depth of 20 with dynamic calculation - Use Math.max(commitCount, 20) to ensure minimum context * Accept multiline input for allowed_tools and disallowed_tools (#168) * docs: add uv example for Python MCP servers in mcp_config section (#170) Added documentation showing how to configure Python-based MCP servers using uv with the --directory argument, as requested in issue #130. Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Ashwin Bhat <ashwin-ant@users.noreply.github.com> * feat: add release workflow with beta tag management (#171) - Auto-increment patch version for new releases - Update beta tag to point to latest release - Update major version tag (v0) for simplified action usage - Support dry run mode for testing - Keep beta as the "latest" release channel 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com> * chore: update claude-code-base-action to v0.0.20 * update MCP server image to version 0.5.0 (#175) * refactor: convert error subtype check to switch case Replace if-else chain with switch statement for better readability and maintainability when handling error subtypes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: GitHub Actions <actions@github.com> Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com> Co-authored-by: Bastian Gutschke <bge@medicuja.com> Co-authored-by: Hidetake Iwata <int128@gmail.com> Co-authored-by: Tomohiro Ishibashi <103555868+tomoish@users.noreply.github.com>
219 lines
6.6 KiB
TypeScript
219 lines
6.6 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";
|
|
|
|
// Add error type to header if available
|
|
if (errorDetails) {
|
|
if (errorDetails === "Error during execution") {
|
|
header = "**Claude encountered an error during execution";
|
|
} else if (errorDetails === "Maximum turns exceeded") {
|
|
header = "**Claude exceeded the maximum number of turns";
|
|
}
|
|
}
|
|
|
|
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 (but not if it's just the error type we already showed in header)
|
|
if (
|
|
actionFailed &&
|
|
errorDetails &&
|
|
errorDetails !== "Error during execution" &&
|
|
errorDetails !== "Maximum turns exceeded"
|
|
) {
|
|
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();
|
|
}
|