mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 23:14:13 +08:00
Compare commits
9 Commits
v0.0.59
...
ashwin/ver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6147037db9 | ||
|
|
194fca8b05 | ||
|
|
0f913a6e0e | ||
|
|
68b7ca379c | ||
|
|
900322ca88 | ||
|
|
8f0a7fe9d3 | ||
|
|
db36412854 | ||
|
|
f05d669d5f | ||
|
|
e89411bb6f |
2
.github/workflows/issue-triage.yml
vendored
2
.github/workflows/issue-triage.yml
vendored
@@ -104,3 +104,5 @@ jobs:
|
|||||||
mcp_config: /tmp/mcp-config/mcp-servers.json
|
mcp_config: /tmp/mcp-config/mcp-servers.json
|
||||||
timeout_minutes: "5"
|
timeout_minutes: "5"
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ runs:
|
|||||||
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
DEFAULT_WORKFLOW_TOKEN: ${{ github.token }}
|
||||||
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
|
||||||
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
USE_COMMIT_SIGNING: ${{ inputs.use_commit_signing }}
|
||||||
|
ALL_INPUTS: ${{ toJson(inputs) }}
|
||||||
|
|
||||||
- name: Install Base Action Dependencies
|
- name: Install Base Action Dependencies
|
||||||
if: steps.prepare.outputs.contains_trigger == 'true'
|
if: steps.prepare.outputs.contains_trigger == 'true'
|
||||||
@@ -177,7 +178,7 @@ runs:
|
|||||||
echo "Base-action dependencies installed"
|
echo "Base-action dependencies installed"
|
||||||
cd -
|
cd -
|
||||||
# Install Claude Code globally
|
# Install Claude Code globally
|
||||||
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.85
|
||||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
- name: Setup Network Restrictions
|
- name: Setup Network Restrictions
|
||||||
@@ -212,6 +213,7 @@ runs:
|
|||||||
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
|
INPUT_CLAUDE_ENV: ${{ inputs.claude_env }}
|
||||||
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
|
INPUT_FALLBACK_MODEL: ${{ inputs.fallback_model }}
|
||||||
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
INPUT_EXPERIMENTAL_SLASH_COMMANDS_DIR: ${{ github.action_path }}/slash-commands
|
||||||
|
INPUT_ACTION_INPUTS_PRESENT: ${{ steps.prepare.outputs.action_inputs_present }}
|
||||||
|
|
||||||
# Model configuration
|
# Model configuration
|
||||||
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||||
@@ -286,7 +288,7 @@ runs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Revoke app token
|
- name: Revoke app token
|
||||||
if: always() && inputs.github_token == ''
|
if: always() && inputs.github_token == '' && steps.prepare.outputs.skipped_due_to_workflow_validation_mismatch != 'true'
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
curl -L \
|
curl -L \
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install Claude Code
|
- name: Install Claude Code
|
||||||
shell: bash
|
shell: bash
|
||||||
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.83
|
run: curl -fsSL https://claude.ai/install.sh | bash -s 1.0.85
|
||||||
|
|
||||||
- name: Run Claude Code Action
|
- name: Run Claude Code Action
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|||||||
@@ -110,6 +110,10 @@ export function prepareRunConfig(
|
|||||||
// Parse custom environment variables
|
// Parse custom environment variables
|
||||||
const customEnv = parseCustomEnvVars(options.claudeEnv);
|
const customEnv = parseCustomEnvVars(options.claudeEnv);
|
||||||
|
|
||||||
|
if (process.env.INPUT_ACTION_INPUTS_PRESENT) {
|
||||||
|
customEnv.GITHUB_ACTION_INPUTS = process.env.INPUT_ACTION_INPUTS_PRESENT;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
claudeArgs,
|
claudeArgs,
|
||||||
promptPath,
|
promptPath,
|
||||||
@@ -142,9 +146,11 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
|
|||||||
console.log(`Prompt file size: ${promptSize} bytes`);
|
console.log(`Prompt file size: ${promptSize} bytes`);
|
||||||
|
|
||||||
// Log custom environment variables if any
|
// Log custom environment variables if any
|
||||||
if (Object.keys(config.env).length > 0) {
|
const customEnvKeys = Object.keys(config.env).filter(
|
||||||
const envKeys = Object.keys(config.env).join(", ");
|
(key) => key !== "CLAUDE_ACTION_INPUTS_PRESENT",
|
||||||
console.log(`Custom environment variables: ${envKeys}`);
|
);
|
||||||
|
if (customEnvKeys.length > 0) {
|
||||||
|
console.log(`Custom environment variables: ${customEnvKeys.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output to console
|
// Output to console
|
||||||
|
|||||||
@@ -836,7 +836,7 @@ export async function createPrompt(
|
|||||||
modeContext.claudeBranch,
|
modeContext.claudeBranch,
|
||||||
);
|
);
|
||||||
|
|
||||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -855,7 +855,7 @@ export async function createPrompt(
|
|||||||
|
|
||||||
// Write the prompt file
|
// Write the prompt file
|
||||||
await writeFile(
|
await writeFile(
|
||||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||||
promptContent,
|
promptContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
59
src/entrypoints/collect-inputs.ts
Normal file
59
src/entrypoints/collect-inputs.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
export function collectActionInputsPresence(): void {
|
||||||
|
const inputDefaults: Record<string, string> = {
|
||||||
|
trigger_phrase: "@claude",
|
||||||
|
assignee_trigger: "",
|
||||||
|
label_trigger: "claude",
|
||||||
|
base_branch: "",
|
||||||
|
branch_prefix: "claude/",
|
||||||
|
allowed_bots: "",
|
||||||
|
mode: "tag",
|
||||||
|
model: "",
|
||||||
|
anthropic_model: "",
|
||||||
|
fallback_model: "",
|
||||||
|
allowed_tools: "",
|
||||||
|
disallowed_tools: "",
|
||||||
|
custom_instructions: "",
|
||||||
|
direct_prompt: "",
|
||||||
|
override_prompt: "",
|
||||||
|
mcp_config: "",
|
||||||
|
additional_permissions: "",
|
||||||
|
claude_env: "",
|
||||||
|
settings: "",
|
||||||
|
anthropic_api_key: "",
|
||||||
|
claude_code_oauth_token: "",
|
||||||
|
github_token: "",
|
||||||
|
max_turns: "",
|
||||||
|
use_sticky_comment: "false",
|
||||||
|
use_commit_signing: "false",
|
||||||
|
experimental_allowed_domains: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const allInputsJson = process.env.ALL_INPUTS;
|
||||||
|
if (!allInputsJson) {
|
||||||
|
console.log("ALL_INPUTS environment variable not found");
|
||||||
|
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let allInputs: Record<string, string>;
|
||||||
|
try {
|
||||||
|
allInputs = JSON.parse(allInputsJson);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to parse ALL_INPUTS JSON:", e);
|
||||||
|
core.setOutput("action_inputs_present", JSON.stringify({}));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const presentInputs: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
for (const [name, defaultValue] of Object.entries(inputDefaults)) {
|
||||||
|
const actualValue = allInputs[name] || "";
|
||||||
|
|
||||||
|
const isSet = actualValue !== defaultValue;
|
||||||
|
presentInputs[name] = isSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput("action_inputs_present", JSON.stringify(presentInputs));
|
||||||
|
}
|
||||||
@@ -13,9 +13,12 @@ import { parseGitHubContext, isEntityContext } from "../github/context";
|
|||||||
import { getMode, isValidMode, DEFAULT_MODE } from "../modes/registry";
|
import { getMode, isValidMode, DEFAULT_MODE } from "../modes/registry";
|
||||||
import type { ModeName } from "../modes/types";
|
import type { ModeName } from "../modes/types";
|
||||||
import { prepare } from "../prepare";
|
import { prepare } from "../prepare";
|
||||||
|
import { collectActionInputsPresence } from "./collect-inputs";
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
try {
|
try {
|
||||||
|
collectActionInputsPresence();
|
||||||
|
|
||||||
// Step 1: Get mode first to determine authentication method
|
// Step 1: Get mode first to determine authentication method
|
||||||
const modeInput = process.env.MODE || DEFAULT_MODE;
|
const modeInput = process.env.MODE || DEFAULT_MODE;
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,30 @@ async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
|||||||
const responseJson = (await response.json()) as {
|
const responseJson = (await response.json()) as {
|
||||||
error?: {
|
error?: {
|
||||||
message?: string;
|
message?: string;
|
||||||
|
details?: {
|
||||||
|
error_code?: string;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
type?: string;
|
||||||
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Check for specific workflow validation error codes that should skip the action
|
||||||
|
const errorCode = responseJson.error?.details?.error_code;
|
||||||
|
|
||||||
|
if (errorCode === "workflow_not_found_on_default_branch") {
|
||||||
|
const message =
|
||||||
|
responseJson.message ??
|
||||||
|
responseJson.error?.message ??
|
||||||
|
"Workflow validation failed";
|
||||||
|
core.warning(`Skipping action due to workflow validation: ${message}`);
|
||||||
|
console.log(
|
||||||
|
"Action skipped due to workflow validation error. This is expected when adding Claude Code workflows to new repositories or on PRs with workflow changes. If you're seeing this, your workflow will begin working once you merge your PR.",
|
||||||
|
);
|
||||||
|
core.setOutput("skipped_due_to_workflow_validation_mismatch", "true");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
console.error(
|
console.error(
|
||||||
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
`App token exchange failed: ${response.status} ${response.statusText} - ${responseJson?.error?.message ?? "Unknown error"}`,
|
||||||
);
|
);
|
||||||
@@ -77,6 +99,7 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
core.setOutput("GITHUB_TOKEN", appToken);
|
core.setOutput("GITHUB_TOKEN", appToken);
|
||||||
return appToken;
|
return appToken;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Only set failed if we get here - workflow validation errors will exit(0) before this
|
||||||
core.setFailed(
|
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.`,
|
`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.`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
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, stat } from "fs/promises";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
|
import { constants } from "fs";
|
||||||
import fetch from "node-fetch";
|
import fetch from "node-fetch";
|
||||||
import { GITHUB_API_URL } from "../github/api/config";
|
import { GITHUB_API_URL } from "../github/api/config";
|
||||||
import { retryWithBackoff } from "../utils/retry";
|
import { retryWithBackoff } from "../utils/retry";
|
||||||
@@ -162,6 +163,34 @@ async function getOrCreateBranchRef(
|
|||||||
return baseSha;
|
return baseSha;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get the appropriate Git file mode for a file
|
||||||
|
async function getFileMode(filePath: string): Promise<string> {
|
||||||
|
try {
|
||||||
|
const fileStat = await stat(filePath);
|
||||||
|
if (fileStat.isFile()) {
|
||||||
|
// Check if execute bit is set for user
|
||||||
|
if (fileStat.mode & constants.S_IXUSR) {
|
||||||
|
return "100755"; // Executable file
|
||||||
|
} else {
|
||||||
|
return "100644"; // Regular file
|
||||||
|
}
|
||||||
|
} else if (fileStat.isDirectory()) {
|
||||||
|
return "040000"; // Directory (tree)
|
||||||
|
} else if (fileStat.isSymbolicLink()) {
|
||||||
|
return "120000"; // Symbolic link
|
||||||
|
} else {
|
||||||
|
// Fallback for unknown file types
|
||||||
|
return "100644";
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If we can't stat the file, default to regular file
|
||||||
|
console.warn(
|
||||||
|
`Could not determine file mode for ${filePath}, using default: ${error}`,
|
||||||
|
);
|
||||||
|
return "100644";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Commit files tool
|
// Commit files tool
|
||||||
server.tool(
|
server.tool(
|
||||||
"commit_files",
|
"commit_files",
|
||||||
@@ -223,6 +252,9 @@ server.tool(
|
|||||||
? filePath
|
? filePath
|
||||||
: join(REPO_DIR, filePath);
|
: join(REPO_DIR, filePath);
|
||||||
|
|
||||||
|
// Get the proper file mode based on file permissions
|
||||||
|
const fileMode = await getFileMode(fullPath);
|
||||||
|
|
||||||
// Check if file is binary (images, etc.)
|
// Check if file is binary (images, etc.)
|
||||||
const isBinaryFile =
|
const isBinaryFile =
|
||||||
/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test(
|
/\.(png|jpg|jpeg|gif|webp|ico|pdf|zip|tar|gz|exe|bin|woff|woff2|ttf|eot)$/i.test(
|
||||||
@@ -261,7 +293,7 @@ server.tool(
|
|||||||
// Return tree entry with blob SHA
|
// Return tree entry with blob SHA
|
||||||
return {
|
return {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
mode: "100644",
|
mode: fileMode,
|
||||||
type: "blob",
|
type: "blob",
|
||||||
sha: blobData.sha,
|
sha: blobData.sha,
|
||||||
};
|
};
|
||||||
@@ -270,7 +302,7 @@ server.tool(
|
|||||||
const content = await readFile(fullPath, "utf-8");
|
const content = await readFile(fullPath, "utf-8");
|
||||||
return {
|
return {
|
||||||
path: filePath,
|
path: filePath,
|
||||||
mode: "100644",
|
mode: fileMode,
|
||||||
type: "blob",
|
type: "blob",
|
||||||
content: content,
|
content: content,
|
||||||
};
|
};
|
||||||
@@ -335,6 +367,7 @@ server.tool(
|
|||||||
// We're seeing intermittent 403 "Resource not accessible by integration" errors
|
// We're seeing intermittent 403 "Resource not accessible by integration" errors
|
||||||
// on certain repos when updating git references. These appear to be transient
|
// on certain repos when updating git references. These appear to be transient
|
||||||
// GitHub API issues that succeed on retry.
|
// GitHub API issues that succeed on retry.
|
||||||
|
let lastErrorDetails: any = null;
|
||||||
await retryWithBackoff(
|
await retryWithBackoff(
|
||||||
async () => {
|
async () => {
|
||||||
const updateRefResponse = await fetch(updateRefUrl, {
|
const updateRefResponse = await fetch(updateRefUrl, {
|
||||||
@@ -353,17 +386,48 @@ server.tool(
|
|||||||
|
|
||||||
if (!updateRefResponse.ok) {
|
if (!updateRefResponse.ok) {
|
||||||
const errorText = await updateRefResponse.text();
|
const errorText = await updateRefResponse.text();
|
||||||
|
let errorJson: any = {};
|
||||||
|
try {
|
||||||
|
errorJson = JSON.parse(errorText);
|
||||||
|
} catch {
|
||||||
|
// If not JSON, use the text as-is
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect debugging information
|
||||||
|
const debugInfo = {
|
||||||
|
status: updateRefResponse.status,
|
||||||
|
statusText: updateRefResponse.statusText,
|
||||||
|
headers: Object.fromEntries(updateRefResponse.headers.entries()),
|
||||||
|
errorBody: errorJson || errorText,
|
||||||
|
context: {
|
||||||
|
repository: `${owner}/${repo}`,
|
||||||
|
branch: branch,
|
||||||
|
baseBranch: process.env.BASE_BRANCH,
|
||||||
|
targetSha: newCommitData.sha,
|
||||||
|
parentSha: baseSha,
|
||||||
|
isGitHubAction: !!process.env.GITHUB_ACTIONS,
|
||||||
|
eventName: process.env.GITHUB_EVENT_NAME,
|
||||||
|
isPR: process.env.IS_PR,
|
||||||
|
tokenLength: githubToken?.length || 0,
|
||||||
|
tokenPrefix: githubToken?.substring(0, 10) + "...",
|
||||||
|
apiUrl: updateRefUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
lastErrorDetails = debugInfo;
|
||||||
|
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
|
`Failed to update reference: ${updateRefResponse.status} - ${errorText}\n\nDebug Info: ${JSON.stringify(debugInfo, null, 2)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only retry on 403 errors - these are the intermittent failures we're targeting
|
// Only retry on 403 errors - these are the intermittent failures we're targeting
|
||||||
if (updateRefResponse.status === 403) {
|
if (updateRefResponse.status === 403) {
|
||||||
|
console.error("403 Error encountered (will retry):", debugInfo);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-403 errors, fail immediately without retry
|
// For non-403 errors, fail immediately without retry
|
||||||
console.error("Non-retryable error:", updateRefResponse.status);
|
console.error("Non-retryable error:", debugInfo);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -373,7 +437,23 @@ server.tool(
|
|||||||
maxDelayMs: 5000, // Max 5 seconds delay
|
maxDelayMs: 5000, // Max 5 seconds delay
|
||||||
backoffFactor: 2, // Double the delay each time
|
backoffFactor: 2, // Double the delay each time
|
||||||
},
|
},
|
||||||
);
|
).catch((error) => {
|
||||||
|
// If all retries failed, enhance the error message with collected details
|
||||||
|
if (lastErrorDetails) {
|
||||||
|
throw new Error(
|
||||||
|
`All retry attempts failed for ref update.\n\n` +
|
||||||
|
`Final error: ${error.message}\n\n` +
|
||||||
|
`Debugging hints:\n` +
|
||||||
|
`- Check if branch '${branch}' is protected and the GitHub App has bypass permissions\n` +
|
||||||
|
`- Verify the token has 'contents:write' permission for ${owner}/${repo}\n` +
|
||||||
|
`- Check for concurrent operations updating the same branch\n` +
|
||||||
|
`- Token appears to be: ${lastErrorDetails.context.tokenPrefix}\n` +
|
||||||
|
`- This may be a transient GitHub API issue if it works on retry\n\n` +
|
||||||
|
`Full debug details: ${JSON.stringify(lastErrorDetails, null, 2)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
const simplifiedResult = {
|
const simplifiedResult = {
|
||||||
commit: {
|
commit: {
|
||||||
@@ -541,6 +621,7 @@ server.tool(
|
|||||||
// We're seeing intermittent 403 "Resource not accessible by integration" errors
|
// We're seeing intermittent 403 "Resource not accessible by integration" errors
|
||||||
// on certain repos when updating git references. These appear to be transient
|
// on certain repos when updating git references. These appear to be transient
|
||||||
// GitHub API issues that succeed on retry.
|
// GitHub API issues that succeed on retry.
|
||||||
|
let lastErrorDetails: any = null;
|
||||||
await retryWithBackoff(
|
await retryWithBackoff(
|
||||||
async () => {
|
async () => {
|
||||||
const updateRefResponse = await fetch(updateRefUrl, {
|
const updateRefResponse = await fetch(updateRefUrl, {
|
||||||
@@ -559,18 +640,52 @@ server.tool(
|
|||||||
|
|
||||||
if (!updateRefResponse.ok) {
|
if (!updateRefResponse.ok) {
|
||||||
const errorText = await updateRefResponse.text();
|
const errorText = await updateRefResponse.text();
|
||||||
|
let errorJson: any = {};
|
||||||
|
try {
|
||||||
|
errorJson = JSON.parse(errorText);
|
||||||
|
} catch {
|
||||||
|
// If not JSON, use the text as-is
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect debugging information
|
||||||
|
const debugInfo = {
|
||||||
|
status: updateRefResponse.status,
|
||||||
|
statusText: updateRefResponse.statusText,
|
||||||
|
headers: Object.fromEntries(updateRefResponse.headers.entries()),
|
||||||
|
errorBody: errorJson || errorText,
|
||||||
|
context: {
|
||||||
|
operation: "delete_files",
|
||||||
|
repository: `${owner}/${repo}`,
|
||||||
|
branch: branch,
|
||||||
|
baseBranch: process.env.BASE_BRANCH,
|
||||||
|
targetSha: newCommitData.sha,
|
||||||
|
parentSha: baseSha,
|
||||||
|
isGitHubAction: !!process.env.GITHUB_ACTIONS,
|
||||||
|
eventName: process.env.GITHUB_EVENT_NAME,
|
||||||
|
isPR: process.env.IS_PR,
|
||||||
|
tokenLength: githubToken?.length || 0,
|
||||||
|
tokenPrefix: githubToken?.substring(0, 10) + "...",
|
||||||
|
apiUrl: updateRefUrl,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
lastErrorDetails = debugInfo;
|
||||||
|
|
||||||
const error = new Error(
|
const error = new Error(
|
||||||
`Failed to update reference: ${updateRefResponse.status} - ${errorText}`,
|
`Failed to update reference: ${updateRefResponse.status} - ${errorText}\n\nDebug Info: ${JSON.stringify(debugInfo, null, 2)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only retry on 403 errors - these are the intermittent failures we're targeting
|
// Only retry on 403 errors - these are the intermittent failures we're targeting
|
||||||
if (updateRefResponse.status === 403) {
|
if (updateRefResponse.status === 403) {
|
||||||
console.log("Received 403 error, will retry...");
|
console.error(
|
||||||
|
"403 Error encountered during delete (will retry):",
|
||||||
|
debugInfo,
|
||||||
|
);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For non-403 errors, fail immediately without retry
|
// For non-403 errors, fail immediately without retry
|
||||||
console.error("Non-retryable error:", updateRefResponse.status);
|
console.error("Non-retryable error during delete:", debugInfo);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -580,7 +695,23 @@ server.tool(
|
|||||||
maxDelayMs: 5000, // Max 5 seconds delay
|
maxDelayMs: 5000, // Max 5 seconds delay
|
||||||
backoffFactor: 2, // Double the delay each time
|
backoffFactor: 2, // Double the delay each time
|
||||||
},
|
},
|
||||||
);
|
).catch((error) => {
|
||||||
|
// If all retries failed, enhance the error message with collected details
|
||||||
|
if (lastErrorDetails) {
|
||||||
|
throw new Error(
|
||||||
|
`All retry attempts failed for ref update during file deletion.\n\n` +
|
||||||
|
`Final error: ${error.message}\n\n` +
|
||||||
|
`Debugging hints:\n` +
|
||||||
|
`- Check if branch '${branch}' is protected and the GitHub App has bypass permissions\n` +
|
||||||
|
`- Verify the token has 'contents:write' permission for ${owner}/${repo}\n` +
|
||||||
|
`- Check for concurrent operations updating the same branch\n` +
|
||||||
|
`- Token appears to be: ${lastErrorDetails.context.tokenPrefix}\n` +
|
||||||
|
`- This may be a transient GitHub API issue if it works on retry\n\n` +
|
||||||
|
`Full debug details: ${JSON.stringify(lastErrorDetails, null, 2)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
const simplifiedResult = {
|
const simplifiedResult = {
|
||||||
commit: {
|
commit: {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const agentMode: Mode = {
|
|||||||
|
|
||||||
// TODO: handle by createPrompt (similar to tag and review modes)
|
// TODO: handle by createPrompt (similar to tag and review modes)
|
||||||
// Create prompt directory
|
// Create prompt directory
|
||||||
await mkdir(`${process.env.RUNNER_TEMP}/claude-prompts`, {
|
await mkdir(`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts`, {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
// Write the prompt file - the base action requires a prompt_file parameter,
|
// Write the prompt file - the base action requires a prompt_file parameter,
|
||||||
@@ -57,7 +57,7 @@ export const agentMode: Mode = {
|
|||||||
context.inputs.directPrompt ||
|
context.inputs.directPrompt ||
|
||||||
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
`Repository: ${context.repository.owner}/${context.repository.repo}`;
|
||||||
await writeFile(
|
await writeFile(
|
||||||
`${process.env.RUNNER_TEMP}/claude-prompts/claude-prompt.txt`,
|
`${process.env.RUNNER_TEMP || "/tmp"}/claude-prompts/claude-prompt.txt`,
|
||||||
promptContent,
|
promptContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user