跳到主要内容

第 18 章:GitHub、VCS 与代码托管闭环

18.1 为什么 GitHub 不放进通用 VCS Adapter

源码给出了明确理由:

  • GitHub 使用 App Installation 和短期 Installation Token;
  • Forgejo/Gitea/GitLab 使用用户提供的长期 Access Token;
  • GitHub CI 是 Check Suite;
  • Forgejo/Gitea 是 Commit Status;
  • GitLab 是 Pipeline;
  • GitHub Installation Webhook 有独立生命周期。

所以架构是:

它们在存储层保留不同表,在 Issue PR 展示与关闭聚合上再统一。

18.2 GitHub App 安装流程

GitHub 集成依赖 App 配置:

  • App ID;
  • Private Key;
  • Webhook Secret;
  • Public Callback/URL。

安装大致分两步:

  1. 用户在 GitHub 完成 App Installation;
  2. Multica Callback 把 Installation 绑定到当前 Workspace。

Webhook 可能先于 Callback 到达。系统用 github_pending_installation 保存暂时无法映射 Workspace 的 Installation 元数据,Callback 后再完成绑定。

18.3 一个 Installation 可绑定多个 Workspace

查询 github.sql 明确允许按 GitHub Installation ID 找到多条 Workspace Binding。

收到 PR Webhook 时:

  • 先找全部绑定;
  • 对每个 Workspace 独立镜像;
  • 使用各自 Issue Prefix 与 Settings;
  • 只链接各自 Workspace 中的 Issue;
  • 分别广播 Event。

Repo Scope 来自 GitHub App 的 Installation 权限,不等同于 Multica Workspace Scope。

18.4 GitHub App JWT

Server 用 Private Key 签 RS256 App JWT,再向 GitHub 读取 Installation/Account 信息。

实现考虑:

  • JWT 最长有效窗口;
  • iat 向前回拨一分钟吸收时钟偏差;
  • 私钥解析失败不在日志打印 Key;
  • Installation 请求使用 GitHub Vendor Accept Header;
  • 连接失败与未知 Installation 区分。

App JWT 不是传给 Daemon 的 Agent Credential。

18.5 GitHub Webhook 入口

POST /api/webhooks/github

  1. 要求 Server 配置 Webhook Secret;
  2. 读取 Raw Body;
  3. 常量时间验证 X-Hub-Signature-256
  4. X-GitHub-Event 分派;
  5. 处理 Installation、Pull Request、Check Suite;
  6. 未建模事件仍 ACK,避免 GitHub 把 Endpoint 标成故障。

未配置 Secret 时返回 Service Unavailable,而不是把所有请求当合法。

18.6 PR 镜像

GitHub PR Row 保存:

  • Installation/Workspace、Repo、Number;
  • Title、Body/URL;
  • State、Draft、Mergeable State;
  • Branch、Head SHA;
  • Author;
  • Additions/Deletions/Changed Files;
  • Created/Updated/Merged/Closed 时间。

Webhook Upsert 不是无条件覆盖。例如 GitHub 某些事件不重新计算 Mergeability,derivePRMergeableState 会决定何时保留旧值,避免低信息事件清空已有信息。

18.7 Check Suite 可能先于 PR

Webhook 不保证跨事件类型顺序。Check Suite 到达时,本地 PR 可能还没镜像。

GitHub 路径提供 github_pending_check_suite

  1. 找不到 PR 时暂存 Suite;
  2. 同 Suite 的新事件按更新时间覆盖旧暂存;
  3. PR Webhook 到达并 Upsert 后 Drain;
  4. 通过正常 Check Suite Upsert 重放;
  5. 删除 Pending Row。

这比丢掉事件后等待下一次 CI 更可靠。

18.8 Check 状态聚合

每个 Check Suite 独立存储并按更新时间单调 Upsert。Issue 读取 PR 时聚合:

  • checks_total;
  • checks_passed;
  • checks_failed;
  • checks_pending。

Queued/In-progress 算 Pending;Neutral/Skipped 等不阻塞结果可按 Passed 归一。聚合发生在查询层,PR Row 不保存一份易漂移的计数。

18.9 通用 VCS Provider 接口

integrations/vcs/vcs.go 定义:

  • Kind
  • EventKind(headers)
  • VerifySignature
  • ParsePullRequest
  • ParseCIStatus
  • ValidateToken

支持的 Kind:

  • forgejo;
  • gitea;
  • gitlab。

Handler 只消费规范化的 PR/CI Event,不分支理解各 Provider Payload。

