# PRD #1: Forgejo Platform Support with Upstream-Compatible Architecture **Issue**: [#1](https://git.wxs.ro/actions/claude-code-action/issues/1) **Priority**: High **Created**: 2026-02-25 **Revised**: 2026-02-25 (post senior engineer review) --- ## Problem The `claude-code-action` is tightly coupled to GitHub APIs (Octokit REST, GraphQL, `@actions/github`, OIDC token exchange). This makes it unusable with Forgejo Actions, despite Forgejo being workflow-compatible with GitHub Actions. We need Forgejo support while keeping upstream merges as painless as possible. ## Solution Adopt a **lean adapter approach**: only create new files for functionality that is genuinely different on Forgejo, and **re-use existing `src/github/` code wherever Octokit REST is already compatible** with Forgejo's API. Make minimal, surgical modifications to entry points and `action.yml`. ### Key Insight: Octokit REST Works with Forgejo Octokit REST methods (comments, permissions, branches, actor validation) work with Forgejo when `baseUrl` is set to the Forgejo instance's `/api/v1` endpoint. Forgejo mirrors GitHub's REST API paths. This means most of `src/github/` can be re-used directly — **we do NOT need to duplicate 20+ files**. ### What Truly Needs Forgejo-Specific Code Only **4 areas** require new Forgejo implementations: | Area | Why It's Different | New File | |------|--------------------|----------| | **Token handling** | No OIDC exchange with Anthropic; use `GITHUB_TOKEN`/`FORGEJO_TOKEN` directly | `src/forgejo/token.ts` | | **Data fetching** | Forgejo has no GraphQL; must replace 3 GraphQL queries with multiple REST calls | `src/forgejo/data/fetcher.ts` | | **Comment URL formatting** | Forgejo URLs differ from GitHub URLs (job links, PR links, branch links) | `src/forgejo/comment-logic.ts` | | **CI status / Actions API** | Forgejo Actions API is less mature; `downloadJobLogForWorkflowRun` may not exist | `src/mcp/forgejo-actions-server.ts` | ### What Re-Uses Existing Code (No Duplication) These `src/github/` modules work with Forgejo via Octokit REST as-is: | Module | Why It Works | |--------|-------------| | `validation/permissions.ts` | `octokit.repos.getCollaboratorPermissionLevel()` exists in Forgejo API | | `validation/actor.ts` | `octokit.users.getByUsername()` works on Forgejo | | `validation/trigger.ts` | Pure logic — reads webhook payload fields, no API calls | | `operations/comments/create-initial.ts` | `octokit.rest.issues.createComment()` works on Forgejo | | `operations/comments/update-claude-comment.ts` | `octokit.rest.issues.updateComment()` works on Forgejo | | `operations/branch.ts` | `octokit.rest.git.getRef()`, `octokit.rest.repos.get()` work on Forgejo | | `operations/git-config.ts` | Uses `GITHUB_SERVER_URL` for noreply domain — works since Forgejo sets this var | | `utils/sanitizer.ts` | Pure logic, no API dependency | | `utils/image-downloader.ts` | Uses Octokit REST for fetching — should work | | `context.ts` | `@actions/github` reads `GITHUB_EVENT_PATH` — Forgejo sets this | | `data/formatter.ts` | Pure formatting logic | | `api/config.ts` | Reads `GITHUB_API_URL` / `GITHUB_SERVER_URL` — Forgejo sets these | ### Architecture: Lean Adapter ``` src/ ├── platform/ │ └── detector.ts # NEW - Platform detection (+ action.yml `platform` input) ├── forgejo/ │ ├── token.ts # NEW - Direct token (skip OIDC) │ ├── data/ │ │ └── fetcher.ts # NEW - REST-based data fetching (replaces GraphQL) │ └── comment-logic.ts # NEW - Forgejo URL formatting for comments ├── mcp/ │ ├── forgejo-actions-server.ts # NEW - Forgejo CI status MCP server │ └── install-mcp-server.ts # MODIFIED - platform routing for MCP config ├── github/ # REUSED - existing code, no modifications │ └── ... ├── entrypoints/ │ └── run.ts # MODIFIED - platform branch for token setup ├── modes/ │ ├── tag/index.ts # MODIFIED - swap data fetcher + comment logic │ └── agent/index.ts # MODIFIED - swap token setup action.yml # MODIFIED - platform input, conditional steps ``` **~8 new/modified files** instead of ~25. The existing `src/github/` code is re-used, not duplicated. ### Why This Approach | Approach | Upstream Merge Effort | Maintenance | Chosen? | |----------|----------------------|-------------|---------| | **Lean adapter (reuse + minimal new files)** | Low (3-4 conflict-prone files) | Low | **Yes** | | Full abstraction layer (20+ new files) | Near-zero conflicts but high maintenance syncing duplicated code | High | No | | Modify GitHub files in-place | High conflict rate on every upstream pull | Low | No | | Complete fork with divergence | No merges possible after a while | Very high | No | ### Honest Merge-Conflict Assessment These files **will be modified** and are the merge-conflict surface when pulling upstream: | File | What We Change | Conflict Risk | |------|---------------|---------------| | `src/entrypoints/run.ts` | Platform branch at top for token/client setup | **Medium** - upstream's most active file | | `src/modes/tag/index.ts` | Swap `fetchGitHubData` call + comment logic based on platform | **Medium** | | `src/modes/agent/index.ts` | Swap token setup based on platform | **Low** - simpler file | | `src/mcp/install-mcp-server.ts` | Platform routing for MCP server selection | **Low-Medium** | | `action.yml` | Add `platform` input, conditional Forgejo steps | **Low** - additive changes | **Mitigation strategy**: Keep all platform changes at the **top** of functions (early return / branch pattern). Avoid restructuring existing code flow. When upstream modifies these files, conflicts will be localized to our small additions, not interleaved throughout. ## Technical Analysis ### Platform Detection Strategy Use an **explicit `platform` input** (most reliable) with env-var auto-detection as fallback: ```typescript // src/platform/detector.ts export function detectPlatform(): "github" | "forgejo" { // 1. Explicit input (most reliable) const platformInput = process.env.PLATFORM; if (platformInput === "forgejo") return "forgejo"; if (platformInput === "github") return "github"; // 2. Auto-detect from environment // FORGEJO is set by Forgejo runner v7.0+ (not in older runners) if (process.env.FORGEJO === "true") return "forgejo"; // Check GITHUB_SERVER_URL — if not github.com, likely Forgejo/Gitea const serverUrl = process.env.GITHUB_SERVER_URL || ""; if (serverUrl && !serverUrl.includes("github.com")) { return "forgejo"; } return "github"; } ``` New `action.yml` input: ```yaml platform: description: "Platform to run on: 'auto' (default), 'github', or 'forgejo'" required: false default: "auto" ``` ### Key Technical Differences 1. **No GraphQL**: Forgejo is REST-only. The 3 GraphQL queries in `src/github/api/queries/github.ts` (PR_QUERY, ISSUE_QUERY, USER_QUERY) must be replaced with **5-8 separate paginated REST calls**: - `GET /repos/{owner}/{repo}/pulls/{index}` - PR details - `GET /repos/{owner}/{repo}/pulls/{index}/files` - changed files (paginated) - `GET /repos/{owner}/{repo}/pulls/{index}/commits` - commits (paginated) - `GET /repos/{owner}/{repo}/issues/{index}/comments` - issue-level comments (paginated) - `GET /repos/{owner}/{repo}/pulls/{index}/reviews` - reviews (paginated) - For each review: `GET /repos/{owner}/{repo}/pulls/{index}/reviews/{id}/comments` - `GET /repos/{owner}/{repo}/issues/{index}` - issue details Use `Promise.all()` to run independent calls in parallel and mitigate latency. 2. **API base URL**: Forgejo uses `/api/v1` (vs GitHub's `/api/v3`). Octokit REST methods work when `baseUrl` is set correctly since Forgejo mirrors GitHub's endpoint paths. 3. **Token handling**: No OIDC token exchange with Anthropic. The existing `OVERRIDE_GITHUB_TOKEN` code path in `token.ts` (line 130-135) already short-circuits the OIDC flow — for Forgejo we just need to use `GITHUB_TOKEN` or `FORGEJO_TOKEN` directly. 4. **Bot identity**: No GitHub App bot. The action runs as the token owner. Default `bot_id` (41898282) is meaningless on Forgejo — need sensible defaults. 5. **`pull_request_target` event**: May not exist in Forgejo. The existing `context.ts` maps it to `pull_request` (line 188), so it degrades gracefully. 6. **GitHub MCP Server**: The `ghcr.io/github/github-mcp-server` Docker image referenced in `install-mcp-server.ts` (line 214) won't work. Skip on Forgejo — it's only included when `hasGitHubMcpTools` is true. 7. **Forgejo Actions API gaps**: `actions.downloadJobLogForWorkflowRun()` used in the CI MCP server may not exist. Need to verify and implement alternative (direct API call or skip). ### `action.yml` Required Modifications | Line(s) | Current | Problem | Fix | |----------|---------|---------|-----| | 103 | `default: "41898282"` (bot_id) | Meaningless on Forgejo | Make platform-aware; use token owner identity on Forgejo | | 107 | `default: "claude[bot]"` (bot_name) | Doesn't exist on Forgejo | Same — detect from token or use configurable default | | 163-166 | `uses: oven-sh/setup-bun@...` | References GitHub release; may fail on isolated Forgejo runners | Add conditional: if bun already available, skip; otherwise use direct curl install | | 166 | `token: ${{ inputs.github_token \|\| github.token }}` | Used to fetch bun release from GitHub | On Forgejo, `github.token` is the Forgejo token — may not auth to GitHub; provide fallback | | 285-294 | "Revoke app token" step | Calls `${GITHUB_API_URL}/installation/token` which doesn't exist on Forgejo | Make conditional: `if: platform != 'forgejo'` or detect and skip | | N/A | Missing | No `platform` input | Add `platform` input (auto/github/forgejo) | ### `base-action/` Directory `run.ts` imports directly from `base-action/src/`: - `validate-env.ts` - checks env vars for Claude CLI (platform-agnostic) - `setup-claude-code-settings.ts` - writes Claude settings (platform-agnostic) - `install-plugins.ts` - installs Claude plugins (platform-agnostic) - `prepare-prompt.ts` - prepares prompt file (platform-agnostic) - `run-claude.ts` / `run-claude-sdk.ts` - runs Claude CLI (platform-agnostic) **Conclusion**: `base-action/` is platform-independent. No modifications needed. ### MCP Server Naming Strategy **Keep existing server names** (`github_comment`, `github_ci`, `github_file_ops`, `github_inline_comment`) even for Forgejo implementations. These are internal identifiers referenced in hardcoded tool name lists in `src/modes/tag/index.ts` (lines 127-152): ```typescript "mcp__github_comment__update_claude_comment", "mcp__github_ci__get_ci_status", "mcp__github_file_ops__commit_files", ``` Renaming would require modifying the tool name lists — more merge-conflict surface for zero user benefit. The MCP server config in `install-mcp-server.ts` will point these names to either GitHub or Forgejo server implementations based on platform. ## CI/CD Workflows ### Handling Upstream `.github/workflows/` The upstream repo has 13 workflows in `.github/workflows/`. Strategy: - **Do NOT delete or modify** `.github/workflows/` files (merge conflicts) - Create `.forgejo/workflows/` with our custom workflows - **Configure Forgejo runner** to only read from `.forgejo/workflows/` (runner-level `DEFAULT_ACTIONS_URL` and workflow directory settings) - **Document this as a setup requirement** — if runners aren't configured, upstream workflows will attempt to run and fail loudly (missing secrets, OIDC endpoints, etc.) ### Custom Forgejo Workflows #### 1. CI Test Workflow (`.forgejo/workflows/ci.yml`) - Triggers on push to main and PRs - Runs `bun install` and `bun test` - Lints with biome (matching upstream's toolchain) #### 2. Action Self-Test Workflow (`.forgejo/workflows/test-action.yml`) - **Tests the action itself** on Forgejo - Creates a test issue via API, triggers the action with a simple prompt - Verifies: platform detection returns "forgejo", comment is created on the issue, action completes successfully - Validates end-to-end: token handling, Octokit REST calls, comment posting - Runs on a schedule (weekly) and on PRs that modify `src/`, `action.yml`, or `.forgejo/` #### 3. Upstream Sync Check (`.forgejo/workflows/upstream-sync.yml`) - Scheduled workflow (weekly) + manual trigger - Fetches `upstream/main`, compares with our `main` using `git rev-list` - Creates an issue if upstream has new commits we haven't merged - Includes commit count and summary of changed files to help assess merge effort ## Milestones ### Phase 1 - Foundation (parallel, no dependencies) - [x] **M1: Platform detection + `action.yml` modifications** — Add `platform` input to `action.yml`. Create `src/platform/detector.ts` with explicit input + env-var auto-detection. Add conditional logic to skip "Revoke app token" step on Forgejo. Fix bun installation to handle Forgejo runners (direct curl fallback). - Files: `action.yml` (MODIFIED), `src/platform/detector.ts` (NEW) - Success: `detectPlatform()` returns "forgejo" in Forgejo runner, "github" in GitHub runner. `action.yml` doesn't error on Forgejo. - [x] **M2: Forgejo token handling** — Create `src/forgejo/token.ts` that returns `GITHUB_TOKEN` / `FORGEJO_TOKEN` directly (no OIDC, no Anthropic exchange). Wire into `run.ts` with an early platform branch before the existing `setupGitHubToken()` call. - Files: `src/forgejo/token.ts` (NEW), `src/entrypoints/run.ts` (MODIFIED — surgical addition at top of `run()`) - Depends on: M1 - Success: Token is available without OIDC on Forgejo. GitHub path unchanged. - [ ] **M3: Forgejo CI workflows** — Create `.forgejo/workflows/` with CI, self-test, and upstream sync workflows. Document runner configuration requirements. - Files: `.forgejo/workflows/ci.yml`, `.forgejo/workflows/test-action.yml`, `.forgejo/workflows/upstream-sync.yml` (all NEW) - Success: CI runs tests on push. Upstream `.github/workflows/` don't interfere. ### Phase 2 - Core Data Fetching (the hard part) - [ ] **M4a: Forgejo REST data fetching — PR/Issue basics** — Implement REST-based fetching for PR details, issue details, changed files, and commits. Produce output matching the `FetchDataResult` type that existing code expects. - Files: `src/forgejo/data/fetcher.ts` (NEW) - Depends on: M1, M2 - Success: Can fetch PR/issue title, body, author, state, files, commits from Forgejo REST API. - [ ] **M4b: Forgejo REST data fetching — Comments, reviews, inline comments** — Extend the fetcher with paginated comment/review fetching, including nested inline review comments (requires per-review API calls). Implement TOCTOU filtering using the same `filterCommentsToTriggerTime` / `filterReviewsToTriggerTime` functions (reuse from `src/github/data/fetcher.ts` — these are pure logic). Use `Promise.all()` for parallel API calls. - Files: `src/forgejo/data/fetcher.ts` (extend from M4a) - Depends on: M4a - Success: Full parity with GitHub GraphQL data fetch. All comments, reviews, and inline comments included with proper TOCTOU filtering. - [ ] **M5: Forgejo comment URL formatting** — Create `src/forgejo/comment-logic.ts` with Forgejo-specific URL patterns for job links, branch links, and PR creation links (used in tracking comment updates). - Files: `src/forgejo/comment-logic.ts` (NEW) - Depends on: M1 - Success: Comment formatting produces correct Forgejo URLs. ### Phase 3 - Integration (wiring it together) - [ ] **M6: MCP server platform routing** — Modify `install-mcp-server.ts` to select GitHub or Forgejo server implementations based on platform. Create `src/mcp/forgejo-actions-server.ts` for CI status (may be a subset of GitHub's if some Actions API endpoints don't exist in Forgejo). Keep MCP server names identical (`github_comment`, `github_ci`, etc.) to avoid touching tool name lists. For comment/file-ops/inline-comment servers, the existing GitHub implementations likely work via Octokit REST — verify and use directly. - Files: `src/mcp/install-mcp-server.ts` (MODIFIED), `src/mcp/forgejo-actions-server.ts` (NEW, possibly) - Depends on: M4b, M5 - Success: Claude can update comments, query CI status on Forgejo. MCP tools work end-to-end. - [ ] **M7: Entry point + mode handler integration** — Wire platform detection into mode handlers. In `tag/index.ts`: swap `fetchGitHubData` for Forgejo fetcher + comment logic. In `agent/index.ts`: swap token setup. Keep changes surgical — platform branch at the top of functions, existing flow untouched. - Files: `src/modes/tag/index.ts` (MODIFIED), `src/modes/agent/index.ts` (MODIFIED) - Depends on: M4b, M5, M6 - Success: Action runs end-to-end on Forgejo for both tag and agent modes. ### Phase 4 - Validation & Polish - [ ] **M8: End-to-end testing on Forgejo** — Test complete workflows: issue trigger (@claude mention), PR review, agent mode (workflow_dispatch with prompt), tag mode. Verify comment updates, branch creation, file commits, CI status queries. - Depends on: M7 - Success: All supported trigger types work on Forgejo. Self-test workflow passes. - [ ] **M9: Documentation & upstream merge validation** — Document Forgejo-specific setup (runner configuration, token setup, `platform: forgejo` input). Create `CLAUDE.md` for the project. Perform a fresh `git fetch upstream && git merge upstream/main` to verify conflicts are minimal and easily resolvable. Document the merge process. - Depends on: M8 - Success: Clean or trivially-resolvable upstream merge. Clear setup docs. ### Milestone Dependency Graph ``` M1 ──> M2 ──> M4a ──> M4b ──┬──> M6 ──> M7 ──> M8 ──> M9 M1 ──> M5 ───────────────────┘ M3 (independent, anytime) ``` **Parallel opportunities:** - M3 is fully independent — can run anytime - M5 and M2 can run in parallel (both only depend on M1) - M4a can start as soon as M2 is done - M6 and M7 are sequential (M7 depends on M6) ## Execution Plan | Phase | Milestones | Files Touched | Parallel? | |-------|-----------|---------------|-----------| | 1 | M1, M2, M3 | `action.yml`, `src/platform/detector.ts`, `src/forgejo/token.ts`, `src/entrypoints/run.ts`, `.forgejo/workflows/*` | M1 first, then M2+M3 in parallel | | 2 | M4a, M4b, M5 | `src/forgejo/data/fetcher.ts`, `src/forgejo/comment-logic.ts` | M5 parallel with M4a/M4b | | 3 | M6, M7 | `src/mcp/install-mcp-server.ts`, `src/mcp/forgejo-actions-server.ts`, `src/modes/tag/index.ts`, `src/modes/agent/index.ts` | Sequential | | 4 | M8, M9 | Tests, docs, `CLAUDE.md` | Sequential | ## Risk Assessment | Risk | Likelihood | Impact | Mitigation | |------|-----------|--------|------------| | Upstream refactors `run.ts` or mode handlers | Medium | High | Keep entry point changes to <10 lines; use early-return pattern; platform branch at top of functions | | Octokit REST methods behave differently on Forgejo | Low | Medium | Test each used method early in M4a; fall back to raw `fetch()` if needed | | Forgejo webhook payloads differ from `@octokit/webhooks-types` | Medium | Medium | Test with real Forgejo webhooks; key check: `payload.issue.pull_request` field for `is_pull` detection | | `pull_request_target` event missing in Forgejo | High | Low | Already handled — `context.ts:188` maps it to `pull_request` | | Forgejo Actions API missing endpoints (downloadJobLog) | Medium | Medium | Degrade gracefully — CI MCP server may offer fewer tools on Forgejo | | `oven-sh/setup-bun` action fails on isolated Forgejo runners | Medium | Medium | Add direct curl bun install as fallback in `action.yml` | | Upstream `.github/workflows/` run and fail on Forgejo | Medium | Low | Document runner configuration; provide `.forgejo/workflows/` overrides | | Upstream adds new features requiring new GraphQL fields | Certain | Low | New features won't have Forgejo support until we add REST equivalents to `forgejo/data/fetcher.ts` | ## Success Criteria 1. Action runs on Forgejo for issue, PR, and workflow_dispatch triggers 2. Claude can read issue/PR context, post comments, create branches, and commit files on Forgejo 3. `git fetch upstream && git merge upstream/main` produces conflicts only in the 3-4 files we modified, and those conflicts are resolvable in <15 minutes 4. Self-test workflow validates the action works on every PR 5. Upstream sync workflow alerts when new upstream changes are available 6. Total new files: ~6-8 (not 25+) ## Out of Scope (for now) - GitHub Enterprise Server support - Gitea support (similar to Forgejo but may diverge) - `pull_request_target` event support - SSH signing on Forgejo (can be added later) - GitHub MCP Server equivalent for Forgejo (the `ghcr.io/github/github-mcp-server` image) - Full Forgejo Actions API parity (downloadJobLog may be missing)