Skip to content

Gitea and Forgejo behind the code tools, with a self-hosted server URL (OP2-84) - #16

Merged
BrewingCoder merged 1 commit into
mainfrom
claude/op2-84-gitea-forgejo
Nov 5, 2026
Merged

BrewingCoder merged 1 commit into
mainfrom
claude/op2-84-gitea-forgejo

Conversation

@BrewingCoder

@BrewingCoder BrewingCoder commented Nov 4, 2026 •

Copy link
Copy Markdown
Owner

Implements OP2-84: Gitea / Forgejo as a code provider with a self-hosted base URL, covering the whole GitHub tool surface (repos, commits, issues, pull requests, triage, files).

Shape

It plugs in the way Gmail joined mail, so none of the tools have to know which provider they're talking to:

  • ProviderKind.Gitea is one provider for both Gitea and Forgejo, since Forgejo serves the same /api/v1. It's stored by name like the other providers, so no migration.
  • ICodeProvider : IGitHubService plus CodeServiceRouter (src/OutPost2.Infrastructure/Code/CodeProviders.cs). These do dispatch only. Two providers registered for one kind is a startup error, and an unsupported kind gets a one-sentence GitHubException instead of a call made with the wrong token. GitHubService is now one ICodeProvider; IGitHubService resolves to the router, so CodeTools and RepoResolver are unchanged apart from the connection filter.
  • The tools keep their github_* names. Renaming them would break every MCP client already configured. Their descriptions now mention Gitea and Forgejo. GitHubException is still the single exception type the code tools catch, documented as historical.
  • Capabilities: Gitea declares code.read and code.write. Read starts on and write starts off, as for GitHub. CodeTools now accepts GitHub or Gitea connections, so the per-provider-default rules apply unchanged.

Gitea is not wire-compatible with GitHub: the differences, all in GiteaService

GitHub Gitea / Forgejo
search/issues?q=review-requested:@me … repos/issues/search?type=pulls&review_requested=true (also created, assigned, mentioned). review_requested needs Gitea 1.20+; that's in the README
repo: qualifier in the query No repository filter on search. A per-repository personal search asks by owner and keeps only the matching repository.full_name, paging until it has enough
total_count in the body X-Total-Count header. Triage counts use limit=1
draft pull_request.draft on newer servers, or a WIP:/[WIP] title. That's Gitea's own convention, and the only signal on older servers
per_page=100 limit, capped at the server's MAX_RESPONSE_ITEMS (50 by default). Repository listing stops at the same 500 ceiling as GitHub
pushed_at No equivalent. updated_at moves on a push, so it's used instead and the list is sorted client-side
Authorization: Bearer Authorization: token …, which every release accepts
raw media type repos/{repo}/raw/{path}, for files the contents API doesn't inline
contents/ at the root contents with nothing after it

Unassigned and all-open questions without a repository are refused, as on GitHub. Repo-scoped all-open and unassigned use repos/{repo}/issues?type=…. A 409 on commits is an empty repository. 404, 401 and 403 each get their own sentence naming the host.

