Compare commits

..

9 Commits

Author SHA1 Message Date
Ashwin Bhat
5c093bf762 chore: update claude-code-base-action to v0.0.7 2025-05-28 17:16:24 -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
Ashwin Bhat
253f2c6796 Pin GitHub Action dependencies to commit SHAs for security (#66)
Pin oven-sh/setup-bun and anthropics/claude-code-base-action to specific commit SHAs instead of version tags to ensure reproducible builds and improve supply chain security.

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-05-27 10:14:11 -07:00
Ashwin Bhat
3c6a85b54b Improve error messages for GitHub Action authentication failures (#50)
- Add helpful hint about workflow permissions when OIDC token is not found
- Include response body in app token exchange failure errors for better debugging

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

Co-authored-by: Claude <noreply@anthropic.com>
2025-05-25 18:43:54 -07:00
16 changed files with 215 additions and 941 deletions

BIN
.DS_Store vendored

Binary file not shown.

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

@@ -31,6 +31,6 @@ jobs:
- name: Run Claude Code - name: Run Claude Code
id: claude id: claude
uses: ./ uses: anthropics/claude-code-action@beta
with: with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

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

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.DS_Store
node_modules node_modules
**/.claude/settings.local.json **/.claude/settings.local.json

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. 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 ## License

View File

@@ -67,7 +67,7 @@ runs:
using: "composite" using: "composite"
steps: steps:
- name: Install Bun - 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: with:
bun-version: 1.2.11 bun-version: 1.2.11
@@ -94,7 +94,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@beta uses: anthropics/claude-code-base-action@78eef48a8f466f7a800a2315134506d4c7ad9163 # v0.0.7
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 }}

View File

@@ -1,93 +0,0 @@
#!/usr/bin/env bun
// Check the status of PR #105775
const GITHUB_API_URL = "https://api.github.com";
async function checkPRStatus(token: string) {
const headers = {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
};
try {
// Check PR details
console.log("Checking PR #105775...\n");
const prResponse = await fetch(
`${GITHUB_API_URL}/repos/anthropics/anthropic/pulls/105775`,
{ headers }
);
console.log(`PR Status: ${prResponse.status}`);
if (!prResponse.ok) {
if (prResponse.status === 404) {
console.log("PR not found - it might be in a private repo or deleted");
}
const error = await prResponse.text();
console.error(error);
return;
}
const prData = await prResponse.json();
console.log(`Title: ${prData.title}`);
console.log(`State: ${prData.state}`);
console.log(`Branch: ${prData.head.ref}`);
console.log(`Base: ${prData.base.ref}`);
console.log(`Created: ${prData.created_at}`);
console.log(`Updated: ${prData.updated_at}`);
// Check if branch still exists
console.log(`\nChecking if branch '${prData.head.ref}' still exists...`);
const branchResponse = await fetch(
`${GITHUB_API_URL}/repos/anthropics/anthropic/git/refs/heads/${prData.head.ref}`,
{ headers }
);
if (branchResponse.ok) {
const branchData = await branchResponse.json();
console.log(`✓ Branch exists with SHA: ${branchData.object.sha}`);
console.log(` PR head SHA: ${prData.head.sha}`);
if (branchData.object.sha !== prData.head.sha) {
console.log(` ⚠️ Branch has been updated since PR was created`);
}
} else {
console.log(`✗ Branch does not exist (${branchResponse.status})`);
}
// Get recent comments
console.log(`\nFetching recent comments...`);
const commentsResponse = await fetch(
`${GITHUB_API_URL}/repos/anthropics/anthropic/issues/105775/comments?per_page=5&sort=created&direction=desc`,
{ headers }
);
if (commentsResponse.ok) {
const comments = await commentsResponse.json();
console.log(`Found ${comments.length} recent comments:`);
comments.reverse().forEach((comment: any, index: number) => {
console.log(`\nComment ${index + 1}:`);
console.log(` Author: ${comment.user.login}`);
console.log(` Created: ${comment.created_at}`);
console.log(` Body preview: ${comment.body.substring(0, 100)}...`);
// Check if it's a claude-code-action comment
if (comment.body.includes("claude") || comment.user.login.includes("bot")) {
console.log(` → Appears to be a Claude-related comment`);
}
});
}
} catch (error) {
console.error("Error:", error);
}
}
const token = process.argv[2];
if (!token) {
console.log("Usage: bun check-pr-status.ts <github-pat>");
process.exit(1);
}
checkPRStatus(token);

View File

