# Forgejo Platform Support This action supports [Forgejo](https://forgejo.org/) (and Forgejo Actions) as an alternative to GitHub. The same action code runs on both platforms — platform detection is automatic or explicitly configured. ## Prerequisites - **Forgejo instance** with [Forgejo Actions](https://forgejo.org/docs/latest/user/actions/) enabled - **Forgejo runner** registered and operational (e.g., a `grunt` or `ubuntu-latest` runner) - **Anthropic API key** or **Claude Code OAuth token** for Claude API access - **Forgejo token** with repo access (automatically available as `github.token` in workflows, or use a [dedicated bot user](#dedicated-bot-user-recommended)) ## Quick Start ### 1. Add Secrets Add secrets at the **organization level** (Settings > Actions > Secrets) so all repos can share them: - `ANTHROPIC_API_KEY` — Your Anthropic API key (starting with `sk-ant-`) - `CLAUDE_BOT_TOKEN` — API token for the dedicated bot user (see [Dedicated Bot User](#dedicated-bot-user-recommended) below) ### 2. Create Workflow Create `.forgejo/workflows/claude.yml`: ```yaml name: Claude Assistant on: issue_comment: types: [created] pull_request_review_comment: types: [created] issues: types: [opened, assigned, labeled] pull_request_review: types: [submitted] jobs: claude-response: runs-on: grunt # Your Forgejo runner label steps: - uses: https://git.wxs.ro/actions/claude-code-action@v1 # Or your mirror URL with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID (see step 5 below) bot_name: "claude" bot_email: "claude@yourdomain.com" # Must match the bot user's registered email ``` ### 3. Agent Mode (Automation) For automation workflows triggered by `workflow_dispatch`, push events, or schedules: ```yaml name: Claude Automation on: workflow_dispatch: inputs: task: description: "Task for Claude" required: true jobs: run-claude: runs-on: grunt steps: - uses: https://git.wxs.ro/actions/claude-code-action@v1 with: prompt: ${{ github.event.inputs.task }} anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} bot_id: "YOUR_BOT_ID" # Replace with your bot's Forgejo user ID bot_name: "claude" bot_email: "claude@yourdomain.com" ``` ## Platform Detection The action detects the platform using a three-tier strategy: | Priority | Method | Details | | -------- | --------------------------------- | -------------------------------------------------------- | | 1 | **Explicit input** | `platform: "forgejo"` in workflow — most reliable | | 2 | **`FORGEJO` env var** | Set to `"true"` by Forgejo runner v7.0+ | | 3 | **`GITHUB_SERVER_URL` heuristic** | If the URL doesn't contain `github.com`, assumes Forgejo | **Recommendation**: Always set `platform: "forgejo"` explicitly. Auto-detection works but the explicit input eliminates ambiguity. > **Security note**: Setting `platform: "forgejo"` on a `github.com` runner is rejected — the action cross-validates the `GITHUB_SERVER_URL` and falls through to standard detection. This prevents bypassing GitHub-specific security checks (e.g., actor validation). > **Note for GitHub Enterprise Server (GHES) users**: The tier-3 heuristic classifies any non-`github.com` URL as Forgejo. If you run on GHES (e.g., `https://github.mycompany.com`), you **must** set `platform: "github"` explicitly to avoid misdetection. ## Differences from GitHub ### Authentication | Aspect | GitHub | Forgejo | | ------------------------ | ------------------------------- | ------------------------------------------------------------- | | **Default auth** | GitHub App OIDC token exchange | Direct `GITHUB_TOKEN` / `FORGEJO_TOKEN` | | **Token setup** | Automatic via Claude GitHub App | Manual — provide `github_token` input | | **App token revocation** | Automatic cleanup step | Skipped (no app token) | | **Bot identity** | `claude[bot]` (ID: 41898282) | Dedicated bot user via `bot_id`/`bot_name`/`bot_email` inputs | On Forgejo, there is no OIDC token exchange with Anthropic. The action uses the workflow token (`github.token`) or a user-provided `github_token` directly. ### API Differences | Feature | GitHub | Forgejo | | --------------------- | ------------------------------------------- | ------------------------------------------------------------------------ | | **GraphQL API** | Used for PR/issue data fetching | Not available — REST API only | | **Data fetching** | Single GraphQL query per entity | 5-8 parallel REST API calls | | **Actor validation** | Checks for bot/app accounts | Bot-type check skipped; self-trigger guard on comment/review events only | | **GitHub MCP server** | `ghcr.io/github/github-mcp-server` included | Skipped — GitHub-specific | | **CI/Actions MCP** | Full GitHub Actions API | Works via Octokit REST + `GITHUB_API_URL` | | **Permission check** | `getCollaboratorPermissionLevel()` | `getCollaboratorPermissionLevel()` with `repos.get()` fallback on 403 | | **Git ref lookup** | `git.getRef()` (`/git/ref/heads/...`) | `repos.getBranch()` (`/git/refs` is plural on Forgejo) | ### Webhook & Event Differences Forgejo's webhook payloads and event naming diverge from GitHub in several ways. These are handled automatically by the action but important to understand when debugging: | Behavior | GitHub | Forgejo | | ------------------------------------ | -------------------------------------------------------- | -------------------------------------------------------------------------- | | **PR review comment event name** | `pull_request_review_comment` | `pull_request_comment` (same event for both review comments and reviews) | | **PR review event name** | `pull_request_review` | `pull_request_comment` (merged with review comments) | | **Issue label event action** | `labeled` | `label_updated` (normalized by `FORGEJO_EVENT_ACTION_MAP` in `context.ts`) | | **Issue assigned event action** | `assigned` | `assigned` (matches GitHub — no normalization needed) | | **Review comment payload** | Top-level `comment` object present | No top-level `comment` — tracking comment falls back to issue comment | | **Assigned payload** | Top-level `assignee` object present | No top-level `assignee` — must read from `issue.assignee` | | **PR compare URL parameter** | `quick_pull=1` | `expand=1` | | **Workflow `if:` body fields** | `github.event.comment.body` populated | Not populated for `pull_request_comment` events | | **Workflow `if:` action field** | `github.event.action` populated for `issues` events | Not populated — `if:` cannot gate on action | | **Workflow `if:` expression parser** | Allows `#` comments | Rejects `#` as invalid token — comments must go outside `if:` block | | **Creating issue with label** | Fires `opened` only | Fires both `opened` and `label_updated` as separate events | | **Assignees API** | `PATCH /issues/{id}` with `{"assignees": [...]}` (array) | Array form returns 404 — must use singular `{"assignee": "user"}` | **Practical impact**: The `@claude` mention check and event action validation happen in application code (`trigger.ts`, `context.ts`) rather than the workflow `if:` condition. The workflow lets all events through and the action decides what to act on. ### Bun Installation The upstream action uses `oven-sh/setup-bun`. On Forgejo, this is replaced with a universal `curl` install because Forgejo runners pre-fetch all `uses:` action references before evaluating `if:` conditions, causing failures for GitHub-hosted actions that aren't mirrored. ### Comment URLs Comment tracking URLs (job links, branch links, PR creation links) use Forgejo URL patterns instead of GitHub patterns. This is handled automatically. ## Action Inputs The `platform` input is the only Forgejo-specific addition: ```yaml platform: description: "Platform to run on: 'auto' (default auto-detects), 'github', or 'forgejo'" required: false default: "auto" ``` All other inputs work identically on both platforms. See the [Usage Guide](./usage.md) for the full list. ## Fork-Specific Fixes (All Platforms) This fork fixes upstream bugs that affect **both GitHub and Forgejo** usage: ### `allowed_tools` / `disallowed_tools` inputs now work (upstream #181, #287, #724) Upstream's `allowed_tools` and `disallowed_tools` inputs are completely broken — they are never wired through to Claude execution. This affects tag mode (the `@claude` mention flow) where the tool list is hardcoded and user inputs are silently ignored. **What was broken:** - `allowed_tools` was not defined in `action.yml`, so the env var was never set - Tag mode only merged `mcp__github_*` tools from `claude_args`, filtering out everything else - `run.ts` never passed `allowedTools`/`disallowedTools` to `runClaude()` - `createPrompt()` hardcoded `WebSearch` and `WebFetch` as disallowed with no way to override **What this fork fixes:** - Added `allowed_tools` and `disallowed_tools` as proper action inputs - User tools are merged into the tag-mode tool list - `disallowedTools` is passed through to the SDK - `WebSearch`/`WebFetch` are removed from the disallowed list when included in `allowed_tools` - `run.ts` now passes `ALLOWED_TOOLS_INPUT` as `options.allowedTools` to `runClaude()`, fixing agent mode where tools were silently ignored **Important: Bash sub-patterns are required for CI auto-approval.** Bare `Bash` in `allowed_tools` makes the tool available but individual commands still require interactive approval (which can't happen in CI — there's nobody to click "approve"). Use sub-patterns to pre-approve command families: ```yaml - uses: actions/claude-code-action@main with: # ❌ WRONG — bare Bash requires interactive approval for every command allowed_tools: "Bash,Read,Write,Edit" # ✅ CORRECT — sub-patterns auto-approve specific command families allowed_tools: "Bash(git:*),Bash(bun:*),Bash(curl:*),Read,Write,Edit" ``` Common sub-patterns: - `Bash(git:*)` — all git commands (push, commit, add, etc.) - `Bash(bun:*)` — all bun commands (test, run, install, etc.) - `Bash(curl:*)` — curl for API calls - `Bash(npm test:*)` — specific command with any args ## Dedicated Bot User (Recommended) On GitHub, Claude gets its own identity via the GitHub App (`claude[bot]`). On Forgejo, there's no such mechanism — by default the action runs under whatever token you provide, meaning commits and comments appear as the human user. Creating a dedicated bot user solves this: - **Clear attribution**: Commits and comments show as `claude`, not your personal account - **Least privilege**: The bot token has only the permissions Claude needs - **Audit trail**: Easy to distinguish human actions from bot actions in repo history - **Security**: If the action is compromised, only the bot's scoped token is exposed ### Setup Steps #### 1. Create the bot user Go to **Site Administration > User Accounts > Create User Account** or use the admin API: - **Username**: `claude` - **Email**: e.g., `claude@yourdomain.com` - **Password**: any strong password (you'll use API tokens, not password login) - **Uncheck** "Require user to change password" - Leave "Admin" **unchecked** - Optionally set a display name and avatar to distinguish from human users #### 2. Add to organizations via teams For each organization where Claude should operate: 1. Go to **Org Settings > Teams > New Team** 2. Create a team named `Claude` with: - **Permission**: `Write` - **Access**: All Repositories (so new repos are automatically covered) 3. Add `claude` to the team's **Members** #### 3. Generate a scoped API token Log in as `claude`, go to **Settings > Applications**, and create a token: - **Token Name**: `claude-code-action` - **Scopes**: - `repo` — read/write (issues, PRs, comments, branches, commits) - `user:read` — needed for API identity - Leave `admin`, `organization`, `package` **unchecked** #### 4. Store the token as an org-level secret Add the token as `CLAUDE_BOT_TOKEN` at the **organization level** (Settings > Actions > Secrets) with visibility set to "All repositories". This way all repos in the org share the same secret. #### 5. Get the bot user's ID You'll need the Forgejo user ID for the `bot_id` input: ```bash curl -s "https://your-forgejo-instance/api/v1/users/claude" | jq '.id' ``` #### 6. Configure the workflow ```yaml - uses: https://git.wxs.ro/actions/claude-code-action@v1 with: anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} platform: "forgejo" github_token: ${{ secrets.CLAUDE_BOT_TOKEN }} bot_id: "YOUR_BOT_ID" # Replace with the ID from step 5 bot_name: "claude" bot_email: "claude@yourdomain.com" # Must match the bot user's registered email ``` The `bot_email` input overrides the default noreply email format. When it matches the bot user's registered email in Forgejo, commits will be linked to the bot's profile in the UI. ### Token Rotation Forgejo API tokens can expire. Plan for rotation: 1. **Set a calendar reminder** for before the token expires 2. **Generate a new token** by logging in as `claude` (same scopes as above) 3. **Update the org secret** `CLAUDE_BOT_TOKEN` with the new token value 4. **No code changes needed** — the workflow reads the secret at runtime Alternatively, generate a token without an expiration date (if your Forgejo instance allows it). ## Included CI Workflows This fork includes four Forgejo-native workflows in `.forgejo/workflows/`: ### `claude.yml` — Tag Mode (@claude Mentions) Responds to `@claude` mentions on issues, PR review comments, and PR reviews. Also triggers on issue assignment and label events. This is the main interactive workflow for using Claude on Forgejo. ### `ci.yml` — Tests, Formatting, Type Checking Runs on push to `main` and pull requests. Three parallel jobs: - `test` — `bun test` (with `GITHUB_SERVER_URL` override so GitHub-context unit tests pass) - `prettier` — `bun run format:check` - `typecheck` — `bun run typecheck` ### `test-action.yml` — Action Self-Test Tests the action itself on Forgejo in agent mode. Runs weekly and on PRs that modify `src/`, `action.yml`, `.forgejo/`, or `base-action/`. Validates: - Claude Code executes successfully with `platform: "forgejo"` - Output file is produced with expected content ### `upstream-sync.yml` — Upstream Sync Check Runs weekly. Compares `main` with `upstream/main` (anthropics/claude-code-action). If upstream has new commits: - Creates or updates an issue with the `upstream-sync` label - Includes commit log, diff stats, and merge instructions - Lists conflict-prone files ## Merging Upstream Changes This fork is designed for easy upstream merges. Only 4-5 files have Forgejo-specific modifications, and changes are placed at function entry points using an early-return pattern. ### Merge Process ```bash # Fetch latest upstream git fetch upstream main # Merge into your main branch git merge upstream/main # If conflicts occur, they'll be in these files: # - src/entrypoints/run.ts (platform branch at top) # - src/modes/tag/index.ts (data fetcher swap) # - src/mcp/install-mcp-server.ts (platform routing) # - action.yml (platform input, bun install) # Resolve conflicts, keeping both the upstream changes and # the Forgejo platform branches at function entry points # Run tests to verify bun test # Push git push origin main ``` ### Conflict Mitigation All Forgejo changes follow the **early-return pattern** — platform-specific logic is placed at the top of functions, before the existing GitHub code. This means: - Upstream changes to the GitHub code path rarely conflict with Forgejo additions - When conflicts do occur, they're localized to the few lines where the platform branch starts - Resolution is straightforward: keep both the platform check and the upstream changes ### What Happens When Upstream Adds New Features New upstream features that use GraphQL or GitHub-specific APIs won't automatically work on Forgejo. The Forgejo data fetcher (`src/forgejo/data/fetcher.ts`) would need REST equivalents added. The action will still run — features that depend on missing data will simply lack that context. ## E2E Test Results All execution modes and trigger types have been validated end-to-end on a Forgejo instance (git.wxs.ro): | Mode | Trigger | Status | Details | | -------------- | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Agent mode** | `workflow_dispatch` | Validated | Platform detection, token auth, Claude execution, output file — all green | | **Tag mode** | `issue_comment` (`@claude`) | Validated | Issue context (title + body) read correctly, platform detected as `forgejo`, tracking comment created with spinner then updated with response (31s), job link URL correct | | **Tag mode** | `pull_request_review_comment` | Validated | PR diff and review comment body visible to Claude. Tracking comment created as issue comment (not review reply — see [webhook quirks](#webhook--event-differences) below). 3 bugs fixed: missing top-level `comment` in payload, event name mismatch, MCP comment routing | | **Tag mode** | `pull_request_review` | Validated | Claude responds to `@claude` in PR review body. Forgejo sends `pull_request_comment` instead of `pull_request_review` — the `@claude` check runs in `trigger.ts`, not the workflow `if:` condition | | **Tag mode** | `issues` with `labeled` action | Validated | Claude responds when the configured label is applied. 3 bugs fixed: workflow `if:` expression parser rejects `#` comments, `github.event.action` not populated in `if:` context, event action is `label_updated` not `labeled` | | **Tag mode** | `issues` with `assigned` action | Validated | Claude responds when assigned to an issue. 1 bug fixed: `assignee` field missing from top-level payload (nested under `issue.assignee` instead) | | **Tag mode** | Code changes (branch + commits) | Validated | Claude creates branch, pushes commits, links branch in tracking comment. Bot identity (`claude `) correct. PR compare URL uses `expand=1` (Forgejo) instead of `quick_pull=1` (GitHub) | ### Tag Mode Verified Capabilities - `@claude` trigger phrase detected in issue comments, PR review comments, and PR reviews - Issue body, title, PR diff, and review comment body fully visible to Claude - Tracking comment created with progress spinner, then updated with final response - Forgejo job run URL correctly formatted in tracking comment - Branch creation works via `repos.getBranch()` API (Forgejo uses `/git/refs` not `/git/ref`) - Permission check passes via `repos.get()` fallback - Label trigger fires when configured label is applied to an issue - Assignee trigger fires when the bot user is assigned to an issue - Code changes: branch creation, file commits, and PR compare URL all work correctly - Bot identity (commits attributed to `claude` user) verified via git log and API ## Known Limitations - **GitHub Enterprise Server (GHES) misdetection**: The tier-3 platform detection heuristic treats any `GITHUB_SERVER_URL` that isn't `github.com` as Forgejo. GHES instances (e.g., `https://github.mycompany.com`) will be misidentified. Set `platform: "github"` explicitly when running on GHES. - **Git ref API**: Forgejo uses `/git/refs` (plural) instead of GitHub's `/git/ref` (singular). The action handles this by using `repos.getBranch()` on Forgejo for branch SHA lookups. - **User display name fetch**: `GET /users/{username}` returns 403 without `read:user` scope. Non-fatal — falls back to login name. - **`pull_request_target` event**: May not exist in Forgejo. The action degrades gracefully (maps to `pull_request`). - **GitHub MCP server**: The `ghcr.io/github/github-mcp-server` Docker image is GitHub-specific and is skipped on Forgejo. - **Commit signing**: SSH signing has not been tested on Forgejo. The `use_commit_signing` option (GitHub API signing) won't work since it uses GitHub's commit API. - **`downloadJobLogForWorkflowRun`**: Forgejo's Actions API may lack this endpoint. The CI MCP server attempts it via Octokit REST but may return limited data. - **Bot filtering**: `allowed_bots` checks won't match `claude[bot]` since Forgejo has no bot accounts. Bot-type actor validation is skipped on Forgejo. Self-trigger prevention only blocks comment/review events (`issue_comment`, `issues`, `pull_request_review`, `pull_request_review_comment`) where the actor matches `bot_name` — these are the events that can cause infinite loops. PR events, workflow_dispatch, and schedule events are allowed through, enabling CI workflows (like upstream-sync) that are triggered by the bot user. - **`pull_request_review` trigger is dead code**: Forgejo sends `pull_request_comment` for both review comments and review submissions. The `pull_request_review` trigger in the workflow never fires — all review-related events are handled through the `pull_request_comment` codepath. - **Review comment tracking comments are always issue comments**: On Forgejo, review comment events lack the top-level `comment` object needed to create PR review comment replies. The tracking comment is always created as a regular issue comment instead. - **Workflow `if:` conditions are unreliable for Forgejo events**: Forgejo does not populate `github.event.comment.body`, `github.event.review.body`, or `github.event.action` in the workflow expression context for several event types. All trigger validation happens in application code (`trigger.ts`), not in workflow conditions. - **Assignees API (plural array form)**: `PATCH /repos/{owner}/{repo}/issues/{index}` with `{"assignees": ["user"]}` returns 404 on Forgejo. Use the deprecated singular `{"assignee": "user"}` field instead. ## Troubleshooting ### "Resource not accessible by integration" errors This usually means the token lacks permissions. On Forgejo: - Use a personal access token with `repo` scope via the `github_token` input - The default `github.token` in Forgejo workflows may have limited permissions depending on your runner configuration ### Tests fail with platform detection errors Unit tests assume GitHub context by default. When running on a Forgejo runner, set `GITHUB_SERVER_URL=https://github.com` before running tests (the CI workflow handles this automatically). ### Bun installation fails If `curl -fsSL https://bun.sh/install | bash` fails on your runner, pre-install Bun on the runner image or use the `path_to_bun_executable` input to point to a custom Bun binary. ### Claude's tracking comment doesn't appear as a review reply On Forgejo, review comment triggers (`pull_request_review_comment`) always create a regular issue comment — not a reply in the review thread. This is because Forgejo's webhook payload lacks the top-level `comment` object needed to create a review reply. The response still appears on the PR, just as a standalone comment. ### Workflow `if:` condition skips events it shouldn't Forgejo doesn't populate `github.event.comment.body`, `github.event.review.body`, or `github.event.action` in the workflow expression context for `pull_request_comment` and `issues` events. If your workflow `if:` condition checks these fields, events will be silently skipped. The fix is to let all events through and handle validation in `trigger.ts` instead. ### Workflow `if:` expression fails with "invalid token" error Forgejo's expression parser may reject `#` characters as invalid tokens when using `|` (literal block) scalars, since each line is preserved and `#` starts a comment that extends to end-of-line. With `>-` (folded scalar), newlines become spaces, and `#` comments may be tolerated by some Forgejo versions — but for maximum compatibility, move comments outside the `if:` block entirely. ### Label trigger fires twice when creating issue with label If you create an issue with a label in one operation, Forgejo fires both `opened` and `label_updated` as separate events. The action handles this correctly — it only checks labels on `label_updated` events, not on `opened`. ### `mock.module()` breaks tests on Forgejo CI (Linux Bun) Bun's `mock.module()` replaces the module globally in the process module registry. With Bun's non-deterministic test file ordering on Linux, this causes cross-file pollution: one test file's mock leaks into another. Use dependency injection or `spyOn` on individual functions instead. If the real function is safe to call (e.g., returns empty results for test data), avoid mocking entirely. ### Upstream workflows run and fail If `.github/workflows/` from upstream are executing on your Forgejo runner, configure the runner to only read from `.forgejo/workflows/`. This is a runner-level setting — see your Forgejo instance's runner configuration.