docs: M3 开工分析 8 份报告入档 + ADR-016~021 拍板决策
CI / docs-build (push) Successful in 55s

- iteration-3 报告 01-08(PM 拆解/后端/Flutter/RC 首个 CERTIFIED/UI/埋点/证据基线/Git)
- ADR-016 自托管 MinIO 起步预留迁云(用户确认现无云存储)、ADR-017 patbond-community
  :8084 + media 归 user + 作者信息跨 schema 只读、ADR-018 范围裁剪(话题剪出/单层评论)、
  ADR-019 幂等按域(PUT/DELETE + request_hash)、ADR-020 聚合 feed_viewed/字典 v3/
  北极星不变/队列三项升第一波、ADR-021 Git 修订(main 更正/PR 情形触发/首发布重建 main/
  防泄漏 grep 先行)
- mkdocs 挂「第三迭代」导航,build --strict 通过

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-08 16:02:53 +08:00
parent e68b6553ca
commit d2867826d3
10 changed files with 2072 additions and 0 deletions
@@ -0,0 +1,139 @@
# 08 M3 Git 与 CI 工作流核查规划
- 执行人:Git Workflow Master
- 日期:2026-09-08
- 范围:第三迭代(M3 社区)开工前的三仓状态核查、ADR-011 PR 条款实践复盘、发布分支启用规划、对象存储凭证防泄漏、CI 增量评估。**本报告只核查与规划,未改动任何代码、工作流或 mkdocs.yml,未执行 commit/push。**
---
## 1. 三仓当前状态核查(2026-09-08 实测)
| 仓库 | 分支 | 相对 origin | 工作区 | stash | 最新提交 CI 状态 |
| --- | --- | --- | --- | --- | --- |
| patbond-api | dev | 同步(fetch --prune 后确认) | 干净 | 无 | **success**`64c9b72`CI / backend-testrun 315m18s |
| patbond-flutter | dev | 同步 | 干净 | 无 | **success**`720865b`CI / flutter-gatesrun 372m12s |
| patbond-doc | main | 同步 | 干净 | 无 | **success**`e68b655`CI / docs-buildrun 3929s |
CI 状态经 Gitea commit status API 逐仓核实,非转述。**未提交内容清单:无**(本报告文件本身除外,按波次规则随下一波提交)。M2 收官时的「三仓 commit + push + CI 绿」闭环纪律保持完好。
### 1.1 历史遗留分支的新发现(比 M2 报告掌握的更严重一档)
M2 报告只记录了「api 本地孤儿 `master` 上游已删」。本次为发布分支规划做了祖先关系核查,发现:
- **patbond-api 的 `ff876bc`(本地孤儿 master、远端 `origin/main` 共同指向的初始 README 提交)不是 dev 的祖先**——`git merge-base --is-ancestor ff876bc dev` 判定失败,dev 的根提交是 `b1252b9`Initialize patbond microservice modules)。即 **api 远端默认分支 main 与 dev 是两条不相干历史**unrelated histories)。这直接影响第 3 节「dev→发布分支」怎么做第一次合并。
- patbond-flutter 的 `main``030b11f`**是** dev 祖先,未来 dev→main 可干净 fast-forward。
- M2 报告建议的「核实后删本地孤儿 master」当时的前提(`ff876bc` 已被 dev 包含)实测**不成立**,但结论不变:该提交仅是初始 README,远端 `origin/main` 仍保留它,本地 `git branch -D master` 无信息损失,可顺手做。
## 2. M2 工作流实践复盘:ADR-011「高风险走 PR」条款何去何从
### 2.1 实践事实(git log + Gitea Actions 全量核查)
- **PR 使用次数:0。** 三仓 M2 期间(09-07 至 09-08)无任何 merge commit,历史全程线性。
- **四类「高风险」全部直推了**Flyway V3/V4`49299fb`T2-01)、契约冻结 v1.2.0doc `511617b`)、事件字典白名单扩充(`64c9b72`)均直推 dev/main。
- **风险事件清点:零。** 具体证据:
1. M2 期间三仓 CI **零失败**——Actions 全量 run 列表中的 7 次 failure 全部集中在 09-04(M1 末 CI 搭建期),且全是流水线自身配置问题(外部 action 不可达、JDK 安装方式、format 未跑),无一是业务代码直推打红 dev;
2. **零 revert**`--grep` 回退/回滚/revert 无命中);
3. **迁移不可变规则守住了**V3/V4 文件推送后零修改(`git log --follow` 各只有一次提交);
4. **契约冻结守住了**`openapi.yaml` 在冻结提交 `511617b` 之后零改动;
5. 无 force push 痕迹(线性历史 + 各推送头全绿)。
### 2.2 为什么直推没出事——机制归因,而非运气
M2 的安全性不是来自 PR 的缺席碰巧无事,而是四道机制已经覆盖了 PR 想防的东西:
1. **Flyway 迁移**`./mvnw clean test` 经 Testcontainers 起真库执行完整迁移链,每次 push 都等于迁移演练——PR 合入前 CI 与 push 后 CI 跑的是同一条命令,对串行开发者而言只差「红了是否已在 dev 上」,而两人+AI 模式下红 dev 的传播面就是自己。
2. **契约破坏**:T2-09 契约一致性测试把破坏性变更变成红测试,比人工 PR review 更机械可靠。
3. **波次收尾 compose 实测 + E2E 烟囱**兜住了集成层。
4. **串行作业**:M2 全程实质单线程推进(AI 辅助不产生 git 并发),四类触发条件中真正指向并发风险的「两人并行期」从未发生。
### 2.3 结论建议(**待拍板 #1**):条款降级为「按情形触发」,不是纪律失效
判定:**这不是纪律失效,是条款的触发条件设计错了**——它按「改动类别」(迁移/契约/依赖)触发,而 M2 证明这些类别在串行+CI 全量门禁下并无 PR 才能拦住的残余风险。真正需要 PR 的是「情形」:
- **建议修订 ADR-011 备注**:Flyway 迁移、契约变更、依赖升级在串行开发期**直推 dev + CI 绿 + 波次实测**即为足够实践,不再列为 PR 推荐触发项;
- **PR 保留为强制的仅两种情形**:
1. **两人并行改同一仓库期间**(唯一真实的并发冲突风险源);
2. **首次 dev→发布分支合并之后**,凡影响已发布版本的破坏性变更(不可变迁移的例外处理、已冻结契约的破坏性修订、发布分支 hotfix)——发布后爆炸半径从「自己人」扩大到「装了 App 的用户」,性质不同。
- 三仓 ci.yml 的 `pull_request:` 触发器保留不动(零成本待命);M2 待拍板 #2 的分支保护同理**降级为「随首次发布对发布分支启用」**,dev 不开(见 3.3 checklist)。
这样条款从「写了但没人执行的推荐」变成「触发即无争议的强制」,规范与实践重新一致。
## 3. M3 发布分支启用规划
### 3.1 先解决命名与历史两个前置问题
**命名不一致(待拍板 #2**ADR-011 写的是「`master` 保留为发布分支」,但远端实况是:api 的 `origin/master` 已删除(默认分支为 main)、flutter/doc 默认分支均为 `main`,**三仓远端今天没有任何一个 master 分支**。建议:**统一以 `main` 为发布分支名**,修订 ADR-011 措辞(master→main),顺手删除 api 本地孤儿 master(§1.1,无信息损失)。反向方案(重建三仓 master)多一次全员改默认分支操作,无收益。
**api 的 main 与 dev 历史不相干(待拍板 #3)**`origin/main``ff876bc`)不是 dev 祖先,首次 dev→main 无法 fast-forward,普通 merge 需要 `--allow-unrelated-histories` 且会把一条孤儿历史永久缝进发布线。三个选项:
| 选项 | 操作 | 评价 |
| --- | --- | --- |
| A(推荐) | Gitea 仓库设置将默认分支临时切到 dev → 删除远端 main → 从 dev 重建 main → 默认分支按需切回 | 零 force push、历史干净,纯平台操作 |
| B | 一次性 `git push --force origin dev:main`,在 ADR 中记录为例外 | 结果等价,但破「不 force push 共享分支」戒律,留坏先例 |
| C | `merge --allow-unrelated-histories` | 永久保留无意义的孤儿历史缝合点,不推荐 |
flutter 无此问题(main 是 dev 祖先,直接 ff);doc 仓 main 即日常分支,不参与发布分支语义。
### 3.2 何时启用:建议 M3 末做第一次 dev→main 发布(**待拍板 #4**
理由:M2 收官已具备「E2E 烟囱脚本 + 全绿测试基线 + 冻结契约」的可发布形态,缺的只是发布动作本身;北极星指标出数(ADR-012,M3 末 A/B 前置目标全绿)需要一个稳定版本承载;再往后拖,发布流程的首次演练会和 M4 首实验挤在一起。M3 末做第一次,把流程走通比版本内容重要。
### 3.3 发布 checklist 草案(首次发布用,验证后固化进 git-workflow.md
1. **冻结**:发布波次收尾,三仓 commit + push + CI 绿(既有纪律);
2. **实测**compose 全栈起,跑 M2+M3 两份 E2E 烟囱脚本,全场景 PASS,证据入波次报告;
3. **前置一次性项**(仅首次):完成 §3.1 的命名统一与 api main 重建;
4. **合并**api/flutter 各执行 `git checkout main && git merge --ff-only dev && git push origin main`(此后每次发布 dev→main 都应 ff-only 可过,过不了说明 main 被绕过 dev 改动,先查明);
5. **打标**:两仓 `git tag -a v0.3.0 -m "M3 社区"`(版本号待拍板时一并定)并 push tag;doc 仓同点位打同名 tag,三仓互为对照;
6. **平台侧**Gitea 为 api/flutter 的 main 开启分支保护(禁直推、合并需 CI 状态检查通过)——dev 仍不开,保持直推流;
7. **记录**:发布说明入 doc 仓(版本、三仓 tag 哈希、E2E 证据链接、已知遗留);
8. **发布后**:影响 main 的 hotfix 一律走短命分支 + PR(§2.3 强制情形之二正式生效)。
## 4. 对象存储凭证防泄漏(M2 方案未实施,重新评估)
### 4.1 现状核查
- M2 报告 §5 的两层纯 shell 方案**零实施**:三仓均无 `scripts/hooks/``core.hooksPath` 均未设置,ci.yml 均无检查 step。
- M2 没出事的原因和 PR 条款同理:M2 引入的凭证(RS256 密钥对、DB 密码、internal token)全部由 `deploy/init-secrets.sh` 生成且不入库,`*.sample` 占位约定执行到位——但这套卫生依赖「凭证只在本机生成」这个前提。
### 4.2 M3 威胁面变化:这次不一样,建议先落第二层(**待拍板 #5**)
M3 的对象存储凭证(ADR-010 剪出项回归:COS/OSS/MinIO 的 AccessKey/SecretKey)与 M2 的密钥有本质区别:**它是云厂商控制台签发的长期凭证,泄漏即可被外部直接使用且常绑计费**,不是本机自生成的内部秘密。AI 辅助开发下,凭证从「配置文件」流向「示例代码/测试/报告」的路径变多,纯约定不够。重新评估结论:
- **第二层(CI 兜底 grep)从「可选」升为「M3 第一波、对象存储凭证进入任何开发机之前必须上线」**。各仓 ci.yml 加一个纯 shell step(零外部依赖,秒级),模式清单在 M2 方案基础上增补云凭证特征:`AKID[A-Za-z0-9]{13,}`(腾讯云)、`LTAI[A-Za-z0-9]{12,}`(阿里云)、`(access|secret)[-_]?key\s*[:=]` 后跟非占位值、40 位以上连续 base64/hex;文件名黑名单增补 `.env``credentials``*.csv`(控制台导出的密钥文件形态)。
- 第一层(共享 pre-commit 脚本)维持推荐;若继续搁置,第二层单独上线也成立(拦「已提交的」比拦「即将提交的」在两人团队更关键——push 即触发,无 `--no-verify` 逃逸)。
- 沿用 M2 结论:不引入 gitleaks 等外部工具;真泄漏的第一动作是**去云控制台轮换/禁用密钥**,历史清理其后——此条随实施写进 git-workflow.md。
- 实施时顺手核对三仓 .gitignore 对 `.env` 的覆盖(api 仓 compose 依赖 `.env`,规则应已有,实施时以 `git check-ignore` 取证)。
## 5. CI 增量评估
### 5.1 新模块接入:确认零成本(同仓模块方案下)
patbond-api 是 maven 聚合工程(根 pom `<modules>` 现有 common/user/auth/pet 四个)。若 M3 沿 ADR-009 模式在 api 仓内新建 `patbond-community` / `patbond-media` 模块:**根 pom 加一行 `<module>`ci.yml 零改动**`./mvnw -B clean test` 自动覆盖新模块(含其 Testcontainers 测试)。**确认零成本,无待拍板。**
仅当选择独立新仓(当前无此计划)才有增量:复制既有 ci.yml(三仓模板已统一:手动 checkout + apt/镜像装工具链)+ 仓库设置启用 Actions,runner 是实例级共享的,无需新注册,估计半小时内。
### 5.2 E2E 烟囱进 CI:技术可行但不建议进 push 门禁(**待拍板 #6,倾向不做**)
可行性核查(基于 ci-runner-setup.md 与 act_runner 现状):
- **docker.sock 已挂进 job 容器**Testcontainers 依赖,实测可用),job 内跑 `docker compose up` 起的是宿主 sibling 容器——技术上通。
- 但有四项实际成本:
1. **网络**compose 端口发布在宿主,job 容器内 `127.0.0.1:8081-8083` 不可达,E2E 脚本的 base URL 需改造为可注入,并让 job 容器走宿主网关 IP 或直接加入 compose 网络;
2. **工具链**job 镜像需补 docker CLI + compose 插件;
3. **时长**`mvnw package` + 三个镜像 build + 全栈起 + 11 场景,估计给流水线加 5–10 分钟(现 backend-test 5m18s,翻倍以上);
4. **跨仓**:脚本在 flutter 仓、compose 在 api 仓,任一仓的 push CI 跑它都要 clone 另一仓,触发归属含糊。
- **建议**:push 门禁维持现状(快、单仓、职责清晰);E2E 保持 M2 已验证的「波次收尾手动跑、证据入档」模式,并作为发布 checklist 第 2 步的强制项。若要自动化,做成独立的 `workflow_dispatch` 手动触发工作流(发布前一键跑),M3 内低优先,不占开工路径。
## 6. 待拍板事项汇总
| # | 事项 | 推荐 | 见 |
| --- | --- | --- | --- |
| 1 | ADR-011 PR 条款降级:类别触发(迁移/契约/依赖)取消,改为仅「两人并行同仓」与「首次发布后影响 main 的变更」两种情形强制 PR;分支保护随之改为只对发布分支启用 | 采纳修订 | 2.3 |
| 2 | 发布分支统一命名为 `main`(修订 ADR-011 的 master 措辞),顺手删 api 本地孤儿 master | 采纳 | 3.1 |
| 3 | api 远端 main 与 dev 历史不相干的一次性处理:Gitea 平台删除重建(选项 A) | 选项 A | 3.1 |
| 4 | M3 末执行第一次 dev→main 发布,采纳 §3.3 checklist(含版本号定名) | 采纳 | 3.2/3.3 |
| 5 | 防泄漏第二层(CI 兜底 grep + 云凭证模式增补)升为 M3 第一波必做、先于任何对象存储凭证落地;第一层 pre-commit 维持推荐 | 采纳 | 4.2 |
| 6 | E2E 烟囱不进 push 门禁;可选做 workflow_dispatch 手动工作流(低优先) | 不进门禁 | 5.2 |
采纳后需要落实的改动(本报告未执行):ADR-011 修订、git-workflow.md 增补(PR 情形条款、发布流程、泄漏应急)、三仓 ci.yml 加防泄漏 step、api main 重建操作、本报告挂入 mkdocs 导航。