From f640f381022edc660fcd81ad1acadd6aaef03507 Mon Sep 17 00:00:00 2001 From: zinglax Date: Wed, 4 Jun 2025 09:44:05 -0400 Subject: [PATCH 1/4] Adding --depth=1 to fetchs to save time See https://github.com/anthropics/claude-code-action/issues/52 --- src/github/operations/branch.ts | 4 ++-- src/github/utils/local-git.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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( From 1fd3bbc91bfc775df7f09303c44771e038017889 Mon Sep 17 00:00:00 2001 From: woehrer12 Date: Sun, 6 Jul 2025 23:48:46 +0200 Subject: [PATCH 2/4] chore: claude-code to v0.0.24 (#3) --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index 140264f..196a96c 100644 --- a/action.yml +++ b/action.yml @@ -113,7 +113,7 @@ runs: - name: Run Claude Code id: claude-code if: steps.prepare.outputs.contains_trigger == 'true' - uses: anthropics/claude-code-base-action@c8e31bd52d9a149b3f8309d7978c6edaa282688d # v0.0.8 + uses: anthropics/claude-code-base-action@v0.0.24 with: prompt_file: /tmp/claude-prompts/claude-prompt.txt allowed_tools: ${{ env.ALLOWED_TOOLS }} From eb1aa3696e9b5af1cbf74ce2dc9900b0659587c4 Mon Sep 17 00:00:00 2001 From: Oleg Date: Fri, 26 Sep 2025 23:19:04 +0200 Subject: [PATCH 3/4] 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, From 04892eb63d2f110a5c819b732b8888622f0e3a85 Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Fri, 26 Sep 2025 22:23:30 +0100 Subject: [PATCH 4/4] Fix: Prioritize GITEA_SERVER_URL over GITHUB_SERVER_URL (#5) --- README.md | 25 ++++ examples/gitea-custom-url.yml | 23 +++ package-lock.json | 273 ---------------------------------- src/github/api/config.ts | 21 ++- test/gitea-server-url.test.ts | 83 +++++++++++ tsconfig.json | 2 +- 6 files changed, 151 insertions(+), 276 deletions(-) create mode 100644 examples/gitea-custom-url.yml create mode 100644 test/gitea-server-url.test.ts diff --git a/README.md b/README.md index 511d322..5d4c82e 100644 --- a/README.md +++ b/README.md @@ -85,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" 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/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/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 b84ba7b..9439602 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",