@@ -1,183 +0,0 @@
#!/usr/bin/env bun
// Diagnose why GitHub App permissions are inconsistent
const GITHUB_API_URL = "https://api.github.com";
async function diagnosePermissions(token: string, owner: string, repo: string) {
const headers = {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
};
console.log(`\n=== Diagnosing GitHub App Permission Issues ===`);
console.log(`Repository: ${owner}/${repo}\n`);
try {
// 1. Check what type of token we have
console.log("1. Token Analysis:");
const authHeader = headers.Authorization;
if (authHeader.includes('ghs_')) {
console.log("✓ GitHub App installation token detected");
} else if (authHeader.includes('ghp_')) {
console.log("✓ Personal Access Token detected");
} else {
console.log("? Unknown token type");
}
// 2. Check rate limit headers (different for apps vs users)
console.log("\n2. Rate Limit Analysis:");
const rateLimitResponse = await fetch(`${GITHUB_API_URL}/rate_limit`, { headers });
if (rateLimitResponse.ok) {
const rateData = await rateLimitResponse.json();
console.log(` Core limit: ${rateData.rate.remaining}/${rateData.rate.limit}`);
if (rateData.rate.limit > 5000) {
console.log(" → Higher limit suggests GitHub App token");
} else {
console.log(" → Standard limit suggests user token");
}
}
// 3. Test different API endpoints to find permission boundaries
console.log("\n3. Testing API Endpoints:");
// Test regular content API
console.log("\n a) Content API (high-level):");
const contentResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/contents/README.md`,
{ headers }
);
console.log(` GET contents: ${contentResponse.status} ${contentResponse.ok ? '✓' : '✗'}`);
// Test git database read
console.log("\n b) Git Database API (read):");
const branchResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/branches`,
{ headers }
);
if (branchResponse.ok) {
const branches = await branchResponse.json();
const defaultBranch = branches.find((b: any) => b.name === 'main' || b.name === 'master' || b.name === 'staging');
if (defaultBranch) {
const commitSha = defaultBranch.commit.sha;
// Try to read commit
const commitResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${commitSha}`,
{ headers }
);
console.log(` GET commit: ${commitResponse.status} ${commitResponse.ok ? '✓' : '✗'}`);
// Try to read tree
if (commitResponse.ok) {
const commitData = await commitResponse.json();
const treeResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees/${commitData.tree.sha}`,
{ headers }
);
console.log(` GET tree: ${treeResponse.status} ${treeResponse.ok ? '✓' : '✗'}`);
}
}
}
// Test git database write
console.log("\n c) Git Database API (write):");
// Get a base commit to test with
const testBranch = branches[0];
if (testBranch) {
const baseCommitResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${testBranch.commit.sha}`,
{ headers }
);
if (baseCommitResponse.ok) {
const baseCommit = await baseCommitResponse.json();
// Try to create a blob
const blobResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/blobs`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
content: "test",
encoding: "utf-8",
}),
}
);
console.log(` POST blob: ${blobResponse.status} ${blobResponse.ok ? '✓' : '✗'}`);
// Try to create a tree
const treeResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
base_tree: baseCommit.tree.sha,
tree: [{
path: "test-permission-check.txt",
mode: "100644",
type: "blob",
content: "test",
}],
}),
}
);
console.log(` POST tree: ${treeResponse.status} ${treeResponse.ok ? '✓' : '✗'}`);
if (!treeResponse.ok) {
const error = await treeResponse.text();
console.log(` Error: ${error}`);
}
}
}
// 4. Check webhook/app events
console.log("\n4. Checking Recent Activity:");
const eventsResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/events?per_page=10`,
{ headers }
);
if (eventsResponse.ok) {
const events = await eventsResponse.json();
const appEvents = events.filter((e: any) =>
e.actor.login.includes('[bot]') ||
e.actor.type === 'Bot'
);
console.log(` Found ${appEvents.length} bot/app events in last 10 events`);
appEvents.forEach((event: any) => {
console.log(` - ${event.actor.login}: ${event.type} at ${event.created_at}`);
});
}
// 5. Summary and recommendations
console.log("\n=== Analysis Summary ===");
console.log("\nPossible causes for inconsistent 500 errors:");
console.log("1. Race conditions with other bots (check events above)");
console.log("2. Token scope varies based on who triggered the action");
console.log("3. GitHub App needs to be reinstalled/reconfigured");
console.log("4. Branch-specific protection rules");
} catch (error) {
console.error("\nError during diagnosis:", error);
}
}
const [token, owner, repo] = process.argv.slice(2);
if (!token || !owner || !repo) {
console.log("Usage: bun diagnose-app-permissions.ts <token> <owner> <repo>");
console.log("\nThis script helps diagnose why GitHub App permissions are inconsistent.");
process.exit(1);
}
diagnosePermissions(token, owner, repo);

View File

@@ -1,112 +0,0 @@
#!/usr/bin/env bun
// Revert the test commit we just made
const GITHUB_API_URL = "https://api.github.com";
async function revertTestCommit(token: string) {
const owner = "anthropics";
const repo = "anthropic";
const branch = "monty/fixing-pipeline-runner";
const headers = {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
};
console.log(`\n=== Reverting test commit ===`);
console.log(`Repository: ${owner}/${repo}`);
console.log(`Branch: ${branch}\n`);
try {
// Get current branch state
console.log("Getting current branch reference...");
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
const refResponse = await fetch(refUrl, { headers });
if (!refResponse.ok) {
console.error(`Failed to get branch: ${refResponse.status}`);
return;
}
const refData = await refResponse.json();
const currentSha = refData.object.sha;
console.log(`Current branch SHA: ${currentSha}`);
// Get the current commit to find its parent
console.log("\nGetting current commit details...");
const commitResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${currentSha}`,
{ headers }
);
if (!commitResponse.ok) {
console.error(`Failed to get commit: ${commitResponse.status}`);
return;
}
const commitData = await commitResponse.json();
console.log(`Current commit message: "${commitData.message}"`);
if (!commitData.message.includes("Debug: Test commit to reproduce 500 error")) {
console.log("⚠️ Current commit doesn't look like our test commit");
console.log("Are you sure you want to revert this?");
console.log("Current message:", commitData.message);
return;
}
if (commitData.parents.length === 0) {
console.error("Cannot revert: this appears to be the initial commit");
return;
}
const parentSha = commitData.parents[0].sha;
console.log(`Parent SHA: ${parentSha}`);
// Reset the branch to the parent commit
console.log("\nReverting branch to parent commit...");
const updateRefResponse = await fetch(refUrl, {
method: "PATCH",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
sha: parentSha,
force: true, // Force is needed for this kind of reset
}),
});
if (!updateRefResponse.ok) {
console.error(`Failed to revert: ${updateRefResponse.status}`);
const error = await updateRefResponse.text();
console.error(error);
return;
}
console.log("✅ Successfully reverted test commit!");
console.log(`Branch ${branch} is now back to SHA: ${parentSha}`);
// Verify the revert
console.log("\nVerifying revert...");
const verifyResponse = await fetch(refUrl, { headers });
const verifyData = await verifyResponse.json();
if (verifyData.object.sha === parentSha) {
console.log("✅ Revert confirmed");
} else {
console.log("⚠️ Unexpected SHA after revert:", verifyData.object.sha);
}
} catch (error) {
console.error("Error:", error);
}
}
const token = process.argv[2];
if (!token) {
console.log("Usage: bun revert-test-commit.ts <github-pat>");
process.exit(1);
}
revertTestCommit(token);

