Compare commits

...

7 Commits

Author SHA1 Message Date
Ashwin Bhat
5a787ed8ab bump base action (#90) 2025-05-29 11:24:24 -07:00
Ashwin Bhat
507e4a6cd1 grant more tools in workflow (#85) 2025-05-29 10:18:16 -07:00
Ashwin Bhat
fcbdac91f2 feat: add base_branch input to specify source branch for new Claude branches (#72)
* feat: add base_branch input to specify source branch for new Claude branches

- Add base_branch input parameter to action.yml allowing users to specify which branch to use as source
- Update setupBranch function to accept and use the base branch parameter  
- Defaults to repository default branch if no base branch is specified
- Addresses issue #62 for better branch control

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>

* perf: optimize setupBranch to avoid unnecessary default branch fetch

Only fetch repository default branch when actually needed:
- Skip initial fetch when baseBranch is provided
- Fetch default branch at end only for return value and GitHub Actions output
- Eliminates unnecessary API call when users specify base branch

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>

* fix: properly handle base branch throughout the action workflow

- Fix TypeScript error where defaultBranch was used before being assigned
- Replace DEFAULT_BRANCH with BASE_BRANCH in subsequent workflow steps
- Update PR creation and branch comparison to use the actual base branch
- Ensure custom base_branch input is respected in all operations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor: move BASE_BRANCH env reading into parseGitHubContext

- Move BASE_BRANCH environment variable reading into parseGitHubContext for consistency
- Update setupBranch to use context.inputs.baseBranch instead of process.env
- Fix test descriptions to correctly reference BASE_BRANCH instead of DEFAULT_BRANCH
- Update test environment setup to use BASE_BRANCH

🤖 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>
2025-05-29 10:08:00 -07:00
Marc-Antoine
03e5dcc3a1 Support ANTHROPIC_BASE_URL override from env (#80)
* Support ANTHROPIC_BASE_URL override from env

* prettier

---------

Co-authored-by: Ashwin Bhat <ashwin@anthropic.com>
2025-05-29 10:02:12 -07:00
Ashwin Bhat
52efa5e498 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>
2025-05-29 09:58:52 -07:00
Ashwin Bhat
37c3c29341 feat: allow user override of hardcoded disallowed tools (#71)
* feat: allow user override of hardcoded disallowed tools

Allow users to override hardcoded disallowed tools (WebSearch, WebFetch) by including them in their allowed_tools configuration. This provides users with the ability to control tool access based on their security requirements.

Changes:
- Modified buildDisallowedToolsString() to accept allowedTools parameter
- Added logic to filter out hardcoded disallowed tools if present in allowed tools
- Updated function call site to pass allowedTools
- Added comprehensive test coverage for override behavior
- Maintains backward compatibility

Resolves #49

Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>

* prettier

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
Co-authored-by: ashwin-ant <ashwin-ant@users.noreply.github.com>
2025-05-29 09:40:32 -07:00
Ashwin Bhat
9c9859aff1 chore: update claude-code-base-action to v0.0.7 (#84) 2025-05-28 17:20:23 -07:00
13 changed files with 238 additions and 105 deletions

View File

@@ -34,3 +34,5 @@ jobs:
uses: anthropics/claude-code-action@beta uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
allowed_tools: "Bash(bun install),Bash(bun test:*),Bash(bun run format),Bash(bun typecheck)"
custom_instructions: "You have also been granted tools for editing files and running bun commands (install, run, test, typecheck) for testing your changes: bun install, bun test, bun run format, bun typecheck."

View File

@@ -12,6 +12,9 @@ inputs:
assignee_trigger: assignee_trigger:
description: "The assignee username that triggers the action (e.g. @claude)" description: "The assignee username that triggers the action (e.g. @claude)"
required: false required: false
base_branch:
description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)"
required: false
# Claude Code configuration # Claude Code configuration
model: model:
@@ -85,6 +88,7 @@ runs:
env: env:
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }} TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }} ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
BASE_BRANCH: ${{ inputs.base_branch }}
ALLOWED_TOOLS: ${{ inputs.allowed_tools }} ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }} CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
DIRECT_PROMPT: ${{ inputs.direct_prompt }} DIRECT_PROMPT: ${{ inputs.direct_prompt }}
@@ -94,7 +98,7 @@ runs:
- name: Run Claude Code - name: Run Claude Code
id: claude-code id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true' if: steps.prepare.outputs.contains_trigger == 'true'
uses: anthropics/claude-code-base-action@266585c92dd90d61d3806a3367582c4f6224e892 # https://github.com/anthropics/claude-code-base-action/releases/tag/v0.0.6 uses: anthropics/claude-code-base-action@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8
with: with:
prompt_file: /tmp/claude-prompts/claude-prompt.txt prompt_file: /tmp/claude-prompts/claude-prompt.txt
allowed_tools: ${{ env.ALLOWED_TOOLS }} allowed_tools: ${{ env.ALLOWED_TOOLS }}
@@ -110,6 +114,9 @@ runs:
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }} ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
# Provider configuration
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
# AWS configuration # AWS configuration
AWS_REGION: ${{ env.AWS_REGION }} AWS_REGION: ${{ env.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
@@ -143,10 +150,12 @@ runs:
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }} TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }} CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }} IS_PR: ${{ github.event.issue.pull_request != null || github.event_name == 'pull_request_review_comment' }}
DEFAULT_BRANCH: ${{ steps.prepare.outputs.DEFAULT_BRANCH }} BASE_BRANCH: ${{ steps.prepare.outputs.BASE_BRANCH }}
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 != ''

