From eb1aa3696e9b5af1cbf74ce2dc9900b0659587c4 Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 26 Sep 2025 23:19:04 +0200 Subject: [PATCH] Feature/cca v1 refresh (#9) Co-authored-by: Oleg Zaimkin --- .gitignore | 1 + README.md | 75 +++++++++++++++----------------------- action.yml | 70 ++++++++++++++++++++++++++++++++--- src/claude/oauth-setup.ts | 63 -------------------------------- src/entrypoints/prepare.ts | 34 ++++++----------- 5 files changed, 105 insertions(+), 138 deletions(-) delete mode 100644 src/claude/oauth-setup.ts diff --git a/.gitignore b/.gitignore index eac47d7..848e94c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store node_modules +dist **/.claude/settings.local.json diff --git a/README.md b/README.md index b3aad36..511d322 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ A Gitea action that provides a general-purpose [Claude Code](https://claude.ai/c **Requirements**: You must be a repository admin to complete these steps. -1. Add `ANTHROPIC_API_KEY` or `CLAUDE_CREDENTIALS` to your repository secrets +1. Add `ANTHROPIC_API_KEY` to your repository secrets 2. Add `GITEA_TOKEN` to your repository secrets (a personal access token with repository read/write permissions) 3. Copy the workflow file from [`examples/gitea-claude.yml`](./examples/gitea-claude.yml) into your repository's `.gitea/workflows/` @@ -47,7 +47,6 @@ jobs: - uses: markwylde/claude-code-gitea-action@v1.0.5 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} # if you want to use direct API - claude_credentials: ${{ secrets.CLAUDE_CREDENTIALS }} # if you have a Claude Max subscription gitea_token: ${{ secrets.GITEA_TOKEN }} # could be another users token (specific Claude user?) claude_git_name: Claude # optional claude_git_email: claude@anthropic.com # optional @@ -57,8 +56,8 @@ jobs: | Input | Description | Required | Default | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- | -| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex). Set to 'use-oauth' when using claude_credentials | No\* | - | -| `claude_credentials` | Claude OAuth credentials JSON for Claude AI Max subscription authentication | No | - | +| `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 | - | | `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | | `timeout_minutes` | Timeout in minutes for execution | No | `30` | | `gitea_token` | Gitea token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | @@ -78,45 +77,6 @@ jobs: > **Note**: This action is currently in beta. Features and APIs may change as we continue to improve the integration. -## Claude Max Authentication - -This action supports authentication using Claude Max OAuth credentials. This allows users with Claude Max subscriptions to use their existing authentication. - -### Setup - -1. **Get OAuth Credentials**: Use Claude Code to generate OAuth credentials: - - ``` - /auth-setup - ``` - -2. **Add Credentials to Repository**: Add the generated JSON credentials as a repository secret named `CLAUDE_CREDENTIALS`. - -It should look like this: - -```json -{ - "claudeAiOauth": { - "accessToken": "sk-ant-xxx", - "refreshToken": "sk-ant-xxx", - "expiresAt": 1748707000000, - "scopes": ["user:inference", "user:profile"] - } -} -``` - -3. **Configure Workflow**: Set up your workflow to use OAuth authentication: - -```yaml -- uses: markwylde/claude-code-gitea-action@v1.0.5 - with: - anthropic_api_key: "use-oauth" - claude_credentials: ${{ secrets.CLAUDE_CREDENTIALS }} - gitea_token: ${{ secrets.GITEA_TOKEN }} -``` - -When `anthropic_api_key` is set to `'use-oauth'`, the action will use the OAuth credentials provided in `claude_credentials` instead of a direct API key. - ## Gitea Configuration This action has been enhanced to work with Gitea installations. The main differences from GitHub are: @@ -311,10 +271,33 @@ Use a specific Claude model: ## Cloud Providers -You can authenticate with Claude using any of these three methods: +You can authenticate with Claude using any of these methods: -1. Direct Anthropic API (default) -2. Anthropic OAuth credentials (Claude Max subscription) +1. **Direct Anthropic API** (default) - Use your Anthropic API key +2. **Claude Code OAuth Token** - Use OAuth token from Claude Code application + +### Using Claude Code OAuth Token + +If you have access to [Claude Code](https://claude.ai/code), you can use OAuth authentication instead of an API key: + +1. **Generate OAuth Token**: run the following command and follow instructions: + ``` + claude setup-token + ``` + This will generate an OAuth token that you can use for authentication. + +2. **Add Token to Repository**: Add the generated token as a repository secret named `CLAUDE_CODE_OAUTH_TOKEN`. + +3. **Configure Workflow**: Use the OAuth token in your workflow: + +```yaml +- uses: markwylde/claude-code-gitea-action@v1.0.5 + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + gitea_token: ${{ secrets.GITEA_TOKEN }} +``` + +When `claude_code_oauth_token` is provided, it will be used instead of `anthropic_api_key` for authentication. ## Security diff --git a/action.yml b/action.yml index 196a96c..58e341e 100644 --- a/action.yml +++ b/action.yml @@ -40,13 +40,36 @@ inputs: required: false default: "" + # New Claude Code settings + settings: + description: "Path to Claude Code settings JSON file, or settings JSON string" + required: false + default: "" + system_prompt: + description: "Override system prompt" + required: false + default: "" + append_system_prompt: + description: "Append to system prompt" + required: false + default: "" + claude_env: + description: "Custom environment variables to pass to Claude Code execution (YAML multiline format)" + required: false + default: "" + fallback_model: + description: "Enable automatic fallback to specified model when default model is overloaded" + required: false + default: "" + # Auth configuration anthropic_api_key: - description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex). Set to 'use-oauth' when using claude_credentials" + description: "Anthropic API key (required for direct API, not needed for Bedrock/Vertex)" required: false - claude_credentials: - description: "Claude OAuth credentials JSON for Claude AI Max subscription authentication" + claude_code_oauth_token: + description: "Claude Code OAuth token (alternative to anthropic_api_key)" required: false + default: "" gitea_token: description: "Gitea token with repo and pull request permissions (defaults to GITHUB_TOKEN)" required: false @@ -58,6 +81,10 @@ inputs: description: "Use Google Vertex AI with OIDC authentication instead of direct Anthropic API" required: false default: "false" + use_node_cache: + description: "Whether to use Node.js dependency caching (set to true only for Node.js projects with lock files)" + required: false + default: "false" timeout_minutes: description: "Timeout in minutes for execution" @@ -108,12 +135,28 @@ runs: GITHUB_RUN_ID: ${{ github.run_id }} GITEA_API_URL: ${{ env.GITHUB_SERVER_URL }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} - CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }} + + - name: Install Claude + if: steps.prepare.outputs.contains_trigger == 'true' + shell: bash + run: | + # Install Claude Code if no custom executable is provided + if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then + echo "Installing Claude Code..." + curl -fsSL https://claude.ai/install.sh | bash -s 1.0.117 + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + else + echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" + # Add the directory containing the custom executable to PATH + CLAUDE_DIR=$(dirname "${{ inputs.path_to_claude_code_executable }}") + echo "$CLAUDE_DIR" >> "$GITHUB_PATH" + fi + # TODO pass claude_code_executable as input and use it here - name: Run Claude Code id: claude-code if: steps.prepare.outputs.contains_trigger == 'true' - uses: anthropics/claude-code-base-action@v0.0.24 + uses: anthropics/claude-code-base-action@v0.0.63 with: prompt_file: /tmp/claude-prompts/claude-prompt.txt allowed_tools: ${{ env.ALLOWED_TOOLS }} @@ -124,6 +167,13 @@ runs: use_bedrock: ${{ inputs.use_bedrock }} use_vertex: ${{ inputs.use_vertex }} anthropic_api_key: ${{ inputs.anthropic_api_key }} + claude_code_oauth_token: ${{ inputs.claude_code_oauth_token }} + settings: ${{ inputs.settings }} + system_prompt: ${{ inputs.system_prompt }} + append_system_prompt: ${{ inputs.append_system_prompt }} + claude_env: ${{ inputs.claude_env }} + fallback_model: ${{ inputs.fallback_model }} + use_node_cache: ${{ inputs.use_node_cache }} env: # Core configuration PROMPT_FILE: /tmp/claude-prompts/claude-prompt.txt @@ -136,7 +186,15 @@ runs: USE_BEDROCK: ${{ inputs.use_bedrock }} USE_VERTEX: ${{ inputs.use_vertex }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} - CLAUDE_CREDENTIALS: ${{ inputs.claude_credentials }} + CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }} + + # New settings support + SETTINGS: ${{ inputs.settings }} + SYSTEM_PROMPT: ${{ inputs.system_prompt }} + APPEND_SYSTEM_PROMPT: ${{ inputs.append_system_prompt }} + CLAUDE_ENV: ${{ inputs.claude_env }} + FALLBACK_MODEL: ${{ inputs.fallback_model }} + USE_NODE_CACHE: ${{ inputs.use_node_cache }} # GitHub token for repository access GITHUB_TOKEN: ${{ steps.prepare.outputs.GITHUB_TOKEN }} diff --git a/src/claude/oauth-setup.ts b/src/claude/oauth-setup.ts deleted file mode 100644 index e44f274..0000000 --- a/src/claude/oauth-setup.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { mkdir, writeFile } from "fs/promises"; -import { join } from "path"; -import { homedir } from "os"; - -interface OAuthCredentials { - accessToken: string; - refreshToken: string; - expiresAt: string; -} - -interface ClaudeCredentialsInput { - claudeAiOauth: { - accessToken: string; - refreshToken: string; - expiresAt: number; - scopes: string[]; - }; -} - -export async function setupOAuthCredentials(credentialsJson: string) { - try { - // Parse the credentials JSON - const parsedCredentials: ClaudeCredentialsInput = - JSON.parse(credentialsJson); - - if (!parsedCredentials.claudeAiOauth) { - throw new Error("Invalid credentials format: missing claudeAiOauth"); - } - - const { accessToken, refreshToken, expiresAt } = - parsedCredentials.claudeAiOauth; - - if (!accessToken || !refreshToken || !expiresAt) { - throw new Error( - "Invalid credentials format: missing required OAuth fields", - ); - } - - const claudeDir = join(homedir(), ".claude"); - const credentialsPath = join(claudeDir, ".credentials.json"); - - // Create the .claude directory if it doesn't exist - await mkdir(claudeDir, { recursive: true }); - - // Create the credentials JSON structure - const credentialsData = { - claudeAiOauth: { - accessToken, - refreshToken, - expiresAt, - scopes: ["user:inference", "user:profile"], - }, - }; - - // Write the credentials file - await writeFile(credentialsPath, JSON.stringify(credentialsData, null, 2)); - - console.log(`OAuth credentials written to ${credentialsPath}`); - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - throw new Error(`Failed to setup OAuth credentials: ${errorMessage}`); - } -} diff --git a/src/entrypoints/prepare.ts b/src/entrypoints/prepare.ts index 774e335..2f3ec12 100644 --- a/src/entrypoints/prepare.ts +++ b/src/entrypoints/prepare.ts @@ -18,29 +18,17 @@ import { createPrompt } from "../create-prompt"; import { createClient } from "../github/api/client"; import { fetchGitHubData } from "../github/data/fetcher"; import { parseGitHubContext } from "../github/context"; -import { setupOAuthCredentials } from "../claude/oauth-setup"; async function run() { try { - // Step 1: Setup OAuth credentials if provided - const claudeCredentials = process.env.CLAUDE_CREDENTIALS; - const anthropicApiKey = process.env.ANTHROPIC_API_KEY; - - if (claudeCredentials && anthropicApiKey === "use-oauth") { - await setupOAuthCredentials(claudeCredentials); - console.log( - "OAuth credentials configured for Claude AI Max subscription", - ); - } - - // Step 2: Setup GitHub token + // Step 1: Setup GitHub token const githubToken = await setupGitHubToken(); const client = createClient(githubToken); - // Step 3: Parse GitHub context (once for all operations) + // Step 2: Parse GitHub context (once for all operations) const context = parseGitHubContext(); - // Step 4: Check write permissions + // Step 3: Check write permissions const hasWritePermissions = await checkWritePermissions( client.api, context, @@ -51,7 +39,7 @@ async function run() { ); } - // Step 5: Check trigger conditions + // Step 4: Check trigger conditions const containsTrigger = await checkTriggerAction(context); // Set outputs that are always needed @@ -63,14 +51,14 @@ async function run() { return; } - // Step 6: Check if actor is human + // Step 5: Check if actor is human await checkHumanActor(client.api, context); - // Step 7: Create initial tracking comment + // Step 6: Create initial tracking comment const commentId = await createInitialComment(client.api, context); core.setOutput("claude_comment_id", commentId.toString()); - // Step 8: Fetch GitHub data (once for both branch setup and prompt creation) + // Step 7: Fetch GitHub data (once for both branch setup and prompt creation) const githubData = await fetchGitHubData({ client: client, repository: `${context.repository.owner}/${context.repository.repo}`, @@ -78,14 +66,14 @@ async function run() { isPR: context.isPR, }); - // Step 9: Setup branch + // Step 8: Setup branch const branchInfo = await setupBranch(client, githubData, context); core.setOutput("BASE_BRANCH", branchInfo.baseBranch); if (branchInfo.claudeBranch) { core.setOutput("CLAUDE_BRANCH", branchInfo.claudeBranch); } - // Step 10: Update initial comment with branch link (only if a claude branch was created) + // Step 9: Update initial comment with branch link (only if a claude branch was created) if (branchInfo.claudeBranch) { await updateTrackingComment( client, @@ -95,7 +83,7 @@ async function run() { ); } - // Step 11: Create prompt file + // Step 10: Create prompt file await createPrompt( commentId, branchInfo.baseBranch, @@ -104,7 +92,7 @@ async function run() { context, ); - // Step 12: Get MCP configuration + // Step 11: Get MCP configuration const mcpConfig = await prepareMcpConfig( githubToken, context.repository.owner,