Connecting

  • GiteaConnectionService.Normalise takes what a person types: git.example.com, a URL with a trailing slash, the API URL itself, a server under a sub-path (https://example.com/git), or http://ip:port. It returns …/api/v1/. A blank address is refused, because there is no public default host.
  • The probe calls GET /user and gives a fix for each failure:
    • 401: the token was rejected.
    • 403: the token needs the read:user scope, plus read:repository and read:issue.
    • 404: there's no Gitea/Forgejo API at that address.
    • An HTML response: something answered, but it isn't the API.
  • It also reads /version and logs whether the server is Gitea or Forgejo (Forgejo reports …+gitea-…). Nothing behaves differently based on it.
  • Web changes:
    • A new Gitea / Forgejo option in the add-connection dialog. The server field is required and there's a token scope hint.
    • POST /connections/gitea/add, replace-token support, and the health check.
    • The repo-names page and endpoint include Gitea connections.
    • The voice-phrase preview has a Gitea entry.
  • There's a new gitea HTTP client with the private-CA handler (OP2-45), since these servers are self-hosted by definition.

Tests

tests/OutPost2.Tests/Code/GiteaTests.cs has 38 tests, all against mocked HTTP:

  • URL normalisation (7 forms, plus blank).
  • Connect: token header, the stored provider and instance, the capabilities, and that a failed connect writes nothing.
  • Each probe failure's message, including an HTML page.
  • Triage counts from the header, with draft detection by flag and by WIP:.
  • The flag for each scope.
  • The owner-plus-filter path for a single repository.
  • Unassigned filtering in a repository, and refusing unscoped all-open queries.
  • Repositories: paging and ordering.
  • Commits: the author fallback and the empty-repository 409.
  • An item lookup that finds a PR through the issues endpoint.
  • Contents: the root route, path encoding, base64 decoding, the raw fallback, binary and submodule refusals, and a directory asked for as a file.
  • The router: dispatch, unsupported provider, duplicate registration.
  • CodeTools choosing a Gitea connection and answering github_triage from it end to end.

Full suite (Linux, .NET 10.0.403, local Postgres): 1042 passed, 20 failed, 52 skipped. The 20 failures are the FormBindingTests/OAuthFlowTests web-page tests that fail the same way on unmodified main in this environment (HTTP 500 without Vault/Garage); this branch adds none. Since those page tests can't render here, the new add-connection form hasn't been rendered. It copies the YouTrack and GitHub forms' markup.

Not done / worth knowing

https://claude.ai/code/session_01V7aurnqD63TPgNSBec8xrk


Generated by Claude Code

One connector covers both, since Forgejo serves Gitea's /api/v1. It works
the way Gmail joined mail: a new ProviderKind.Gitea (stored by name, so no
migration), an ICodeProvider per code host, and a CodeServiceRouter that the
code tools and the repo resolver get as IGitHubService. The github_* tools
keep their names and now serve either host.

- GiteaConnectionService takes a server URL (required; there is no public
  default) in any form a person types, sub-path included, and normalises it
  to the /api/v1 base. It probes /user with a "token" authorization header
  and names the fix for 401, 403, 404, or a web page where the API should
  be. As for GitHub, read is on and write is off.
- GiteaService maps Gitea's API onto the same models. "Awaiting my review",
  "mine", "assigned" and "mentioned" are flags on repos/issues/search.
  Counts come from X-Total-Count. Per-repository personal searches go by
  owner and are filtered by name, because the endpoint has no repository
  filter. Drafts are the draft flag or a "WIP:" title. Contents use
  "contents" at the root and the raw route for files the API won't inline.
- Connections page: an "add Gitea / Forgejo" form, replace-token, and a
  health check. The repo-names page includes Gitea connections. The
  "gitea" HTTP client gets the private-CA handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7aurnqD63TPgNSBec8xrk
@BrewingCoder
BrewingCoder merged commit 49de4a6 into main Nov 5, 2026
1 check passed
BrewingCoder added a commit that referenced this pull request Nov 5, 2026
OP2-285: a Google reconnect now forgets the remembered "this account has no
readable zone". That answer is cached for an hour so every listing does not
make a doomed call, but reconnecting is exactly what grants the Calendar
settings scope — the README tells people to do it — so keeping the old answer
made the fix appear to do nothing for up to an hour.

OP2-84: Normalise cut the server path at the first place the four characters
"/api" appeared, not at the /api segment, so a server under a sub-path of its
own such as https://example.com/api-git normalised to the wrong root and every
call went to https://example.com/api/v1/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V7aurjxxqEYmeNDuAinHyQ
@BrewingCoder
BrewingCoder deleted the claude/op2-84-gitea-forgejo branch November 5, 2026 01:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants