GitHub App Installation Flow
详细设计见 openspec/changes/add-connect-repo/design.md。本文是开发期的快速参考。
主流程时序
┌──────────────────┐ ┌───────────────────┐ ┌──────────────────┐
│ Browser │ │ GitHub │ │ NestJS (Fly) │
│ web.douglas-... │ │ github.com │ │ api.douglas-... │
└────────┬─────────┘ └─────────┬─────────┘ └─────────┬────────┘
│ │ │
1. /projects 空态 → 点「连接代码仓库」 │
│ │ │
│ GET /installations/start ──────────────────────────────▶│
│ │ │
│◀── 302 to github.com/apps/<slug>/installations/new?state=...
│ (写 __Host-install_state cookie) │
│ │ │
2. 用户在 GitHub 选 org / 选 repo / 点 Install ─────────────────▶│
│ │ │
│◀── 302 to api/installations/callback?installation_id=...&state=...&setup_action=install
│ │ │
3. callback ─────────────────────────────────────────────────────▶│
│ │ │
│ │◀── App JWT → GET /app/installations/<id> ─
│ │ (拉 account 元数据) │
│ │ │
│ │◀── installation token → GET /installation/repositories
│ │ (per_page=100, 分页拉到 last) │
│ │ │
│ │ ┌─ Postgres ──────┐ │
│ │ │ github_install. │ ◀── upsert │
│ │ │ github_repos │ ◀── upsert │
│ │ └─────────────────┘ │
│◀── 302 web/repos/connect?installation_id=<id> │
│ │ │
4. /repos/connect 渲染 02b 选择器 → 用户多选 → POST /repos/connect ──────────▶│
│ │
5. 后续 GitHub 端任何变更(add repo / remove repo / uninstall):
GitHub → POST /webhooks/github(带 X-Hub-Signature-256)
后端 HMAC 验签 → 更新 DB ─────────────────────────────────────────────────▶│State cookie
| Cookie 名 | 用途 | 属性 |
|---|---|---|
__Host-oauth_state | user OAuth state(已有) | Path=/、HttpOnly、Secure、SameSite=Lax、Max-Age=600 |
__Host-install_state | installation state(本次新增) | 同上 |
两个 cookie 单 host 锁死(api.douglasdong.com),不跨子域。
Webhook 验签
expected = "sha256=" + HMAC_SHA256(rawBody, GITHUB_APP_WEBHOOK_SECRET).hex
verify timingSafeEqual(expected, X-Hub-Signature-256 header)关键约束:必须用 req.rawBody(NestFactory 加 { rawBody: true }),不能用 parsed body 再序列化 —— 字节级不一致签名必然失败。
错误码
?error= | 来源 | 含义 |
|---|---|---|
invalid_install_state | /installations/callback | state cookie 与 query 不匹配,或 setup_action 异常 |
install_failed | /installations/callback | 调 GitHub API 拉元数据/repos 失败 |
repos_unavailable | /repos/connect 页 SSR | /repos 不返回 200,向 /projects 回退 |
pending_request=1 | /repos/connect 页 SSR | 用户没权限直接装 App,向 org admin 发了 install request |
Installation token
- 用
@octokit/auth-app的createAppAuth({...})({ type: 'installation', installationId }) - 进程内 Map 缓存(key: installation_id);过期前 5min 触发刷新
- token 在 GitHub 那边寿命 1 小时
- 多实例部署后需把缓存换成 Redis(当前 Fly 单实例不用考虑)
数据流:UI → API → DB
ConnectRepoCard.tsx
│
│ on toggle on submit
▼ ▼
local Set<id> POST /repos/connect
│
▼
ReposService.markConnected
│
├── assertOwnership(防越权)
└── UPDATE github_repos SET connected_at = now()
WHERE id IN (...) AND connected_at IS NULL