diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc2b70e76..c24dfdf96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@v2 with: @@ -24,7 +24,7 @@ jobs: prettier: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@v1 with: @@ -39,7 +39,7 @@ jobs: typecheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: oven-sh/setup-bun@v2 with: diff --git a/.github/workflows/claude-review.yml b/.github/workflows/claude-review.yml index df164f5ae..fc70561a7 100644 --- a/.github/workflows/claude-review.yml +++ b/.github/workflows/claude-review.yml @@ -13,7 +13,7 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index b4e80f0a2..32e3b4df4 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -25,7 +25,7 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index 94817d550..599df15f5 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7b534501b..3d611fac2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,7 @@ jobs: next_version: ${{ steps.next_version.outputs.next_version }} steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -91,7 +91,7 @@ jobs: contents: write steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -116,7 +116,7 @@ jobs: environment: production steps: - name: Checkout base-action repo - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: repository: anthropics/claude-code-base-action token: ${{ secrets.CLAUDE_CODE_BASE_ACTION_PAT }} diff --git a/CLAUDE.md b/CLAUDE.md index 7834fc2d6..f9c96d622 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/action.yml b/action.yml index 4dce179c2..cd1527e1e 100644 --- a/action.yml +++ b/action.yml @@ -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 diff --git a/base-action/README.md b/base-action/README.md index 504763181..0889fa160 100644 --- a/base-action/README.md +++ b/base-action/README.md @@ -85,29 +85,32 @@ Add the following to your workflow file: ## Inputs -| Input | Description | Required | Default | -| ------------------------- | ------------------------------------------------------------------------------------------------- | -------- | ---------------------------- | -| `prompt` | The prompt to send to Claude Code | No\* | '' | -| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' | -| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' | -| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' | -| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' | -| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' | -| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' | -| `system_prompt` | Override system prompt | No | '' | -| `append_system_prompt` | Append to system prompt | No | '' | -| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' | -| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' | -| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' | -| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' | -| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' | -| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' | -| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' | +| Input | Description | Required | Default | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------------- | +| `prompt` | The prompt to send to Claude Code | No\* | '' | +| `prompt_file` | Path to a file containing the prompt to send to Claude Code | No\* | '' | +| `allowed_tools` | Comma-separated list of allowed tools for Claude Code to use | No | '' | +| `disallowed_tools` | Comma-separated list of disallowed tools that Claude Code cannot use | No | '' | +| `max_turns` | Maximum number of conversation turns (default: no limit) | No | '' | +| `mcp_config` | Path to the MCP configuration JSON file, or MCP configuration JSON string | No | '' | +| `settings` | Path to Claude Code settings JSON file, or settings JSON string | No | '' | +| `system_prompt` | Override system prompt | No | '' | +| `append_system_prompt` | Append to system prompt | No | '' | +| `claude_env` | Custom environment variables to pass to Claude Code execution (YAML multiline format) | No | '' | +| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | 'claude-4-0-sonnet-20250219' | +| `anthropic_model` | DEPRECATED: Use 'model' instead | No | 'claude-4-0-sonnet-20250219' | +| `fallback_model` | Enable automatic fallback to specified model when default model is overloaded | No | '' | +| `anthropic_api_key` | Anthropic API key (required for direct Anthropic API) | No | '' | +| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | '' | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | 'false' | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | 'false' | +| `use_node_cache` | Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files) | No | 'false' | +| `show_full_output` | Show full JSON output (⚠️ May expose secrets - see [security docs](../docs/security.md#️-full-output-security-warning)) | No | 'false'\*\* | \*Either `prompt` or `prompt_file` must be provided, but not both. +\*\*`show_full_output` is automatically enabled when GitHub Actions debug mode is active. See [security documentation](../docs/security.md#️-full-output-security-warning) for important security considerations. + ## Outputs | Output | Description | @@ -336,7 +339,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/base-action/examples/issue-triage.yml b/base-action/examples/issue-triage.yml index 9ea0737c6..15a532433 100644 --- a/base-action/examples/issue-triage.yml +++ b/base-action/examples/issue-triage.yml @@ -32,7 +32,7 @@ jobs: "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", - "ghcr.io/github/github-mcp-server:sha-7aced2b" + "ghcr.io/github/github-mcp-server:sha-23fa0dd" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}" diff --git a/base-action/src/index.ts b/base-action/src/index.ts index bd61825a0..e8f4bfd51 100644 --- a/base-action/src/index.ts +++ b/base-action/src/index.ts @@ -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 @@ -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, diff --git a/base-action/src/proxy-server.ts b/base-action/src/proxy-server.ts new file mode 100644 index 000000000..f2c73c6de --- /dev/null +++ b/base-action/src/proxy-server.ts @@ -0,0 +1,247 @@ +/** + * 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 chosen to avoid common conflicts +const AI_GATEWAY_ACCOUNT = process.env.AI_GATEWAY_ACCOUNT || 'b6641681fe423910342b9ffa1364c76d'; +const AI_GATEWAY_ID = process.env.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 { + 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 { + 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; +} + +let proxyServer: any = null; + +export async function startProxyServer(): Promise { + // 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 + + async fetch(req: Request): Promise { + 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(); + + // Track errors from both providers for debugging + let bedrockError: string | undefined; + + // 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 + const bedrockErrorBody = await bedrockResponse.text(); + bedrockError = `Bedrock returned ${bedrockResponse.status}: ${bedrockErrorBody}`; + console.log(`⚠️ Bedrock returned ${bedrockResponse.status} - failing over to Anthropic`); + } catch (error) { + bedrockError = `Bedrock request error: ${String(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', + bedrockError, + anthropicError: String(error), + stats + }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } + } + }); + + proxyServer = server; + + 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`); + + // Register graceful shutdown handlers + const shutdownHandler = () => { + console.log('\n🛑 Shutting down proxy server...'); + stopProxyServer(); + process.exit(0); + }; + + process.on('SIGTERM', shutdownHandler); + process.on('SIGINT', shutdownHandler); + + return server.port; +} + +export function stopProxyServer(): void { + if (proxyServer) { + proxyServer.stop(); + proxyServer = null; + console.log('✅ Proxy server stopped'); + } +} + +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; + } +} diff --git a/base-action/src/run-claude.ts b/base-action/src/run-claude.ts index 58c58c01c..9b5c4ceeb 100644 --- a/base-action/src/run-claude.ts +++ b/base-action/src/run-claude.ts @@ -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) => { @@ -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 @@ -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'); +} diff --git a/base-action/src/validate-env.ts b/base-action/src/validate-env.ts index 6e48a6843..08448c8ba 100644 --- a/base-action/src/validate-env.ts +++ b/base-action/src/validate-env.ts @@ -23,17 +23,22 @@ export function validateEnvironmentVariables() { ); } } else if (useBedrock) { - const requiredBedrockVars = { - AWS_REGION: process.env.AWS_REGION, - AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, - AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, - }; + // AWS_REGION is always required + if (!process.env.AWS_REGION) { + errors.push("AWS_REGION is required when using AWS Bedrock."); + } - Object.entries(requiredBedrockVars).forEach(([key, value]) => { - if (!value) { - errors.push(`${key} is required when using AWS Bedrock.`); - } - }); + // Support bearer token authentication (simpler) or full AWS credentials + const hasBearerToken = !!process.env.AWS_BEARER_TOKEN_BEDROCK; + const hasAwsCredentials = + !!process.env.AWS_ACCESS_KEY_ID && + !!process.env.AWS_SECRET_ACCESS_KEY; + + if (!hasBearerToken && !hasAwsCredentials) { + errors.push( + "AWS Bedrock requires either AWS_BEARER_TOKEN_BEDROCK (for Bedrock API keys) or both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY (for IAM credentials).", + ); + } } else if (useVertex) { const requiredVertexVars = { ANTHROPIC_VERTEX_PROJECT_ID: process.env.ANTHROPIC_VERTEX_PROJECT_ID, diff --git a/docs/configuration.md b/docs/configuration.md index ecc75bc16..46c2687c5 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -130,7 +130,7 @@ To allow Claude to view workflow run results, job logs, and CI status: 2. **Configure the action with additional permissions**: ```yaml - - uses: anthropics/claude-code-action@beta + - uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} additional_permissions: | @@ -162,7 +162,7 @@ jobs: claude-ci-helper: runs-on: ubuntu-latest steps: - - uses: anthropics/claude-code-action@beta + - uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} additional_permissions: | diff --git a/docs/create-app.html b/docs/create-app.html new file mode 100644 index 000000000..05f74c876 --- /dev/null +++ b/docs/create-app.html @@ -0,0 +1,744 @@ + + + + + + Create Claude Code GitHub App + + + +
+
+

