mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
Compare commits
11 Commits
ashwin/err
...
ashwin/err
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44e276ae4f | ||
|
|
baa1ecb265 | ||
|
|
81181ca658 | ||
|
|
176dbc369d | ||
|
|
8ae72a97c6 | ||
|
|
0eb34ae441 | ||
|
|
804959ac41 | ||
|
|
21e17bd590 | ||
|
|
4b925ddf0c | ||
|
|
253f2c6796 | ||
|
|
3c6a85b54b |
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
36
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
104
.github/workflows/issue-triage.yml
vendored
Normal file
104
.github/workflows/issue-triage.yml
vendored
Normal 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 }}
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
|
||||
**/.claude/settings.local.json
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Install Bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
uses: oven-sh/setup-bun@735343b667d3e6f658f44d0eca948eb6282f2b76 # https://github.com/oven-sh/setup-bun/releases/tag/v2.0.2
|
||||
with:
|
||||
bun-version: 1.2.11
|
||||
|
||||
@@ -94,7 +94,7 @@ runs:
|
||||
- name: Run Claude Code
|
||||
id: claude-code
|
||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||
uses: anthropics/claude-code-base-action@beta
|
||||
uses: anthropics/claude-code-base-action@266585c92dd90d61d3806a3367582c4f6224e892 # https://github.com/anthropics/claude-code-base-action/releases/tag/v0.0.6
|
||||
with:
|
||||
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
||||
@@ -147,6 +147,8 @@ runs:
|
||||
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 != ''
|
||||
|
||||
BIN
src/.DS_Store
vendored
BIN
src/.DS_Store
vendored
Binary file not shown.
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -51,8 +51,9 @@ 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}`);
|
||||
@@ -98,8 +99,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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -39,25 +39,19 @@ async function retryWithBackoff<T>(
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Operation failed after ${maxAttempts} attempts. Last error: ${
|
||||
lastError?.message ?? "Unknown error"
|
||||
}`,
|
||||
);
|
||||
console.error(`Operation failed after ${maxAttempts} attempts`);
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function getOidcToken(): Promise<string> {
|
||||
try {
|
||||
const oidcToken = await core.getIDToken("claude-code-github-action");
|
||||
|
||||
if (!oidcToken) {
|
||||
throw new Error("OIDC token not found");
|
||||
}
|
||||
|
||||
return oidcToken;
|
||||
} catch (error) {
|
||||
console.error("Failed to get OIDC token:", error);
|
||||
throw new Error(
|
||||
`Failed to get OIDC token: ${error instanceof Error ? error.message : String(error)}`,
|
||||
"Could not fetch an OIDC token. Did you remember to add `id-token: write` to your workflow permissions?",
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -74,9 +68,15 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`App token exchange failed: ${response.status} ${response.statusText}`,
|
||||
const responseJson = (await response.json()) as {
|
||||
error?: {
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
console.error(
|
||||
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
||||
);
|
||||
throw new Error(`${responseJson?.error?.message ?? "Unknown error"}`);
|
||||
}
|
||||
|
||||
const appTokenData = (await response.json()) as {
|
||||
@@ -117,7 +117,9 @@ export async function setupGitHubToken(): Promise<string> {
|
||||
core.setOutput("GITHUB_TOKEN", appToken);
|
||||
return appToken;
|
||||
} catch (error) {
|
||||
core.setFailed(`Failed to setup GitHub token: ${error}`);
|
||||
core.setFailed(
|
||||
`Failed to setup GitHub token: ${error}.\n\nIf you instead wish to use this action with a custom GitHub token or custom GitHub app, provide a \`github_token\` in the \`uses\` section of the app in your workflow yml file.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user