chore: pull in latest

This commit is contained in:
Mark Wylde
2025-09-26 22:34:52 +01:00
12 changed files with 281 additions and 443 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.DS_Store
node_modules
dist
**/.claude/settings.local.json

100
README.md
View File

@@ -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

View File

@@ -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 }}

View File

@@ -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

273
package-lock.json generated
View File

@@ -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",

View File

@@ -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}`);
}
}

View File

@@ -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,

View File

@@ -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);

View File

@@ -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}...`);

View File

@@ -85,7 +85,7 @@ export async function branchHasChanges(
*/
export async function fetchBranch(branchName: string): Promise<boolean> {
try {
await $`git fetch origin ${branchName}`;
await $`git fetch origin --depth=1 ${branchName}`;
return true;
} catch (error) {
console.log(

View File

@@ -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/)");
});
});

View File

@@ -1,7 +1,7 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",