mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-06 19:00:01 +02:00
* fix(github): populate connected account name on install [MUL-3078]
The Settings → GitHub connection card was rendering 'Connected to
unknown' because:
1. fetchInstallationAccount in the setup callback hit GitHub's
/app/installations/{id} endpoint unauthenticated. That endpoint
requires App JWT auth; the call returned 401, and the function
fell through to the 'unknown' placeholder which was persisted as
account_login.
2. The installation webhook handler did upsert the row with the real
login when GitHub later delivered installation.created, but it
never published a github_installation:created event. The frontend
query stayed stale, so the UI kept showing 'unknown' even after
the row had been refreshed.
Fix:
- Add optional GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY env vars. When
set, signGitHubAppJWT mints a short-lived RS256 JWT (back-dated 60s
for clock skew, capped at 9m to stay inside GitHub's 10m max) and
fetchInstallationAccount uses it as a Bearer token. The setup
callback now writes the real org/user name on install.
- When the new env vars are not configured, the call still falls
through to 'unknown' as before — but the webhook handler now
publishes EventGitHubInstallationCreated after the upsert, so the
realtime listener invalidates the installations query and the UI
converges to the real value within seconds, no manual refresh.
Tests cover JWT signing (claims, signature, malformed PEM, partial
config), fetchInstallationAccount with a JWT-gated httptest mock,
and the webhook refresh + broadcast on a seeded 'unknown' row.
Docs updated for .env.example and github-integration /
environment-variables in en, zh, ja, ko.
Co-authored-by: multica-agent <github@multica.ai>
* test(github): defuse JWT clock-bomb by injecting parser time [MUL-3078]
PR review caught that TestSignGitHubAppJWT_ClaimsAndSignature signed the
token with a fixed 'now' (2026-06-05T12:00:00Z) but parsed it with a
default jwt.Parse, which uses real time.Now() for exp validation. Once
real wall clock crossed the token's exp (now + 9m = 12:09:00Z), the
test would have flipped to a deterministic failure on every CI run.
Inject the same fixed 'now' into the parser via jwt.WithTimeFunc so
both signing and validation share one clock. Verified independently
that without the fix the parser rejects the token as 'expired', and
with the fix it accepts.
Also clarified the fetchInstallationAccount comment to be unambiguous
about what 'do not block' actually means: the HTTP call IS synchronous
(no independent timeout, pre-existing wart), but a failure here just
falls back to the unknown placeholder rather than aborting the
callback.
Co-authored-by: multica-agent <github@multica.ai>
---------
Co-authored-by: Eve <eve@multica-ai.local>
Co-authored-by: multica-agent <github@multica.ai>
190 lines
11 KiB
Plaintext
190 lines
11 KiB
Plaintext
---
|
||
title: GitHub 集成
|
||
description: 一次性连接 GitHub App,之后 PR 的分支名、标题或正文里写了 issue 编号(例如 MUL-123),就会自动挂到那个 issue 上——PR 合并时 issue 自动转 Done。
|
||
---
|
||
|
||
import { Callout } from "fumadocs-ui/components/callout";
|
||
|
||
在 **Settings → GitHub** 里一次性连一个 GitHub 账号或组织。之后任何 PR 只要分支名、标题或正文里出现 issue 编号(例如 `MUL-123`),就会**自动关联**到那个 [issue](/issues),出现在 issue 详情页右侧的 **Pull requests** 区块里——PR 合并时,issue 自动转 **Done**。
|
||
|
||
没有 per-issue 的配置,整个流程是「编号驱动」的。
|
||
|
||
## 集成做了什么
|
||
|
||
| 出现位置 | 行为 |
|
||
|---|---|
|
||
| **Settings → GitHub** | 工作区 owner / admin 看到 GitHub 这个 tab,里面有主开关、**Connect GitHub** 按钮,以及功能开关(PR 侧栏、Co-authored-by、auto-link)。点 Connect 会打开 GitHub 的 App 安装页;装好后跳回 GitHub tab。 |
|
||
| **Issue 详情侧栏 → Pull requests** | 列出所有自动关联到该 issue 的 PR,含标题、仓库、状态(`Open` / `Draft` / `Merged` / `Closed`)和作者。点一行跳到 GitHub。 |
|
||
| **Webhook(后台)** | 每次 `pull_request` 事件触发:upsert PR 行 → 扫描里面的 issue 编号 →(重新)建立 link。幂等——重投 delivery 不会产生重复记录。 |
|
||
| **Merge 自动改 status** | PR 转 `merged` 时,所有已关联且状态不是 `Done` / `Cancelled` 的 issue 会被推到 `Done`。时间线里以 source 为 `github_pr_merged` 记录。 |
|
||
|
||
只镜像 PR 本身。Commit、没开 PR 的分支、CI 检查状态都**不**入库——集成有意保持窄边界。
|
||
|
||
## 编号是怎么匹配的
|
||
|
||
Webhook 从三个字段抽取编号,顺序是:**PR head 分支** → **PR 标题** → **PR 正文**。匹配规则:
|
||
|
||
- 大小写不敏感——`mul-123`、`MUL-123`、`Mul-123` 都能匹配
|
||
- 有边界——左侧 `\b`、右侧只接数字,避免误抓 `v1.2-3`、email 地址等
|
||
- 限定到本工作区——只匹配本工作区的 [issue prefix](/workspaces)。前缀是 `MUL` 的工作区里,PR 出现 `FOO-1` 不会匹配,即使数字撞另一个 issue 也不会
|
||
- 自动去重——`Closes MUL-1, MUL-1` 只关联一次
|
||
|
||
一个 PR 里**可以同时引用多个 issue**。比如 `Closes MUL-1, MUL-2`:PR 同时关联两个 issue,合并时两个 issue 都会转 `Done`。
|
||
|
||
## Merge 自动转 Done 的规则
|
||
|
||
PR 的 `merged` 字段翻成 `true` 时,逐个评估关联的 issue:
|
||
|
||
| Issue 当前状态 | 结果 |
|
||
|---|---|
|
||
| `done` | 不变(已经是终态)|
|
||
| `cancelled` | **不变**——cancelled 是用户明确放弃工作的信号,集成不覆盖 |
|
||
| 其他(`todo` / `in_progress` / `in_review` / `blocked` / `backlog`)| 转成 `done` |
|
||
|
||
PR **关闭但没合并**——只更新 PR 卡片的状态为 `Closed`,issue 状态不变。"关闭但不合并"语义因团队而异,Multica 不替用户做决定。
|
||
|
||
<Callout type="info">
|
||
状态变更的 actor 是 `system`。订阅了该 issue 的成员会收到 inbox 通知,和成员手动改状态时一致。
|
||
</Callout>
|
||
|
||
## 哪些情况不会自动关联
|
||
|
||
- **Commit message 里的编号**——只扫 PR 的分支 / 标题 / 正文。一个 commit message 写 `MUL-123: fix login` 不会触发关联,除非同样的字符串也出现在 PR 标题或正文里
|
||
- **PR 评论里的编号**——只扫 PR 自己的元数据,后续的 GitHub comment 不读
|
||
- **App 没安装的仓库里的 PR**——没 App,Multica 收不到 webhook
|
||
- **手动把 PR 关联到 issue**——暂时没有这个 UI。如果你们的约定把编号放到 Multica 不扫的地方,请改放到 PR 标题或正文里
|
||
|
||
## 断开连接
|
||
|
||
**Settings → GitHub** 里没有 installation 列表——现有 installation 直接到 GitHub 上管理:
|
||
|
||
- **从 GitHub 卸载** —— 个人在 `https://github.com/settings/installations`、组织在 `https://github.com/organizations/<org>/settings/installations` 卸载 Multica App。Multica 收到 `installation.deleted` webhook 后立刻删行;任何已打开的 Settings tab 实时更新,不用刷新
|
||
- **Multica 这边的断开是 admin only** —— GitHub tab 上的 Disconnect 控件对非 admin 不显示;主开关关掉时 Disconnect 仍然可用,方便 admin 一键关闭功能后再单独清理已连接的 installation
|
||
|
||
断开之后,已经镜像的 PR 行保留在数据库里——历史 issue 侧栏仍能显示当时关联的 PR,但来自这个 installation 的新 webhook 事件不再被接受。
|
||
|
||
## 权限和可见性
|
||
|
||
- **Connect / Disconnect** 需要工作区 **owner 或 admin**。普通成员能看到卡片描述但看不到 Connect 按钮
|
||
- **Pull requests** 侧栏对所有能看到该 issue 的成员可见——和 issue 详情页其他部分权限一致
|
||
- GitHub App 申请的是 PR 和 Metadata 的 **只读** 权限。Multica 从不向 GitHub 推 commit、评论或 status check
|
||
|
||
## Self-Host 配置
|
||
|
||
如果你在 Multica Cloud 上,集成已经配好——跳过本节。
|
||
|
||
Self-Host 需要:建一个 GitHub App、指向你的 server、设两个环境变量。完整流程如下。
|
||
|
||
### 1. 创建一个 GitHub App
|
||
|
||
到下面其中一个页面:
|
||
|
||
- 个人账号 → `https://github.com/settings/apps/new`
|
||
- 组织 → `https://github.com/organizations/<org>/settings/apps/new`
|
||
|
||
按下表填写:
|
||
|
||
| 字段 | 值 |
|
||
|---|---|
|
||
| **GitHub App name** | 任何能辨识的名字,例如 `Multica` 或 `Multica (staging)` |
|
||
| **Homepage URL** | 你的 Multica 前端,例如 `https://multica.example.com` |
|
||
| **Callback URL** | 留空——本集成不使用 OAuth 用户身份 |
|
||
| **Setup URL** | `https://<api-host>/api/github/setup`。**勾选 "Redirect on update"** |
|
||
| **Webhook → Active** | 启用 |
|
||
| **Webhook URL** | `https://<api-host>/api/webhooks/github` |
|
||
| **Webhook secret** | 生成一个长随机字符串(例如 `openssl rand -hex 32`)。这个值会同样填到 step 2 的 env 里 |
|
||
| **Permissions → Repository → Pull requests** | **Read-only** |
|
||
| **Permissions → Repository → Metadata** | Read-only(必填)|
|
||
| **Subscribe to events** | 勾选 **Pull request** |
|
||
| **Where can this GitHub App be installed?** | 自选。单组织部署建议选 `Only on this account` |
|
||
|
||
点 **Create GitHub App** 之后,从详情页记下两件事:
|
||
|
||
- 顶部 **public link** 的尾部即 slug。`https://github.com/apps/multica-acme` → slug = `multica-acme`
|
||
- 你刚生成的 **webhook secret**(GitHub 之后不会再让你读取这个值——现在就保存好)
|
||
|
||
<Callout type="warning">
|
||
**Webhook secret ≠ Client secret。** App 设置页里两个字段紧挨着。**Webhook secret** 用于签 `pull_request` payload,这才是 Multica 需要的那个;**Client secret** 是 OAuth 用的,和本集成无关。混淆这两个会得到「每条 webhook 都 `401 invalid signature`」的诡异症状。
|
||
</Callout>
|
||
|
||
### 2. 配置环境变量
|
||
|
||
API server 上:
|
||
|
||
```dotenv
|
||
GITHUB_APP_SLUG=multica-acme
|
||
GITHUB_WEBHOOK_SECRET=<你刚生成的 webhook secret>
|
||
|
||
# 可选但建议配置——安装完成时直接拿到关联账号名,而不是等第一次 webhook 才刷新:
|
||
GITHUB_APP_ID=<App 设置页上的数字 App ID>
|
||
GITHUB_APP_PRIVATE_KEY=<完整 PEM 块,包含 BEGIN/END 行>
|
||
```
|
||
|
||
`GITHUB_APP_SLUG` 和 `GITHUB_WEBHOOK_SECRET` 必填。任何一个缺失:
|
||
|
||
- Settings 里 `Connect GitHub` 按钮会被 **disable**,并显示「not configured」提示
|
||
- `/api/webhooks/github` 直接返回 **`503 github webhooks not configured`**——Multica 在 secret 没配置时拒绝处理事件,不会出现「没 secret 也接受 webhook」的安全坑
|
||
|
||
`GITHUB_APP_ID` 和 `GITHUB_APP_PRIVATE_KEY` **可选**。配上之后,setup 回调可以用 App JWT 鉴权调用 GitHub `/app/installations/{id}`,安装完成那一刻就拿到真实的组织名/用户名。不配的话,连接卡片会先显示 `已连接到 unknown`,等 GitHub 的 `installation.created` webhook 到达(通常几秒内),Multica 会刷新 row 并通过 realtime 推送让 Settings 页面无需手动刷新即可更新。私钥从 App 设置页 **Private keys → Generate a private key** 生成,把整段 PEM(含 `-----BEGIN/END RSA PRIVATE KEY-----` 两行)粘到 env 里,保留换行。
|
||
|
||
`FRONTEND_ORIGIN` 也必须设置(任何生产 self-host 都已经设了)——setup 回调结束后用它把用户跳回 `<FRONTEND_ORIGIN>/settings?tab=github`。
|
||
|
||
设完 env 重启 API。
|
||
|
||
### 3. 执行 migration
|
||
|
||
集成的表在 migration `079_github_integration` 里。如果是升级既有部署:
|
||
|
||
```bash
|
||
make migrate-up
|
||
```
|
||
|
||
会创建三张表:`github_installation`、`github_pull_request`、`issue_pull_request`。三张表都 cascade 跟随 workspace——删工作区会自动清理。
|
||
|
||
### 4. 在 UI 里连接
|
||
|
||
到 Multica:
|
||
|
||
1. 以 owner 或 admin 身份打开 **Settings → GitHub**
|
||
2. 点 **Connect GitHub**,GitHub 在新 tab 打开
|
||
3. 选择要授权的仓库,点 **Install**
|
||
4. GitHub 跳回 `<api-host>/api/github/setup`,落库后再跳到 `<FRONTEND_ORIGIN>/settings?tab=github&github_connected=1`
|
||
|
||
之后在任意一个仓库开一个分支 / 标题 / 正文带本工作区 issue 编号的 PR——几秒内对应 issue 的详情页上就能看到 Pull requests 区块。
|
||
|
||
### 5. 用 curl 自检
|
||
|
||
如果 GitHub 的 **Recent Deliveries** 里第一次 PR 事件就报 `401 invalid signature`,说明两边的 secret 不一致。绕过 GitHub 直接测 server 是最快的定位方法:
|
||
|
||
```bash
|
||
SECRET="<你填给 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 状态 | 含义 | 修法 |
|
||
|---|---|---|
|
||
| `200` `{"ok":"pong"}` | server 加载的 secret 和你 `$SECRET` 一致——GitHub 那边的 secret 才是错的 | 编辑 App → Webhook secret 字段**粘相同的值** → **必须点 Save changes**(不点 Save 等于没改)→ Redeliver |
|
||
| `401 invalid signature` | server 加载的 secret **不是**你以为的那个 | 进容器确认 env 实际生效(例如 `kubectl exec` → `echo -n "$GITHUB_WEBHOOK_SECRET" \| wc -c`),重新部署 |
|
||
| `503 github webhooks not configured` | `GITHUB_WEBHOOK_SECRET` 在进程里是空的 | 配上 env,重启 API |
|
||
|
||
## 已知限制
|
||
|
||
目前还没做的几个边界:
|
||
|
||
- **手动 link UI 暂未提供**——关联 PR 的唯一方法是把 issue 编号写到 PR 分支 / 标题 / 正文
|
||
- **不读 CI / check 状态**——只镜像 PR 本身,构建状态、reviewer 评论、reviewer 列表都没接进 Multica
|
||
- **没有工作区级别的 merge → status 映射配置**——默认固定是 `merged → done`(cancelled 除外)。可配置映射是后续迭代
|
||
- **同 issue 多 PR 时,merge 行为偏激进**——两个 PR 都引用 `MUL-123` 时,第一个 merge 就把 issue 转 Done。"等所有关联 PR 都解决再推进 issue 状态"的优化已经在做了
|
||
|
||
## 下一步
|
||
|
||
- [Issues](/issues) —— PR 引用的 issue 编号(`MUL-123`)的来源
|
||
- [工作区](/workspaces) —— 工作区 issue prefix 的设置位置
|
||
- [环境变量](/environment-variables) —— 完整 env 清单,包含上面提到的 GitHub 变量
|