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:
192
MIGRATION.md
Normal file
192
MIGRATION.md
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# Migration Guide: Pure Actions & Gitea Compatibility
|
||||||
|
|
||||||
|
This document outlines the changes made to migrate from GitHub App authentication to pure GitHub Actions and add Gitea compatibility.
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
### 1. Removed GitHub App Dependencies
|
||||||
|
- **Before**: Used OIDC token exchange with Anthropic's GitHub App service
|
||||||
|
- **After**: Uses standard `GITHUB_TOKEN` from workflow environment
|
||||||
|
- **Benefit**: No external dependencies, works with any Git provider
|
||||||
|
|
||||||
|
### 2. Self-Contained Implementation
|
||||||
|
- **Before**: Depended on external `anthropics/claude-code-base-action`
|
||||||
|
- **After**: Includes built-in Claude execution engine
|
||||||
|
- **Benefit**: Complete control over functionality, no external action dependencies
|
||||||
|
|
||||||
|
### 3. Gitea Compatibility
|
||||||
|
- **Before**: GitHub-specific triggers and authentication
|
||||||
|
- **After**: Compatible with Gitea Actions (with some limitations)
|
||||||
|
- **Benefit**: Works with self-hosted Gitea instances
|
||||||
|
|
||||||
|
## Required Changes for Existing Users
|
||||||
|
|
||||||
|
### Workflow Permissions
|
||||||
|
Update your workflow permissions:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Before (GitHub App)
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: read
|
||||||
|
issues: read
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
# After (Pure Actions)
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Token Input
|
||||||
|
Now required to explicitly provide a GitHub token:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Before (optional)
|
||||||
|
- uses: anthropics/claude-code-action@beta
|
||||||
|
with:
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
|
||||||
|
# After (required)
|
||||||
|
- uses: anthropics/claude-code-action@beta
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gitea Setup
|
||||||
|
|
||||||
|
### 1. Basic Gitea Workflow
|
||||||
|
Use the example in `examples/gitea-claude.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Claude Assistant for Gitea
|
||||||
|
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
issues:
|
||||||
|
types: [opened, assigned]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude-assistant:
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Run Claude Assistant
|
||||||
|
uses: ./ # Adjust path as needed for your Gitea setup
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Gitea Limitations
|
||||||
|
Be aware of these Gitea Actions limitations:
|
||||||
|
|
||||||
|
- **`issue_comment` on PRs**: May not trigger reliably in some Gitea versions
|
||||||
|
- **`pull_request_review_comment`**: Limited support compared to GitHub
|
||||||
|
- **Cross-repository access**: Token permissions may be more restrictive
|
||||||
|
- **Workflow triggers**: Some advanced trigger conditions may not work
|
||||||
|
|
||||||
|
### 3. Gitea Workarounds
|
||||||
|
|
||||||
|
#### For PR Comments
|
||||||
|
Use `issue_comment` instead of `pull_request_review_comment`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
on:
|
||||||
|
issue_comment:
|
||||||
|
types: [created] # This covers both issue and PR comments
|
||||||
|
```
|
||||||
|
|
||||||
|
#### For Code Review Comments
|
||||||
|
Gitea has limited support for code review comment webhooks. Consider using:
|
||||||
|
- Regular issue comments on PRs
|
||||||
|
- Manual trigger via issue assignment
|
||||||
|
- Custom webhooks (advanced setup)
|
||||||
|
|
||||||
|
## Benefits of Migration
|
||||||
|
|
||||||
|
### 1. Simplified Authentication
|
||||||
|
- No OIDC token setup required
|
||||||
|
- Uses standard workflow tokens
|
||||||
|
- Works with custom GitHub tokens
|
||||||
|
|
||||||
|
### 2. Provider Independence
|
||||||
|
- No dependency on Anthropic's GitHub App service
|
||||||
|
- Works with any Git provider supporting Actions
|
||||||
|
- Self-contained functionality
|
||||||
|
|
||||||
|
### 3. Enhanced Control
|
||||||
|
- Direct control over Claude execution
|
||||||
|
- Customizable tool management
|
||||||
|
- Easier debugging and modifications
|
||||||
|
|
||||||
|
### 4. Gitea Support
|
||||||
|
- Compatible with self-hosted Gitea
|
||||||
|
- Reduced external dependencies
|
||||||
|
- Standard Actions workflow patterns
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
#### 1. Token Permissions
|
||||||
|
**Error**: "GitHub token authentication failed"
|
||||||
|
**Solution**: Ensure workflow has required permissions:
|
||||||
|
```yaml
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Gitea Trigger Issues
|
||||||
|
**Error**: Workflow not triggering on PR comments
|
||||||
|
**Solution**: Use `issue_comment` instead of `pull_request_review_comment`
|
||||||
|
|
||||||
|
#### 3. Missing Dependencies
|
||||||
|
**Error**: "Module not found" or TypeScript errors
|
||||||
|
**Solution**: Run `npm install` or `bun install` to update dependencies
|
||||||
|
|
||||||
|
### Gitea-Specific Issues
|
||||||
|
|
||||||
|
#### 1. Limited Event Support
|
||||||
|
Some GitHub Events may not be fully supported in Gitea. Use basic triggers:
|
||||||
|
- `issue_comment` for comments
|
||||||
|
- `issues` for issue events
|
||||||
|
- `push` for code changes
|
||||||
|
|
||||||
|
#### 2. Token Scope Limitations
|
||||||
|
Gitea tokens may have different scope limitations. Ensure your Gitea instance allows:
|
||||||
|
- Repository write access
|
||||||
|
- Issue/PR comment creation
|
||||||
|
- Branch creation and updates
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
- [ ] Update workflow permissions to include `write` access
|
||||||
|
- [ ] Add `github_token` input to action configuration
|
||||||
|
- [ ] Remove `id-token: write` permission if not used elsewhere
|
||||||
|
- [ ] Test with GitHub Actions
|
||||||
|
- [ ] Test with Gitea Actions (if applicable)
|
||||||
|
- [ ] Update any custom triggers for Gitea compatibility
|
||||||
|
- [ ] Verify token permissions in target environment
|
||||||
|
|
||||||
|
## Example Workflows
|
||||||
|
|
||||||
|
See the `examples/` directory for complete workflow examples:
|
||||||
|
- `claude.yml` - Updated GitHub Actions workflow
|
||||||
|
- `gitea-claude.yml` - Gitea-compatible workflow
|
||||||
48
action.yml
48
action.yml
@@ -45,7 +45,7 @@ inputs:
|
|||||||
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)"
|
description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)"
|
||||||
required: false
|
required: false
|
||||||
github_token:
|
github_token:
|
||||||
description: "GitHub token with repo and pull request permissions (optional if using GitHub App)"
|
description: "GitHub token with repo and pull request permissions (defaults to GITHUB_TOKEN)"
|
||||||
required: false
|
required: false
|
||||||
use_bedrock:
|
use_bedrock:
|
||||||
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API"
|
||||||
@@ -93,44 +93,42 @@ runs:
|
|||||||
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }}
|
||||||
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
DIRECT_PROMPT: ${{ inputs.direct_prompt }}
|
||||||
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
OVERRIDE_GITHUB_TOKEN: ${{ inputs.github_token }}
|
||||||
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
GITHUB_RUN_ID: ${{ github.run_id }}
|
GITHUB_RUN_ID: ${{ github.run_id }}
|
||||||
|
|
||||||
- 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@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8
|
shell: bash
|
||||||
with:
|
run: |
|
||||||
prompt_file: /tmp/claude-prompts/claude-prompt.txt
|
bun run ${{ github.action_path }}/src/entrypoints/execute-claude.ts
|
||||||
allowed_tools: ${{ env.ALLOWED_TOOLS }}
|
|
||||||
disallowed_tools: ${{ env.DISALLOWED_TOOLS }}
|
|
||||||
timeout_minutes: ${{ inputs.timeout_minutes }}
|
|
||||||
model: ${{ inputs.model || inputs.anthropic_model }}
|
|
||||||
mcp_config: ${{ steps.prepare.outputs.mcp_config }}
|
|
||||||
use_bedrock: ${{ inputs.use_bedrock }}
|
|
||||||
use_vertex: ${{ inputs.use_vertex }}
|
|
||||||
anthropic_api_key: ${{ inputs.anthropic_api_key }}
|
|
||||||
env:
|
env:
|
||||||
# Model configuration
|
# Core configuration
|
||||||
|
PROMPT_FILE: /tmp/claude-prompts/claude-prompt.txt
|
||||||
|
ALLOWED_TOOLS: ${{ env.ALLOWED_TOOLS }}
|
||||||
|
DISALLOWED_TOOLS: ${{ env.DISALLOWED_TOOLS }}
|
||||||
|
TIMEOUT_MINUTES: ${{ inputs.timeout_minutes }}
|
||||||
|
MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||||
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
ANTHROPIC_MODEL: ${{ inputs.model || inputs.anthropic_model }}
|
||||||
|
MCP_CONFIG: ${{ steps.prepare.outputs.mcp_config }}
|
||||||
|
USE_BEDROCK: ${{ inputs.use_bedrock }}
|
||||||
|
USE_VERTEX: ${{ inputs.use_vertex }}
|
||||||
|
ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }}
|
||||||
|
|
||||||
|
# GitHub token for repository access
|
||||||
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Provider configuration
|
# Provider configuration (for future cloud provider support)
|
||||||
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
ANTHROPIC_BASE_URL: ${{ env.ANTHROPIC_BASE_URL }}
|
||||||
|
|
||||||
# AWS configuration
|
|
||||||
AWS_REGION: ${{ env.AWS_REGION }}
|
AWS_REGION: ${{ env.AWS_REGION }}
|
||||||
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
AWS_ACCESS_KEY_ID: ${{ env.AWS_ACCESS_KEY_ID }}
|
||||||
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
|
AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }}
|
||||||
AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
|
AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }}
|
||||||
ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL }}
|
ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL }}
|
||||||
|
|
||||||
# GCP configuration
|
|
||||||
ANTHROPIC_VERTEX_PROJECT_ID: ${{ env.ANTHROPIC_VERTEX_PROJECT_ID }}
|
ANTHROPIC_VERTEX_PROJECT_ID: ${{ env.ANTHROPIC_VERTEX_PROJECT_ID }}
|
||||||
CLOUD_ML_REGION: ${{ env.CLOUD_ML_REGION }}
|
CLOUD_ML_REGION: ${{ env.CLOUD_ML_REGION }}
|
||||||
GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }}
|
GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }}
|
||||||
ANTHROPIC_VERTEX_BASE_URL: ${{ env.ANTHROPIC_VERTEX_BASE_URL }}
|
ANTHROPIC_VERTEX_BASE_URL: ${{ env.ANTHROPIC_VERTEX_BASE_URL }}
|
||||||
|
|
||||||
# Model-specific regions for Vertex
|
|
||||||
VERTEX_REGION_CLAUDE_3_5_HAIKU: ${{ env.VERTEX_REGION_CLAUDE_3_5_HAIKU }}
|
VERTEX_REGION_CLAUDE_3_5_HAIKU: ${{ env.VERTEX_REGION_CLAUDE_3_5_HAIKU }}
|
||||||
VERTEX_REGION_CLAUDE_3_5_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_5_SONNET }}
|
VERTEX_REGION_CLAUDE_3_5_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_5_SONNET }}
|
||||||
VERTEX_REGION_CLAUDE_3_7_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_7_SONNET }}
|
VERTEX_REGION_CLAUDE_3_7_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_7_SONNET }}
|
||||||
@@ -166,13 +164,3 @@ runs:
|
|||||||
cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY
|
cat "${{ steps.claude-code.outputs.execution_file }}" >> $GITHUB_STEP_SUMMARY
|
||||||
echo '```' >> $GITHUB_STEP_SUMMARY
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
- name: Revoke app token
|
|
||||||
if: always() && inputs.github_token == ''
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
curl -L \
|
|
||||||
-X DELETE \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
|
||||||
-H "Authorization: Bearer ${{ steps.prepare.outputs.GITHUB_TOKEN }}" \
|
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
||||||
${GITHUB_API_URL:-https://api.github.com}/installation/token
|
|
||||||
|
|||||||
@@ -19,10 +19,9 @@ jobs:
|
|||||||
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: write
|
||||||
pull-requests: read
|
pull-requests: write
|
||||||
issues: read
|
issues: write
|
||||||
id-token: write
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -32,5 +31,6 @@ jobs:
|
|||||||
- name: Run Claude PR Action
|
- name: Run Claude PR Action
|
||||||
uses: anthropics/claude-code-action@beta
|
uses: anthropics/claude-code-action@beta
|
||||||
with:
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
timeout_minutes: "60"
|
timeout_minutes: "60"
|
||||||
|
|||||||
43
examples/gitea-claude.yml
Normal file
43
examples/gitea-claude.yml
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
name: Claude Assistant for Gitea
|
||||||
|
|
||||||
|
on:
|
||||||
|
# Trigger on issue comments (works on both issues and pull requests in Gitea)
|
||||||
|
issue_comment:
|
||||||
|
types: [created]
|
||||||
|
# Trigger on issues being opened or assigned
|
||||||
|
issues:
|
||||||
|
types: [opened, assigned]
|
||||||
|
# Note: pull_request_review_comment has limited support in Gitea
|
||||||
|
# Use issue_comment instead which covers PR comments
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
claude-assistant:
|
||||||
|
# Basic trigger detection - check for @claude in comments or issue body
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
|
||||||
|
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || github.event.action == 'assigned'))
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
issues: write
|
||||||
|
# Note: Gitea Actions may not require id-token: write for basic functionality
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Run Claude Assistant
|
||||||
|
uses: ./ # Use local action (adjust path as needed)
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.GITHUB_TOKEN }} # Use standard workflow token
|
||||||
|
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||||
|
timeout_minutes: "60"
|
||||||
|
trigger_phrase: "@claude"
|
||||||
|
# Optional: Customize for Gitea environment
|
||||||
|
custom_instructions: |
|
||||||
|
You are working in a Gitea environment. Be aware that:
|
||||||
|
- Some GitHub Actions features may behave differently
|
||||||
|
- Focus on core functionality and avoid advanced GitHub-specific features
|
||||||
|
- Use standard git operations when possible
|
||||||
1988
package-lock.json
generated
Normal file
1988
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.1",
|
"@actions/core": "^1.10.1",
|
||||||
"@actions/github": "^6.0.1",
|
"@actions/github": "^6.0.1",
|
||||||
|
"@anthropic-ai/sdk": "^0.30.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.11.0",
|
"@modelcontextprotocol/sdk": "^1.11.0",
|
||||||
"@octokit/graphql": "^8.2.2",
|
"@octokit/graphql": "^8.2.2",
|
||||||
"@octokit/rest": "^21.1.1",
|
"@octokit/rest": "^21.1.1",
|
||||||
|
|||||||
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";
|
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> {
|
export async function setupGitHubToken(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
@@ -103,22 +16,19 @@ export async function setupGitHubToken(): Promise<string> {
|
|||||||
return providedToken;
|
return providedToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Requesting OIDC token...");
|
// Use the standard GITHUB_TOKEN from the workflow environment
|
||||||
const oidcToken = await retryWithBackoff(() => getOidcToken());
|
const workflowToken = process.env.GITHUB_TOKEN;
|
||||||
console.log("OIDC token successfully obtained");
|
|
||||||
|
|
||||||
console.log("Exchanging OIDC token for app token...");
|
if (workflowToken) {
|
||||||
const appToken = await retryWithBackoff(() =>
|
console.log("Using workflow GITHUB_TOKEN for authentication");
|
||||||
exchangeForAppToken(oidcToken),
|
core.setOutput("GITHUB_TOKEN", workflowToken);
|
||||||
);
|
return workflowToken;
|
||||||
console.log("App token successfully obtained");
|
}
|
||||||
|
|
||||||
console.log("Using GITHUB_TOKEN from OIDC");
|
throw new Error("No GitHub token available. Please provide a github_token input or ensure GITHUB_TOKEN is available in the workflow environment.");
|
||||||
core.setOutput("GITHUB_TOKEN", appToken);
|
|
||||||
return appToken;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
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\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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user