BIN
src/.DS_Store vendored

Binary file not shown.

View File

@@ -51,8 +51,9 @@ export async function setupBranch(
const branchName = prData.headRefName; const branchName = prData.headRefName;
// Execute git commands to checkout PR branch // Execute git commands to checkout PR branch (shallow fetch for performance)
await $`git fetch origin ${branchName}`; // 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}`; await $`git checkout ${branchName}`;
console.log(`Successfully checked out PR branch for PR #${entityNumber}`); console.log(`Successfully checked out PR branch for PR #${entityNumber}`);
@@ -98,8 +99,8 @@ export async function setupBranch(
sha: currentSHA, sha: currentSHA,
}); });
// Checkout the new branch // Checkout the new branch (shallow fetch for performance)
await $`git fetch origin ${newBranch}`; await $`git fetch origin --depth=1 ${newBranch}`;
await $`git checkout ${newBranch}`; await $`git checkout ${newBranch}`;
console.log( console.log(

View File

@@ -39,25 +39,19 @@ async function retryWithBackoff<T>(
} }
} }
throw new Error( console.error(`Operation failed after ${maxAttempts} attempts`);
`Operation failed after ${maxAttempts} attempts. Last error: ${ throw lastError;
lastError?.message ?? "Unknown error"
}`,
);
} }
async function getOidcToken(): Promise<string> { async function getOidcToken(): Promise<string> {
try { try {
const oidcToken = await core.getIDToken("claude-code-github-action"); const oidcToken = await core.getIDToken("claude-code-github-action");
if (!oidcToken) {
throw new Error("OIDC token not found");
}
return oidcToken; return oidcToken;
} catch (error) { } catch (error) {
console.error("Failed to get OIDC token:", error);
throw new 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) { if (!response.ok) {
throw new Error( const responseJson = (await response.json()) as {
`App token exchange failed: ${response.status} ${response.statusText}`, 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 { const appTokenData = (await response.json()) as {
@@ -117,7 +117,9 @@ export async function setupGitHubToken(): Promise<string> {
core.setOutput("GITHUB_TOKEN", appToken); core.setOutput("GITHUB_TOKEN", appToken);
return appToken; return appToken;
} catch (error) { } 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); process.exit(1);
} }
} }

View File

@@ -1,13 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
// GitHub File Operations MCP Server - Enhanced with detailed error logging // GitHub File Operations MCP Server
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; import { z } from "zod";
import { readFile } from "fs/promises"; import { readFile } from "fs/promises";
import { join } from "path"; import { join } from "path";
import fetch from "node-fetch"; import fetch from "node-fetch";
// Import removed - define inline to ensure subprocess gets the value import { GITHUB_API_URL } from "../github/api/config";
const GITHUB_API_URL = process.env.GITHUB_API_URL || "https://api.github.com";
type GitHubRef = { type GitHubRef = {
object: { object: {
@@ -52,18 +51,6 @@ const server = new McpServer({
version: "0.0.1", version: "0.0.1",
}); });
// Enhanced error logging helper
function logDetailedError(prefix: string, error: any) {
console.error(`[${prefix}] FULL ERROR CAUGHT:`, error);
console.error(`[${prefix}] Error type:`, typeof error);
console.error(`[${prefix}] Error constructor:`, error?.constructor?.name);
console.error(`[${prefix}] Error stack:`, error instanceof Error ? error.stack : 'No stack trace');
if (error && typeof error === 'object') {
console.error(`[${prefix}] Error properties:`, Object.keys(error));
console.error(`[${prefix}] Error JSON:`, JSON.stringify(error, null, 2));
}
}
// Commit files tool // Commit files tool
server.tool( server.tool(
"commit_files", "commit_files",
@@ -80,19 +67,6 @@ server.tool(
const owner = REPO_OWNER; const owner = REPO_OWNER;
const repo = REPO_NAME; const repo = REPO_NAME;
const branch = BRANCH_NAME; const branch = BRANCH_NAME;
console.error(`[commit_files] Starting commit for ${files.length} files to ${owner}/${repo}:${branch}`);
console.error(`[commit_files] REPO_DIR: ${REPO_DIR}`);
console.error(`[commit_files] Input files:`, files);
console.error(`[commit_files] Environment check:`, {
GITHUB_TOKEN: process.env.GITHUB_TOKEN ? 'Present' : 'Missing',
REPO_OWNER,
REPO_NAME,
BRANCH_NAME,
REPO_DIR,
GITHUB_API_URL,
CWD: process.cwd(),
});
try { try {
const githubToken = process.env.GITHUB_TOKEN; const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) { if (!githubToken) {
@@ -149,12 +123,7 @@ server.tool(
? filePath ? filePath
: join(REPO_DIR, filePath); : join(REPO_DIR, filePath);
console.error(`[commit_files] Reading file: ${fullPath}`); const content = await readFile(fullPath, "utf-8");
const content = await readFile(fullPath, "utf-8").catch((error) => {
console.error(`[commit_files] Failed to read file '${fullPath}':`, error);
throw new Error(`Failed to read file '${fullPath}': ${error.message || error}`);
});
console.error(`[commit_files] Successfully read file: ${fullPath} (${content.length} chars)`);
return { return {
path: filePath, path: filePath,
mode: "100644", mode: "100644",
@@ -217,158 +186,24 @@ server.tool(
// 6. Update the reference to point to the new commit // 6. Update the reference to point to the new commit
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`; const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
console.error(`[commit_files] Updating reference: ${updateRefUrl}`); const updateRefResponse = await fetch(updateRefUrl, {
console.error(`[commit_files] New commit SHA: ${newCommitData.sha}`); method: "PATCH",
console.error(`[commit_files] Base SHA was: ${baseSha}`);
// Log full request context before making the request
const requestBody = JSON.stringify({
sha: newCommitData.sha,
force: false,
});
const requestHeaders = {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
};
console.error(`[commit_files] Full request details:`, {
url: updateRefUrl,
method: 'PATCH',
headers: { headers: {
...requestHeaders, Accept: "application/vnd.github+json",
Authorization: `Bearer [TOKEN_LENGTH:${githubToken?.length || 0}]`, Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
}, },
body: requestBody, body: JSON.stringify({
timestamp: new Date().toISOString(), sha: newCommitData.sha,
environment: { force: false,
NODE_VERSION: process.version, }),
PLATFORM: process.platform,
ARCH: process.arch,
},
previousOperations: {
treeCreated: treeData?.sha ? 'YES' : 'NO',
commitCreated: newCommitData?.sha ? 'YES' : 'NO',
treeSha: treeData?.sha,
commitSha: newCommitData?.sha,
baseSha: baseSha,
}
}); });
// Log memory usage before request
const memoryBefore = process.memoryUsage();
console.error(`[commit_files] Memory before request:`, {
rss: `${(memoryBefore.rss / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(memoryBefore.heapUsed / 1024 / 1024).toFixed(2)} MB`,
});
let updateRefResponse;
const requestStartTime = Date.now();
try {
updateRefResponse = await fetch(updateRefUrl, {
method: "PATCH",
headers: requestHeaders,
body: requestBody,
});
} catch (fetchError) {
const requestDuration = Date.now() - requestStartTime;
console.error(`[commit_files] FETCH ERROR during reference update after ${requestDuration}ms:`, fetchError);
logDetailedError('commit_files_fetch', fetchError);
throw new Error(`Network error during reference update after ${requestDuration}ms: ${fetchError?.message || 'Unknown fetch error'}`);
}
const requestDuration = Date.now() - requestStartTime;
console.error(`[commit_files] Request completed in ${requestDuration}ms`);
console.error(`[commit_files] Response received at: ${new Date().toISOString()}`);
console.error(`[commit_files] Update reference response status: ${updateRefResponse.status}`);
console.error(`[commit_files] Response headers:`, Object.fromEntries(updateRefResponse.headers.entries()));
// Log specific important headers
console.error(`[commit_files] Key response headers:`, {
'x-github-request-id': updateRefResponse.headers.get('x-github-request-id'),
'x-ratelimit-remaining': updateRefResponse.headers.get('x-ratelimit-remaining'),
'x-ratelimit-reset': updateRefResponse.headers.get('x-ratelimit-reset'),
'content-type': updateRefResponse.headers.get('content-type'),
'content-length': updateRefResponse.headers.get('content-length'),
'server': updateRefResponse.headers.get('server'),
});
if (!updateRefResponse.ok) { if (!updateRefResponse.ok) {
console.error(`[commit_files] ERROR RESPONSE - Status: ${updateRefResponse.status} ${updateRefResponse.statusText}`); const errorText = await updateRefResponse.text();
// Capture the entire raw response body
let responseArrayBuffer;
let responseText = '';
let responseHex = '';
let responseBase64 = '';
try {
// Clone the response so we can read it multiple ways
const clonedResponse = updateRefResponse.clone();
// Get raw bytes
responseArrayBuffer = await updateRefResponse.arrayBuffer();
const responseBytes = new Uint8Array(responseArrayBuffer);
// Convert to text (with error handling for non-UTF8)
responseText = new TextDecoder('utf-8', { fatal: false }).decode(responseBytes);
// Convert to hex for debugging binary responses
responseHex = Array.from(responseBytes.slice(0, 1000)) // First 1000 bytes to avoid huge logs
.map(b => b.toString(16).padStart(2, '0'))
.join(' ');
// Convert to base64
responseBase64 = Buffer.from(responseBytes).toString('base64');
console.error(`[commit_files] COMPLETE ERROR RESPONSE:`);
console.error(`[commit_files] ===== RESPONSE BODY (TEXT) =====`);
console.error(responseText);
console.error(`[commit_files] ===== END RESPONSE BODY =====`);
console.error(`[commit_files] Response body length: ${responseBytes.length} bytes`);
console.error(`[commit_files] Response body (first 1000 bytes as hex): ${responseHex}${responseBytes.length > 1000 ? '...' : ''}`);
console.error(`[commit_files] Response body (base64): ${responseBase64}`);
// Try to parse as JSON if it looks like JSON
if (responseText.trim().startsWith('{') || responseText.trim().startsWith('[')) {
try {
const parsedError = JSON.parse(responseText);
console.error(`[commit_files] Parsed error object:`, JSON.stringify(parsedError, null, 2));
} catch (e) {
console.error(`[commit_files] Response looks like JSON but failed to parse:`, e);
}
}
} catch (readError) {
console.error(`[commit_files] CRITICAL: Failed to read error response:`, readError);
logDetailedError('commit_files_response_read', readError);
responseText = `Failed to read response: ${readError}`;
}
// Log memory state after error
const memoryAfter = process.memoryUsage();
console.error(`[commit_files] Memory after error:`, {
rss: `${(memoryAfter.rss / 1024 / 1024).toFixed(2)} MB`,
heapUsed: `${(memoryAfter.heapUsed / 1024 / 1024).toFixed(2)} MB`,
});
// Special handling for 500 errors
if (updateRefResponse.status === 500) {
const requestId = updateRefResponse.headers.get('x-github-request-id');
console.error(`[commit_files] ===== GITHUB 500 ERROR DETAILS =====`);
console.error(`[commit_files] GitHub Request ID: ${requestId}`);
console.error(`[commit_files] This is an internal GitHub server error`);
console.error(`[commit_files] The error may be transient - consider retrying`);
console.error(`[commit_files] Note: Tree (${treeData?.sha}) and commit (${newCommitData?.sha}) were created successfully`);
console.error(`[commit_files] Only the reference update failed`);
console.error(`[commit_files] ===================================`);
}
throw new Error( throw new Error(
`Failed to update reference: ${updateRefResponse.status} - ${responseText || 'No response body'}`, `Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
); );
} }
@@ -394,21 +229,18 @@ server.tool(
], ],
}; };
} catch (error) { } catch (error) {
logDetailedError('commit_files', error);
const errorMessage = const errorMessage =
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
console.error(`[commit_files] Final error message being thrown: "${errorMessage}"`); return {
content: [
// Ensure we're throwing a proper Error object with a message {
if (!errorMessage || errorMessage === 'undefined' || errorMessage === '[object Object]') { type: "text",
console.error(`[commit_files] WARNING: Error message is undefined or object, using fallback`); text: `Error: ${errorMessage}`,
const fallbackMessage = error instanceof Error && error.stack },
? `Failed to commit files: ${error.stack.split('\n')[0]}` ],
: 'Failed to commit files: Unknown error occurred'; error: errorMessage,
throw new Error(fallbackMessage); isError: true,
} };
throw new Error(errorMessage);
} }
}, },
); );
@@ -548,71 +380,22 @@ server.tool(
// 6. Update the reference to point to the new commit // 6. Update the reference to point to the new commit
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`; const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
console.error(`[delete_files] Updating reference: ${updateRefUrl}`); const updateRefResponse = await fetch(updateRefUrl, {
console.error(`[delete_files] New commit SHA: ${newCommitData.sha}`); method: "PATCH",
console.error(`[delete_files] Base SHA was: ${baseSha}`); headers: {
console.error(`[delete_files] Request body:`, JSON.stringify({ Accept: "application/vnd.github+json",
sha: newCommitData.sha, Authorization: `Bearer ${githubToken}`,
force: false, "X-GitHub-Api-Version": "2022-11-28",
})); "Content-Type": "application/json",
},
let updateRefResponse; body: JSON.stringify({
try { sha: newCommitData.sha,
updateRefResponse = await fetch(updateRefUrl, { force: false,
method: "PATCH", }),
headers: { });
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
},
body: JSON.stringify({
sha: newCommitData.sha,
force: false,
}),
});
} catch (fetchError) {
console.error(`[delete_files] FETCH ERROR during reference update:`, fetchError);
logDetailedError('delete_files_fetch', fetchError);
throw new Error(`Network error during reference update: ${fetchError?.message || 'Unknown fetch error'}`);
}
console.error(`[delete_files] Update reference response status: ${updateRefResponse.status}`);
console.error(`[delete_files] Response headers:`, Object.fromEntries(updateRefResponse.headers.entries()));
if (!updateRefResponse.ok) { if (!updateRefResponse.ok) {
let errorText; const errorText = await updateRefResponse.text();
try {
errorText = await updateRefResponse.text();
} catch (textError) {
console.error(`[delete_files] Failed to read error response text:`, textError);
errorText = 'Unable to read error response';
}
console.error(`[delete_files] Update reference error body: "${errorText}"`);
console.error(`[delete_files] Error body length: ${errorText?.length}`);
console.error(`[delete_files] Error body type: ${typeof errorText}`);
// Log additional debugging info for 500 errors
if (updateRefResponse.status === 500) {
const requestId = updateRefResponse.headers.get('x-github-request-id');
console.error(`[delete_files] GitHub Request ID: ${requestId}`);
console.error(`[delete_files] This appears to be an internal GitHub error`);
console.error(`[delete_files] Token was valid for tree/commit creation but failed for ref update`);
console.error(`[delete_files] Branch protection rules or permissions might be an issue`);
}
// Parse error if it's JSON
let parsedError;
try {
if (errorText && errorText.trim().startsWith('{')) {
parsedError = JSON.parse(errorText);
console.error(`[delete_files] Parsed error:`, parsedError);
}
} catch (e) {
console.error(`[delete_files] Error text is not JSON`);
}
throw new Error( throw new Error(
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`, `Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
); );
@@ -640,21 +423,18 @@ server.tool(
], ],
}; };
} catch (error) { } catch (error) {
logDetailedError('delete_files', error);
const errorMessage = const errorMessage =
error instanceof Error ? error.message : String(error); error instanceof Error ? error.message : String(error);
console.error(`[delete_files] Final error message being thrown: "${errorMessage}"`); return {
content: [
// Ensure we're throwing a proper Error object with a message {
if (!errorMessage || errorMessage === 'undefined' || errorMessage === '[object Object]') { type: "text",
console.error(`[delete_files] WARNING: Error message is undefined or object, using fallback`); text: `Error: ${errorMessage}`,
const fallbackMessage = error instanceof Error && error.stack },
? `Failed to delete files: ${error.stack.split('\n')[0]}` ],
: 'Failed to delete files: Unknown error occurred'; error: errorMessage,
throw new Error(fallbackMessage); isError: true,
} };
throw new Error(errorMessage);
} }
}, },
); );
@@ -667,4 +447,4 @@ async function runServer() {
}); });
} }
runServer().catch(console.error); runServer().catch(console.error);

View File

@@ -35,7 +35,6 @@ export async function prepareMcpConfig(
REPO_NAME: repo, REPO_NAME: repo,
BRANCH_NAME: branch, BRANCH_NAME: branch,
REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(), REPO_DIR: process.env.GITHUB_WORKSPACE || process.cwd(),
GITHUB_API_URL: process.env.GITHUB_API_URL || "https://api.github.com",
}, },
}, },
}, },

View File

@@ -1,261 +0,0 @@
#!/usr/bin/env bun
// Test script that creates a new branch to test the commit_files flow
// Run with: bun test-with-new-branch.ts <github-pat> <owner> <repo>
const GITHUB_API_URL = "https://api.github.com";
async function testCommitFilesWithNewBranch(token: string, owner: string, repo: string) {
const headers = {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
};
// Create a unique branch name for testing
const timestamp = Date.now();
const testBranch = `claude-debug-500-test-${timestamp}`;
console.log(`\n=== Testing commit_files flow ===`);
console.log(`Repository: ${owner}/${repo}`);
console.log(`Test branch: ${testBranch}\n`);
try {
// First, get the default branch to branch from
console.log("Getting repository info...");
const repoResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}`,
{ headers }
);
if (!repoResponse.ok) {
console.error(`Cannot access repository: ${repoResponse.status}`);
const error = await repoResponse.text();
console.error(error);
return;
}
const repoData = await repoResponse.json();
const defaultBranch = repoData.default_branch;
console.log(`✓ Default branch: ${defaultBranch}`);
// Get the SHA of the default branch
console.log(`\nGetting ${defaultBranch} branch SHA...`);
const defaultBranchResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${defaultBranch}`,
{ headers }
);
if (!defaultBranchResponse.ok) {
console.error(`Cannot get default branch: ${defaultBranchResponse.status}`);
return;
}
const defaultBranchData = await defaultBranchResponse.json();
const baseSha = defaultBranchData.object.sha;
console.log(`✓ Base SHA: ${baseSha}`);
// Create a new branch
console.log(`\nCreating test branch: ${testBranch}...`);
const createBranchResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs`,
{
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify({
ref: `refs/heads/${testBranch}`,
sha: baseSha,
}),
}
);
if (!createBranchResponse.ok) {
console.error(`Failed to create branch: ${createBranchResponse.status}`);
const error = await createBranchResponse.text();
console.error(error);
return;
}
console.log(`✓ Created test branch: ${testBranch}`);
// Now replicate the commit_files flow
console.log("\n--- Starting commit_files flow ---");
// Step 1: Get the branch reference (should be same as baseSha)
console.log("\nStep 1: Getting branch reference...");
const refUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${testBranch}`;
const refResponse = await fetch(refUrl, { headers });
if (!refResponse.ok) {
console.error(`Failed: ${refResponse.status}`);
return;
}
const refData = await refResponse.json();
console.log(`✓ Branch SHA: ${refData.object.sha}`);
// Step 2: Get the base commit
console.log("\nStep 2: Getting base commit...");
const commitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits/${baseSha}`;
const commitResponse = await fetch(commitUrl, { headers });
if (!commitResponse.ok) {
console.error(`Failed: ${commitResponse.status}`);
return;
}
const commitData = await commitResponse.json();
const baseTreeSha = commitData.tree.sha;
console.log(`✓ Base tree SHA: ${baseTreeSha}`);
// Step 3: Create a new tree
console.log("\nStep 3: Creating new tree...");
const treeUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/trees`;
const testFileContent = `# Test file for debugging 500 error
# Created at: ${new Date().toISOString()}
# This simulates the commit_files operation from claude-code-action
def test_function():
# This simulates fixing a code issue
result = "Fixed code"
return result
`;
const treeBody = {
base_tree: baseTreeSha,
tree: [{
path: "test-debug-500.py",
mode: "100644",
type: "blob",
content: testFileContent,
}],
};
const treeResponse = await fetch(treeUrl, {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(treeBody),
});
if (!treeResponse.ok) {
console.error(`Failed to create tree: ${treeResponse.status}`);
const error = await treeResponse.text();
console.error(error);
return;
}
const treeData = await treeResponse.json();
console.log(`✓ Tree SHA: ${treeData.sha}`);
// Step 4: Create commit
console.log("\nStep 4: Creating commit...");
const newCommitUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/commits`;
const commitBody = {
message: "Test: Debugging 500 error in commit_files",
tree: treeData.sha,
parents: [baseSha],
};
const newCommitResponse = await fetch(newCommitUrl, {
method: "POST",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(commitBody),
});
if (!newCommitResponse.ok) {
console.error(`Failed to create commit: ${newCommitResponse.status}`);
const error = await newCommitResponse.text();
console.error(error);
return;
}
const newCommitData = await newCommitResponse.json();
console.log(`✓ Commit SHA: ${newCommitData.sha}`);
// Step 5: Update reference (this is where the 500 error happens)
console.log("\nStep 5: Updating branch reference (the critical step)...");
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${testBranch}`;
const updateBody = {
sha: newCommitData.sha,
force: false,
};
console.log(`URL: PATCH ${updateRefUrl}`);
console.log(`Body: ${JSON.stringify(updateBody)}`);
const startTime = Date.now();
const updateRefResponse = await fetch(updateRefUrl, {
method: "PATCH",
headers: {
...headers,
"Content-Type": "application/json",
},
body: JSON.stringify(updateBody),
});
const duration = Date.now() - startTime;
console.log(`\nStatus: ${updateRefResponse.status} (took ${duration}ms)`);
console.log(`Headers:`, {
'x-ratelimit-remaining': updateRefResponse.headers.get('x-ratelimit-remaining'),
'x-github-request-id': updateRefResponse.headers.get('x-github-request-id'),
});
if (!updateRefResponse.ok) {
console.error(`\n✗ FAILED: ${updateRefResponse.status}`);
const errorText = await updateRefResponse.text();
console.error(`Error body: "${errorText}"`);
if (updateRefResponse.status === 500) {
console.error(`\n🔍 500 ERROR REPRODUCED!`);
console.error(`This confirms the issue exists with PAT as well.`);
console.error(`GitHub Request ID: ${updateRefResponse.headers.get('x-github-request-id')}`);
}
} else {
console.log(`\n✓ SUCCESS: Branch updated!`);
console.log(`The 500 error might be specific to certain conditions.`);
// Cleanup: delete the test branch
console.log(`\nCleaning up test branch...`);
const deleteResponse = await fetch(
`${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${testBranch}`,
{
method: "DELETE",
headers,
}
);
if (deleteResponse.ok) {
console.log(`✓ Test branch deleted`);
}
}
} catch (error) {
console.error(`\nUnexpected error:`, error);
}
}
// Main execution
const [token, owner, repo] = process.argv.slice(2);
if (!token || !owner || !repo) {
console.log("Usage: bun test-with-new-branch.ts <github-pat> <owner> <repo>");
console.log("");
console.log("Examples:");
console.log(" bun test-with-new-branch.ts ghp_xxx myorg myrepo");
console.log(" bun test-with-new-branch.ts ghp_xxx anthropics anthropic");
console.log("");
console.log("This creates a test branch and replicates the commit_files flow.");
process.exit(1);
}
console.log("Starting test with new branch...");
testCommitFilesWithNewBranch(token, owner, repo);