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 44b31d0..ea23ab0 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: @@ -125,6 +85,31 @@ This action has been enhanced to work with Gitea installations. The main differe 2. **API URL Configuration**: You must specify your Gitea server URL using the `gitea_api_url` input. +3. **Custom Server URL**: For Gitea instances running in containers, you can override link generation using the `GITEA_SERVER_URL` environment variable. + +### Custom Server URL Configuration + +When running Gitea in containers, the action may generate links using internal container URLs (e.g., `http://gitea:3000`) instead of your public URL. To fix this, set the `GITEA_SERVER_URL` environment variable: + +```yaml +- uses: markwylde/claude-code-gitea-action@v1.0.5 + with: + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + gitea_token: ${{ secrets.GITEA_TOKEN }} + env: + # Override the internal container URL with your public URL + GITEA_SERVER_URL: https://gitea.example.com +``` + +**How it works:** +- The action first checks for `GITEA_SERVER_URL` (user-configurable) +- Falls back to `GITHUB_SERVER_URL` (automatically set by Gitea Actions) +- Uses `https://github.com` as final fallback + +This ensures that all links in Claude's comments (job runs, branches, etc.) point to your public Gitea instance instead of internal container addresses. + +See [`examples/gitea-custom-url.yml`](./examples/gitea-custom-url.yml) for a complete example. + ### Gitea Setup Notes - Use a Gitea personal access token "GITEA_TOKEN" @@ -575,10 +560,33 @@ For a complete list of available settings and their descriptions, see the [Claud ## 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 bcacba5..45892c8 100644 --- a/action.yml +++ b/action.yml @@ -16,19 +16,17 @@ inputs: description: "The label that triggers the action (e.g. claude)" required: false default: "claude" - base_branch: - description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)" - required: false branch_prefix: description: "The prefix to use for Claude branches (defaults to 'claude/', use 'claude-' for dash format)" required: false default: "claude/" - - # Mode configuration mode: - description: "Execution mode for the action. Valid modes: 'tag' (default - triggered by mentions/assignments), 'agent' (for automation with no trigger checking)" + description: "Execution mode for the action. Valid modes: 'tag' (default) or 'agent'" required: false default: "tag" + base_branch: + description: "The branch to use as the base/source when creating new branches (defaults to repository default branch)" + required: false # Claude Code configuration model: @@ -37,9 +35,6 @@ inputs: anthropic_model: description: "DEPRECATED: Use 'model' instead. Model to use (provider-specific format required for Bedrock/Vertex)" required: false - fallback_model: - description: "Enable automatic fallback to specified model when primary model is unavailable" - required: false allowed_tools: description: "Additional tools for Claude to use (the base GitHub tools will always be included)" required: false @@ -60,31 +55,48 @@ inputs: description: "Complete replacement of Claude's prompt with custom template (supports variable substitution)" required: false default: "" - mcp_config: - description: "Additional MCP configuration (JSON string) that merges with the built-in GitHub MCP servers" + + # 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: "" additional_permissions: description: "Additional permissions to enable. Currently supports 'actions: read' for viewing workflow results" required: false default: "" - claude_env: - description: "Custom environment variables to pass to Claude Code execution (YAML format)" - required: false - default: "" - settings: - description: "Claude Code settings as JSON string or path to settings JSON file" + 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 + path_to_claude_code_executable: + description: "Path to a custom Claude Code executable to use instead of installing" + required: false + default: "" use_bedrock: description: "Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API" required: false @@ -93,6 +105,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" max_turns: description: "Maximum number of conversation turns" @@ -130,14 +146,14 @@ runs: - name: Install Dependencies shell: bash run: | - cd ${GITHUB_ACTION_PATH} + cd ${{ github.action_path }} bun install - name: Prepare action id: prepare shell: bash run: | - bun run ${GITHUB_ACTION_PATH}/src/entrypoints/prepare.ts + bun run ${{ github.action_path }}/src/entrypoints/prepare.ts env: MODE: ${{ inputs.mode }} TRIGGER_PHRASE: ${{ inputs.trigger_phrase }} @@ -149,21 +165,53 @@ runs: DISALLOWED_TOOLS: ${{ inputs.disallowed_tools }} CUSTOM_INSTRUCTIONS: ${{ inputs.custom_instructions }} DIRECT_PROMPT: ${{ inputs.direct_prompt }} + OVERRIDE_PROMPT: ${{ inputs.override_prompt }} + ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }} OVERRIDE_GITHUB_TOKEN: ${{ inputs.gitea_token }} GITHUB_TOKEN: ${{ github.token }} 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' - shell: bash - run: | - - # Run the base-action - bun run ${GITHUB_ACTION_PATH}/base-action/src/index.ts + uses: anthropics/claude-code-base-action@v0.0.63 + with: + prompt_file: /tmp/claude-prompts/claude-prompt.txt + allowed_tools: ${{ env.ALLOWED_TOOLS }} + disallowed_tools: ${{ env.DISALLOWED_TOOLS }} + max_turns: ${{ inputs.max_turns }} + 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 }} + 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 @@ -176,7 +224,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 }} @@ -193,8 +249,6 @@ runs: AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }} AWS_SESSION_TOKEN: ${{ env.AWS_SESSION_TOKEN }} ANTHROPIC_BEDROCK_BASE_URL: ${{ env.ANTHROPIC_BEDROCK_BASE_URL }} - - # GCP configuration ANTHROPIC_VERTEX_PROJECT_ID: ${{ env.ANTHROPIC_VERTEX_PROJECT_ID }} CLOUD_ML_REGION: ${{ env.CLOUD_ML_REGION }} GOOGLE_APPLICATION_CREDENTIALS: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }} @@ -207,7 +261,7 @@ runs: if: steps.prepare.outputs.contains_trigger == 'true' && steps.prepare.outputs.claude_comment_id && always() shell: bash run: | - bun run ${GITHUB_ACTION_PATH}/src/entrypoints/update-comment-link.ts + bun run ${{ github.action_path }}/src/entrypoints/update-comment-link.ts env: REPOSITORY: ${{ github.repository }} PR_NUMBER: ${{ github.event.issue.number || github.event.pull_request.number }} diff --git a/examples/gitea-custom-url.yml b/examples/gitea-custom-url.yml new file mode 100644 index 0000000..92051e7 --- /dev/null +++ b/examples/gitea-custom-url.yml @@ -0,0 +1,23 @@ +name: Claude PR Review with Custom Gitea URL +on: + pull_request: + types: [opened, synchronize] + issue_comment: + types: [created] + +jobs: + claude-review: + runs-on: ubuntu-latest + steps: + - name: Claude Code Analysis + uses: markwylde/claude-code-gitea-action@gitea + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + claude-api-key: ${{ secrets.CLAUDE_API_KEY }} + env: + # Set this to your public Gitea URL to override the internal container URL + # This ensures that links in comments point to the correct public URL + GITEA_SERVER_URL: https://gitea.example.com + + # Note: GITHUB_SERVER_URL is automatically set by Gitea Actions to the internal URL + # but it will be overridden by GITEA_SERVER_URL if set above diff --git a/package-lock.json b/package-lock.json index 0b78f60..a92835b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,6 @@ "@actions/github": "^6.0.1", "@anthropic-ai/sdk": "^0.30.0", "@modelcontextprotocol/sdk": "^1.11.0", - "@octokit/graphql": "^8.2.2", - "@octokit/rest": "^21.1.1", "@octokit/webhooks-types": "^7.6.1", "node-fetch": "^3.3.2", "zod": "^3.24.4" @@ -211,82 +209,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/graphql": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz", - "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==", - "license": "MIT", - "dependencies": { - "@octokit/request": "^9.2.3", - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/graphql/node_modules/@octokit/endpoint": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "license": "MIT" - }, - "node_modules/@octokit/graphql/node_modules/@octokit/request": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", - "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^10.1.4", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^2.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/graphql/node_modules/@octokit/request-error": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/graphql/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/graphql/node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "license": "ISC" - }, "node_modules/@octokit/openapi-types": { "version": "24.2.0", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", @@ -382,185 +304,6 @@ "node": ">= 18" } }, - "node_modules/@octokit/rest": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-21.1.1.tgz", - "integrity": "sha512-sTQV7va0IUVZcntzy1q3QqPm/r8rWtDCqpRAmb8eXXnKkjoQEtFe3Nt5GTVsHft+R6jJoHeSiVLcgcvhtue/rg==", - "license": "MIT", - "dependencies": { - "@octokit/core": "^6.1.4", - "@octokit/plugin-paginate-rest": "^11.4.2", - "@octokit/plugin-request-log": "^5.3.1", - "@octokit/plugin-rest-endpoint-methods": "^13.3.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/auth-token": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz", - "integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==", - "license": "MIT", - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/core": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz", - "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==", - "license": "MIT", - "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.2.2", - "@octokit/request": "^9.2.3", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "before-after-hook": "^3.0.2", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/core/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/endpoint": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz", - "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/endpoint/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/openapi-types": { - "version": "25.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz", - "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==", - "license": "MIT" - }, - "node_modules/@octokit/rest/node_modules/@octokit/plugin-paginate-rest": { - "version": "11.6.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.6.0.tgz", - "integrity": "sha512-n5KPteiF7pWKgBIBJSk8qzoZWcUkza2O6A0za97pMGVrGfPdltxrfmfF5GucHYvHGZD8BdaZmmHGz5cX/3gdpw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^13.10.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/plugin-request-log": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-5.3.1.tgz", - "integrity": "sha512-n/lNeCtq+9ofhC15xzmJCNKP2BWTv8Ih2TTy+jatNCCq/gQP/V7rK3fjIfuz0pDWDALO/o/4QY4hyOF6TQQFUw==", - "license": "MIT", - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-13.5.0.tgz", - "integrity": "sha512-9Pas60Iv9ejO3WlAX3maE1+38c5nqbJXV5GrncEfkndIpZrJ/WPMRd2xYDcPPEt5yzpxcjw9fWNoPhsSGzqKqw==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^13.10.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request": { - "version": "9.2.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz", - "integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==", - "license": "MIT", - "dependencies": { - "@octokit/endpoint": "^10.1.4", - "@octokit/request-error": "^6.1.8", - "@octokit/types": "^14.0.0", - "fast-content-type-parse": "^2.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request-error": { - "version": "6.1.8", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz", - "integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==", - "license": "MIT", - "dependencies": { - "@octokit/types": "^14.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request-error/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/rest/node_modules/@octokit/request/node_modules/@octokit/types": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz", - "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==", - "license": "MIT", - "dependencies": { - "@octokit/openapi-types": "^25.1.0" - } - }, - "node_modules/@octokit/rest/node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "license": "Apache-2.0" - }, - "node_modules/@octokit/rest/node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", - "license": "ISC" - }, "node_modules/@octokit/types": { "version": "13.10.0", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", @@ -1042,22 +785,6 @@ "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, - "node_modules/fast-content-type-parse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz", - "integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 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, diff --git a/src/github/api/config.ts b/src/github/api/config.ts index 9daddae..5776532 100644 --- a/src/github/api/config.ts +++ b/src/github/api/config.ts @@ -7,8 +7,25 @@ function deriveApiUrl(serverUrl: string): string { return `${serverUrl}/api/v1`; } -export const GITEA_SERVER_URL = - process.env.GITHUB_SERVER_URL || "https://github.com"; +// Get the appropriate server URL, prioritizing GITEA_SERVER_URL for custom Gitea instances +function getServerUrl(): string { + // First check for GITEA_SERVER_URL (can be set by user) + const giteaServerUrl = process.env.GITEA_SERVER_URL; + if (giteaServerUrl && giteaServerUrl !== "") { + return giteaServerUrl; + } + + // Fall back to GITHUB_SERVER_URL (set by Gitea/GitHub Actions environment) + const githubServerUrl = process.env.GITHUB_SERVER_URL; + if (githubServerUrl && githubServerUrl !== "") { + return githubServerUrl; + } + + // Default fallback + return "https://github.com"; +} + +export const GITEA_SERVER_URL = getServerUrl(); export const GITEA_API_URL = process.env.GITEA_API_URL || deriveApiUrl(GITEA_SERVER_URL); diff --git a/src/github/operations/branch.ts b/src/github/operations/branch.ts index e02e884..6726fb6 100644 --- a/src/github/operations/branch.ts +++ b/src/github/operations/branch.ts @@ -52,7 +52,7 @@ export async function setupBranch( ); // Check out the base branch and let Claude create branches as needed - await $`git fetch origin ${sourceBranch}`; + await $`git fetch origin --depth=1 ${sourceBranch}`; await $`git checkout ${sourceBranch}`; await $`git pull origin ${sourceBranch}`; @@ -99,7 +99,7 @@ export async function setupBranch( // Ensure we have the latest version of the source branch console.log(`Fetching latest ${sourceBranch}...`); - await $`git fetch origin ${sourceBranch}`; + await $`git fetch origin --depth=1 ${sourceBranch}`; // Checkout the source branch console.log(`Checking out ${sourceBranch}...`); diff --git a/src/github/utils/local-git.ts b/src/github/utils/local-git.ts index 2b36f2b..188d33c 100644 --- a/src/github/utils/local-git.ts +++ b/src/github/utils/local-git.ts @@ -85,7 +85,7 @@ export async function branchHasChanges( */ export async function fetchBranch(branchName: string): Promise { try { - await $`git fetch origin ${branchName}`; + await $`git fetch origin --depth=1 ${branchName}`; return true; } catch (error) { console.log( diff --git a/test/gitea-server-url.test.ts b/test/gitea-server-url.test.ts new file mode 100644 index 0000000..fd3adb4 --- /dev/null +++ b/test/gitea-server-url.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, beforeEach, afterEach } from "bun:test"; + +describe("GITEA_SERVER_URL configuration", () => { + const originalEnv = process.env; + + beforeEach(() => { + // Reset environment variables + process.env = { ...originalEnv }; + delete process.env.GITEA_SERVER_URL; + delete process.env.GITHUB_SERVER_URL; + + // Clear module cache to force re-evaluation + delete require.cache[require.resolve("../src/github/api/config")]; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + it("should prioritize GITEA_SERVER_URL over GITHUB_SERVER_URL", async () => { + process.env.GITEA_SERVER_URL = "https://gitea.example.com"; + process.env.GITHUB_SERVER_URL = "http://gitea:3000"; + + const { GITEA_SERVER_URL } = await import("../src/github/api/config"); + expect(GITEA_SERVER_URL).toBe("https://gitea.example.com"); + }); + + it("should fall back to GITHUB_SERVER_URL when GITEA_SERVER_URL is not set", async () => { + process.env.GITHUB_SERVER_URL = "http://gitea:3000"; + + const { GITEA_SERVER_URL } = await import("../src/github/api/config"); + expect(GITEA_SERVER_URL).toBe("http://gitea:3000"); + }); + + it("should use default when neither GITEA_SERVER_URL nor GITHUB_SERVER_URL is set", async () => { + const { GITEA_SERVER_URL } = await import("../src/github/api/config"); + expect(GITEA_SERVER_URL).toBe("https://github.com"); + }); + + it("should ignore empty GITEA_SERVER_URL and use GITHUB_SERVER_URL", async () => { + process.env.GITEA_SERVER_URL = ""; + process.env.GITHUB_SERVER_URL = "http://gitea:3000"; + + const { GITEA_SERVER_URL } = await import("../src/github/api/config"); + expect(GITEA_SERVER_URL).toBe("http://gitea:3000"); + }); + + it("should derive correct API URL from custom GITEA_SERVER_URL", async () => { + process.env.GITEA_SERVER_URL = "https://gitea.example.com"; + + const { GITEA_API_URL } = await import("../src/github/api/config"); + expect(GITEA_API_URL).toBe("https://gitea.example.com/api/v1"); + }); + + it("should handle GitHub.com URLs correctly", async () => { + process.env.GITEA_SERVER_URL = "https://github.com"; + + const { GITEA_API_URL } = await import("../src/github/api/config"); + expect(GITEA_API_URL).toBe("https://api.github.com"); + }); + + it("should create correct job run links with custom GITEA_SERVER_URL", async () => { + process.env.GITEA_SERVER_URL = "https://gitea.example.com"; + + // Clear module cache and re-import + delete require.cache[require.resolve("../src/github/operations/comments/common")]; + const { createJobRunLink } = await import("../src/github/operations/comments/common"); + + const link = createJobRunLink("owner", "repo", "123"); + expect(link).toBe("[View job run](https://gitea.example.com/owner/repo/actions/runs/123)"); + }); + + it("should create correct branch links with custom GITEA_SERVER_URL", async () => { + process.env.GITEA_SERVER_URL = "https://gitea.example.com"; + + // Clear module cache and re-import + delete require.cache[require.resolve("../src/github/operations/comments/common")]; + const { createBranchLink } = await import("../src/github/operations/comments/common"); + + const link = createBranchLink("owner", "repo", "feature-branch"); + expect(link).toBe("\n[View branch](https://gitea.example.com/owner/repo/src/branch/feature-branch/)"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 52796b5..83d74c9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { // Environment setup & latest features - "lib": ["ESNext"], + "lib": ["ESNext", "DOM"], "target": "ESNext", "module": "ESNext", "moduleDetection": "force",