Compare commits

..

3 Commits

Author SHA1 Message Date
Ashwin Bhat
2f766f1151 add issue triage workflow 2025-05-27 13:22:05 -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
10 changed files with 172 additions and 936 deletions

View File

@@ -31,6 +31,6 @@ jobs:
- name: Run Claude Code
id: claude
uses: ./
uses: anthropics/claude-code-action@beta
with:
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 }}

View File

@@ -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@5097b6cdfe5fc5a3ac0166cc344c34ed23c93982 # https://github.com/anthropics/claude-code-base-action/releases/tag/v0.0.5
with:
prompt_file: /tmp/claude-prompts/claude-prompt.txt
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);

View File

@@ -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);
}
}

View File

@@ -1,13 +1,12 @@
#!/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 { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFile } from "fs/promises";
import { join } from "path";
import fetch from "node-fetch";
// Import removed - define inline to ensure subprocess gets the value
const GITHUB_API_URL = process.env.GITHUB_API_URL || "https://api.github.com";
import { GITHUB_API_URL } from "../github/api/config";
type GitHubRef = {
object: {
@@ -52,18 +51,6 @@ const server = new McpServer({
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
server.tool(
"commit_files",
@@ -80,19 +67,6 @@ server.tool(
const owner = REPO_OWNER;
const repo = REPO_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 {
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
@@ -149,12 +123,7 @@ server.tool(
? filePath
: join(REPO_DIR, filePath);
console.error(`[commit_files] Reading file: ${fullPath}`);
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)`);
const content = await readFile(fullPath, "utf-8");
return {
path: filePath,
mode: "100644",
@@ -217,158 +186,24 @@ server.tool(
// 6. Update the reference to point to the new commit
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
console.error(`[commit_files] Updating reference: ${updateRefUrl}`);
console.error(`[commit_files] New commit SHA: ${newCommitData.sha}`);
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',
const updateRefResponse = await fetch(updateRefUrl, {
method: "PATCH",
headers: {
...requestHeaders,
Authorization: `Bearer [TOKEN_LENGTH:${githubToken?.length || 0}]`,
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
"Content-Type": "application/json",
},
body: requestBody,
timestamp: new Date().toISOString(),
environment: {
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,
}
body: JSON.stringify({
sha: newCommitData.sha,
force: false,
}),
});
// 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) {
console.error(`[commit_files] ERROR RESPONSE - Status: ${updateRefResponse.status} ${updateRefResponse.statusText}`);
// 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] ===================================`);
}
const errorText = await updateRefResponse.text();
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) {
logDetailedError('commit_files', error);
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[commit_files] Final error message being thrown: "${errorMessage}"`);
// Ensure we're throwing a proper Error object with a message
if (!errorMessage || errorMessage === 'undefined' || errorMessage === '[object Object]') {
console.error(`[commit_files] WARNING: Error message is undefined or object, using fallback`);
const fallbackMessage = error instanceof Error && error.stack
? `Failed to commit files: ${error.stack.split('\n')[0]}`
: 'Failed to commit files: Unknown error occurred';
throw new Error(fallbackMessage);
}
throw new Error(errorMessage);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
@@ -548,71 +380,22 @@ server.tool(
// 6. Update the reference to point to the new commit
const updateRefUrl = `${GITHUB_API_URL}/repos/${owner}/${repo}/git/refs/heads/${branch}`;
console.error(`[delete_files] Updating reference: ${updateRefUrl}`);
console.error(`[delete_files] New commit SHA: ${newCommitData.sha}`);
console.error(`[delete_files] Base SHA was: ${baseSha}`);
console.error(`[delete_files] Request body:`, JSON.stringify({
sha: newCommitData.sha,
force: false,
}));
let updateRefResponse;
try {
updateRefResponse = await fetch(updateRefUrl, {
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'}`);
}
const updateRefResponse = await fetch(updateRefUrl, {
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,
}),
});
console.error(`[delete_files] Update reference response status: ${updateRefResponse.status}`);
console.error(`[delete_files] Response headers:`, Object.fromEntries(updateRefResponse.headers.entries()));
if (!updateRefResponse.ok) {
let errorText;
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`);
}
const errorText = await updateRefResponse.text();
throw new Error(
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
);
@@ -640,21 +423,18 @@ server.tool(
],
};
} catch (error) {
logDetailedError('delete_files', error);
const errorMessage =
error instanceof Error ? error.message : String(error);
console.error(`[delete_files] Final error message being thrown: "${errorMessage}"`);
// Ensure we're throwing a proper Error object with a message
if (!errorMessage || errorMessage === 'undefined' || errorMessage === '[object Object]') {
console.error(`[delete_files] WARNING: Error message is undefined or object, using fallback`);
const fallbackMessage = error instanceof Error && error.stack
? `Failed to delete files: ${error.stack.split('\n')[0]}`
: 'Failed to delete files: Unknown error occurred';
throw new Error(fallbackMessage);
}
throw new Error(errorMessage);
return {
content: [
{
type: "text",
text: `Error: ${errorMessage}`,
},
],
error: errorMessage,
isError: true,
};
}
},
);
@@ -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,
BRANCH_NAME: branch,
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);