Skip to content

Commit f798a2d

Browse files
anyideaclaude
andcommitted
feat(gitea): add Gitea integration as a sibling to the GitHub integration
Teams that run a private/self-hosted Gitea instance instead of GitHub can now connect it in Settings → Gitea via a Personal/Bot Access Token (Gitea has no App-marketplace install concept), mirror pull requests, and get the same identifier-driven issue auto-link / auto-close-on-merge behavior the GitHub integration provides. Built as a fully independent vertical (own tables, own handler, own sqlc queries, own frontend package, own settings tab, own webhook route keyed by workspace id since Gitea payloads carry no tenant identifier) so it never touches github.go, github.sql, github-tab.tsx, or packages/core/github/ — keeping this fork mergeable with upstream GitHub changes. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 571cdea commit f798a2d

51 files changed

Lines changed: 3931 additions & 4 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ JWT_SECRET=change-me-in-production
4545
MULTICA_APP_URL=${FRONTEND_ORIGIN}
4646
# Public URL the API is reachable at from the open internet (no trailing
4747
# slash). Used to mint absolute webhook URLs for autopilot webhook
48-
# triggers and to show correct daemon setup commands in the web UI. Leave
48+
# triggers, the Gitea integration's repo webhooks, and to show correct
49+
# daemon setup commands in the web UI. Leave
4950
# unset behind a same-origin reverse proxy or for plain localhost dev —
5051
# the frontend will compose the URL from window.origin + webhook_path in
5152
# that case. Headers are intentionally not used to derive this value, to
@@ -270,6 +271,23 @@ GITHUB_WEBHOOK_SECRET=
270271
GITHUB_APP_ID=
271272
GITHUB_APP_PRIVATE_KEY=
272273

274+
# Gitea integration (Settings → Gitea) — for teams running a private/
275+
# self-hosted Gitea instance instead of GitHub. Unlike the GitHub App above,
276+
# there is no marketplace install: each workspace admin pastes a Personal/Bot
277+
# Access Token generated on the Gitea instance, and Multica registers repo
278+
# webhooks via the Gitea API. See docs/gitea-integration.
279+
# GITEA_BASE_URL is the origin of the single Gitea instance this deployment
280+
# talks to, e.g. https://gitea.internal.example.com (no trailing slash).
281+
GITEA_BASE_URL=
282+
# Shared secret Multica sends when registering each repo webhook and verifies
283+
# against the X-Gitea-Signature header on inbound webhook deliveries.
284+
GITEA_WEBHOOK_SECRET=
285+
# Base64-encoded 32-byte key that encrypts each workspace's pasted PAT at
286+
# rest (mirrors MULTICA_SLACK_SECRET_KEY below). Required — without it the
287+
# Gitea integration stays disabled even if the two vars above are set.
288+
# Generate one with: openssl rand -base64 32
289+
MULTICA_GITEA_SECRET_KEY=
290+
273291
# Lark / Feishu bot integration (Settings → Integrations "Bind to Lark")
274292
# Off until MULTICA_LARK_SECRET_KEY is set — a base64-encoded 32-byte key
275293
# that encrypts each Bot's app secret at rest. Leave empty to disable.

apps/docs/content/docs/developers/conventions.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Chinese term reference:
123123

124124
| Category | Terms |
125125
| --- | --- |
126-
| Brands | **Multica**, GitHub, Slack, Google, Anthropic, OpenAI, Claude, Codex, Cursor, Linear, Jira |
126+
| Brands | **Multica**, GitHub, Gitea, Slack, Google, Anthropic, OpenAI, Claude, Codex, Cursor, Linear, Jira |
127127
| Acronyms | API, CLI, URL, SDK, OAuth, JWT, SSO, WebSocket, HTTP, JSON, YAML, SQL |
128128

129129
### Translate fully — concepts

apps/docs/content/docs/developers/conventions.zh.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ Multica 的产品名词分两类:
142142

143143
| 类别 ||
144144
| --- | --- |
145-
| 品牌 | **Multica**、GitHub、Slack、Google、Anthropic、OpenAI、Claude、Codex、Cursor、Linear、Jira |
145+
| 品牌 | **Multica**、GitHub、Gitea、Slack、Google、Anthropic、OpenAI、Claude、Codex、Cursor、Linear、Jira |
146146
| 缩写 | API、CLI、URL、SDK、OAuth、JWT、SSO、WebSocket、HTTP、JSON、YAML、SQL |
147147