Create Your Custom GitHub App

+

+ Set up a custom GitHub App for Claude Code Action with all required + permissions automatically configured. +

+
+ + +
+
+ 🚀 +

Quick Setup

+
+

+ Create your GitHub App with one click. All permissions will be + automatically configured for Claude Code Action. +

+ +
+ +
+ +
+ + + +
+ +
+
+ + +
+ +
+
+ +
+ +
+
+
+ + +
+
+ +

Configured Permissions

+
+

+ Your GitHub App will be created with these permissions: +

+ +
+
+ + Contents + Read & Write +
+
+ + Issues + Read & Write +
+
+ + Pull Requests + Read & Write +
+
+ + Actions + Read +
+
+ + Metadata + Read +
+
+
+ + +
+
+ 📋 +

Next Steps

+
+

+ After creating your app, complete these steps: +

+ +
+
+
1
+
+

+ Generate a private key: In your app settings, + scroll to "Private keys" and click "Generate a private key" +

+
+
+
+
2
+
+

+ Install the app: Click "Install App" and select + the repositories where you want to use Claude +

+
+
+
+
3
+
+

+ Configure your workflow: Add your app's ID and + private key to your repository secrets +

+
+
+
+
+ + +
+
+ ⚙️ +

Manual Setup

+
+

+ If the buttons above don't work, you can manually create the app by + copying the manifest JSON below: +

