mirror of
https://github.com/markwylde/claude-code-gitea-action.git
synced 2026-02-20 02:22:49 +08:00
First attempt
This commit is contained in:
140
src/claude/executor.ts
Normal file
140
src/claude/executor.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import Anthropic from "@anthropic-ai/sdk";
|
||||
import * as fs from "fs";
|
||||
|
||||
export interface ClaudeExecutorConfig {
|
||||
apiKey?: string;
|
||||
model?: string;
|
||||
promptFile?: string;
|
||||
prompt?: string;
|
||||
maxTurns?: number;
|
||||
timeoutMinutes?: number;
|
||||
mcpConfig?: string;
|
||||
allowedTools?: string;
|
||||
disallowedTools?: string;
|
||||
useBedrock?: boolean;
|
||||
useVertex?: boolean;
|
||||
}
|
||||
|
||||
export interface ClaudeExecutorResult {
|
||||
conclusion: "success" | "failure";
|
||||
executionFile?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ClaudeExecutor {
|
||||
private config: ClaudeExecutorConfig;
|
||||
private anthropic?: Anthropic;
|
||||
|
||||
constructor(config: ClaudeExecutorConfig) {
|
||||
this.config = config;
|
||||
this.initializeClient();
|
||||
}
|
||||
|
||||
private initializeClient() {
|
||||
if (this.config.useBedrock || this.config.useVertex) {
|
||||
throw new Error("Bedrock and Vertex AI not supported in simplified implementation");
|
||||
}
|
||||
|
||||
if (!this.config.apiKey) {
|
||||
throw new Error("Anthropic API key is required");
|
||||
}
|
||||
|
||||
this.anthropic = new Anthropic({
|
||||
apiKey: this.config.apiKey,
|
||||
});
|
||||
}
|
||||
|
||||
private async readPrompt(): Promise<string> {
|
||||
if (this.config.prompt) {
|
||||
return this.config.prompt;
|
||||
}
|
||||
|
||||
if (this.config.promptFile) {
|
||||
if (!fs.existsSync(this.config.promptFile)) {
|
||||
throw new Error(`Prompt file not found: ${this.config.promptFile}`);
|
||||
}
|
||||
return fs.readFileSync(this.config.promptFile, "utf-8");
|
||||
}
|
||||
|
||||
throw new Error("Either prompt or promptFile must be provided");
|
||||
}
|
||||
|
||||
private parseTools(): { allowed: string[]; disallowed: string[] } {
|
||||
const allowed = this.config.allowedTools
|
||||
? this.config.allowedTools.split(",").map(t => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
const disallowed = this.config.disallowedTools
|
||||
? this.config.disallowedTools.split(",").map(t => t.trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
return { allowed, disallowed };
|
||||
}
|
||||
|
||||
private createExecutionLog(result: any, error?: string): string {
|
||||
const logData = {
|
||||
conclusion: error ? "failure" : "success",
|
||||
model: this.config.model || "claude-3-7-sonnet-20250219",
|
||||
timestamp: new Date().toISOString(),
|
||||
result,
|
||||
error,
|
||||
};
|
||||
|
||||
const logFile = "/tmp/claude-execution.json";
|
||||
fs.writeFileSync(logFile, JSON.stringify(logData, null, 2));
|
||||
return logFile;
|
||||
}
|
||||
|
||||
async execute(): Promise<ClaudeExecutorResult> {
|
||||
try {
|
||||
const prompt = await this.readPrompt();
|
||||
const tools = this.parseTools();
|
||||
|
||||
console.log(`Executing Claude with model: ${this.config.model || "claude-3-7-sonnet-20250219"}`);
|
||||
console.log(`Allowed tools: ${tools.allowed.join(", ") || "none"}`);
|
||||
console.log(`Disallowed tools: ${tools.disallowed.join(", ") || "none"}`);
|
||||
|
||||
if (!this.anthropic) {
|
||||
throw new Error("Anthropic client not initialized");
|
||||
}
|
||||
|
||||
// Create a simple message request
|
||||
const response = await this.anthropic.messages.create({
|
||||
model: this.config.model || "claude-3-7-sonnet-20250219",
|
||||
max_tokens: 8192,
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: prompt,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
console.log("Claude response received successfully");
|
||||
|
||||
const executionFile = this.createExecutionLog(response);
|
||||
|
||||
return {
|
||||
conclusion: "success",
|
||||
executionFile,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Claude execution failed:", error);
|
||||
|
||||
const executionFile = this.createExecutionLog(null, String(error));
|
||||
|
||||
return {
|
||||
conclusion: "failure",
|
||||
executionFile,
|
||||
error: String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function runClaude(config: ClaudeExecutorConfig): Promise<ClaudeExecutorResult> {
|
||||
const executor = new ClaudeExecutor(config);
|
||||
return await executor.execute();
|
||||
}
|
||||
46
src/entrypoints/execute-claude.ts
Normal file
46
src/entrypoints/execute-claude.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bun
|
||||
|
||||
import * as core from "@actions/core";
|
||||
import { runClaude, type ClaudeExecutorConfig } from "../claude/executor";
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const config: ClaudeExecutorConfig = {
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
model: process.env.ANTHROPIC_MODEL || process.env.MODEL,
|
||||
promptFile: process.env.PROMPT_FILE,
|
||||
prompt: process.env.PROMPT,
|
||||
maxTurns: process.env.MAX_TURNS ? parseInt(process.env.MAX_TURNS) : undefined,
|
||||
timeoutMinutes: process.env.TIMEOUT_MINUTES ? parseInt(process.env.TIMEOUT_MINUTES) : 30,
|
||||
mcpConfig: process.env.MCP_CONFIG,
|
||||
allowedTools: process.env.ALLOWED_TOOLS,
|
||||
disallowedTools: process.env.DISALLOWED_TOOLS,
|
||||
useBedrock: process.env.USE_BEDROCK === "true",
|
||||
useVertex: process.env.USE_VERTEX === "true",
|
||||
};
|
||||
|
||||
console.log("Starting Claude execution...");
|
||||
const result = await runClaude(config);
|
||||
|
||||
// Set outputs for GitHub Actions
|
||||
core.setOutput("conclusion", result.conclusion);
|
||||
if (result.executionFile) {
|
||||
core.setOutput("execution_file", result.executionFile);
|
||||
}
|
||||
|
||||
if (result.conclusion === "failure") {
|
||||
core.setFailed(result.error || "Claude execution failed");
|
||||
} else {
|
||||
console.log("Claude execution completed successfully");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to execute Claude:", error);
|
||||
core.setFailed(`Failed to execute Claude: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("Unhandled error:", error);
|
||||
core.setFailed(`Unhandled error: ${error}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -2,95 +2,8 @@
|
||||
|
||||
import * as core from "@actions/core";
|
||||
|
||||
type RetryOptions = {
|
||||
maxAttempts?: number;
|
||||
initialDelayMs?: number;
|
||||
maxDelayMs?: number;
|
||||
backoffFactor?: number;
|
||||
};
|
||||
|
||||
async function retryWithBackoff<T>(
|
||||
operation: () => Promise<T>,
|
||||
options: RetryOptions = {},
|
||||
): Promise<T> {
|
||||
const {
|
||||
maxAttempts = 3,
|
||||
initialDelayMs = 5000,
|
||||
maxDelayMs = 20000,
|
||||
backoffFactor = 2,
|
||||
} = options;
|
||||
|
||||
let delayMs = initialDelayMs;
|
||||
let lastError: Error | undefined;
|
||||
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
console.log(`Attempt ${attempt} of ${maxAttempts}...`);
|
||||
return await operation();
|
||||
} catch (error) {
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
console.error(`Attempt ${attempt} failed:`, lastError.message);
|
||||
|
||||
if (attempt < maxAttempts) {
|
||||
console.log(`Retrying in ${delayMs / 1000} seconds...`);
|
||||
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
||||
delayMs = Math.min(delayMs * backoffFactor, maxDelayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.error(`Operation failed after ${maxAttempts} attempts`);
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
async function getOidcToken(): Promise<string> {
|
||||
try {
|
||||
const oidcToken = await core.getIDToken("claude-code-github-action");
|
||||
|
||||
return oidcToken;
|
||||
} catch (error) {
|
||||
console.error("Failed to get OIDC token:", error);
|
||||
throw new Error(
|
||||
"Could not fetch an OIDC token. Did you remember to add `id-token: write` to your workflow permissions?",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function exchangeForAppToken(oidcToken: string): Promise<string> {
|
||||
const response = await fetch(
|
||||
"https://api.anthropic.com/api/github/github-app-token-exchange",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${oidcToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
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 {
|
||||
token?: string;
|
||||
app_token?: string;
|
||||
};
|
||||
const appToken = appTokenData.token || appTokenData.app_token;
|
||||
|
||||
if (!appToken) {
|
||||
throw new Error("App token not found in response");
|
||||
}
|
||||
|
||||
return appToken;
|
||||
}
|
||||
|
||||
export async function setupGitHubToken(): Promise<string> {
|
||||
try {
|
||||
@@ -103,22 +16,19 @@ export async function setupGitHubToken(): Promise<string> {
|
||||
return providedToken;
|
||||
}
|
||||
|
||||
console.log("Requesting OIDC token...");
|
||||
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
||||
console.log("OIDC token successfully obtained");
|
||||
// Use the standard GITHUB_TOKEN from the workflow environment
|
||||
const workflowToken = process.env.GITHUB_TOKEN;
|
||||
|
||||
if (workflowToken) {
|
||||
console.log("Using workflow GITHUB_TOKEN for authentication");
|
||||
core.setOutput("GITHUB_TOKEN", workflowToken);
|
||||
return workflowToken;
|
||||
}
|
||||
|
||||
console.log("Exchanging OIDC token for app token...");
|
||||
const appToken = await retryWithBackoff(() =>
|
||||
exchangeForAppToken(oidcToken),
|
||||
);
|
||||
console.log("App token successfully obtained");
|
||||
|
||||
console.log("Using GITHUB_TOKEN from OIDC");
|
||||
core.setOutput("GITHUB_TOKEN", appToken);
|
||||
return appToken;
|
||||
throw new Error("No GitHub token available. Please provide a github_token input or ensure GITHUB_TOKEN is available in the workflow environment.");
|
||||
} catch (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.`,
|
||||
`Failed to setup GitHub token: ${error}.\n\nPlease provide a \`github_token\` in the \`with\` section of the action in your workflow yml file, or ensure the workflow has access to the default GITHUB_TOKEN.`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user