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 <noreply@anthropic.com>
This commit is contained in:
Vlad MocanuandClaude Opus 4.6 committed 2026-02-25 15:54:10 +02:00
1 parent ade221fd1c
commit 411eb3d69d
1 file changed
+335
+335
View File
@@ -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)