From 411eb3d69d03f8ad5d366eba4bf286bfef884645 Mon Sep 17 00:00:00 2001 From: Vlad Mocanu Date: Wed, 25 Feb 2026 15:54:10 +0200 Subject: [PATCH] docs(prd-1): create PRD #1 - Forgejo platform support [skip ci] - Lean adapter architecture: ~8 new files, reuse existing Octokit REST code - REST-based data fetching to replace GitHub GraphQL queries - Platform detection via explicit input + env-var auto-detection - Custom Forgejo CI workflows with action self-test - 10 milestones across 4 phases Co-Authored-By: Claude Opus 4.6 --- prds/1-forgejo-platform-support.md | 335 +++++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 prds/1-forgejo-platform-support.md diff --git a/prds/1-forgejo-platform-support.md b/prds/1-forgejo-platform-support.md new file mode 100644 index 0000000..0cb050d --- /dev/null +++ b/prds/1-forgejo-platform-support.md @@ -0,0 +1,335 @@ +# 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) + +- [ ] **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. + +- [ ] **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)