Files
multica/apps/docs/content/docs/github-integration.mdx
Multica Eve aa110aac9c MUL-5088: import repositories from GitHub App (#5896)
* feat(github): import repositories from app installations

Co-authored-by: multica-agent <github@multica.ai>

* fix(github): address repository import review nits

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-28 16:19:35 +08:00

243 lines
18 KiB
Plaintext

---
title: GitHub integration
description: Connect a GitHub App once, 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.
---
import { Callout } from "fumadocs-ui/components/callout";
Connect a GitHub account or organization once in **Settings → GitHub**. 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** in the issue sidebar, and — when the PR is merged — moves the issue to **Done**.
There is no per-issue setup. The whole flow is identifier-driven.
## What the integration does
| Surface | Behavior |
|---|---|
| **Settings → GitHub** | Workspace admins see the GitHub tab with a master toggle, **Connect GitHub** button, and feature switches (PR sidebar, Co-authored-by, auto-link). After install you bounce back to the GitHub tab. |
| **Issue sidebar → Pull requests** | 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 GitHub. |
| **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. |
| **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 `github_pr_merged`. |
| **PR card → CI & mergeability** | For each linked PR, Multica fetches an authenticated GitHub API snapshot and mirrors its **CI status** and **mergeability** onto the card (see **What the PR card shows** below). |
Alongside the PR's own state, the integration mirrors — for each linked PR — its **CI status** (all checks passed, N failed with the failing check names, or N running) and its **mergeability** (ready to merge, has conflicts, blocked, or behind), fetched from an authenticated GitHub API snapshot. Commits and branch refs without an open PR are still not modeled.
## What the PR card shows
Each linked PR's card carries two **independent** elements, each built from an authenticated GitHub API snapshot:
- **CI status** — one of: **all checks passed** (with the count, e.g. `7/7`), **N failed** (naming the failing checks), or **N running** (still in progress). A PR with no checks configured yet shows neither — "no checks" is never reported as passed.
- **Mergeability** — one of: **ready to merge** (shown only when GitHub reports the merge state is clean, i.e. no conflicts *and* branch protection satisfied), **has conflicts**, **blocked**, or **behind**.
The two never mask each other — a PR can show failed checks *and* a merge conflict at the same time. Merged and closed PRs show neither; the row's own state icon already says so.
The snapshot is fetched from the GitHub API and refreshed on webhook events (`pull_request`, `check_run`, `check_suite`, `status`), when you open the card, and periodically while the PR is still open and undecided. During a temporary GitHub outage the card keeps showing the last-known snapshot marked **stale** rather than going blank.
## Multiple workspaces
One GitHub App installation can be connected in **several** workspaces at once — for example when different teams in the same organization run separate workspaces. When it is, every connected workspace receives each repository's `pull_request` events **independently**:
- Each workspace mirrors the PR and auto-links it against **its own** [issue prefix](/workspaces) and GitHub feature switches. A PR that references `MUL-1` and `ENG-2` links `MUL-1` in the workspace whose prefix is `MUL` and `ENG-2` in the one whose prefix is `ENG` — neither sees the other's issues.
- Disconnecting the installation from one workspace only stops **that** workspace from receiving events; the others keep working.
Repository scope is whatever you granted the GitHub App (all repositories, or a chosen subset). Connecting the installation in a workspace is itself the subscription — there is no separate per-repository selection inside Multica.
<Callout type="warning">
**Upgrading a self-host deployment:** event delivery is keyed on the GitHub connection, not on a workspace's code-repository list. If a workspace previously received a repo's PRs *without* connecting GitHub in that workspace (by listing the repo in its code-repositories entry alone), it will stop receiving events after this upgrade. To restore delivery, **connect the same GitHub installation** in that workspace under **Settings → GitHub**.
</Callout>
## How identifiers are matched
The webhook extracts identifiers from three fields, in this order: **PR head branch**, **PR title**, **PR body**. The matcher is:
- Case-insensitive — `mul-123`, `MUL-123`, `Mul-123` all match.
- Bounded — a `\b` on the left and a digit anchor on the right keep it from grabbing version numbers like `v1.2-3` or email-style strings.
- Workspace-scoped — only matches the workspace's own [issue prefix](/workspaces). `FOO-1` in a workspace whose prefix is `MUL` is ignored, even if the integer matches another issue.
- Deduplicated — listing `MUL-1, MUL-1` in the body links the issue once.
You can reference **multiple issues** in one PR. `Closes MUL-1, MUL-2` links the PR to both, and merging it advances both to `Done`.
## The auto-merge-to-Done rule
When a PR's `merged` field flips to `true`, every linked issue is evaluated:
| Issue current status | Result |
|---|---|
| `done` | No change (already terminal). |
| `cancelled` | **No change** — cancelled means the user explicitly abandoned the work; the integration does not override that signal. |
| Anything else (`todo`, `in_progress`, `in_review`, `blocked`, `backlog`) | Moved to `done`. |
Closing a PR **without** merging it only updates the PR card's state to `Closed`. The linked issues stay where they were — the user is the one who decides what closing-without-merge means.
<Callout type="info">
The action is attributed to the `system` actor on the timeline. Subscribers of the issue receive an inbox notification for the status change, the same way they would if a human had moved it.
</Callout>
## What's not auto-linked
- **Identifiers in commit messages** — only branch / title / body are scanned. A commit titled `MUL-123: fix login` does not auto-link unless the same string also appears in the PR title or body.
- **Identifiers in PR comments** — only the PR's own metadata is scanned; later GitHub comments are ignored.
- **PRs in repos the App isn't installed on** — without the App, Multica never receives the webhook.
- **Manually linking a PR to an issue** — there is no UI for this yet. If your team's convention puts identifiers in a place Multica isn't reading, add them to the PR title or body.
## Disconnecting
In **Settings → GitHub** there is no installation list — you manage existing installations from GitHub directly:
- **From GitHub** — uninstall the Multica GitHub App at `https://github.com/settings/installations` (personal) or `https://github.com/organizations/<org>/settings/installations` (org). Multica receives the `installation.deleted` webhook and drops the row in real time; any open Settings tab updates without a refresh.
- **Disconnect from inside Multica is admin-only** — the Disconnect control on the GitHub tab is hidden for non-admins. It stays available even when the master GitHub switch is off, so admins can still revoke a stale installation after one-click-disabling the feature.
After disconnect, mirrored PR rows stay in the database so historical issue sidebars still show what was linked, but no new webhook events from that installation will be accepted.
## Permissions and visibility
- **Connect / disconnect** require workspace **owner or admin**. Members see the card description but no Connect button.
- The **Pull requests** sidebar on an issue is visible to anyone who can read the issue — same permissions as the rest of issue detail.
- The GitHub App requests **read-only** access to pull requests, checks, commit statuses, and metadata. Multica never pushes commits, comments, or status checks back to GitHub — the CI status and mergeability shown on the PR card are fetched read-only, never written.
## Self-host setup
If you're running Multica on Multica Cloud, the integration is already configured — skip this section.
For self-host, you create one GitHub App, point it at your server, and set its environment variables. The whole flow is below.
### 1. Create a GitHub App
Go to one of:
- Personal account → `https://github.com/settings/apps/new`
- Organization → `https://github.com/organizations/<org>/settings/apps/new`
Fill in:
| Field | Value |
|---|---|
| **GitHub App name** | Anything recognizable, e.g. `Multica` or `Multica (staging)`. |
| **Homepage URL** | Your Multica frontend, e.g. `https://multica.example.com`. |
| **Callback URL** | Leave blank — Multica doesn't use OAuth user identity. |
| **Setup URL** | `https://<api-host>/api/github/setup`. **Check "Redirect on update"**. |
| **Webhook → Active** | Enabled. |
| **Webhook URL** | `https://<api-host>/api/webhooks/github`. |
| **Webhook secret** | Generate a long random string (e.g. `openssl rand -hex 32`). You'll paste the same value into Multica's env in step 2. |
| **Permissions → Repository → Pull requests** | **Read-only**. |
| **Permissions → Repository → Checks** | **Read-only** — powers the PR card's CI status. |
| **Permissions → Repository → Commit statuses** | **Read-only** — the legacy status-context half of the CI rollup. |
| **Permissions → Repository → Metadata** | Read-only (mandatory). |
| **Subscribe to events** | Tick **Pull request**, **Check suite**, **Check run**, and **Status**. |
| **Where can this GitHub App be installed?** | Your choice. `Only on this account` is fine for single-org setups. |
<Callout type="warning">
**Grant read, never write, and nothing more.** Do **not** grant **Checks: Read and write** — write access makes GitHub expect this App to report its own check runs, which leaves them stuck permanently pending. Read-only is correct. The feature also does **not** need **Actions: read** or **Contents: read** — don't add them.
</Callout>
<Callout type="warning">
**Adding these permissions to an App that's already installed needs approval.** When you extend an existing App with Checks, Commit statuses, Check run, and Status, GitHub holds the change as a **pending permission request** — it does not take effect until each installation's owner approves it (on GitHub, under that installation's settings). Until an installation approves, the PR card stays without CI or merge status on its repos.
</Callout>
After **Create GitHub App**, note two things from the App's detail page:
- The **public link** at the top — its tail is the slug. `https://github.com/apps/multica-acme` → slug = `multica-acme`.
- The **webhook secret** you just generated (you can't read it back from GitHub later — save it now).
<Callout type="warning">
**Webhook secret ≠ Client secret.** The App settings page has both fields stacked together. The **Webhook secret** is what signs `pull_request` payloads — that's the one Multica needs. The **Client secret** is for OAuth and is not used by this integration. Mixing them up produces a confusing `401 invalid signature` on every webhook delivery.
</Callout>
### 2. Set environment variables
On the API server:
```dotenv
GITHUB_APP_SLUG=multica-acme
GITHUB_WEBHOOK_SECRET=<the webhook secret you generated>
# Required for the PR card's CI status and mergeability. Without them the
# integration still mirrors PRs and auto-links / auto-closes issues, but the
# PR card shows no CI or merge status:
GITHUB_APP_ID=<numeric App ID from the App's settings page>
GITHUB_APP_PRIVATE_KEY=<full PEM block, including BEGIN/END lines>
```
`GITHUB_APP_SLUG` and `GITHUB_WEBHOOK_SECRET` are required for the integration to start at all. If either is missing:
- `Connect GitHub` in Settings is **disabled** and shows a "not configured" hint.
- The `/api/webhooks/github` endpoint returns **`503 github webhooks not configured`** — Multica refuses to process events with no secret, rather than silently treating every signature as valid.
`GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` are required for both **PR-card CI status and mergeability** and **Settings → Repositories → Choose from GitHub**. They let Multica authenticate as the App (App JWT → installation token), pull each PR card's GitHub API snapshot, and mint a short-lived metadata-only token to list an installation's repositories. Generate the key under **Private keys → Generate a private key** on the App's settings page; paste the full PEM block (including the `-----BEGIN/END RSA PRIVATE KEY-----` lines) into the env var, preserving newlines. Without them the integration **degrades cleanly**: PRs still mirror, issues still auto-link and move to Done on merge, and the connected account name still resolves from the `installation.created` webhook — but the PR card shows no CI or merge status and repository browsing is disabled.
`FRONTEND_ORIGIN` must also be set (it already is for any production self-host); the setup callback bounces the user back to `<FRONTEND_ORIGIN>/settings?tab=github` after install.
Restart the API after setting the env vars.
### 3. Run migrations
The integration ships its tables in migration `079_github_integration`. If you're upgrading an older deployment:
```bash
make migrate-up
```
Three tables get created: `github_installation`, `github_pull_request`, `issue_pull_request`. They cascade-delete with their workspace, so removing a workspace cleans them up automatically.
### 4. Connect from the UI
In Multica:
1. Open **Settings → GitHub** as an owner or admin.
2. Click **Connect GitHub**. GitHub opens in a new tab.
3. Pick the repositories to grant access to and **Install**.
4. GitHub redirects back to `<api-host>/api/github/setup`, which records the installation and bounces you to `<FRONTEND_ORIGIN>/settings?tab=github&github_connected=1`.
After that, open any PR whose branch / title / body contains an issue identifier — within a few seconds the Pull requests block appears on that issue's detail page.
### 5. Verify with a curl probe
If GitHub's **Recent Deliveries** page reports `401 invalid signature` after install, the two sides have different secrets. The fastest way to find out which side is wrong is to bypass GitHub:
```bash
SECRET="<the value you put in GITHUB_WEBHOOK_SECRET>"
BODY='{"zen":"test"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | awk '{print $NF}')
curl -i -X POST https://<api-host>/api/webhooks/github \
-H "X-Hub-Signature-256: sha256=$SIG" \
-H "X-GitHub-Event: ping" \
-H "Content-Type: application/json" \
-d "$BODY"
```
| HTTP status | Meaning | Fix |
|---|---|---|
| `200` `{"ok":"pong"}` | Server's loaded secret matches your `$SECRET`. The mismatch is on GitHub. | Edit the App → Webhook secret → **paste the same value** → **Save changes** (clicking out of the field without Save keeps the old secret). Redeliver. |
| `401 invalid signature` | Server's loaded secret is **not** what you think it is. | Confirm the env var landed in the running process (e.g. `kubectl exec` → `echo -n "$GITHUB_WEBHOOK_SECRET" | wc -c`). Re-deploy. |
| `503 github webhooks not configured` | `GITHUB_WEBHOOK_SECRET` is empty in the process. | Set the env var, restart the API. |
**Confirm the App's events and permissions.** The public App-metadata endpoint reports exactly what you granted — no auth needed:
```bash
curl -s https://api.github.com/apps/<slug>
```
- The `events` array should include `pull_request`, `check_suite`, `check_run`, and `status`.
- The `permissions` object should include `checks: read` and `statuses: read`, alongside `pull_requests: read` and `metadata: read`.
If any are missing, fix the App's **Subscribe to events** / **Permissions** — and remember that adding permissions to an already-installed App only takes effect once each installation's owner approves the pending request.
**"PR card shows no CI or merge status."** The card mirrors the PR fine but never shows checks or mergeability:
- Confirm `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY` are set in the running API process — they are what authenticate the snapshot fetch.
- Confirm each installation has **approved the pending permission request** for Checks and Commit statuses (see the probe above).
## Limitations
A few rough edges to be aware of today:
- **No manual link UI yet** — the only way to link a PR is to have the identifier in its branch, title, or body.
- **No review threads or reviewer list** — CI status and mergeability are now surfaced on the PR card (see **What the PR card shows**), but review comments, individual reviewers, and their approval state are not.
- **No workspace-level config** for the merge → Done rule — it's a fixed default (`merged → done`, unless `cancelled`). Workspace-customizable mappings are a future addition.
- **Multi-PR-to-one-issue is conservative on merge** — if two PRs both reference `MUL-123` and the first one merges, the issue is moved to `Done` immediately. A follow-up change to wait for all linked PRs to resolve before advancing is in progress.
## Next
- [Issues](/issues) — the issue identifiers (`MUL-123`) referenced from PRs
- [Workspaces](/workspaces) — where the workspace-specific issue prefix is set
- [Environment variables](/environment-variables) — full env reference, including the GitHub variables above