Files
claude-code-action/prds/1-forgejo-platform-support.md
T
Vlad MocanuandClaude Opus 4.6 0802ac18eb feat(prd-1): implement M5 - Forgejo comment URL formatting
- Create src/forgejo/comment-logic.ts with Forgejo-specific URL builders:
  - Unicode spinner (replaces GitHub CDN image)
  - PR compare URL with expand=1 (no quick_pull=1)
  - Dynamic extractOwnerRepoFromUrl() fixing hardcoded github.com regex
  - updateForgejoCommentBody() with cross-platform URL extraction
- 41 tests covering all URL patterns, spinner, and the critical regex fix
- No upstream files modified (lean adapter approach)

Progress: 56% complete (5/9) - next: M6 (MCP server platform routing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 17:48:16 +02:00

24 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 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:

// 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

  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):

"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)