18.10 Forgejo 与 Gitea

forgejo.go 用一个 Adapter 服务两者,因为它们当前相关 Wire 形态相同:

  • /api/v1/user 校验 Token;
  • Authorization: token ...
  • X-Gitea-Event
  • X-Gitea-Signature HMAC-SHA256;
  • Pull Request / Status Payload。

Signature 通常是裸 Hex;实现也容忍 sha256= 前缀。空 Secret 永远拒绝。

18.11 GitLab

gitlab.go 处理差异:

  • /api/v4/user
  • PRIVATE-TOKEN
  • X-Gitlab-Event
  • X-Gitlab-Token 常量时间比较;
  • Merge Request → PullRequestEvent;
  • Pipeline → CIStatusEvent;
  • GitLab 时间格式先规范化为 RFC3339。

时间规范化必须在 Adapter 层完成,否则 Shared Handler 回退到 Ingestion Time,会破坏乱序保护。

18.12 规范化状态

PR 状态统一为:

  • open;
  • draft;
  • closed;
  • merged。

CI 状态统一为:

  • passed;
  • failed;
  • pending。

未知 CI 状态保守地映射 Pending,避免把尚未理解的新状态当成功。

GitLab Pipeline 当前用稳定的合成 Context 表示每个 Commit 的单一状态;Merge-train Synthetic SHA 等高级场景是这层简化的已知限制。

18.13 建立 VCS Connection

handler/vcs.go 的连接流程:

  1. 检查 Deployment 是否开放 Self-host VCS;
  2. 检查 Secret Box 配置;
  3. 校验 Provider Kind;
  4. 规范化绝对 HTTP(S) Instance URL;
  5. 在线验证 Access Token;
  6. 生成 32 字节随机 Webhook Secret;
  7. 分别加密 Access Token 与 Webhook Secret;
  8. Upsert Workspace + Provider + Instance Connection;
  9. 返回 Connection 与一次性明文 Secret

以后列表不再返回 Secret;需要时只能 Rotate。

18.14 为什么 Managed Cloud 可以关闭通用 VCS

源码将两种状态分开:

  • available:产品/部署是否提供此能力;
  • configured:Operator 是否配置加密 Key。

Managed Cloud 可直接不开放用户任意 Self-host Instance;UI 隐藏整个区块。Self-host 开启但漏配 Key,则返回配置错误帮助 Operator 修复。

18.15 VCS Webhook 路由

路径为:

/api/webhooks/vcs/{connectionId}

Connection ID 决定:

  • Workspace;
  • Provider Kind;
  • 解密哪个 Secret。

Handler:

  1. 最多读取 10 MiB Body;
  2. 查 Connection;
  3. 解密 Webhook Secret;
  4. 调 Provider 验签;
  5. 分类 PR/CI;
  6. 解析成统一 Event;
  7. 镜像并广播;
  8. 未建模事件返回 Accepted。

未知 Connection 与不可用部署不暴露内部配置细节。

18.16 PR 的单调更新

Webhook 可能乱序重投。通用 vcs.sql 的 PR Upsert 对每个字段使用:

只有新事件的 pr_updated_at 不早于持久 Row,才覆盖。

即使事件过期,Query 仍返回现有 PR ID,便于 Link 操作;Handler 随后比较 Event Timestamp,严格更旧就停止,不重写 Link Metadata 或重复 Publish。

18.17 CI 的单调更新

Commit Status 也按 Provider 自己的 updated_at 保护:

  • Forgejo/Gitea 优先 Status Updated At;
  • GitLab 优先 Pipeline Finished At;
  • 无时间才使用 Ingestion Time;
  • 同 SHA + Context 的旧状态不能覆盖新状态。

若一条旧 Pending 在新 Passed 后重放,UI 不应倒退。

18.18 Issue 标识自动链接

PR Title、Body、Branch 中的 PREFIX-NUMBER 会被提取,再按当前 Workspace Prefix 查 Issue。

链接分两类:

  • Qualifying Link:Title/Branch 引用,或 Body 中 Closing Keyword;
  • Reference-only Link:只在 Body 普通提及。

Reference-only 可保留历史关系,但不作为主要工作 PR,也不参与自动关闭聚合。

18.19 Closing Keyword

系统识别 GitHub 风格词形,例如 Close/Fix/Resolve 的时态变化,且标识必须紧随其后。

只扫描 Title/Body 的 Closing Intent,不把 Branch 名当“请求关闭”。Branch 引用可建立 Qualifying Link,但不会因为叫 fix/MUL-1 就自动完成 Issue。

