- Created docs/forgejo.md: comprehensive setup guide (prerequisites, quick start, platform detection, GitHub differences, CI workflows, upstream merge process, known limitations, troubleshooting) - Updated README.md with Forgejo support mention and docs link - Updated CLAUDE.md with Forgejo architecture section - Added platform input to docs/usage.md inputs table - Added 5 Forgejo FAQ entries to docs/faq.md - Upstream merge validated: zero conflicts (based on latest upstream) - PRD #1 100% complete: all milestones M1-M9 done, 794 tests passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
29 KiB
PRD #1: Forgejo Platform Support with Upstream-Compatible Architecture
Issue: #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 |
Skipped entirely on Forgejo — read:user scope required for getByUsername() but unavailable; Forgejo has no bot/app accounts so check is unnecessary |
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:
// 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:
platform:
description: "Platform to run on: 'auto' (default), 'github', or 'forgejo'"
required: false
default: "auto"
Key Technical Differences
-
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 detailsGET /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. -
API base URL: Forgejo uses
/api/v1(vs GitHub's/api/v3). Octokit REST methods work whenbaseUrlis set correctly since Forgejo mirrors GitHub's endpoint paths. -
Token handling: No OIDC token exchange with Anthropic. The existing
OVERRIDE_GITHUB_TOKENcode path intoken.ts(line 130-135) already short-circuits the OIDC flow — for Forgejo we just need to useGITHUB_TOKENorFORGEJO_TOKENdirectly. -
Bot identity: No GitHub App bot. The action runs as the token owner. Default
bot_id(41898282) is meaningless on Forgejo — need sensible defaults. -
pull_request_targetevent: May not exist in Forgejo. The existingcontext.tsmaps it topull_request(line 188), so it degrades gracefully. -
GitHub MCP Server: The
ghcr.io/github/github-mcp-serverDocker image referenced ininstall-mcp-server.ts(line 214) won't work. Skip on Forgejo — it's only included whenhasGitHubMcpToolsis true. -
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):
"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-levelDEFAULT_ACTIONS_URLand 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 installandbun 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 ourmainusinggit 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.ymlmodifications — Addplatforminput toaction.yml. Createsrc/platform/detector.tswith 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.ymldoesn't error on Forgejo.
- Files:
-
M2: Forgejo token handling — Create
src/forgejo/token.tsthat returnsGITHUB_TOKEN/FORGEJO_TOKENdirectly (no OIDC, no Anthropic exchange). Wire intorun.tswith an early platform branch before the existingsetupGitHubToken()call.- Files:
src/forgejo/token.ts(NEW),src/entrypoints/run.ts(MODIFIED — surgical addition at top ofrun()) - Depends on: M1
- Success: Token is available without OIDC on Forgejo. GitHub path unchanged.
- Files:
-
M3: Forgejo CI workflows — Created
.forgejo/workflows/with three workflows adapted for Forgejogruntrunner:ci.yml(3 separate jobs: test, prettier, typecheck using curl-based bun install +xternal/checkout),test-action.yml(agent-mode self-test withFORGEJO_PLATFORM_TEST_OKmarker, runs weekly + on PRs to src/action.yml/.forgejo),upstream-sync.yml(weekly upstream check, creates/updates issue withupstream-synclabel including commit log, diff stat, and merge instructions).- 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.
- Files:
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
FetchDataResulttype 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.
- Files:
-
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/filterReviewsToTriggerTimefunctions (reuse fromsrc/github/data/fetcher.ts— these are pure logic). UsePromise.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.
- Files:
-
M5: Forgejo comment URL formatting — Create
src/forgejo/comment-logic.tswith 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.
- Files:
Phase 3 - Integration (wiring it together)
-
M6: MCP server platform routing — Modified
install-mcp-server.tsto add platform detection. Skips GitHub public MCP server (ghcr.io/github/github-mcp-server) on Forgejo. SkipscheckActionsReadPermissionon Forgejo (workflow token has full repo access). Did NOT createsrc/mcp/forgejo-actions-server.ts— existinggithub-actions-server.tsworks on Forgejo via Octokit REST +GITHUB_API_URL(zero duplication, easier maintenance). Server names remaingithub_comment,github_ci, etc.- Files:
src/mcp/install-mcp-server.ts(MODIFIED),test/install-mcp-server.test.ts(MODIFIED — 6 new Forgejo tests) - Depends on: M4b, M5
- Success: All MCP servers route correctly on both platforms. 772 tests pass.
- Files:
-
M7: Entry point + mode handler integration — Wired platform detection into mode handlers. In
tag/index.ts: platform branch routes tofetchForgejoDatavsfetchGitHubData.createInitialCommentauto-detects platform for Forgejo spinner/URL format.update-comment-link.tsusesupdateForgejoCommentBody+ Forgejo PR compare URL on Forgejo. Agent mode needed NO changes (token already inrun.ts, rest uses Octokit REST). 11 new integration tests intest/m7-integration.test.ts.- Files:
src/modes/tag/index.ts(MODIFIED),src/entrypoints/update-comment-link.ts(MODIFIED),src/github/operations/comments/create-initial.ts(MODIFIED),src/github/operations/comments/update-with-branch.ts(MODIFIED),test/m7-integration.test.ts(NEW) - Depends on: M4b, M5, M6
- Success: All platform routing correct on both platforms. 785 tests pass.
- Files:
Phase 4 - Validation & Polish
-
M8: End-to-end testing on Forgejo — Tested agent mode (workflow_dispatch) end-to-end on Forgejo runner. Fixed multiple issues discovered during E2E: permission check switched to
repos.get()(Forgejo lacks collaborator permission endpoint for workflow tokens), actor check skipped entirely on Forgejo (no bot accounts +read:userscope unavailable),oven-sh/setup-bunreplaced with universal curl install inaction.yml(Forgejo runner pre-fetches alluses:refs even whenif:would skip them), CI test env override forGITHUB_SERVER_URL(module-level constants evaluated at import time). Self-test workflow passes: platform detection, token auth, actor skip, Claude Code execution, output validation all green. CI tests: 794 pass, 0 fail.- Files:
action.yml(MODIFIED — curl bun install),src/github/validation/actor.ts(MODIFIED — Forgejo skip),test/actor.test.ts(MODIFIED — Forgejo tests),.forgejo/workflows/ci.yml(MODIFIED — inline export fix) - Depends on: M7
- Success: Agent mode works on Forgejo. Self-test workflow passes. CI tests pass. Note: tag mode (issue trigger, PR review) not yet E2E tested — code paths are wired but await real-world validation.
- Files:
-
M9: Documentation & upstream merge validation — Created comprehensive
docs/forgejo.mdsetup guide covering prerequisites, quick start (tag + agent mode), platform detection, GitHub vs Forgejo differences (auth, API, bun install, comment URLs), CI workflows, upstream merge process with conflict mitigation, known limitations, and troubleshooting. UpdatedREADME.md(Forgejo in description, feature bullet, docs link),CLAUDE.md(Forgejo architecture section with key files and integration points),docs/usage.md(platforminput in table),docs/faq.md(5 Forgejo FAQ entries). Upstream merge validation:git merge upstream/main→ zero conflicts — branch based on latest upstreamade221f. All 794 tests pass.- Files:
docs/forgejo.md(NEW),README.md(MODIFIED),CLAUDE.md(MODIFIED),docs/usage.md(MODIFIED),docs/faq.md(MODIFIED) - Depends on: M8
- Success: Zero-conflict upstream merge. Complete Forgejo setup documentation with troubleshooting guide.
- Files:
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 |
RESOLVED | Medium | Fixed: Replaced oven-sh/setup-bun entirely with universal curl install in action.yml. Forgejo runner pre-fetches all uses: refs before evaluating if: conditions, so conditional skip was insufficient. Mirror created at xternal/setup-bun for future reference. |
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
- Action runs on Forgejo for issue, PR, and workflow_dispatch triggers
- Claude can read issue/PR context, post comments, create branches, and commit files on Forgejo
git fetch upstream && git merge upstream/mainproduces conflicts only in the 3-4 files we modified, and those conflicts are resolvable in <15 minutes- Self-test workflow validates the action works on every PR
- Upstream sync workflow alerts when new upstream changes are available
- 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_targetevent support- SSH signing on Forgejo (can be added later)
- GitHub MCP Server equivalent for Forgejo (the
ghcr.io/github/github-mcp-serverimage) - Full Forgejo Actions API parity (downloadJobLog may be missing)