148148
### 完整翻译 —— 通用 UI 词

apps/docs/content/docs/environment-variables.mdx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,23 @@ The [GitHub PR ↔ issue integration](/github-integration) needs two variables.
231231

232232
**Note:** `GITHUB_WEBHOOK_SECRET` is reused as the signing key for the install-flow state token, so operators only need to manage one secret. It is **not** the GitHub App's *Client* secret — Client secrets are OAuth-related and not used by this integration. See [GitHub integration → Self-host setup](/github-integration#self-host-setup) for the full walkthrough.
233233

234+
## Gitea integration
235+
236+
The [Gitea PR ↔ issue integration](/gitea-integration) is for teams running a private/self-hosted Gitea instance instead of GitHub. Unlike the GitHub App above, there is no marketplace install — each workspace admin pastes a Personal/Bot Access Token. All three variables below are required to enable it.
237+
238+
| Variable | Default | Description |
239+
|---|---|---|
240+
| `GITEA_BASE_URL` | empty | The origin of the single Gitea instance this deployment talks to, e.g. `https://gitea.internal.example.com` (no trailing slash) |
241+
| `GITEA_WEBHOOK_SECRET` | empty | Shared secret Multica sends when registering each repo webhook and verifies against the `X-Gitea-Signature` header on inbound deliveries |
242+
| `MULTICA_GITEA_SECRET_KEY` | empty | Base64-encoded 32-byte key that encrypts each workspace's pasted PAT at rest. Generate with `openssl rand -base64 32` |
243+
244+
**Behavior when any of the three variables is unset:**
245+
246+
- `Connect Gitea` in Settings → Gitea is **disabled** and shows a "not configured" hint to admins.
247+
- The `/api/webhooks/gitea/{workspaceId}` endpoint returns **`503 gitea webhooks not configured`** when `GITEA_WEBHOOK_SECRET` is unset.
248+
249+
`MULTICA_PUBLIC_URL` (above) is also required — it is used to mint the absolute webhook target URL Multica registers on each repo via the Gitea API.
250+
234251
## Usage analytics
235252

236253
By default, the server reports to Multica's official PostHog instance. To opt out, set `ANALYTICS_DISABLED=true`.
@@ -245,5 +262,6 @@ By default, the server reports to Multica's official PostHog instance. To opt ou
245262