18.20 Close Intent 必须冻结

PR 在 Open/Edit 阶段还可改变 Description,因此 Close Intent 可更新;一旦收到 Terminal Merge/Close Event:

  • 持久化最终 Close Intent;
  • 后续乱序的 Open/Edit/Synchronize 不得改写;
  • 自动推进依据持久 Link State,而不是“本次 Webhook 是否带关键词”。

否则一个合并后晚到的旧事件可能把 Closing PR 变成普通 Reference。

18.21 跨 Provider 关闭聚合

同一个 Issue 可以同时链接 GitHub PR 和 GitLab/Forgejo PR。完成判断必须跨两组表:

  • Open Count = 0;
  • 至少一个 Merged PR 带 Close Intent;
  • Reference-only 不参与;
  • Issue 还不是 Done/Cancelled。

vcs.sql 联合 GitHub 与通用 VCS 数据。任一 Provider 的 Terminal Event 都会重新计算全部 PR,避免“GitLab 合并时看不见仍打开的 GitHub PR”。

18.22 自动推进到 Done

满足关闭聚合时,advanceIssueToDone 走共享 Issue 更新逻辑:

  • 改状态为 done;
  • 写 Activity/Source;
  • 发布 Issue Event;
  • 触发 Parent/Child 相关处理;
  • 同步 Autopilot Run 等派生状态。

Integration 不直接执行一条裸 SQL,否则会绕过业务副作用。

18.23 Head SHA 与 CI

PR 的 head_sha 连接 Commit Status:

  1. 新 PR/Synchronize 更新 Head SHA;
  2. CI Event 按 SHA Upsert;
  3. 查询找到所有 Head SHA 匹配的 PR/Issue;
  4. 发布 Pull Request Updated;
  5. UI 重算 Checks。

GitHub 与通用 VCS 的 Head SHA 去重/查询也需要跨 Provider 协调,不能假设 Issue 只连一种 Git。

18.24 安全边界

  • GitHub Webhook 和 Forgejo HMAC 使用常量时间比较;
  • GitLab Token 比较也用常量时间;
  • 空 Secret 一律拒绝;
  • Access Token 与 Webhook Secret 加密存储;
  • 明文 Secret 只在创建/旋转响应返回一次;
  • Secret 不出现在 List Response;
  • 未配置 GitHub Secret 时拒绝全部 Webhook;
  • Workspace 来自 Connection/Installation,不信任请求 Header;
  • Instance Token 验证错误不回显上游敏感 Body;
  • 删除 Connection 同时清理镜像和链接数据。

18.25 GitHub 与通用 VCS 的差异表

维度GitHubForgejo/GiteaGitLab
连接身份App InstallationAccess TokenAccess Token
Token 校验App API/JWTapi/v1/userapi/v4/user
Webhook 鉴权HMAC sha256=HMAC HexShared Header Token
PR 名称Pull RequestPull RequestMerge Request
CICheck SuiteCommit StatusPipeline
未到 PR 的 CIPending Check Suite依 SHA 后续查询依 SHA 后续查询
Workspace 绑定一个 Installation 可多 WorkspaceConnection 属于一个 WorkspaceConnection 属于一个 Workspace

18.26 增加一个通用 Provider

  1. 定义持久 Kind;
  2. 实现 Header Event Classifier;
  3. 实现严格验签;
  4. 实现 Token Validation;
  5. 规范化 PR 状态、字段、时间;
  6. 规范化 CI 状态与真实更新时间;
  7. 注册 Provider;
  8. 写 Payload/Signature/Token 测试;
  9. 验证 Issue Identifier 与 Close Intent;
  10. 验证乱序事件不会回退;
  11. 说明 Provider 特有的 CI/SHA 局限;
  12. 检查设置 UI 与 Secret 一次性展示。

如果 Provider 采用 Installation/OAuth、Review/CI 语义差异巨大,应该像 GitHub 一样保留特化路径,而不是把通用接口扩成一堆可选方法。

18.27 本章结论

代码托管闭环不只是展示一个 PR 链接。Multica 做了:

  • 安全连接外部 Provider;
  • 镜像 PR 与 CI;
  • 抵抗乱序和重放;
  • 从 Issue Identifier 建立关系;
  • 冻结 Closing Intent;
  • 跨 Provider 聚合开放 PR;
  • 在真正满足条件时推进 Issue。

专用 GitHub 路径与通用 VCS Adapter 的分界,体现了“统一业务语义,不强行统一认证和协议”。