Compare commits

..

13 Commits

Author SHA1 Message Date
Ashwin Bhat
52ab0b1422 bump base action 2025-05-29 11:23:28 -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
Ashwin Bhat
176dbc369d bump base action to 0.0.6 (#79) 2025-05-28 13:19:10 -07:00
Erjan K
8ae72a97c6 Fix readme typo (#58) 2025-05-28 10:20:00 -07:00
Ashwin Bhat
0eb34ae441 Add shallow fetch to improve performance for large repositories (#53)
* Add shallow fetch to improve performance for large repositories

This change adds `--depth=1` to git fetch operations to perform shallow
fetches instead of full history downloads. This significantly reduces
checkout time for large repositories as reported in issue #52.

Changes:
- Line 55: Added --depth=1 to PR branch fetch
- Line 102: Added --depth=1 to new branch fetch

Fixes #52

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

* fetch 50 commits for PRs

---------

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-27 16:31:06 -07:00
Ashwin Bhat
804959ac41 add issue triage workflow (#70) 2025-05-27 14:04:41 -07:00
Ashwin Bhat
21e17bd590 remove .DS_Store (#69) 2025-05-27 13:26:03 -07:00
Ashwin Bhat
4b925ddf0c Update issue templates (#51) 2025-05-27 13:18:29 -07:00
16 changed files with 384 additions and 110 deletions

36
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,36 @@
---
name: Bug report
about: Create a report to help us improve
title: ""
labels: bug
assignees: ""
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Workflow yml file**
If it's not sensitive, consider including a paste of your full Claude workflow.yml file.
**API Provider**
[ ] Anthropic First-Party API (default)
[ ] AWS Bedrock
[ ] GCP Vertex
**Additional context**
Add any other context about the problem here.

View File

@@ -34,3 +34,5 @@ jobs:
uses: anthropics/claude-code-action@beta
with:
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."

104
.github/workflows/issue-triage.yml vendored Normal file
View File

@@ -0,0 +1,104 @@
name: Claude Issue Triage
description: Run Claude Code for issue triage in GitHub Actions
on:
issues:
types: [opened]
jobs:
triage-issue:
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
issues: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup GitHub MCP Server
run: |
mkdir -p /tmp/mcp-config
cat > /tmp/mcp-config/mcp-servers.json << 'EOF'
{
"github": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"GITHUB_PERSONAL_ACCESS_TOKEN",
"ghcr.io/github/github-mcp-server:sha-7aced2b"
],
"env": {
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
}
}
}
EOF
- name: Create triage prompt
run: |
mkdir -p /tmp/claude-prompts
cat > /tmp/claude-prompts/triage-prompt.txt << 'EOF'
You're an issue triage assistant for GitHub issues. Your task is to analyze the issue and select appropriate labels from the provided list.
IMPORTANT: Don't post any comments or messages to the issue. Your only action should be to apply labels.
Issue Information:
- REPO: ${{ github.repository }}
- ISSUE_NUMBER: ${{ github.event.issue.number }}
TASK OVERVIEW:
1. First, fetch the list of labels available in this repository by running: `gh label list`. Run exactly this command with nothing else.
2. Next, use the GitHub tools to get context about the issue:
- You have access to these tools:
- mcp__github__get_issue: Use this to retrieve the current issue's details including title, description, and existing labels
- mcp__github__get_issue_comments: Use this to read any discussion or additional context provided in the comments
- mcp__github__update_issue: Use this to apply labels to the issue (do not use this for commenting)
- mcp__github__search_issues: Use this to find similar issues that might provide context for proper categorization and to identify potential duplicate issues
- mcp__github__list_issues: Use this to understand patterns in how other issues are labeled
- Start by using mcp__github__get_issue to get the issue details
3. Analyze the issue content, considering:
- The issue title and description
- The type of issue (bug report, feature request, question, etc.)
- Technical areas mentioned
- Severity or priority indicators
- User impact
- Components affected
4. Select appropriate labels from the available labels list provided above:
- Choose labels that accurately reflect the issue's nature
- Be specific but comprehensive
- Select priority labels if you can determine urgency (high-priority, med-priority, or low-priority)
- Consider platform labels (android, ios) if applicable
- If you find similar issues using mcp__github__search_issues, consider using a "duplicate" label if appropriate. Only do so if the issue is a duplicate of another OPEN issue.
5. Apply the selected labels:
- Use mcp__github__update_issue to apply your selected labels
- DO NOT post any comments explaining your decision
- DO NOT communicate directly with users
- If no labels are clearly applicable, do not apply any labels
IMPORTANT GUIDELINES:
- Be thorough in your analysis
- Only select labels from the provided list above
- DO NOT post any comments to the issue
- Your ONLY action should be to apply labels using mcp__github__update_issue
- It's okay to not add any labels if none are clearly applicable
EOF
- name: Run Claude Code for Issue Triage
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: /tmp/claude-prompts/triage-prompt.txt
allowed_tools: "Bash(gh label list),mcp__github__get_issue,mcp__github__get_issue_comments,mcp__github__update_issue,mcp__github__search_issues,mcp__github__list_issues"
mcp_config_file: /tmp/mcp-config/mcp-servers.json
timeout_minutes: "5"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

View File

@@ -446,7 +446,7 @@ anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
```
This applies to all sensitive values including API keys, access tokens, and credentials.
We also reccomend that you always use short-lived tokens when possible
We also recommend that you always use short-lived tokens when possible
## License

View File

@@ -12,6 +12,9 @@ inputs:
assignee_trigger:
description: "The assignee username that triggers the action (e.g. @claude)"
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
model:
@@ -85,6 +88,7 @@ runs:
env:
TRIGGER_PHRASE: ${{ inputs.trigger_phrase }}
ASSIGNEE_TRIGGER: ${{ inputs.assignee_trigger }}
BASE_BRANCH: ${{ inputs.base_branch }}
ALLOWED_TOOLS: ${{ inputs.allowed_tools }}
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
@@ -94,7 +98,7 @@ runs:
- name: Run Claude Code
id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true'
uses: anthropics/claude-code-base-action@5097b6cdfe5fc5a3ac0166cc344c34ed23c93982 # https://github.com/anthropics/claude-code-base-action/releases/tag/v0.0.5
uses: anthropics/claude-code-base-action@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8
with:
prompt_file: /tmp/claude-prompts/claude-prompt.txt
allowed_tools: ${{ env.ALLOWED_TOOLS }}
@@ -110,6 +114,9 @@ runs:
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
# Provider configuration
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
# AWS configuration
AWS_REGION: ${{ env.AWS_REGION }}
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
@@ -143,10 +150,12 @@ runs:
TRIGGER_COMMENT_ID: ${{ github.event.comment.id }}
CLAUDE_BRANCH: ${{ steps.prepare.outputs.CLAUDE_BRANCH }}
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' }}
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 != ''

View File

@@ -58,10 +58,27 @@ export function buildAllowedToolsString(
export function buildDisallowedToolsString(
customDisallowedTools?: string,
allowedTools?: 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) {
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
if (allDisallowedTools) {
allDisallowedTools = `${allDisallowedTools},${customDisallowedTools}`;
} else {
allDisallowedTools = customDisallowedTools;
}
}
return allDisallowedTools;
}
@@ -69,7 +86,7 @@ export function buildDisallowedToolsString(
export function prepareContext(
context: ParsedGitHubContext,
claudeCommentId: string,
defaultBranch?: string,
baseBranch?: string,
claudeBranch?: string,
): PreparedContext {
const repository = context.repository.full_name;
@@ -147,7 +164,7 @@ export function prepareContext(
...(commentId && { commentId }),
commentBody,
...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }),
...(baseBranch && { baseBranch }),
};
break;
@@ -169,7 +186,7 @@ export function prepareContext(
prNumber,
commentBody,
...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }),
...(baseBranch && { baseBranch }),
};
break;
@@ -194,13 +211,13 @@ export function prepareContext(
prNumber,
commentBody,
...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }),
...(baseBranch && { baseBranch }),
};
break;
} else if (!claudeBranch) {
throw new Error("CLAUDE_BRANCH is required for issue_comment event");
} else if (!defaultBranch) {
throw new Error("DEFAULT_BRANCH is required for issue_comment event");
} else if (!baseBranch) {
throw new Error("BASE_BRANCH is required for issue_comment event");
} else if (!issueNumber) {
throw new Error(
"ISSUE_NUMBER is required for issue_comment event for issues",
@@ -212,7 +229,7 @@ export function prepareContext(
commentId,
isPR: false,
claudeBranch: claudeBranch,
defaultBranch,
baseBranch,
issueNumber,
commentBody,
};
@@ -228,8 +245,8 @@ export function prepareContext(
if (isPR) {
throw new Error("IS_PR must be false for issues event");
}
if (!defaultBranch) {
throw new Error("DEFAULT_BRANCH is required for issues event");
if (!baseBranch) {
throw new Error("BASE_BRANCH is required for issues event");
}
if (!claudeBranch) {
throw new Error("CLAUDE_BRANCH is required for issues event");
@@ -246,7 +263,7 @@ export function prepareContext(
eventAction: "assigned",
isPR: false,
issueNumber,
defaultBranch,
baseBranch,
claudeBranch,
assigneeTrigger,
};
@@ -256,7 +273,7 @@ export function prepareContext(
eventAction: "opened",
isPR: false,
issueNumber,
defaultBranch,
baseBranch,
claudeBranch,
};
} else {
@@ -277,7 +294,7 @@ export function prepareContext(
isPR: true,
prNumber,
...(claudeBranch && { claudeBranch }),
...(defaultBranch && { defaultBranch }),
...(baseBranch && { baseBranch }),
};
break;
@@ -524,13 +541,13 @@ ${context.directPrompt ? ` - DIRECT INSTRUCTION: A direct instruction was prov
${
eventData.claudeBranch
? `- 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 (..)
Example: ${GITHUB_SERVER_URL}/${context.repository}/compare/main...feature-branch (correct)
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
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 body should include:
- 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(
claudeCommentId: number,
defaultBranch: string | undefined,
baseBranch: string | undefined,
claudeBranch: string | undefined,
githubData: FetchDataResult,
context: ParsedGitHubContext,
@@ -624,7 +641,7 @@ export async function createPrompt(
const preparedContext = prepareContext(
context,
claudeCommentId.toString(),
defaultBranch,
baseBranch,
claudeBranch,
);
@@ -648,6 +665,7 @@ export async function createPrompt(
);
const allDisallowedTools = buildDisallowedToolsString(
preparedContext.disallowedTools,
preparedContext.allowedTools,
);
core.exportVariable("ALLOWED_TOOLS", allAllowedTools);

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ async function run() {
const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!);
const githubToken = process.env.GITHUB_TOKEN!;
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 context = parseGitHubContext();
@@ -92,7 +92,7 @@ async function run() {
owner,
repo,
claudeBranch,
defaultBranch,
baseBranch,
);
// 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
const serverUrlPattern = serverUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const prUrlPattern = new RegExp(
`${serverUrlPattern}\\/.+\\/compare\\/${defaultBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
`${serverUrlPattern}\\/.+\\/compare\\/${baseBranch.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\.\\.\\.`,
);
const containsPRUrl = currentBody.match(prUrlPattern);
@@ -113,7 +113,7 @@ async function run() {
await octokit.rest.repos.compareCommitsWithBasehead({
owner,
repo,
basehead: `${defaultBranch}...${claudeBranch}`,
basehead: `${baseBranch}...${claudeBranch}`,
});
// If there are changes (commits or file changes), add the PR URL
@@ -128,7 +128,7 @@ async function run() {
const prBody = encodeURIComponent(
`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})`;
}
} catch (error) {
@@ -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);

View File

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

View File

@@ -6,7 +6,7 @@ export async function checkAndDeleteEmptyBranch(
owner: string,
repo: string,
claudeBranch: string | undefined,
defaultBranch: string,
baseBranch: string,
): Promise<{ shouldDeleteBranch: boolean; branchLink: string }> {
let branchLink = "";
let shouldDeleteBranch = false;
@@ -18,7 +18,7 @@ export async function checkAndDeleteEmptyBranch(
await octokit.rest.repos.compareCommitsWithBasehead({
owner,
repo,
basehead: `${defaultBranch}...${claudeBranch}`,
basehead: `${baseBranch}...${claudeBranch}`,
});
// 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";
export type BranchInfo = {
defaultBranch: string;
baseBranch: string;
claudeBranch?: string;
currentBranch: string;
};
@@ -26,15 +26,9 @@ export async function setupBranch(
): Promise<BranchInfo> {
const { owner, repo } = context.repository;
const entityNumber = context.entityNumber;
const { baseBranch } = context.inputs;
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) {
const prData = githubData.contextData as GitHubPullRequest;
const prState = prData.state;
@@ -42,7 +36,7 @@ export async function setupBranch(
// Check if PR is closed or merged
if (prState === "CLOSED" || prState === "MERGED") {
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
} else {
@@ -51,23 +45,43 @@ export async function setupBranch(
const branchName = prData.headRefName;
// Execute git commands to checkout PR branch
await $`git fetch origin ${branchName}`;
// Execute git commands to checkout PR branch (shallow fetch for performance)
// Fetch the branch with a depth of 20 to avoid fetching too much history, while still allowing for some context
await $`git fetch origin --depth=20 ${branchName}`;
await $`git checkout ${branchName}`;
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 {
defaultBranch,
baseBranch,
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
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()
.toISOString()
@@ -79,14 +93,14 @@ export async function setupBranch(
const newBranch = `claude/${entityType}-${entityNumber}-${timestamp}`;
try {
// Get the SHA of the default branch
const defaultBranchRef = await octokits.rest.git.getRef({
// Get the SHA of the source branch
const sourceBranchRef = await octokits.rest.git.getRef({
owner,
repo,
ref: `heads/${defaultBranch}`,
ref: `heads/${sourceBranch}`,
});
const currentSHA = defaultBranchRef.data.object.sha;
const currentSHA = sourceBranchRef.data.object.sha;
console.log(`Current SHA: ${currentSHA}`);
@@ -98,8 +112,8 @@ export async function setupBranch(
sha: currentSHA,
});
// Checkout the new branch
await $`git fetch origin ${newBranch}`;
// Checkout the new branch (shallow fetch for performance)
await $`git fetch origin --depth=1 ${newBranch}`;
await $`git checkout ${newBranch}`;
console.log(
@@ -108,9 +122,9 @@ export async function setupBranch(
// Set outputs for GitHub Actions
core.setOutput("CLAUDE_BRANCH", newBranch);
core.setOutput("DEFAULT_BRANCH", defaultBranch);
core.setOutput("BASE_BRANCH", sourceBranch);
return {
defaultBranch,
baseBranch: sourceBranch,
claudeBranch: newBranch,
currentBranch: newBranch,
};

View File

@@ -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\`\`\`\n${errorDetails}\n\`\`\``;
}
newBody += `\n\n---\n`;
// Clean up the body content
// 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**");
});
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", () => {
const input = {
...baseInput,

View File

@@ -127,7 +127,7 @@ describe("generatePrompt", () => {
eventName: "issue_comment",
commentId: "67890",
isPR: false,
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000",
issueNumber: "67890",
commentBody: "@claude please fix this",
@@ -183,7 +183,7 @@ describe("generatePrompt", () => {
eventAction: "opened",
isPR: false,
issueNumber: "789",
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000",
},
};
@@ -210,7 +210,7 @@ describe("generatePrompt", () => {
eventAction: "assigned",
isPR: false,
issueNumber: "999",
defaultBranch: "develop",
baseBranch: "develop",
claudeBranch: "claude/issue-999-20240101_120000",
assigneeTrigger: "claude-bot",
},
@@ -238,7 +238,7 @@ describe("generatePrompt", () => {
eventAction: "opened",
isPR: false,
issueNumber: "789",
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000",
},
};
@@ -285,7 +285,7 @@ describe("generatePrompt", () => {
commentId: "67890",
isPR: false,
issueNumber: "123",
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000",
commentBody: "@claude please fix this",
},
@@ -307,7 +307,7 @@ describe("generatePrompt", () => {
commentId: "67890",
isPR: false,
issueNumber: "123",
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101_120000",
commentBody: "@claude please fix this",
},
@@ -362,7 +362,7 @@ describe("generatePrompt", () => {
eventAction: "opened",
isPR: false,
issueNumber: "789",
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101_120000",
},
};
@@ -400,7 +400,7 @@ describe("generatePrompt", () => {
commentId: "67890",
isPR: false,
issueNumber: "123",
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-123-20240101_120000",
commentBody: "@claude please fix this",
},
@@ -432,7 +432,7 @@ describe("generatePrompt", () => {
prNumber: "456",
commentBody: "@claude please fix this",
claudeBranch: "claude/pr-456-20240101_120000",
defaultBranch: "main",
baseBranch: "main",
},
};
@@ -470,7 +470,7 @@ describe("generatePrompt", () => {
isPR: true,
prNumber: "456",
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",
commentBody: "@claude please update this",
claudeBranch: "claude/pr-789-20240101_123000",
defaultBranch: "develop",
baseBranch: "develop",
},
};
@@ -531,7 +531,7 @@ describe("generatePrompt", () => {
commentId: "review-comment-123",
commentBody: "@claude fix this issue",
claudeBranch: "claude/pr-999-20240101_140000",
defaultBranch: "main",
baseBranch: "main",
},
};
@@ -559,7 +559,7 @@ describe("generatePrompt", () => {
isPR: true,
prNumber: "555",
claudeBranch: "claude/pr-555-20240101_150000",
defaultBranch: "main",
baseBranch: "main",
},
};
@@ -604,7 +604,7 @@ describe("getEventTypeAndContext", () => {
eventAction: "assigned",
isPR: false,
issueNumber: "999",
defaultBranch: "main",
baseBranch: "main",
claudeBranch: "claude/issue-999-20240101_120000",
assigneeTrigger: "claude-bot",
},
@@ -722,4 +722,51 @@ describe("buildDisallowedToolsString", () => {
expect(parts).toContain("BadTool1");
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(() => {
process.env = {
...BASE_ENV,
DEFAULT_BRANCH: "main",
BASE_BRANCH: "main",
CLAUDE_BRANCH: "claude/issue-67890-20240101_120000",
};
});
@@ -62,7 +62,7 @@ describe("parseEnvVarsWithContext", () => {
expect(result.eventData.claudeBranch).toBe(
"claude/issue-67890-20240101_120000",
);
expect(result.eventData.defaultBranch).toBe("main");
expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.commentBody).toBe(
"@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");
});
test("should throw error when DEFAULT_BRANCH is missing", () => {
test("should throw error when BASE_BRANCH is missing", () => {
expect(() =>
prepareContext(
mockIssueCommentContext,
@@ -83,7 +83,7 @@ describe("parseEnvVarsWithContext", () => {
undefined,
"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(() => {
process.env = {
...BASE_ENV,
DEFAULT_BRANCH: "main",
BASE_BRANCH: "main",
CLAUDE_BRANCH: "claude/issue-42-20240101_120000",
};
});
@@ -172,7 +172,7 @@ describe("parseEnvVarsWithContext", () => {
result.eventData.eventAction === "opened"
) {
expect(result.eventData.issueNumber).toBe("42");
expect(result.eventData.defaultBranch).toBe("main");
expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.claudeBranch).toBe(
"claude/issue-42-20240101_120000",
);
@@ -195,7 +195,7 @@ describe("parseEnvVarsWithContext", () => {
result.eventData.eventAction === "assigned"
) {
expect(result.eventData.issueNumber).toBe("123");
expect(result.eventData.defaultBranch).toBe("main");
expect(result.eventData.baseBranch).toBe("main");
expect(result.eventData.claudeBranch).toBe(
"claude/issue-123-20240101_120000",
);
@@ -209,7 +209,7 @@ describe("parseEnvVarsWithContext", () => {
).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(() =>
prepareContext(
mockIssueOpenedContext,
@@ -217,7 +217,7 @@ describe("parseEnvVarsWithContext", () => {
undefined,
"claude/issue-42-20240101_120000",
),
).toThrow("DEFAULT_BRANCH is required for issues event");
).toThrow("BASE_BRANCH is required for issues event");
});
});