246263
- [Sign-in and signup configuration](/auth-setup) — how to actually configure the auth-related variables above and where the traps are
247264
- [GitHub integration](/github-integration) — how to set up the GitHub App that backs `GITHUB_APP_SLUG` / `GITHUB_WEBHOOK_SECRET`
265+
- [Gitea integration](/gitea-integration) — how to set up the Gitea PAT connection that backs `GITEA_BASE_URL` / `GITEA_WEBHOOK_SECRET`
248266
- [Troubleshooting](/troubleshooting) — symptoms and fixes for common misconfigurations
249267
- [Daemon and runtimes](/daemon-runtimes) — what the `MULTICA_DAEMON_*` parameters actually do
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
title: Gitea integration
3+
description: Connect a self-hosted Gitea instance with a Personal Access Token, then PRs whose branch, title, or body reference an issue identifier auto-attach to that issue — and merging the PR moves the issue to Done.
4+
---
5+
6+
import { Callout } from "fumadocs-ui/components/callout";
7+
8+
Connect a private/self-hosted Gitea instance once in **Settings → Gitea** by pasting a Personal or Bot Access Token. After that, any pull request whose branch name, title, or body contains an issue identifier (for example `MUL-123`) is **auto-linked** to that [issue](/issues), appears under **Pull requests (Gitea)** in the issue sidebar, and — when the PR is merged — moves the issue to **Done**.
9+
10+
This mirrors the [GitHub integration](/github-integration) closely, but the connection model is different: Gitea has no App-marketplace "installation" concept, so Multica uses a workspace-admin-pasted Access Token instead, and registers repo webhooks itself via the Gitea API rather than relying on GitHub's install-time repo grant.
11+
12+
<Callout type="info">
13+
If your team uses GitHub instead, see the [GitHub integration](/github-integration) doc — the two integrations are fully independent and can both be connected in the same workspace if needed.
14+
</Callout>
15+
16+
## What the integration does
17+
18+
| Surface | Behavior |
19+
|---|---|
20+
| **Settings → Gitea** | Workspace admins see the Gitea tab with a master toggle, **Connect Gitea** dialog (paste an Access Token), a **Sync repositories** action, and the auto-link feature switch. |
21+
| **Issue sidebar → Pull requests (Gitea)** | Every PR auto-linked to this issue, with title, repo, state (`Open` / `Draft` / `Merged` / `Closed`), and author. Click a row to jump to the PR on Gitea. |
22+
| **Webhook (background)** | On every `pull_request` event, Multica upserts the PR row, scans the PR for issue identifiers, and (re)builds the link rows. Idempotent — replaying a delivery is a no-op. |
23+
| **Auto-status on merge** | When a PR transitions to `merged`, every linked issue not already `Done` or `Cancelled` is moved to `Done`. The status change is timeline-logged with source `gitea_pr_merged`. |
24+
25+
Only the PR itself is mirrored. Commits, branch refs without an open PR, and CI check states are **not** modeled in this integration (Gitea's CI sources vary — Actions, Drone, Jenkins — with no unified webhook shape).
26+
27+
## Repository scope
28+
29+
Gitea has no per-installation repo grant like GitHub Apps do, so Multica reuses the workspace's existing **Repositories** list (Settings → Repositories — the same list the agent daemon clones from) as the source of truth for which repos to subscribe to:
30+
31+
- Any repo URL in that list whose host matches your configured `GITEA_BASE_URL` is a sync candidate.
32+
- **Sync repositories** (on the Gitea tab, or automatically once right after connecting) registers a webhook on every matching repo that doesn't have one yet, and removes webhooks for repos no longer in the list.
33+
- Re-run **Sync repositories** any time you add or remove a repo from the Repositories list — it does not happen automatically on every edit.
34+
35+
## How identifiers are matched
36+
37+
Identical matching rules to the GitHub integration: the webhook extracts identifiers from **PR head branch**, **PR title**, and **PR body**, case-insensitively, scoped to the workspace's own [issue prefix](/workspaces), and deduplicated. You can reference multiple issues in one PR — `Closes MUL-1, MUL-2` links both, and merging advances both to `Done`.
38+
39+
## The auto-merge-to-Done rule
40+
41+
When a PR's `merged` field flips to `true`, every linked issue is evaluated:
42+
43+
| Issue current status | Result |
44+
|---|---|
45+
| `done` | No change (already terminal). |
46+
| `cancelled` | **No change** — cancelled means the user explicitly abandoned the work; the integration does not override that signal. |
47+
| Anything else (`todo`, `in_progress`, `in_review`, `blocked`, `backlog`) | Moved to `done`. |
48+
49+
Closing a PR **without** merging it only updates the PR card's state to `Closed`. The linked issues stay where they were.
50+
51+
## Disconnecting
52+
53+
The Disconnect control on the Gitea tab is admin-only and removes every webhook Multica registered (best-effort — a repo that's unreachable at disconnect time just leaves an orphaned webhook on the Gitea side, harmless since Multica has already forgotten the secret) before deleting the connection. Mirrored PR rows stay in the database so historical issue sidebars still show what was linked.
54+
55+
## Permissions and visibility
56+
57+
- **Connect / disconnect / sync** require workspace **owner or admin**. Members see the connection card but no management controls.
58+
- The **Pull requests (Gitea)** sidebar on an issue is visible to anyone who can read the issue.
59+
- The Access Token you paste should be scoped to **read/write repository** access (read to see PRs, write to let Multica register webhooks) — a token with broader admin scope is not required.
60+
61+
## Self-host setup
62+
63+
Gitea support is self-host only — it targets a private Gitea instance you control, so there is nothing to configure on Multica Cloud.
64+
65+
### 1. Generate a Personal or Bot Access Token
66+
67+
In your Gitea instance: **Settings → Applications → Generate New Token**. Grant it `repo` (read/write) scope. A dedicated bot account (rather than a personal account) is recommended so the "connected by" identity and webhook ownership survive an individual leaving the team.
68+
69+
### 2. Set environment variables
70+
71+
On the API server:
72+
73+
```dotenv
74+
GITEA_BASE_URL=https://gitea.internal.example.com
75+
GITEA_WEBHOOK_SECRET=<a long random string, e.g. `openssl rand -hex 32`>
76+
MULTICA_GITEA_SECRET_KEY=<base64 32-byte key, e.g. `openssl rand -base64 32`>
77+
```
78+
79+
All three are required. If any is missing:
80+
81+
- `Connect Gitea` in Settings is **disabled** and shows a "not configured" hint.
82+
- The `/api/webhooks/gitea/{workspaceId}` endpoint returns **`503 gitea webhooks not configured`** when `GITEA_WEBHOOK_SECRET` is unset — Multica refuses to process events with no secret, rather than silently treating every signature as valid.
83+
84+
`MULTICA_PUBLIC_URL` must also be set — it's used to mint the absolute webhook URL Multica registers on each repo via the Gitea API.
85+
86+
Restart the API after setting the env vars.
87+
88+
### 3. Run migrations
89+
90+
The integration ships its tables in migration `167_gitea_integration`. If you're upgrading an older deployment:
91+
92+
```bash
93+
make migrate-up
94+
```
95+
96+
Four tables get created: `gitea_connection`, `gitea_webhook`, `gitea_pull_request`, `issue_gitea_pull_request`. They cascade-delete with their workspace.
97+
98+
### 4. Connect from the UI
99+
100+
1. Open **Settings → Gitea** as an owner or admin.
101+
2. Click **Connect Gitea** and paste the Access Token from step 1.
102+
3. Add the repo(s) you want synced under **Settings → Repositories** (if not already there).
103+
4. Click **Sync repositories** — Multica registers a webhook on each matching repo via the Gitea API.
104+
105+
After that, open any PR whose branch / title / body contains an issue identifier — within a few seconds the **Pull requests (Gitea)** block appears on that issue's detail page.
106+
107+
### 5. Verify with a curl probe
108+
109+
If a webhook delivery in Gitea's repo settings reports a failure, the fastest way to isolate which side is wrong is to bypass Gitea. Note Gitea's `X-Gitea-Signature` header is a **raw hex HMAC-SHA256**, unlike GitHub's `sha256=`-prefixed `X-Hub-Signature-256`:
110+
111+
```bash
112+
SECRET="<the value you put in GITEA_WEBHOOK_SECRET>"
113+
WORKSPACE_ID="<the workspace's UUID>"
114+
BODY='{}'
115+
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
116+
curl -i -X POST "https://<api-host>/api/webhooks/gitea/$WORKSPACE_ID" \
117+
-H "X-Gitea-Signature: $SIG" \
118+
-H "X-Gitea-Event: ping" \
119+
-H "Content-Type: application/json" \
120+
-d "$BODY"
121+
```
122+
123+
| HTTP status | Meaning | Fix |
124+
|---|---|---|
125+
| `200` `{"ok":"pong"}` | Server's loaded secret matches your `$SECRET`. The mismatch is on the Gitea side. | Edit the repo's webhook → **Secret** → paste the same value → **Update Webhook**. |
126+
| `401 invalid signature` | Server's loaded secret is **not** what you think it is. | Confirm the env var landed in the running process. Re-deploy. |
127+
| `503 gitea webhooks not configured` | `GITEA_WEBHOOK_SECRET` is empty in the process. | Set the env var, restart the API. |
128+
| `400 workspace id` | The workspace UUID in the URL is malformed. | Double-check the UUID; the webhook URL Multica registers is always `.../api/webhooks/gitea/<uuid>`. |
129+
130+
## Limitations
131+
132+
- **No manual link UI yet** — the only way to link a PR is to have the identifier in its branch, title, or body.
133+
- **No CI / check state** — only the PR itself is mirrored.
134+
- **Repository scope requires an explicit sync** — unlike GitHub's install-time repo grant, adding a repo to the workspace's Repositories list does not auto-register a webhook; click **Sync repositories** afterward.
135+
- **Single Gitea instance per deployment**`GITEA_BASE_URL` is one env var for the whole Multica deployment, not per-workspace. Every workspace that connects Gitea connects to the same instance (with its own token).
136+
137+
## Next
138+
139+
- [Issues](/issues) — the issue identifiers (`MUL-123`) referenced from PRs
140+
- [Workspaces](/workspaces) — where the workspace-specific issue prefix is set
141+
- [Environment variables](/environment-variables) — full env reference, including the Gitea variables above
142+
- [GitHub integration](/github-integration) — the equivalent flow for GitHub

0 commit comments

Comments
 (0)