+ +
+
+ github-app-manifest.json + +
+
+
+ +
+
+
1
+
+

Copy the manifest JSON above

+
+
+
+
2
+
+

+ Go to + GitHub App Settings +

+
+
+
+
3
+
+

Look for "Create from manifest" option and paste the JSON

+
+
+
+
+ + +
+ ⚠️ +
+ Important: Keep your private key secure! Never commit + it to your repository. Always use GitHub secrets to store sensitive + credentials. +
+
+
+ + + + diff --git a/docs/faq.md b/docs/faq.md index 183635435..26af2d637 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -127,7 +127,7 @@ For performance, Claude uses shallow clones: If you need full history, you can configure this in your workflow before calling Claude in the `actions/checkout` step. ``` -- uses: actions/checkout@v4 +- uses: actions/checkout@v5 depth: 0 # will fetch full repo history ``` diff --git a/docs/security.md b/docs/security.md index fe7188909..dcc95b7ba 100644 --- a/docs/security.md +++ b/docs/security.md @@ -56,3 +56,31 @@ claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} anthropic_api_key: "sk-ant-api03-..." # Exposed and vulnerable! claude_code_oauth_token: "oauth_token_..." # Exposed and vulnerable! ``` + +## ⚠️ Full Output Security Warning + +The `show_full_output` option is **disabled by default** for security reasons. When enabled, it outputs ALL Claude Code messages including: + +- Full outputs from tool executions (e.g., `ps`, `env`, file reads) +- API responses that may contain tokens or credentials +- File contents that may include secrets +- Command outputs that may expose sensitive system information + +**These logs are publicly visible in GitHub Actions for public repositories!** + +### Automatic Enabling in Debug Mode + +Full output is **automatically enabled** when GitHub Actions debug mode is active (when `ACTIONS_STEP_DEBUG` secret is set to `true`). This helps with debugging but carries the same security risks. + +### When to Enable Full Output + +Only enable `show_full_output: true` or GitHub Actions debug mode when: + +- Working in a private repository with controlled access +- Debugging issues in a non-production environment +- You have verified no secrets will be exposed in the output +- You understand the security implications + +### Recommended Practice + +For debugging, prefer using `show_full_output: false` (the default) and rely on Claude Code's sanitized output, which shows only essential information like errors and completion status without exposing sensitive data. diff --git a/docs/setup.md b/docs/setup.md index aed109084..e0c7f56c8 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -20,7 +20,48 @@ If you prefer not to install the official Claude app, you can create your own Gi - Organization policies prevent installing third-party apps - You're using AWS Bedrock or Google Vertex AI -**Steps to create and use a custom GitHub App:** +### Option 1: Quick Setup with App Manifest (Recommended) + +The fastest way to create a custom GitHub App is using our pre-configured manifest. This ensures all permissions are correctly set up with a single click. + +**Steps:** + +1. **Create the app:** + + **🚀 [Download the Quick Setup Tool](./create-app.html)** (Right-click → "Save Link As" or "Download Linked File") + + After downloading, open `create-app.html` in your web browser: + + - **For Personal Accounts:** Click the "Create App for Personal Account" button + - **For Organizations:** Enter your organization name and click "Create App for Organization" + + The tool will automatically configure all required permissions and submit the manifest. + + Alternatively, you can use the manifest file directly: + + - Use the [`github-app-manifest.json`](../github-app-manifest.json) file from this repository + - Visit https://github.com/settings/apps/new (for personal) or your organization's app settings + - Look for the "Create from manifest" option and paste the JSON content + +2. **Complete the creation flow:** + + - GitHub will show you a preview of the app configuration + - Confirm the app name (you can customize it) + - Click "Create GitHub App" + - The app will be created with all required permissions automatically configured + +3. **Generate and download a private key:** + + - After creating the app, you'll be redirected to the app settings + - Scroll down to "Private keys" + - Click "Generate a private key" + - Download the `.pem` file (keep this secure!) + +4. **Continue with installation** - Skip to step 3 in the manual setup below to install the app and configure your workflow. + +### Option 2: Manual Setup + +If you prefer to configure the app manually or need custom permissions: 1. **Create a new GitHub App:** @@ -76,7 +117,7 @@ If you prefer not to install the official Claude app, you can create your own Gi private-key: ${{ secrets.APP_PRIVATE_KEY }} # Use Claude with your custom app's token - - uses: anthropics/claude-code-action@beta + - uses: anthropics/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} github_token: ${{ steps.app-token.outputs.token }} diff --git a/docs/solutions.md b/docs/solutions.md index 7b3982f4c..231506460 100644 --- a/docs/solutions.md +++ b/docs/solutions.md @@ -35,7 +35,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 1 @@ -89,7 +89,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 1 @@ -153,7 +153,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 1 @@ -211,7 +211,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 1 @@ -268,7 +268,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 1 @@ -344,7 +344,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 0 @@ -456,7 +456,7 @@ jobs: pull-requests: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: ref: ${{ github.event.pull_request.head.ref }} fetch-depth: 0 @@ -513,7 +513,7 @@ jobs: security-events: write id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/docs/usage.md b/docs/usage.md index a8eb7a7b9..818b0c8ab 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -32,6 +32,11 @@ jobs: # --max-turns 10 # --model claude-4-0-sonnet-20250805 + # Optional: add custom plugin marketplaces + # plugin_marketplaces: "https://github.com/user/marketplace1.git\nhttps://github.com/user/marketplace2.git" + # Optional: install Claude Code plugins + # plugins: "code-review@claude-code-plugins\nfeature-dev@claude-code-plugins" + # Optional: add custom trigger phrase (default: @claude) # trigger_phrase: "/claude" # Optional: add assignee trigger for issues @@ -47,32 +52,34 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------- | -| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | -| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | -| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | -| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | -| `claude_args` | Additional arguments to pass directly to Claude CLI (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | -| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | -| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | -| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | -| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | -| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | -| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | -| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | -| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | -| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | -| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | -| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | -| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | -| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | -| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` | -| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` | -| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" | -| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" | -| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" | -| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" | +| Input | Description | Required | Default | +| -------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | +| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | +| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No\* | - | +| `prompt` | Instructions for Claude. Can be a direct prompt or custom template for automation workflows | No | - | +| `track_progress` | Force tag mode with tracking comments. Only works with specific PR/issue events. Preserves GitHub context | No | `false` | +| `claude_args` | Additional [arguments to pass directly to Claude CLI](https://docs.claude.com/en/docs/claude-code/cli-reference#cli-flags) (e.g., `--max-turns 10 --model claude-4-0-sonnet-20250805`) | No | "" | +| `base_branch` | The base branch to use for creating new branches (e.g., 'main', 'develop') | No | - | +| `use_sticky_comment` | Use just one comment to deliver PR comments (only applies for pull_request event workflows) | No | `false` | +| `github_token` | GitHub token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | +| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | +| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | +| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue assignment | No | - | +| `label_trigger` | The label name that triggers the action when applied to an issue (e.g. "claude") | No | - | +| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | +| `branch_prefix` | The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format) | No | `claude/` | +| `settings` | Claude Code settings as JSON string or path to settings JSON file | No | "" | +| `additional_permissions` | Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results | No | "" | +| `experimental_allowed_domains` | Restrict network access to these domains only (newline-separated). | No | "" | +| `use_commit_signing` | Enable commit signing using GitHub's commit signature verification. When false, Claude uses standard git commands | No | `false` | +| `bot_id` | GitHub user ID to use for git operations (defaults to Claude's bot ID) | No | `41898282` | +| `bot_name` | GitHub username to use for git operations (defaults to Claude's bot name) | No | `claude[bot]` | +| `allowed_bots` | Comma-separated list of allowed bot usernames, or '\*' to allow all bots. Empty string (default) allows no bots | No | "" | +| `allowed_non_write_users` | **⚠️ RISKY**: Comma-separated list of usernames to allow without write permissions, or '\*' for all users. Only works with `github_token` input. See [Security](./security.md) | No | "" | +| `path_to_claude_code_executable` | Optional path to a custom Claude Code executable. Skips automatic installation. Useful for Nix, custom containers, or specialized environments | No | "" | +| `path_to_bun_executable` | Optional path to a custom Bun executable. Skips automatic Bun installation. Useful for Nix, custom containers, or specialized environments | No | "" | +| `plugin_marketplaces` | Newline-separated list of Claude Code plugin marketplace Git URLs to install from (e.g., see example in workflow above). Marketplaces are added before plugin installation | No | "" | +| `plugins` | Newline-separated list of Claude Code plugin names to install (e.g., see example in workflow above). Plugins are installed before Claude Code execution | No | "" | ### Deprecated Inputs diff --git a/examples/ci-failure-auto-fix.yml b/examples/ci-failure-auto-fix.yml index b20f6cd2b..9d4421db9 100644 --- a/examples/ci-failure-auto-fix.yml +++ b/examples/ci-failure-auto-fix.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: ref: ${{ github.event.workflow_run.head_branch }} fetch-depth: 0 diff --git a/examples/claude.yml b/examples/claude.yml index 556b5e6d0..aedb2e257 100644 --- a/examples/claude.yml +++ b/examples/claude.yml @@ -26,7 +26,7 @@ jobs: actions: read # Required for Claude to read CI results on PRs steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/examples/issue-deduplication.yml b/examples/issue-deduplication.yml index b7d187e77..59cb90d3c 100644 --- a/examples/issue-deduplication.yml +++ b/examples/issue-deduplication.yml @@ -15,7 +15,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/examples/issue-triage.yml b/examples/issue-triage.yml index a1f4b6401..91ef2a357 100644 --- a/examples/issue-triage.yml +++ b/examples/issue-triage.yml @@ -14,7 +14,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 diff --git a/examples/manual-code-analysis.yml b/examples/manual-code-analysis.yml index ca3fac9a6..0e4c71dd0 100644 --- a/examples/manual-code-analysis.yml +++ b/examples/manual-code-analysis.yml @@ -23,7 +23,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 2 # Need at least 2 commits to analyze the latest diff --git a/examples/pr-review-comprehensive.yml b/examples/pr-review-comprehensive.yml index 90563c464..3002b4dcc 100644 --- a/examples/pr-review-comprehensive.yml +++ b/examples/pr-review-comprehensive.yml @@ -16,7 +16,7 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/examples/pr-review-filtered-authors.yml b/examples/pr-review-filtered-authors.yml index d46c1b68d..0032720a8 100644 --- a/examples/pr-review-filtered-authors.yml +++ b/examples/pr-review-filtered-authors.yml @@ -18,7 +18,7 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/examples/pr-review-filtered-paths.yml b/examples/pr-review-filtered-paths.yml index a8226a8bf..f465a4bb4 100644 --- a/examples/pr-review-filtered-paths.yml +++ b/examples/pr-review-filtered-paths.yml @@ -19,7 +19,7 @@ jobs: id-token: write steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 1 diff --git a/github-app-manifest.json b/github-app-manifest.json new file mode 100644 index 000000000..e25c95238 --- /dev/null +++ b/github-app-manifest.json @@ -0,0 +1,27 @@ +{ + "name": "Claude Code Custom App", + "description": "Custom GitHub App for Claude Code Action - AI-powered coding assistant for GitHub workflows", + "url": "https://github.com/anthropics/claude-code-action", + "hook_attributes": { + "url": "https://example.com/github/webhook", + "active": false + }, + "redirect_url": "https://github.com/settings/apps/new", + "callback_urls": [], + "setup_url": "https://github.com/anthropics/claude-code-action/blob/main/docs/setup.md", + "public": false, + "default_permissions": { + "contents": "write", + "issues": "write", + "pull_requests": "write", + "actions": "read", + "metadata": "read" + }, + "default_events": [ + "issue_comment", + "issues", + "pull_request", + "pull_request_review", + "pull_request_review_comment" + ] +} diff --git a/src/create-prompt/index.ts b/src/create-prompt/index.ts index 354732641..f34deda79 100644 --- a/src/create-prompt/index.ts +++ b/src/create-prompt/index.ts @@ -684,7 +684,7 @@ ${ - Display the todo list as a checklist in the GitHub comment and mark things off as you go. - REPOSITORY SETUP INSTRUCTIONS: The repository's CLAUDE.md file(s) contain critical repo-specific setup instructions, development guidelines, and preferences. Always read and follow these files, particularly the root CLAUDE.md, as they provide essential context for working with the codebase effectively. - Use h3 headers (###) for section titles in your comments, not h1 headers (#). -- Your comment must always include the job run link (and branch link if there is one) at the bottom. +- Your comment must always include the job run link in the format "[View job run](${GITHUB_SERVER_URL}/${context.repository}/actions/runs/${process.env.GITHUB_RUN_ID})" at the bottom of your response (branch link if there is one should also be included there). CAPABILITIES AND LIMITATIONS: When users ask you to do something, be aware of what you can and cannot do. This section helps you understand how to respond when users request actions outside your scope. diff --git a/src/entrypoints/update-comment-link.ts b/src/entrypoints/update-comment-link.ts index 3a14e66bd..b8d28f206 100644 --- a/src/entrypoints/update-comment-link.ts +++ b/src/entrypoints/update-comment-link.ts @@ -15,6 +15,52 @@ import { GITHUB_SERVER_URL } from "../github/api/config"; import { checkAndCommitOrDeleteBranch } from "../github/operations/branch-cleanup"; import { updateClaudeComment } from "../github/operations/comments/update-claude-comment"; +/** + * Check if the bot submitted a review on the PR within a time window + */ +async function didBotSubmitRecentReview( + octokit: ReturnType, + params: { + owner: string; + repo: string; + pull_number: number; + botUsername: string; + windowMinutes?: number; + }, +): Promise<{ submitted: boolean; reviewId?: number; submittedAt?: string }> { + const { owner, repo, pull_number, botUsername, windowMinutes = 5 } = params; + + try { + const reviews = await octokit.rest.pulls.listReviews({ + owner, + repo, + pull_number, + per_page: 10, // Check last 10 reviews + }); + + const cutoffTime = new Date(Date.now() - windowMinutes * 60 * 1000); + const recentBotReview = reviews.data.find( + (review) => + review.user?.login === botUsername && + review.submitted_at && // Type guard - ensure timestamp exists + new Date(review.submitted_at) > cutoffTime, + ); + + if (recentBotReview) { + return { + submitted: true, + reviewId: recentBotReview.id, + submittedAt: recentBotReview.submitted_at || undefined, + }; + } + + return { submitted: false }; + } catch (error) { + console.log("Could not check for review submission:", error); + return { submitted: false }; + } +} + async function run() { try { const commentId = parseInt(process.env.CLAUDE_COMMENT_ID!); @@ -214,25 +260,91 @@ async function run() { errorDetails, }; - const updatedBody = updateCommentBody(commentInput); + // Check if this is a PR and if a review was submitted + let shouldDeleteComment = false; + if (context.isPR && !actionFailed) { + const botUsername = comment.user?.login; + if (botUsername) { + const reviewCheck = await didBotSubmitRecentReview(octokit, { + owner, + repo, + pull_number: context.entityNumber, + botUsername, + windowMinutes: 5, // Check last 5 minutes + }); - try { - await updateClaudeComment(octokit.rest, { - owner, - repo, - commentId, - body: updatedBody, - isPullRequestReviewComment: isPRReviewComment, - }); - console.log( - `✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`, - ); - } catch (updateError) { - console.error( - `Failed to update ${isPRReviewComment ? "PR review" : "issue"} comment:`, - updateError, - ); - throw updateError; + if (reviewCheck.submitted) { + shouldDeleteComment = true; + console.log( + `✓ Bot submitted review #${reviewCheck.reviewId} at ${reviewCheck.submittedAt} - will delete progress comment instead of updating`, + ); + } + } + } + + if (shouldDeleteComment) { + // Delete the comment since a review was submitted + try { + if (isPRReviewComment) { + await octokit.rest.pulls.deleteReviewComment({ + owner, + repo, + comment_id: commentId, + }); + } else { + await octokit.rest.issues.deleteComment({ + owner, + repo, + comment_id: commentId, + }); + } + console.log( + `✅ Deleted ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} (review was submitted)`, + ); + } catch (deleteError) { + console.error( + `Failed to delete ${isPRReviewComment ? "PR review" : "issue"} comment:`, + deleteError, + ); + // Fall back to updating the comment if deletion fails + try { + const updatedBody = updateCommentBody(commentInput); + await updateClaudeComment(octokit.rest, { + owner, + repo, + commentId, + body: updatedBody, + isPullRequestReviewComment: isPRReviewComment, + }); + console.log( + `✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} (deletion failed, fell back to update)`, + ); + } catch (updateError) { + console.error("Failed to update comment after deletion failed:", updateError); + throw updateError; + } + } + } else { + // Normal update flow - no review was submitted + const updatedBody = updateCommentBody(commentInput); + try { + await updateClaudeComment(octokit.rest, { + owner, + repo, + commentId, + body: updatedBody, + isPullRequestReviewComment: isPRReviewComment, + }); + console.log( + `✅ Updated ${isPRReviewComment ? "PR review" : "issue"} comment ${commentId} with job link`, + ); + } catch (updateError) { + console.error( + `Failed to update ${isPRReviewComment ? "PR review" : "issue"} comment:`, + updateError, + ); + throw updateError; + } } process.exit(0); diff --git a/src/github/data/fetcher.ts b/src/github/data/fetcher.ts index e6cec2c4c..d1709b5b0 100644 --- a/src/github/data/fetcher.ts +++ b/src/github/data/fetcher.ts @@ -163,7 +163,7 @@ export async function fetchGitHubData({ if (prResult.repository.pullRequest) { const pullRequest = prResult.repository.pullRequest; contextData = pullRequest; - changedFiles = pullRequest.files.nodes || []; + changedFiles = pullRequest.files?.nodes || []; comments = filterCommentsToTriggerTime( pullRequest.comments?.nodes || [], triggerTime, @@ -171,6 +171,11 @@ export async function fetchGitHubData({ reviewData = pullRequest.reviews || []; console.log(`Successfully fetched PR #${prNumber} data`); + if (!pullRequest.files || pullRequest.files.nodes === null) { + console.warn( + `Warning: PR #${prNumber} files data is null (likely >100 files or API limit). Changed files list will be empty.`, + ); + } } else { throw new Error(`PR #${prNumber} not found`); } diff --git a/src/mcp/install-mcp-server.ts b/src/mcp/install-mcp-server.ts index 0a2e7b4cf..22de61122 100644 --- a/src/mcp/install-mcp-server.ts +++ b/src/mcp/install-mcp-server.ts @@ -209,7 +209,7 @@ export async function prepareMcpConfig( "GITHUB_PERSONAL_ACCESS_TOKEN", "-e", "GITHUB_HOST", - "ghcr.io/github/github-mcp-server:sha-efef8ae", // https://github.com/github/github-mcp-server/releases/tag/v0.9.0 + "ghcr.io/github/github-mcp-server:sha-23fa0dd", // https://github.com/github/github-mcp-server/releases/tag/v0.17.1 ], env: { GITHUB_PERSONAL_ACCESS_TOKEN: githubToken, diff --git a/src/modes/detector.ts b/src/modes/detector.ts index 8e30aff4f..45ffa5405 100644 --- a/src/modes/detector.ts +++ b/src/modes/detector.ts @@ -67,6 +67,7 @@ export function detectMode(context: GitHubContext): AutoDetectedMode { "synchronize", "ready_for_review", "reopened", + "assigned", ]; if (context.eventAction && supportedActions.includes(context.eventAction)) { // If prompt is provided, use agent mode (default for automation) @@ -114,6 +115,7 @@ function validateTrackProgressEvent(context: GitHubContext): void { "synchronize", "ready_for_review", "reopened", + "assigned", ]; if (!validActions.includes(context.eventAction)) { throw new Error( diff --git a/test/modes/detector.test.ts b/test/modes/detector.test.ts index ed6a3a5da..80349af58 100644 --- a/test/modes/detector.test.ts +++ b/test/modes/detector.test.ts @@ -71,6 +71,34 @@ describe("detectMode with enhanced routing", () => { expect(detectMode(context)).toBe("agent"); }); + it("should use tag mode when track_progress is true for pull_request.assigned", () => { + const context: GitHubContext = { + ...baseContext, + eventName: "pull_request", + eventAction: "assigned", + payload: { pull_request: { number: 1 } } as any, + entityNumber: 1, + isPR: true, + inputs: { ...baseContext.inputs, trackProgress: true }, + }; + + expect(detectMode(context)).toBe("tag"); + }); + + it("should use agent mode when prompt is provided for pull_request.assigned", () => { + const context: GitHubContext = { + ...baseContext, + eventName: "pull_request", + eventAction: "assigned", + payload: { pull_request: { number: 1 } } as any, + entityNumber: 1, + isPR: true, + inputs: { ...baseContext.inputs, prompt: "Fix the issues", trackProgress: false }, + }; + + expect(detectMode(context)).toBe("agent"); + }); + it("should throw error when track_progress is used with unsupported PR action", () => { const context: GitHubContext = { ...baseContext,