Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4d2370c
fix: skip comment update on workflow cancellation
Oct 14, 2025
04a053b
chore: sync with upstream anthropics/claude-code-action
nathanclevenger Oct 18, 2025
41123e6
fix: support pull_request.assigned action with track_progress
nathanclevenger Oct 19, 2025
08dd986
feat(bedrock): add AWS_BEARER_TOKEN_BEDROCK support for Bedrock API keys
nathanclevenger Oct 30, 2025
30e8f60
merge: integrate track_progress support for pull_request.assigned events
nathanclevenger Oct 30, 2025
212b1b9
feat: add exponential backoff retry for rate limit errors
nathanclevenger Oct 31, 2025
07cf46a
Merge pull request #1 from dot-do/feature/retry-on-rate-limit
nathanclevenger Oct 31, 2025
ce72cb0
feat: fallback to Anthropic API on AWS Bedrock rate limit
nathanclevenger Oct 31, 2025
13f3147
Merge pull request #2 from dot-do/feature/fallback-to-anthropic-on-ra…
nathanclevenger Oct 31, 2025
c801b2e
fix: delete progress comment when review is submitted
nathanclevenger Nov 1, 2025
a2e32d6
refactor: extract review detection to helper function with type guards
nathanclevenger Nov 1, 2025
d4042c6
Merge pull request #3 from dot-do/fix/delete-comment-when-review-subm…
nathanclevenger Nov 1, 2025
edb8e53
fix: handle null files data in PR fetcher
nathanclevenger Nov 2, 2025
b50aeb0
fix: ALWAYS try Bedrock first on each attempt, immediate Anthropic fa…
nathanclevenger Nov 2, 2025
690c9c2
Merge pull request #5 from dot-do/fix/reduce-anthropic-retry-delay
nathanclevenger Nov 2, 2025
35bb7ac
feat: route Claude API through claude-lb proxy for instant failover
nathanclevenger Nov 2, 2025
c566a2c
Merge pull request #6 from dot-do/fix/early-429-detection
nathanclevenger Nov 2, 2025
dd4d00c
Merge pull request #4 from dot-do/fix/handle-null-files-pagination
nathanclevenger Nov 2, 2025
029d172
feat: add HTTP proxy for secure pass-through authentication
nathanclevenger Nov 2, 2025
d3d06c1
Merge pull request #8 from dot-do/feature/proxy-auth-headers
nathanclevenger Nov 2, 2025
6fc8661
feat: add anthropic-only proxy mode with intelligent fallback (#9)
nathanclevenger Nov 3, 2025
aa25fcc
fix: move /health endpoint check before 404 response
nathanclevenger Nov 4, 2025
af6557d
fix: move /health endpoint check before 404 response (#10)
nathanclevenger Nov 4, 2025
0cabd93
fix: add idleTimeout and bedrock-only proxy mode (#11)
nathanclevenger Nov 4, 2025
a08b95f
refactor: simplify proxy server - require both API keys (#12)
nathanclevenger Nov 4, 2025
61074b1
fix: set idleTimeout to 255 (Bun max) instead of 300
nathanclevenger Nov 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,53 @@

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## ⚠️ CRITICAL: Repository Context

This is a **FORK** of `anthropics/claude-code-action`. This repository is `dot-do/claude-code-action`.

### Pull Request Rules

**NEVER create PRs against `anthropics/claude-code-action`**

- ✅ **CORRECT**: Create PRs in `dot-do/claude-code-action` (this repository)
- ❌ **WRONG**: Create PRs in `anthropics/claude-code-action` (upstream)

When using `gh pr create`, ALWAYS specify the repository explicitly:

```bash
# CORRECT - Specify our fork explicitly
gh pr create --repo dot-do/claude-code-action --base main --title "..." --body "..."

# WRONG - Default behavior may target upstream
gh pr create --title "..." --body "..."
```

### Repository Structure

```
origin → https://github.com/dot-do/claude-code-action.git (OUR FORK)
upstream → https://github.com/anthropics/claude-code-action.git (UPSTREAM)
```

**Default behavior**: `gh pr create` targets the upstream repository when working in a fork. This is NOT what we want.

### Syncing with Upstream

To pull changes from upstream Anthropic repository:

```bash
git fetch upstream
git merge upstream/main
git push origin main
```

### Why This Matters

- **Upstream pollution**: Creating PRs in upstream confuses Anthropic maintainers
- **Wrong codebase**: Our fork has custom modifications specific to `.do` platform
- **Permission issues**: We don't have merge rights in upstream repository
- **Workflow disruption**: PRs in wrong repo waste time and create confusion

## Development Tools

- Runtime: Bun 1.2.11
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ runs:
VERTEX_REGION_CLAUDE_3_7_SONNET: ${{ env.VERTEX_REGION_CLAUDE_3_7_SONNET }}

- name: Update comment with job link
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always()
if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && !cancelled()
shell: bash
run: |
bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts
Expand Down
18 changes: 16 additions & 2 deletions base-action/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,28 @@

import * as core from "@actions/core";
import { preparePrompt } from "./prepare-prompt";
import { runClaude } from "./run-claude";
import { runClaudeWithRetry } from "./run-claude";
import { setupClaudeCodeSettings } from "./setup-claude-code-settings";
import { validateEnvironmentVariables } from "./validate-env";
import { startProxyServer, getProxyUrl, shouldUseProxy } from "./proxy-server";

async function run() {
try {
validateEnvironmentVariables();

// Start local HTTP proxy server for claude-lb (supports multiple modes)
const proxyPort = await startProxyServer();

// Only route through proxy if credentials are available
if (shouldUseProxy()) {
process.env.ANTHROPIC_BASE_URL = getProxyUrl();
console.log(`\n🔀 Claude API requests routed through claude-lb proxy`);
console.log(` Benefits: Centralized monitoring, observability, and multi-provider failover`);
} else {
console.log(`\n⚡️ Using direct Anthropic API (no proxy)`);
console.log(` To enable monitoring, set ANTHROPIC_API_KEY in secrets`);
}

await setupClaudeCodeSettings(
process.env.INPUT_SETTINGS,
undefined, // homeDir
Expand All @@ -20,7 +34,7 @@ async function run() {
promptFile: process.env.INPUT_PROMPT_FILE || "",
});

await runClaude(promptConfig.path, {
await runClaudeWithRetry(promptConfig.path, {
claudeArgs: process.env.INPUT_CLAUDE_ARGS,
allowedTools: process.env.INPUT_ALLOWED_TOOLS,
disallowedTools: process.env.INPUT_DISALLOWED_TOOLS,
Expand Down
217 changes: 217 additions & 0 deletions base-action/src/proxy-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* HTTP Proxy Server for Claude API via Cloudflare AI Gateway
*
* Intercepts Claude CLI requests and forwards them directly to Cloudflare AI Gateway,
* which routes to AWS Bedrock or Anthropic API based on availability.
*
* This enables:
* - Direct routing through AI Gateway (no intermediate worker hop)
* - Centralized monitoring and observability via Cloudflare AI Gateway
* - Intelligent failover: Bedrock first (uses $100k credits), then Anthropic
* - Zero-delay failover on ANY Bedrock error (429, 430, 500, 403, etc.)
*
* REQUIREMENTS:
* - AWS_BEARER_TOKEN_BEDROCK must be set (for Bedrock access)
* - ANTHROPIC_API_KEY must be set (for failover)
*/

const PROXY_PORT = 18765; // Local proxy port
const AI_GATEWAY_ACCOUNT = 'b6641681fe423910342b9ffa1364c76d';
const AI_GATEWAY_ID = 'claude-gateway';
const AI_GATEWAY_BASE = `https://gateway.ai.cloudflare.com/v1/${AI_GATEWAY_ACCOUNT}/${AI_GATEWAY_ID}`;

interface ProxyStats {
requests: number;
successes: number;
failures: number;
bedrockSuccesses: number;
anthropicFailovers: number;
lastError?: string;
}

const stats: ProxyStats = {
requests: 0,
successes: 0,
failures: 0,
bedrockSuccesses: 0,
anthropicFailovers: 0,
};

/**
* Validate required credentials are present
* Throws error if either is missing
*/
function validateCredentials(): void {
const bedrockToken = process.env.AWS_BEARER_TOKEN_BEDROCK?.trim();
const anthropicKey = process.env.ANTHROPIC_API_KEY?.trim();

const missing: string[] = [];
if (!bedrockToken) missing.push('AWS_BEARER_TOKEN_BEDROCK');
if (!anthropicKey) missing.push('ANTHROPIC_API_KEY');

if (missing.length > 0) {
throw new Error(
`Missing required credentials: ${missing.join(', ')}\n` +
'Both AWS_BEARER_TOKEN_BEDROCK and ANTHROPIC_API_KEY must be set as GitHub org secrets.'
);
}
}

/**
* Forward request to AWS Bedrock via AI Gateway
*/
async function forwardToBedrock(body: string, headers: Headers): Promise<Response> {
const bedrockToken = process.env.AWS_BEARER_TOKEN_BEDROCK!; // Validated at startup

// AI Gateway URL for Bedrock
const bedrockUrl = `${AI_GATEWAY_BASE}/aws-bedrock/bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-sonnet-4-5-v1:0/invoke`;

const bedrockHeaders = new Headers({
'Content-Type': 'application/json',
'Authorization': `Bearer ${bedrockToken}`,
'anthropic-version': headers.get('anthropic-version') || '2023-06-01',
});

console.log('🔵 Attempting Bedrock via AI Gateway...');

const response = await fetch(bedrockUrl, {
method: 'POST',
headers: bedrockHeaders,
body
});

if (response.ok) {
console.log('✅ Bedrock request succeeded via AI Gateway');
} else {
console.warn(`⚠️ Bedrock returned ${response.status}`);
}

return response;
}

/**
* Forward request to Anthropic via AI Gateway
*/
async function forwardToAnthropic(body: string, headers: Headers): Promise<Response> {
const anthropicKey = process.env.ANTHROPIC_API_KEY!; // Validated at startup

// AI Gateway URL for Anthropic
const anthropicUrl = `${AI_GATEWAY_BASE}/anthropic/v1/messages`;

const anthropicHeaders = new Headers({
'Content-Type': 'application/json',
'x-api-key': anthropicKey,
'anthropic-version': headers.get('anthropic-version') || '2023-06-01',
});

console.log('🟢 Attempting Anthropic via AI Gateway...');

const response = await fetch(anthropicUrl, {
method: 'POST',
headers: anthropicHeaders,
body
});

if (response.ok) {
console.log('✅ Anthropic request succeeded via AI Gateway');
} else {
console.error(`❌ Anthropic returned ${response.status}`);
}

return response;
}

export async function startProxyServer(): Promise<number> {
// Validate credentials at startup - fail fast if misconfigured
validateCredentials();

const server = Bun.serve({
port: PROXY_PORT,
hostname: '127.0.0.1',
idleTimeout: 255, // Max allowed by Bun (4.25 minutes) for long-running Claude API requests

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idleTimeout: 0

will make it unlimited


async fetch(req: Request): Promise<Response> {
const url = new URL(req.url);

// Health check endpoint
if (url.pathname === '/health') {
return Response.json(stats);
}

// Only proxy /v1/messages
if (url.pathname !== '/v1/messages' || req.method !== 'POST') {
return new Response('Not Found', { status: 404 });
}

stats.requests++;

const body = await req.text();

// Always try Bedrock first (uses $100k credits)
try {
const bedrockResponse = await forwardToBedrock(body, req.headers);

if (bedrockResponse.ok) {
stats.successes++;
stats.bedrockSuccesses++;
return bedrockResponse;
}

// Any Bedrock error triggers immediate failover to Anthropic
console.log(`⚠️ Bedrock returned ${bedrockResponse.status} - failing over to Anthropic`);
} catch (error) {
console.error('❌ Bedrock request error:', error);
}

// Immediate failover to Anthropic (zero delay)
console.log('🔄 Failing over to Anthropic...');
stats.anthropicFailovers++;

try {
const anthropicResponse = await forwardToAnthropic(body, req.headers);

if (anthropicResponse.ok) {
stats.successes++;
} else {
stats.failures++;
stats.lastError = `Anthropic returned ${anthropicResponse.status}`;
}

return anthropicResponse;
} catch (error) {
console.error('❌ Anthropic request error:', error);
stats.failures++;
stats.lastError = String(error);

return new Response(
JSON.stringify({
error: 'Both Bedrock and Anthropic failed',
message: String(error)
}),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
}
});

console.log(`✅ Proxy server listening on http://127.0.0.1:${server.port}`);
console.log(` Forwarding to Cloudflare AI Gateway (${AI_GATEWAY_BASE})`);
console.log(' 🔒 Bedrock-first with Anthropic failover');
console.log(` 📊 Health endpoint: http://127.0.0.1:${server.port}/health`);

return server.port;
}

export function getProxyUrl(): string {
return `http://127.0.0.1:${PROXY_PORT}`;
}

export function shouldUseProxy(): boolean {
// Proxy should be used if both credentials are available
try {
validateCredentials();
return true;
} catch {
return false;
}
}
44 changes: 44 additions & 0 deletions base-action/src/run-claude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,21 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {

// Capture output for parsing execution metrics
let output = "";
let hasRateLimitError = false;

claudeProcess.stdout.on("data", (data) => {
const text = data.toString();

// Check for rate limit errors (429, throttling, etc.)
if (
text.includes("429") ||
text.includes("Too many requests") ||
text.includes("rate limit") ||
text.includes("throttl")
) {
hasRateLimitError = true;
}

// Try to parse as JSON and pretty print if it's on a single line
const lines = text.split("\n");
lines.forEach((line: string, index: number) => {
Expand Down Expand Up @@ -235,6 +247,13 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
core.setOutput("conclusion", "success");
core.setOutput("execution_file", EXECUTION_FILE);
} else {
// If this was a rate limit error, throw to allow retry
if (hasRateLimitError) {
throw new Error(
`Claude CLI failed with rate limit error (exit code ${exitCode})`,
);
}

core.setOutput("conclusion", "failure");

// Still try to save execution file if we have output
Expand All @@ -255,3 +274,28 @@ export async function runClaude(promptPath: string, options: ClaudeOptions) {
process.exit(exitCode);
}
}

/**
* Run Claude via claude-lb proxy
*
* The claude-lb worker handles all retry logic:
* 1. Always tries AWS Bedrock first (via AI Gateway)
* 2. On 429 rate limit: immediate Anthropic failover (HTTP-level, milliseconds)
* 3. All tracking via Cloudflare AI Gateway
*
* No retry logic needed here - single execution with failover handled by proxy.
*/
export async function runClaudeWithRetry(
promptPath: string,
options: ClaudeOptions,
retryOptions?: {
maxAttempts?: number;
},
) {
console.log('\n🤖 Executing Claude via claude-lb proxy (Bedrock-first with instant failover)');

// Single execution - proxy handles Bedrock->Anthropic failover transparently
await runClaude(promptPath, options);

console.log('✅ Claude execution succeeded');
}
Loading