View File

@@ -58,10 +58,27 @@ export function buildAllowedToolsString(
export function buildDisallowedToolsString( export function buildDisallowedToolsString(
customDisallowedTools?: string, customDisallowedTools?: string,
allowedTools?: string,
): string { ): string {
let allDisallowedTools = DISALLOWED_TOOLS.join(","); let disallowedTools = [...DISALLOWED_TOOLS];
// If user has explicitly allowed some hardcoded disallowed tools, remove them from disallowed list
if (allowedTools) {
const allowedToolsArray = allowedTools
.split(",")
.map((tool) => tool.trim());
disallowedTools = disallowedTools.filter(
(tool) => !allowedToolsArray.includes(tool),
);
}
let allDisallowedTools = disallowedTools.join(",");
if (customDisallowedTools) { if (customDisallowedTools) {
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`; if (allDisallowedTools) {
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
} else {
allDisallowedTools = customDisallowedTools;
}
} }
return allDisallowedTools; return allDisallowedTools;
} }
@@ -69,7 +86,7 @@ export function buildDisallowedToolsString(
export function prepareContext( export function prepareContext(
context: ParsedGitHubContext, context: ParsedGitHubContext,
claudeCommentId: string, claudeCommentId: string,
defaultBranch?: string, baseBranch?: string,
claudeBranch?: string, claudeBranch?: string,
): PreparedContext { ): PreparedContext {
const repository = context.repository.full_name; const repository = context.repository.full_name;
@@ -147,7 +164,7 @@ export function prepareContext(
...(commentId && { commentId }), ...(commentId && { commentId }),
commentBody, commentBody,
...(claudeBranch && { claudeBranch }), ...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }), ...(baseBranch && { baseBranch }),
}; };
break; break;
@@ -169,7 +186,7 @@ export function prepareContext(
prNumber, prNumber,
commentBody, commentBody,
...(claudeBranch && { claudeBranch }), ...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }), ...(baseBranch && { baseBranch }),
}; };
break; break;
@@ -194,13 +211,13 @@ export function prepareContext(
prNumber, prNumber,
commentBody, commentBody,
...(claudeBranch && { claudeBranch }), ...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }), ...(baseBranch && { baseBranch }),
}; };
break; break;
} else if (!claudeBranch) { } else if (!claudeBranch) {
throw new Error("CLAUDE_BRANCH is required for issue_comment event"); throw new Error("CLAUDE_BRANCH is required for issue_comment event");
} else if (!defaultBranch) { } else if (!baseBranch) {
throw new Error("DEFAULT_BRANCH is required for issue_comment event"); throw new Error("BASE_BRANCH is required for issue_comment event");
} else if (!issueNumber) { } else if (!issueNumber) {
throw new Error( throw new Error(
"ISSUE_NUMBER is required for issue_comment event for issues", "ISSUE_NUMBER is required for issue_comment event for issues",
@@ -212,7 +229,7 @@ export function prepareContext(
commentId, commentId,
isPR: false, isPR: false,
claudeBranch: claudeBranch, claudeBranch: claudeBranch,
defaultBranch, baseBranch,
issueNumber, issueNumber,
commentBody, commentBody,
}; };
@@ -228,8 +245,8 @@ export function prepareContext(
if (isPR) { if (isPR) {
throw new Error("IS_PR must be false for issues event"); throw new Error("IS_PR must be false for issues event");
} }
if (!defaultBranch) { if (!baseBranch) {
throw new Error("DEFAULT_BRANCH is required for issues event"); throw new Error("BASE_BRANCH is required for issues event");
} }
if (!claudeBranch) { if (!claudeBranch) {
throw new Error("CLAUDE_BRANCH is required for issues event"); throw new Error("CLAUDE_BRANCH is required for issues event");
@@ -246,7 +263,7 @@ export function prepareContext(
eventAction: "assigned", eventAction: "assigned",
isPR: false, isPR: false,
issueNumber, issueNumber,
defaultBranch, baseBranch,
claudeBranch, claudeBranch,
assigneeTrigger, assigneeTrigger,
}; };
@@ -256,7 +273,7 @@ export function prepareContext(
eventAction: "opened", eventAction: "opened",
isPR: false, isPR: false,
issueNumber, issueNumber,
defaultBranch, baseBranch,
claudeBranch, claudeBranch,
}; };
} else { } else {
@@ -277,7 +294,7 @@ export function prepareContext(
isPR: true, isPR: true,
prNumber, prNumber,
...(claudeBranch && { claudeBranch }), ...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }), ...(baseBranch && { baseBranch }),
}; };
break; break;
@@ -524,13 +541,13 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
${ ${
eventData.claudeBranch eventData.claudeBranch
? `- Provide a URL to create a PR manually in this format: ? `- Provide a URL to create a PR manually in this format:
[Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.defaultBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>) [Create a PR](${GITHUB_SERVER_URL}/${context.repository}/compare/${eventData.baseBranch}...<branch-name>?quick_pull=1&title=<url-encoded-title>&body=<url-encoded-body>)
- IMPORTANT: Use THREE dots (...) between branch names, not two (..) - IMPORTANT: Use THREE dots (...) between branch names, not two (..)
Example: ${GITHUB_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct) Example: ${GITHUB_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
NOT: ${GITHUB_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect) NOT: ${GITHUB_SERVER_URL}/${context.repository}/compare/main..feature-branch (incorrect)
- IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces - IMPORTANT: Ensure all URL parameters are properly encoded - spaces should be encoded as %20, not left as spaces
Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message" Example: Instead of "fix: update welcome message", use "fix%3A%20update%20welcome%20message"
- The target-branch should be '${eventData.defaultBranch}'. - The target-branch should be '${eventData.baseBranch}'.
- The branch-name is the current branch: ${eventData.claudeBranch} - The branch-name is the current branch: ${eventData.claudeBranch}
- The body should include: - The body should include:
- A clear description of the changes - A clear description of the changes
@@ -615,7 +632,7 @@ f. If you are unable to complete certain steps, such as running a linter or test
export async function createPrompt( export async function createPrompt(
claudeCommentId: number, claudeCommentId: number,
defaultBranch: string | undefined, baseBranch: string | undefined,
claudeBranch: string | undefined, claudeBranch: string | undefined,
githubData: FetchDataResult, githubData: FetchDataResult,
context: ParsedGitHubContext, context: ParsedGitHubContext,
@@ -624,7 +641,7 @@ export async function createPrompt(
const preparedContext = prepareContext( const preparedContext = prepareContext(
context, context,
claudeCommentId.toString(), claudeCommentId.toString(),
defaultBranch, baseBranch,
claudeBranch, claudeBranch,
); );
@@ -648,6 +665,7 @@ export async function createPrompt(
); );
const allDisallowedTools = buildDisallowedToolsString( const allDisallowedTools = buildDisallowedToolsString(
preparedContext.disallowedTools, preparedContext.disallowedTools,
preparedContext.allowedTools,
); );
core.exportVariable("ALLOWED_TOOLS", allAllowedTools); core.exportVariable("ALLOWED_TOOLS", allAllowedTools);

View File

@@ -16,7 +16,7 @@ type PullRequestReviewCommentEvent = {
commentId?: string; // May be present for review comments commentId?: string; // May be present for review comments
commentBody: string; commentBody: string;
claudeBranch?: string; claudeBranch?: string;
defaultBranch?: string; baseBranch?: string;
}; };
type PullRequestReviewEvent = { type PullRequestReviewEvent = {
@@ -25,7 +25,7 @@ type PullRequestReviewEvent = {
prNumber: string; prNumber: string;
commentBody: string; commentBody: string;
claudeBranch?: string; claudeBranch?: string;
defaultBranch?: string; baseBranch?: string;
}; };
type IssueCommentEvent = { type IssueCommentEvent = {
@@ -33,7 +33,7 @@ type IssueCommentEvent = {
commentId: string; commentId: string;
issueNumber: string; issueNumber: string;
isPR: false; isPR: false;
defaultBranch: string; baseBranch: string;
claudeBranch: string; claudeBranch: string;
commentBody: string; commentBody: string;
}; };
@@ -46,7 +46,7 @@ type PullRequestCommentEvent = {
isPR: true; isPR: true;
commentBody: string; commentBody: string;
claudeBranch?: string; claudeBranch?: string;
defaultBranch?: string; baseBranch?: string;
}; };
type IssueOpenedEvent = { type IssueOpenedEvent = {
@@ -54,7 +54,7 @@ type IssueOpenedEvent = {
eventAction: "opened"; eventAction: "opened";
isPR: false; isPR: false;
issueNumber: string; issueNumber: string;
defaultBranch: string; baseBranch: string;
claudeBranch: string; claudeBranch: string;
}; };
@@ -63,7 +63,7 @@ type IssueAssignedEvent = {
eventAction: "assigned"; eventAction: "assigned";
isPR: false; isPR: false;
issueNumber: string; issueNumber: string;
defaultBranch: string; baseBranch: string;
claudeBranch: string; claudeBranch: string;
assigneeTrigger: string; assigneeTrigger: string;
}; };
@@ -74,7 +74,7 @@ type PullRequestEvent = {
isPR: true; isPR: true;
prNumber: string; prNumber: string;
claudeBranch?: string; claudeBranch?: string;
defaultBranch?: string; baseBranch?: string;
}; };
// Union type for all possible event types // Union type for all possible event types

View File

@@ -77,7 +77,7 @@ async function run() {
// Step 10: Create prompt file // Step 10: Create prompt file
await createPrompt( await createPrompt(
commentId, commentId,
branchInfo.defaultBranch, branchInfo.baseBranch,
branchInfo.claudeBranch, branchInfo.claudeBranch,
githubData, githubData,
context, context,
@@ -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);
} }
} }

View File

@@ -18,7 +18,7 @@ async function run() {
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!); const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
const githubToken = process.env.GITHUB_TOKEN!; const githubToken = process.env.GITHUB_TOKEN!;
const claudeBranch = process.env.CLAUDE_BRANCH; const claudeBranch = process.env.CLAUDE_BRANCH;
const defaultBranch = process.env.DEFAULT_BRANCH || "main"; const baseBranch = process.env.BASE_BRANCH || "main";
const triggerUsername = process.env.TRIGGER_USERNAME; const triggerUsername = process.env.TRIGGER_USERNAME;
const context = parseGitHubContext(); const context = parseGitHubContext();
@@ -92,7 +92,7 @@ async function run() {
owner, owner,
repo, repo,
claudeBranch, claudeBranch,
defaultBranch, baseBranch,
); );
// Check if we need to add PR URL when we have a new branch // Check if we need to add PR URL when we have a new branch
@@ -102,7 +102,7 @@ async function run() {
// Check if comment already contains a PR URL // Check if comment already contains a PR URL
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const prUrlPattern = new RegExp( const prUrlPattern = new RegExp(
`${serverUrlPattern}\\/.+\\/compare\\/${defaultBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`, `${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
); );
const containsPRUrl = currentBody.match(prUrlPattern); const containsPRUrl = currentBody.match(prUrlPattern);
@@ -113,7 +113,7 @@ async function run() {
await octokit.rest.repos.compareCommitsWithBasehead({ await octokit.rest.repos.compareCommitsWithBasehead({
owner, owner,
repo, repo,
basehead: `${defaultBranch}...${claudeBranch}`, basehead: `${baseBranch}...${claudeBranch}`,
}); });
// If there are changes (commits or file changes), add the PR URL // If there are changes (commits or file changes), add the PR URL
@@ -128,7 +128,7 @@ async function run() {
const prBody = encodeURIComponent( const prBody = encodeURIComponent(
`This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`, `This PR addresses ${entityType.toLowerCase()} #${context.entityNumber}\n\nGenerated with [Claude Code](https://claude.ai/code)`,
); );
const prUrl = `${serverUrl}/${owner}/${repo}/compare/${defaultBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`; const prUrl = `${serverUrl}/${owner}/${repo}/compare/${baseBranch}...${claudeBranch}?quick_pull=1&title=${prTitle}&body=${prBody}`;
prLink = `\n[Create a PR](${prUrl})`; prLink = `\n[Create a PR](${prUrl})`;
} }
} catch (error) { } catch (error) {
@@ -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);

View File

@@ -32,6 +32,7 @@ export type ParsedGitHubContext = {
disallowedTools: string; disallowedTools: string;
customInstructions: string; customInstructions: string;
directPrompt: string; directPrompt: string;
baseBranch?: string;
}; };
}; };
@@ -55,6 +56,7 @@ export function parseGitHubContext(): ParsedGitHubContext {
disallowedTools: process.env.DISALLOWED_TOOLS ?? "", disallowedTools: process.env.DISALLOWED_TOOLS ?? "",
customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "", customInstructions: process.env.CUSTOM_INSTRUCTIONS ?? "",
directPrompt: process.env.DIRECT_PROMPT ?? "", directPrompt: process.env.DIRECT_PROMPT ?? "",
baseBranch: process.env.BASE_BRANCH,
}, },
}; };

View File

@@ -6,7 +6,7 @@ export async function checkAndDeleteEmptyBranch(
owner: string, owner: string,
repo: string, repo: string,
claudeBranch: string | undefined, claudeBranch: string | undefined,
defaultBranch: string, baseBranch: string,
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> { ): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
let branchLink = ""; let branchLink = "";
let shouldDeleteBranch = false; let shouldDeleteBranch = false;
@@ -18,7 +18,7 @@ export async function checkAndDeleteEmptyBranch(
await octokit.rest.repos.compareCommitsWithBasehead({ await octokit.rest.repos.compareCommitsWithBasehead({
owner, owner,
repo, repo,
basehead: `${defaultBranch}...${claudeBranch}`, basehead: `${baseBranch}...${claudeBranch}`,
}); });
// If there are no commits, mark branch for deletion // If there are no commits, mark branch for deletion

View File

@@ -14,7 +14,7 @@ import type { Octokits } from "../api/client";
import type { FetchDataResult } from "../data/fetcher"; import type { FetchDataResult } from "../data/fetcher";
export type BranchInfo = { export type BranchInfo = {
defaultBranch: string; baseBranch: string;
claudeBranch?: string; claudeBranch?: string;
currentBranch: string; currentBranch: string;
}; };
@@ -26,15 +26,9 @@ export async function setupBranch(
): Promise<BranchInfo> { ): Promise<BranchInfo> {
const { owner, repo } = context.repository; const { owner, repo } = context.repository;
const entityNumber = context.entityNumber; const entityNumber = context.entityNumber;
const { baseBranch } = context.inputs;
const isPR = context.isPR; const isPR = context.isPR;
// Get the default branch first
const repoResponse = await octokits.rest.repos.get({
owner,
repo,
});
const defaultBranch = repoResponse.data.default_branch;
if (isPR) { if (isPR) {
const prData = githubData.contextData as GitHubPullRequest; const prData = githubData.contextData as GitHubPullRequest;
const prState = prData.state; const prState = prData.state;
@@ -42,7 +36,7 @@ export async function setupBranch(
// Check if PR is closed or merged // Check if PR is closed or merged
if (prState === "CLOSED" || prState === "MERGED") { if (prState === "CLOSED" || prState === "MERGED") {
console.log( console.log(
`PR #${entityNumber} is ${prState}, creating new branch from default...`, `PR #${entityNumber} is ${prState}, creating new branch from source...`,
); );
// Fall through to create a new branch like we do for issues // Fall through to create a new branch like we do for issues
} else { } else {
@@ -58,17 +52,36 @@ export async function setupBranch(
console.log(`Successfully checked out PR branch for PR #${entityNumber}`); console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
// For open PRs, return branch info // For open PRs, we need to get the base branch of the PR
const baseBranch = prData.baseRefName;
return { return {
defaultBranch, baseBranch,
currentBranch: branchName, currentBranch: branchName,
}; };
} }
} }
// Determine source branch - use baseBranch if provided, otherwise fetch default
let sourceBranch: string;
if (baseBranch) {
// Use provided base branch for source
sourceBranch = baseBranch;
} else {
// No base branch provided, fetch the default branch to use as source
const repoResponse = await octokits.rest.repos.get({
owner,
repo,
});
sourceBranch = repoResponse.data.default_branch;
}
// Creating a new branch for either an issue or closed/merged PR // Creating a new branch for either an issue or closed/merged PR
const entityType = isPR ? "pr" : "issue"; const entityType = isPR ? "pr" : "issue";
console.log(`Creating new branch for ${entityType} #${entityNumber}...`); console.log(
`Creating new branch for ${entityType} #${entityNumber} from source branch: ${sourceBranch}...`,
);
const timestamp = new Date() const timestamp = new Date()
.toISOString() .toISOString()
@@ -80,14 +93,14 @@ export async function setupBranch(
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`; const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
try { try {
// Get the SHA of the default branch // Get the SHA of the source branch
const defaultBranchRef = await octokits.rest.git.getRef({ const sourceBranchRef = await octokits.rest.git.getRef({
owner, owner,
repo, repo,
ref: `heads/${defaultBranch}`, ref: `heads/${sourceBranch}`,
}); });
const currentSHA = defaultBranchRef.data.object.sha; const currentSHA = sourceBranchRef.data.object.sha;
console.log(`Current SHA: ${currentSHA}`); console.log(`Current SHA: ${currentSHA}`);
@@ -109,9 +122,9 @@ export async function setupBranch(
// Set outputs for GitHub Actions // Set outputs for GitHub Actions
core.setOutput("CLAUDE_BRANCH", newBranch); core.setOutput("CLAUDE_BRANCH", newBranch);
core.setOutput("DEFAULT_BRANCH", defaultBranch); core.setOutput("BASE_BRANCH", sourceBranch);
return { return {
defaultBranch, baseBranch: sourceBranch,
claudeBranch: newBranch, claudeBranch: newBranch,
currentBranch: newBranch, currentBranch: newBranch,
}; };

View File

@@ -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

View File

@@ -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,

View File

@@ -127,7 +127,7 @@ describe("generatePrompt", () => {
eventName: "issue_comment", eventName: "issue_comment",
commentId: "67890", commentId: "67890",
isPR: false, isPR: false,
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000", claudeBranch: "claude/issue-67890-20240101_120000",
issueNumber: "67890", issueNumber: "67890",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
@@ -183,7 +183,7 @@ describe("generatePrompt", () => {
eventAction: "opened", eventAction: "opened",
isPR: false, isPR: false,
issueNumber: "789", issueNumber: "789",
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000", claudeBranch: "claude/issue-789-20240101_120000",
}, },
}; };
@@ -210,7 +210,7 @@ describe("generatePrompt", () => {
eventAction: "assigned", eventAction: "assigned",
isPR: false, isPR: false,
issueNumber: "999", issueNumber: "999",
defaultBranch: "develop", baseBranch: "develop",
claudeBranch: "claude/issue-999-20240101_120000", claudeBranch: "claude/issue-999-20240101_120000",
assigneeTrigger: "claude-bot", assigneeTrigger: "claude-bot",
}, },
@@ -238,7 +238,7 @@ describe("generatePrompt", () => {
eventAction: "opened", eventAction: "opened",
isPR: false, isPR: false,
issueNumber: "789", issueNumber: "789",
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000", claudeBranch: "claude/issue-789-20240101_120000",
}, },
}; };
@@ -285,7 +285,7 @@ describe("generatePrompt", () => {
commentId: "67890", commentId: "67890",
isPR: false, isPR: false,
issueNumber: "123", issueNumber: "123",
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000", claudeBranch: "claude/issue-67890-20240101_120000",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
}, },
@@ -307,7 +307,7 @@ describe("generatePrompt", () => {
commentId: "67890", commentId: "67890",
isPR: false, isPR: false,
issueNumber: "123", issueNumber: "123",
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000", claudeBranch: "claude/issue-67890-20240101_120000",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
}, },
@@ -362,7 +362,7 @@ describe("generatePrompt", () => {
eventAction: "opened", eventAction: "opened",
isPR: false, isPR: false,
issueNumber: "789", issueNumber: "789",
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000", claudeBranch: "claude/issue-789-20240101_120000",
}, },
}; };
@@ -400,7 +400,7 @@ describe("generatePrompt", () => {
commentId: "67890", commentId: "67890",
isPR: false, isPR: false,
issueNumber: "123", issueNumber: "123",
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-123-20240101_120000", claudeBranch: "claude/issue-123-20240101_120000",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
}, },
@@ -432,7 +432,7 @@ describe("generatePrompt", () => {
prNumber: "456", prNumber: "456",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
claudeBranch: "claude/pr-456-20240101_120000", claudeBranch: "claude/pr-456-20240101_120000",
defaultBranch: "main", baseBranch: "main",
}, },
}; };
@@ -470,7 +470,7 @@ describe("generatePrompt", () => {
isPR: true, isPR: true,
prNumber: "456", prNumber: "456",
commentBody: "@claude please fix this", commentBody: "@claude please fix this",
// No claudeBranch or defaultBranch for open PRs // No claudeBranch or baseBranch for open PRs
}, },
}; };
@@ -503,7 +503,7 @@ describe("generatePrompt", () => {
prNumber: "789", prNumber: "789",
commentBody: "@claude please update this", commentBody: "@claude please update this",
claudeBranch: "claude/pr-789-20240101_123000", claudeBranch: "claude/pr-789-20240101_123000",
defaultBranch: "develop", baseBranch: "develop",
}, },
}; };
@@ -531,7 +531,7 @@ describe("generatePrompt", () => {
commentId: "review-comment-123", commentId: "review-comment-123",
commentBody: "@claude fix this issue", commentBody: "@claude fix this issue",
claudeBranch: "claude/pr-999-20240101_140000", claudeBranch: "claude/pr-999-20240101_140000",
defaultBranch: "main", baseBranch: "main",
}, },
}; };
@@ -559,7 +559,7 @@ describe("generatePrompt", () => {
isPR: true, isPR: true,
prNumber: "555", prNumber: "555",
claudeBranch: "claude/pr-555-20240101_150000", claudeBranch: "claude/pr-555-20240101_150000",
defaultBranch: "main", baseBranch: "main",
}, },
}; };
@@ -604,7 +604,7 @@ describe("getEventTypeAndContext", () => {
eventAction: "assigned", eventAction: "assigned",
isPR: false, isPR: false,
issueNumber: "999", issueNumber: "999",
defaultBranch: "main", baseBranch: "main",
claudeBranch: "claude/issue-999-20240101_120000", claudeBranch: "claude/issue-999-20240101_120000",
assigneeTrigger: "claude-bot", assigneeTrigger: "claude-bot",
}, },
@@ -722,4 +722,51 @@ describe("buildDisallowedToolsString", () => {
expect(parts).toContain("BadTool1"); expect(parts).toContain("BadTool1");
expect(parts).toContain("BadTool2"); expect(parts).toContain("BadTool2");
}); });
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
const customDisallowedTools = "BadTool1,BadTool2";
const allowedTools = "WebSearch,SomeOtherTool";
const result = buildDisallowedToolsString(
customDisallowedTools,
allowedTools,
);
// WebSearch should be removed from disallowed since it's in allowed
expect(result).not.toContain("WebSearch");
// WebFetch should still be disallowed since it's not in allowed
expect(result).toContain("WebFetch");
// Custom disallowed tools should still be present
expect(result).toContain("BadTool1");
expect(result).toContain("BadTool2");
});
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
const allowedTools = "WebSearch,WebFetch,SomeOtherTool";
const result = buildDisallowedToolsString(undefined, allowedTools);
// Both hardcoded disallowed tools should be removed
expect(result).not.toContain("WebSearch");
expect(result).not.toContain("WebFetch");
// Result should be empty since no custom disallowed tools provided
expect(result).toBe("");
});
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
const customDisallowedTools = "BadTool1,BadTool2";
const allowedTools = "WebSearch,WebFetch";
const result = buildDisallowedToolsString(
customDisallowedTools,
allowedTools,
);
// Hardcoded tools should be removed
expect(result).not.toContain("WebSearch");
expect(result).not.toContain("WebFetch");
// Only custom disallowed tools should remain
expect(result).toBe("BadTool1,BadTool2");
});
}); });

View File

@@ -34,7 +34,7 @@ describe("parseEnvVarsWithContext", () => {
beforeEach(() => { beforeEach(() => {
process.env = { process.env = {
...BASE_ENV, ...BASE_ENV,
DEFAULT_BRANCH: "main", BASE_BRANCH: "main",
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000", CLAUDE_BRANCH: "claude/issue-67890-20240101_120000",
}; };
}); });
@@ -62,7 +62,7 @@ describe("parseEnvVarsWithContext", () => {
expect(result.eventData.claudeBranch).toBe( expect(result.eventData.claudeBranch).toBe(
"claude/issue-67890-20240101_120000", "claude/issue-67890-20240101_120000",
); );
expect(result.eventData.defaultBranch).toBe("main"); expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.commentBody).toBe( expect(result.eventData.commentBody).toBe(
"@claude can you help explain how to configure the logging system?", "@claude can you help explain how to configure the logging system?",
); );
@@ -75,7 +75,7 @@ describe("parseEnvVarsWithContext", () => {
).toThrow("CLAUDE_BRANCH is required for issue_comment event"); ).toThrow("CLAUDE_BRANCH is required for issue_comment event");
}); });
test("should throw error when DEFAULT_BRANCH is missing", () => { test("should throw error when BASE_BRANCH is missing", () => {
expect(() => expect(() =>
prepareContext( prepareContext(
mockIssueCommentContext, mockIssueCommentContext,
@@ -83,7 +83,7 @@ describe("parseEnvVarsWithContext", () => {
undefined, undefined,
"claude/issue-67890-20240101_120000", "claude/issue-67890-20240101_120000",
), ),
).toThrow("DEFAULT_BRANCH is required for issue_comment event"); ).toThrow("BASE_BRANCH is required for issue_comment event");
}); });
}); });
@@ -151,7 +151,7 @@ describe("parseEnvVarsWithContext", () => {
beforeEach(() => { beforeEach(() => {
process.env = { process.env = {
...BASE_ENV, ...BASE_ENV,
DEFAULT_BRANCH: "main", BASE_BRANCH: "main",
CLAUDE_BRANCH: "claude/issue-42-20240101_120000", CLAUDE_BRANCH: "claude/issue-42-20240101_120000",
}; };
}); });
@@ -172,7 +172,7 @@ describe("parseEnvVarsWithContext", () => {
result.eventData.eventAction === "opened" result.eventData.eventAction === "opened"
) { ) {
expect(result.eventData.issueNumber).toBe("42"); expect(result.eventData.issueNumber).toBe("42");
expect(result.eventData.defaultBranch).toBe("main"); expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.claudeBranch).toBe( expect(result.eventData.claudeBranch).toBe(
"claude/issue-42-20240101_120000", "claude/issue-42-20240101_120000",
); );
@@ -195,7 +195,7 @@ describe("parseEnvVarsWithContext", () => {
result.eventData.eventAction === "assigned" result.eventData.eventAction === "assigned"
) { ) {
expect(result.eventData.issueNumber).toBe("123"); expect(result.eventData.issueNumber).toBe("123");
expect(result.eventData.defaultBranch).toBe("main"); expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.claudeBranch).toBe( expect(result.eventData.claudeBranch).toBe(
"claude/issue-123-20240101_120000", "claude/issue-123-20240101_120000",
); );
@@ -209,7 +209,7 @@ describe("parseEnvVarsWithContext", () => {
).toThrow("CLAUDE_BRANCH is required for issues event"); ).toThrow("CLAUDE_BRANCH is required for issues event");
}); });
test("should throw error when DEFAULT_BRANCH is missing for issues", () => { test("should throw error when BASE_BRANCH is missing for issues", () => {
expect(() => expect(() =>
prepareContext( prepareContext(
mockIssueOpenedContext, mockIssueOpenedContext,
@@ -217,7 +217,7 @@ describe("parseEnvVarsWithContext", () => {
undefined, undefined,
"claude/issue-42-20240101_120000", "claude/issue-42-20240101_120000",
), ),
).toThrow("DEFAULT_BRANCH is required for issues event"); ).toThrow("BASE_BRANCH is required for issues event");
}); });
}); });