Files
patbond-doc/docs/development/iterations/iteration-4/08-git-workflow-plan.md
T
lixi 79b33dba31 docs: M4「AI 创作」开工分析八份报告 + 汇总拍板页入档挂导航
八角色并行开工分析,合计 7448 行;另出 00 汇总页(跨角色收敛结论、
13 项待拍板、6 项待仲裁分歧、未取证项汇总),挂第四迭代导航最前。
mkdocs build --strict 通过。

基线实测修正(文档与实况不符):
- api 测试 381(releases.md 记 379,成因待仲裁)
- 埋点白名单 41(四份文档记 42,experiment_exposed 重复计数)
- 真机验证挂起 10 项(转述链 4→6→8→10 每跳丢项)
- E2E 断言机械可数 226(声称 234 无可复核来源)
- v0.4.0 实际发布 09-14 11:17;CI 非红,三仓五上下文全绿

多方独立收敛(无需拍板):
- 队列用 Postgres SKIP LOCKED + 租约列,不引入 Redis/MQ
- 服务端零对象写能力(ObjectStorage 无 put/get),M4 立足点缺地基
- 「四模块字节级快照锁 CI」不存在,实际门禁仅结构断言
- 定稿模型 input_asset_id NOT NULL,即图生图不支持文生图
- 跨 schema 外键补回是 V5 自身指令,裁剪理由已不成立

阻塞项与安全缺口:
- AI provider BLOCKED:零 SDK/endpoint/额度,正典种子即 fixture
- 分支保护必需上下文选错触发器:(push) 限定 branches:[dev],
  致「推 dev 即满足门禁」且「非 dev 分支 PR 永久无法合并」
- check-secrets.sh 对 sk-/sk-ant- 零覆盖,须先于任何 AI key 落地
- 北极星 09-21 窗口已于 09-13 关闭,补救无从下手,建议改事件驱动

本批核心教训:13 处文档/注释与代码相反且多已被下游采信,其中
5 处造成实际规模误判(widthPx M→S、数据模型早已定稿 L→M、
社区侧 purpose 校验实际不存在等)。汇总页 §0 立转述纪律。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 15:48:56 +08:00

64 KiB
Raw Blame History

M4「AI 创作」Git 工作流与发布流程规划

角色Git Workflow Master 日期2026-09-14 性质:只读调研 + 规划,本报告未执行任何 commit / push / tag / 分支创建 / 远端配置变更。 前置v0.4.0 已发布(三仓 tag 齐)。本报告是 iteration-3/08 的 M4 续篇,并接管其 §3.3「发布 checklist 草案」的固化职责。


结论摘要

  1. 用户交付的 6 条分支保护描述,逐条核实全部为真Gitea API 原样贴在 §1.2):api/flutter 的 mainprotected=trueenable_status_check=true、上下文显式填写、required_approvals=0;两仓 dev 均未保护;doc 仓 main 未保护。
  2. 但填的是错的触发器——这是本报告最重要的发现(§1.4)。必需上下文是 CI / backend-test (push),而 push 触发器被限定在 branches: [dev]。推论有两个后果:门禁实际由「推 dev」满足而非 PR 自身;且任何非 dev 分支 → main 的 PR 将永久无法合并,与「hotfix 走短命分支 + PR」的既定纪律直接冲突。修复方向与真实上下文字符串见 §1.4 / 拍板 D1。
  3. 发布 checklist 从未固化releases.md:4 指向的正典是 iteration-3/08 §3.3,标题至今仍是「草案(验证后固化进 git-workflow.md)」,而 git-workflow.md 全文 65 行无任何发布/PR/E2E 章节。所有修正只以散文形式活在 releases.md,且该文件自我矛盾。处置见 §4.0。
  4. M4 分支策略推荐:继续 dev 直推,不引入常态 feature 分支——理由之一是 CI 的 push 触发器不覆盖 feat/**,开分支等于自断快速反馈(§2)。
  5. 契约 v1.4.0 → v1.5.0 需同步 9 处doc 2 处 + api 4 份快照 + 4 个守卫常量 + 4×4 计数断言),且字节级一致性无 CI 保障、全靠人工 md5 —— 操作顺序见 §3。
  6. M4 必须新增第五份 E2E 脚本 test_e2e_m4_manual.dart,发布回归门禁由四份变五份(§4)。
  7. M4 引入 AI provider 凭证,而 check-secrets.sh 的 7 条规则对 sk- / sk-ant- / 通用 api_key 形态零覆盖(§5)。

1. 核实结果(原始配置取证)

1.1 三仓 HEAD / tag / 远端一致性 —— 全部符合

git ls-remote origin 与本地 for-each-ref 双向比对(2026-09-14):

仓库 本地当前分支 origin/dev origin/main tag v0.4.0 → commit 工作区
patbond-api dev @ 3cd8005 3cd8005 3cd8005 46afe8aannotated)→ 3cd8005 clean
patbond-flutter dev @ fbcd734 fbcd734 fbcd734 c963a0aannotated)→ fbcd734 clean
patbond-doc main @ 5cc6361 —(无 dev 5cc6361 08fda68annotated)→ 5cc6361 clean

三仓 dev == main == tag,历史线性,符合描述。三个 v0.4.0 均为 annotated taggit cat-file -t = tag),带完整发布说明正文——延续此惯例。

补充事实(用户未提及,非差异但需知悉):

  • v0.4.0 的 tag 实际打于 2026-09-14 11:17taggerdateapi/flutter 11:17:35~36、doc 11:19:17),tagger Lixi20。v0.3.0 打于 2026-09-10 15:47。即发布动作发生在今天上午,而非 M3.5 收官日 09-11——安全事件导致的 CI 中断修复与发布挤在同一天。
  • api 仓存在本地私有 ref refs/backup/old-main-ff876bcff876bcv0.3.0 重建 main 时保留的孤儿提交备份)。ls-remote 无此 ref ⇒ 仅存于本机。若换机器或本仓重克隆,该备份即消失。
  • 分支拓扑不对称api 本地只有 dev 一个分支;flutter 本地另有 main @ 030b11f落后 origin/mainfbcd734。这是个陷阱:flutter 仓若有人 git checkout main 会拿到陈旧的 main。M4 期间建议直接删掉 flutter 本地 main(发布走 PR,本地根本不需要 main)。
  • doc 仓远端确实只有 mainGET /branches/devnot found),不参与发布分支语义 —— 符合描述。

1.2 分支保护实配 —— 6 条描述全部为真

取证方式:GET /api/v1/repos/zhaoyuxi/{repo}/branches/{branch}Gitea 1.26.4https://132.232.242.77,自签证书需 -k)。该端点匿名可读并直接返回生效的保护字段。原始响应关键字段:

patbond-api      main : protected=true   enable_status_check=true
                        status_check_contexts=["CI / backend-test (push)"]
                        required_approvals=0
patbond-api      dev  : protected=false  enable_status_check=false  status_check_contexts=[]
patbond-flutter  main : protected=true   enable_status_check=true
                        status_check_contexts=["CI / flutter-gates (push)"]
                        required_approvals=0
patbond-flutter  dev  : protected=false  enable_status_check=false  status_check_contexts=[]
patbond-doc      main : protected=false  enable_status_check=false  status_check_contexts=[]
patbond-doc      dev  : {"message":"not found"}

releases.md:112-113 的表格逐格一致,与用户描述逐条一致

取证边界(必须声明)GET /repos/{owner}/{repo}/branch_protections(完整保护规则对象)返回 401 {"message":"token is required"}——本机无 Gitea token(已查:无 ~/.gitea*、无 ~/.netrc、无 ~/.config/tea、无 tea CLI、环境变量无 token)。因此:

  • 已证实protected / enable_status_check / status_check_contexts / required_approvals 四项(上表,来自 /branches/{branch})。

  • ⚠️ 未直接证实:「禁直接推送」的具体机制。响应里的 user_can_push=false匿名身份的结果,不能作为「已认证的 Lixi20 也被禁推」的证据。该结论目前的支撑是间接的:protected=true + v0.4.0 确实被迫走了 PR(§1.5,两仓各有一个 merged PR,而 v0.3.0 是直推)。

  • 复核命令(拿到 token 后执行,建议 M4 开工时补做并把响应贴入本节):

    # 只需 read:repository 权限的 tokenGitea → Settings → Applications → Generate Token
    for r in patbond-api patbond-flutter patbond-doc; do
      echo "=== $r ==="
      curl -sk -H "Authorization: token $GITEA_TOKEN" \
        "https://132.232.242.77/api/v1/repos/zhaoyuxi/$r/branch_protections" |
        python3 -m json.tool
    done
    

    重点看 enable_pushfalse = 禁一切直推)、enable_push_whitelist / push_whitelist_usernamesenable_merge_whitelistblock_on_official_review_requestsrequired_approvalsstatus_check_contexts

1.3 CI 工作流触发器 —— 符合,但覆盖面是 §1.4 问题的根源

三仓均只有 .gitea/workflows/ci.yml.github/workflows/。触发器原文:

仓库 文件:行号 触发器 job 名
patbond-api .gitea/workflows/ci.yml:15-18 on: push: branches: [dev] + pull_request:(无分支过滤) backend-test:26
patbond-flutter .gitea/workflows/ci.yml:9-12 on: push: branches: [dev] + pull_request:(无分支过滤) flutter-gates:19
patbond-doc .gitea/workflows/ci.yml:5-8 on: push: branches: [main] + pull_request:(无分支过滤) docs-build:16

api ci.yml:15-18 原文:

on:
  push:
    branches: [dev]
  pull_request:

关键点:push 触发器被白名单限定在单一分支api/flutter 是 devdoc 是 main);pull_request 无分支过滤,任何 PR 都会跑。

门禁命令与本地门禁同源(git-workflow.md:58-62):api = ./mvnw -B clean testci.yml:55);flutter = dart format --output=none --set-exit-if-changed lib test / flutter analyze / flutter testci.yml:56-58);doc = mkdocs build --strict -d /tmp/site。三仓首个 step 均为 sh scripts/check-secrets.sh --allapi ci.yml:41-42、flutter :31-32、doc :22-24)。

三仓 concurrency: group: ci-${{ github.ref }} + cancel-in-progress: truepush 与 PR 的 github.ref 不同(refs/heads/dev vs refs/pull/N/...)⇒ 两条流水线并行互不取消,与 §1.5 实测的「同一提交同时挂两个上下文」吻合。

1.4 ⚠️ 必需状态检查选错触发器 —— 确认成立,且比预想更严重

Reality Checker 提出、本节独立复核确认成立。取证:GET /repos/zhaoyuxi/{repo}/commits/{sha}/statuses?limit=50(匿名可读)。api 3cd8005= dev tip = main = v0.4.0)的全部 9 条 commit status,按时间倒序:

[success ] ctx='CI / backend-test (pull_request)'  2026-09-14T09:54:37+08:00  runs/77
[pending ] ctx='CI / backend-test (pull_request)'  2026-09-14T09:47:54+08:00  runs/77
[success ] ctx='CI / backend-test (push)'          2026-09-14T09:44:05+08:00  runs/73   ← 唯一被要求的上下文
[pending ] ctx='CI / backend-test (pull_request)'  2026-09-14T09:39:50+08:00  runs/77
[pending ] ctx='CI / backend-test (push)'          2026-09-14T09:38:34+08:00  runs/73
[pending ] ctx='CI / backend-test (push)'          2026-09-14T09:38:30+08:00  runs/73
[failure ] ctx='CI / backend-test (push)'          2026-09-11T10:33:31+08:00  runs/73
[pending ] ctx='CI / backend-test (push)'          2026-09-11T10:33:29+08:00  runs/73
[pending ] ctx='CI / backend-test (push)'          2026-09-11T10:33:28+08:00  runs/73

flutter fbcd734 同构(6 条,CI / flutter-gates (pull_request) success 10:02:08 / CI / flutter-gates (push) success 09:58:23)。doc 5cc6361 只有 CI / docs-build (push) success 11:20:44doc 无 PR)。

合并后的 combined status/commits/{sha}/status):state: success,含两个上下文 —— 即每个 dev 提交同时携带 (push)(pull_request) 两条独立状态

三条事实推出的结论

# 事实 出处
A 必需上下文 = CI / backend-test (push)仅此一个 §1.2 API 响应
B push 触发器限定 branches: [dev] api/.gitea/workflows/ci.yml:16-17
C Gitea 的上下文名格式为 <workflow> / <job> (<event>) §1.4 实测两种 event 各自成名

推论一(门禁语义错位,已成立)(push) 状态只可能由「推送到 dev」产生。因此 dev → main PR 的合并门禁实际由 dev 的 push 流水线满足,而非 PR 自身的流水线。(pull_request) 上下文不在必需列表中 ⇒ 它红也不阻塞合并。M3 当初显式填写上下文名是为堵「留空 = 空集为真 = 放行」的漏洞(releases.md:116 记载该理由),但填成了 (push)——把「空集漏洞」换成了「错触发器漏洞」。

⚠️ 对 v0.4.0 的判断需要修正:releases.md:47 写「等 pull_request 状态检查转绿后合并」「分支保护确实要求该检查通过」——后半句不成立。当时真正解除阻塞的是 09:44:05 转绿的 (push)(pull_request) 在 09:54:37 才绿,两者在 11:14:55 合并前都已绿,所以表象正确、机制归因错误。这正是「文档转述被全链路采信」的又一例。

推论二(更严重,尚未被触发)任何非 dev 分支 → main 的 PR 将永久无法合并。 因为其 head 提交从未被推送到 dev ⇒ 永远不会产生 (push) 上下文 ⇒ 必需检查永远处于「缺失」⇒ 无法合并(除非管理员临时改保护配置)。这与两条既定纪律直接冲突

  • git-workflow.md:9:「改动跨多天、有破坏性风险…从最新 dev 拉出 feat/<主题> / fix/<主题> 分支」
  • iteration-3/08 §3.3 第 8 步:「影响 main 的 hotfix 一律走短命分支 + PR」

当前配置下 hotfix 路径是死的。M4 若出现需要绕过 dev 直修 main 的线上问题,会在最紧急的时刻撞上这个死锁。

修复方向(真实上下文字符串,非猜测)

正确的必需上下文字符串已从 §1.4 的 CI 实跑记录中取到真名:

仓库 应填的上下文(实测真名)
patbond-api CI / backend-test (pull_request)
patbond-flutter CI / flutter-gates (pull_request)

推荐(拍板 D1:把必需上下文替换(pull_request) 单值,而非追加。理由:

  1. 语义正确:PR 的合并门禁应由 PR 自己的流水线把关。
  2. 解锁 hotfix 路径pull_request 触发器无分支过滤(ci.yml:18),任何分支的 PR 都会跑 ⇒ feat/* / hotfix/* → main 的 PR 可正常合并。
  3. 不重新引入空集漏洞:仍是显式命名,只是名字改对。
  4. 不损失保证:有人会担心失去「dev tip 自身 push 绿」的冗余。但 ff-only 合并下 PR head ≡ dev tip,两条流水线跑的是同一棵树、同一套命令 ⇒ 该保证是重复的,不是额外的。
  5. 若改为「两者都要」:严格性更高,但 hotfix 死锁依然存在(push) 仍缺失)⇒ 不解决推论二,不推荐。

变更后必做的一次性验证(否则等于换一个未验证的配置):改配置后在任一仓开一个空改动的试验 PR(或就用 v0.5.0 的正式 PR),确认 Gitea 的 PR 页面把 (pull_request) 显示为 required 且它红时合并按钮确实禁用。M3 的教训是「配置改了但语义没验」——这次要在 v0.5.0 发布之前验完,不要拿正式发布当试验场。

注意上下文名的脆弱性:字符串由 name: CIworkflow 名)+ job keybackend-test+ event 三部分拼成。改任何一个都会使必需上下文永久缺失、PR 永久不可合并。见 §5 的预案。

1.5 v0.4.0 PR 流程实证 —— 符合描述

GET /repos/zhaoyuxi/{repo}/pulls?state=all

仓库 PR 标题 base ← head merged merge_commit_sha merged_at
patbond-api #1 v0.4.0 M3.5 体验补齐 maindev true 3cd8005 2026-09-14T11:14:55+08:00
patbond-flutter #1 v0.4.0 M3.5 体验补齐 maindev true fbcd734 2026-09-14T11:15:05+08:00

merge_commit_sha 恰等于 head shaFast-forward、零合并提交,与描述一致。ls-remote 存在 refs/pull/1/head(无 refs/pull/1/merge)。两仓各仅 1 个 PR ⇒ 历史上从未有过其他 PR,「非 dev 分支 → main」这条路径从未被走过,故 §1.4 推论二至今未暴露。

「PR 会自动跟随 dev 新提交重跑」——间接证实3cd8005 上有 3 条 (pull_request) 状态(09:39:50 pending → 09:47:54 pending → 09:54:37 success),同一 run 77 被重复上报,与 releases.md:48 描述的「发布途中 E2E 脚本提交推入 dev,PR 随即重跑」吻合。

CI 当前并非红:三仓五个上下文在 09-14 全部 successapi 两个、flutter 两个、doc 一个)。iteration-3.5/04 §7.3 记载的 3cd8005 failure"Failing after 2s"runner 装配级故障) 确实存在于 2026-09-11T10:33:31,但同一 run 73 于 2026-09-14 09:38 重跑并于 09:44:05 转绿 ⇒ 该 runner 故障已闭环,不是 M4 的开工障碍。

1.6 契约快照锁实配 —— 符合,但字节一致性无 CI 保障

正典:patbond-doc/docs/api/openapi.yamlinfo.version: 1.4.0,见该文件 :4)。api 侧四份快照 + 正典五路 md5 全等

a7081fb84f1207eef579ab94025f5801  patbond-doc/docs/api/openapi.yaml
a7081fb84f1207eef579ab94025f5801  patbond-api/patbond-auth/src/test/resources/contract/openapi-v1.4.0.yaml
a7081fb84f1207eef579ab94025f5801  patbond-api/patbond-pet/.../openapi-v1.4.0.yaml
a7081fb84f1207eef579ab94025f5801  patbond-api/patbond-user/.../openapi-v1.4.0.yaml
a7081fb84f1207eef579ab94025f5801  patbond-api/patbond-community/.../openapi-v1.4.0.yaml

注意文件名不同:正典是无版本号的 openapi.yaml,快照是 openapi-v1.4.0.yaml(内容字节相同)。

四个守卫常量 + 四组计数断言(升版必改的 8 个文件):

模块 RESOURCE 常量 计数断言(version / paths / operations / schemas
patbond-auth OpenApiContract.java:39 AuthContractConformanceTest.java:401-4041.4.0 / 32 / 45 / 75
patbond-pet OpenApiContract.java:35 ContractConformanceTest.java:757-760 → 同上
patbond-user OpenApiContract.java:39 MediaContractConformanceTest.java:248-251 → 同上
patbond-community OpenApiContract.java:39 CommunityContractConformanceTest.java:524-527 → 同上

关键缺口./mvnw clean test 只读 api 仓内文件 ⇒ CI 能发现「api 内部快照与守卫不自洽」,但完全无法发现「api 快照与 doc 正典不一致」。跨仓字节一致性纯靠人工 md5 比对,无自动门禁。这是 M4 升版最容易静默漂移的一环(§3 给出强制校验命令,§6 拍板 D5 提议补自动化)。

1.7 E2E 脚本清单与命名 —— 四份成立,但命名不齐

patbond-flutter 仓根目录(ls 实测):

迭代 文件名 大小 场景数
M1 test_e2e_manual.dart ⚠️ 无版本号 11015 7
M2 test_e2e_m2_manual.dart 27886 11
M3 test_e2e_m3_manual.dart 51212 14
M3.5 test_e2e_m35_manual.dart 45758 10

差异M1 那份叫 test_e2e_manual.dart不是 test_e2e_m1_manual.dart —— 用户描述的「延续 test_e2e_m4_manual.dart 口径」对 M2/M3/M3.5 成立,M1 是例外。回归清单变五份后,一个不带版本号的名字最容易被误读成「总入口脚本」。见拍板 D7。

运行方式test_e2e_m35_manual.dart:9 头注释):dart run test_e2e_m35_manual.dart;前置为 api 仓 docker compose up -d --build(六容器 postgres + minio + auth:8081 + user:8082 + pet:8083 + community:8084)。

这些脚本与 CI 门禁的关系(三条,M4 新脚本必须遵守)

  1. 不被 flutter test 执行:脚本在仓根而非 test/flutter testci.yml:58)不会收集它们。这是有意的(需要活的后端),保持现状。
  2. flutter analyze 检查analysis_options.yamlinclude: package:flutter_lints/flutter.yaml无任何 exclude ⇒ 分析器覆盖整包含仓根 .dart 文件。故新脚本必须过 analyze,且需照既有惯例在文件头加 // ignore_for_file: avoid_printtest_e2e_m35_manual.dart:3 原文即如此)。
  3. 不被格式门禁检查dart format --output=none --set-exit-if-changed lib testci.yml:56只覆盖 libtest,仓根脚本不在范围内。⇒ 新脚本格式跑偏不会红 CI,但建议仍手动 dart format 保持一致。

另有 4 个 integration_test/*.dartclient_ux_live_test.dart / feed_live_test.dart / profile_avatar_live_test.dart / publish_live_test.dart),是真机/活后端集成测试,与发布回归的五份 E2E 是不同体系,不计入门禁计数。

1.8 其他差异与既存缺口

以下均为本报告独立实测发现,不在用户描述范围内,但属 Git 工作流职责域:

(1) ⚠️ flutter 仓提交前缀大面积违反规范

git-workflow.md:13 原文:「格式:<前缀>: <中文主题>,前缀取 feat / fix / refactor / docs / test / chore」。

flutter 仓全部 43 个提交的前缀普查(git log --format='%s' | sed 's/[:].*//' | sort | uniq -c):

  20 新增        ← 违反
   5 fix
   4 修复        ← 违反
   3 feat
   2 test
   2 chore
   1 重构        ← 违反
   1 style       ← 前缀不在白名单
   1 新增静态演示界面 / 1 完善README / 1 Update project README / 1 update README.md / 1 Initial ...

25/43 用中文前缀(新增 20 + 修复 4 + 重构 1),规范与实况反向——实况才是主流。api 仓相反,全用英文(feat / test / chore / refactor);doc 仓全用 docs

另有规范未覆盖的 scope 形态已在实用:api test(contract): …、doc docs(api): … / docs(architecture): …git-workflow.md:13 的格式串里没有 scope。

⇒ 规范文档与三仓实况三方不一致,M4 需收口(拍板 D6)。

(2) 凭证防泄漏第一层(pre-commit)三仓全部未启用

git-workflow.md:40-47 要求「每人每仓启用一次」git config core.hooksPath scripts/hooks。实测:

patbond-api      core.hooksPath: (unset)   scripts/hooks/pre-commit: 存在
patbond-flutter  core.hooksPath: (unset)   scripts/hooks/pre-commit: 存在
patbond-doc      core.hooksPath: (unset)   scripts/hooks/pre-commit: 存在

钩子脚本入库了但一处都没挂上,第一层完全未生效,当前只有 CI 兜底(第二层)在防。规范写的是「推荐」不是「强制」,故非违规,但 M4 要落 AI provider 凭证(§5),第一层的价值从「锦上添花」变成「拦在 push 之前」。三条 git config 命令即可修复(拍板 D9)。

三仓 scripts/check-secrets.sh md5 全等0d4deed251e9161e33a4f1126def4927)⇒ 副本同步纪律执行到位。

(3) ⚠️ patbond-doc 仓完全没有 .gitignore

实测:cat .gitignore → 「没有那个文件或目录」。而本地存在 site/mkdocs 默认输出目录)。

git -c core.excludesFile=/dev/null check-ignore -v site/未被仓库规则忽略;仅 git check-ignore -v site/ 命中 /home/lx/.gitignore_global:183 —— 即只靠本机用户级全局忽略文件屏蔽

  • 当前 git ls-files site/ | wc -l = 0(尚未误提交,工作区 clean)。
  • 风险:换机器、新 clone、或任何未配置全局忽略的环境下,一次 git add -A 就会把整个 site/ 构建产物提交进去(约百余文件)。
  • 对比:api 仓有 .gitignore:1 target/:10-11 本地 application.yml + .sample 例外、:14 .env),flutter 仓有 .gitignorebuild / .dart_tool 均 repo-ignored)。只有 doc 仓是裸的。
  • 这与 git-workflow.md:62 的「不把 site/ 落进仓库」是同一条纪律 —— 但该纪律只写在文档里,没有机器保障

⇒ 一行修复(doc 仓新建 .gitignore 写入 site/),列为 M4 第一波待办(拍板 D8)。本次只规划不改

(4) 数字口径矛盾(三处,需在 v0.5.0 发布记录中统一)

本报告未运行测试,故下列数字非本报告实测,仅记录来源冲突,供 PM/RC 收口:

releases.md / feature-checklist.md 记载 其他来源 判断
api 测试数 379releases.md:17 / :35feature-checklist.md:5 381iteration-3.5/04 §7.2 原文「BUILD SUCCESS,381 测试全绿」;该报告同时说明测试 379→381 因新增 2 个矩阵方法) 381 应为准379 是冻结前口径,被下游照抄
E2E 断言数 234releases.md:36 226Evidence Collector 机械计数:210 个 check( + M1 的 16 个 倾向 226;本报告未复核计数方法
真机挂起项 v0.4.0 段落列「M2 两项 + M3 四项 + M3.5 新增两项」= 8 10 项Evidence Collector 未复核,以 EC/RC 口径为准
埋点白名单 42 41 未复核,以 EC/RC 口径为准

E2E 场景数 42 正确(7+11+14+10),本报告独立核算一致。

⇒ 纯文档口径问题,不影响 Git 流程,但 v0.5.0 发布记录不要再照抄上游数字,一律以当次实跑输出为准(写进 §4 checklist)。

2. M4 分支与提交规划

2.1 分支策略:继续 dev 直推(不引入常态 feature 分支)

推荐 dev 直推为默认,仅在两种情形开短命分支。理由按证据强度排序:

  1. CI 的 push 触发器不覆盖 feature 分支ci.yml:16-17 branches: [dev])⇒ 推到 feat/xxx 不会跑任何 CI。开分支等于自断快速反馈,除非每个分支都立刻开 PR 换 pull_request 触发(ci.yml:18 无分支过滤,这条是通的)。这是本项目特有的、比通用最佳实践更强的约束。
  2. 既有纪律本就如此git-workflow.md:9 把 feature 分支限定为「跨多天 / 有破坏性风险 / 多人并行同仓」三种情形,M1–M3.5 四个迭代全程 dev 直推,43flutter)/ 数十(api)提交零合并提交、历史线性。没有出现需要分支的问题。
  3. 单人开发ADR-011 的 PR 强制条款已在 iteration-3/08 §6 #1 降级为「两人并行同仓」与「影响 main 的变更」两种情形。M4 若仍是单人推进,feature 分支的核心收益(隔离并行)不存在。

例外——建议开短命分支的两种 M4 情形

情形 分支名 处置
AI provider 接入探针(spike:第三方 SDK/HTTP 选型、prompt 迭代、成本与延迟实测,大概率产生大量废弃代码 spike/ai-provider 探完不合并,结论写报告,实现另起干净提交直推 dev。分支删除。
AI 创作可能触发的跨模块重构(如 media 域被复用为 AI 产物存储) refactor/<主题> 完成后 git rebase dev 整理为原子提交序列,--ff-only 合回 dev 并删分支(git-workflow.md:9 的「短命分支,不留长期分叉」)

两种情形都不要从 feature 分支直接开 PR 到 main —— §1.4 推论二下那种 PR 永久不可合。

dev 必须随时可构建git-workflow.md:7):M4 的契约升版、新模块接入等「一改就全红」的动作必须攒成单个原子提交再推,不要分两次推让 dev 中途红(§3 详述)。

2.2 提交粒度与信息规范

粒度(沿 git-workflow.md:17「一次提交做一件事,可独立回退」)。M4 具体切法建议:

M4 工作项 建议提交粒度
Flyway 迁移(若有) 迁移 + 其验证集成测试 = 一个提交(沿 T3-01 先例:feat: Flyway V5 community schema 基线 + 迁移验证集成测试)。迁移一旦推入 dev 即不可变(git-workflow.md:33),故必须与验证同时落地
新模块骨架(若有) 单独一个提交(沿 T3-02 feat: 新建 patbond-community 模块骨架),含根 pom <module> 一行
每个对外端点 一个 feat: 提交,含实现 + 单测 + 集成测试
契约升版 v1.5.0 doc 侧一个提交、api 侧一个原子提交,两侧都不与业务代码混(§3
客户端每页 一个提交(沿 flutter 惯例,一页一提交)
第五份 E2E 脚本 单独一个提交(沿 v0.4.0 先例 新增:M3.5 E2E 烟囱脚本…(v0.4.0 发布门禁)
报告入档 doc 仓按波次批量一个 docs: 提交(沿既有惯例)

信息规范M4 起统一,见拍板 D6):

<前缀>[(<scope>)]: <中文主题><工单号>[ADR-0xx]

- 关键改动列表
- 门禁:<命令输出结论>(测试数量与结果)
  • 前缀白名单:feat / fix / refactor / docs / test / chore / stylestyle 已在 flutter 实用,补进白名单)。
  • scope 可选,正式承认 test(contract): / docs(api): 形态。
  • 主题用中文一句话;引用 M4 工单号(T4-xx)与 ADR 编号。
  • 正文必须带验收证据git-workflow.md:16)——测试数、门禁命令结论。这是 M4 数字口径不再漂移的第一道防线(§1.8(4))。
  • 不改写已推送历史git-workflow.md:34);不 force push dev/main:32)。

2.3 三仓同步点(契约不漂移的机制)

M4 有 4 个强同步点,其中契约升版是唯一「必须同一时间窗内完成」的:

同步点 涉及仓 顺序约束 漂移检测
S1 契约升版 v1.5.0 doc(正典)→ api(4 份快照+守卫) doc 先,api 后,同一工作时段完成 五路 md5sum 人工比对(无 CI 保障,§1.6+ api 侧守卫计数断言
S2 客户端消费新契约 api → flutter api 端点落地并契约冻结后,flutter 才接线 flutter 数据层测试对齐契约操作数(沿 T3-12「契约 v1.3.0 十九操作全覆盖」惯例)
S3 防泄漏规则增补 api + flutter + doc 三仓同时 无先后,但必须同批提交 md5sum 三仓 scripts/check-secrets.sh 全等(git-workflow.md:38 明文要求;当前全等
S4 发布 api + flutterPR)→ doc(记录+tag 两仓 PR 合并并打 tag 后,doc 记录+打 tag 三仓 tag 同名 v0.5.0,互为对照

S1 防漂移的硬机制(因 CI 不管跨仓,必须靠人工纪律 + 一条命令):

WS=<你的工作区>            # 例:~/workspace/patbond
V=1.5.0
md5sum "$WS/patbond-doc/docs/api/openapi.yaml" \
       "$WS"/patbond-api/patbond-{auth,pet,user,community}/src/test/resources/contract/openapi-v$V.yaml |
  awk '{print $1}' | sort -u | wc -l
# 必须输出 1(五路字节全等);输出 ≥2 即已漂移,禁止提交

把这条命令写进 M4 契约升版工单的验收条件,并在 api 侧升版提交的正文里贴出 md5 值(沿 T3.5-07 先例:其提交正文原文含 md5 与正典逐一比对一致 a7081fb84f1207eef579ab94025f5801)。

S1 的一个易漏点M3.5 踩过,已写入代码注释):/api/v1/me 的契约守卫在 patbond-auth 模块,而实现在 patbond-user。M4 若再扩 /me 面,先看 auth 模块。

3. 契约 v1.5.0 升版的 CI 联动操作顺序

3.1 升版实际要动 9 处(清单)

# 位置 改什么
1 patbond-doc/docs/api/openapi.yaml:4 version: 1.4.01.5.0
2 同文件 :5+ info.description 追加 1.5.0 M4 冻结段落 + 重申「对 v1.4.0 纯增量」承诺
3 patbond-doc/docs/api/index.md:3 「(OpenAPI 3v1.4.0),当前 32 路径 / 45 操作」→ 新版本与新计数
4 同文件 :36 冻结纪律行追加「AI 创作域已冻结(1.5.0)」
5 api ×4 快照文件 新增 openapi-v1.5.0.yaml(正典字节副本)、git rmopenapi-v1.4.0.yaml
6 api ×4 OpenApiContract.java RESOURCE 常量:auth:39 / pet:35 / user:39 / community:39,值改 /contract/openapi-v1.5.0.yaml;顺带改类 javadoc 里的版本字样
7 api ×4 conformance test 计数断言 4 行 × 4 文件:auth:401-404 / pet:757-760 / user:248-251 / community:524-527
8 api ×N conformance test 新增操作的响应矩阵入场(新格数)
9 flutter 数据层测试 对齐新操作数(S2,可延后到客户端波次)

旧快照删除是既定纪律T3-19 先例、T3.5-07 复述):守卫只认一份,保留旧快照是死重,历史版本由 git 与 doc 仓承载。

3.2 操作顺序(避免 CI 红的关键:api 侧必须是单个原子提交

理解为什么能避免红,先记住一条已核实的事实(§1.6):api 的 CI 只读 api 仓内文件。它检测的是「快照 ↔ 守卫 ↔ 矩阵」三者内部自洽,不检测与 doc 正典的一致性。所以:

  • doc 与 api 之间没有 CI 层面的先后依赖doc 先推不会让 api 红,api 后推也不会让 doc 红。
  • api 仓内部只要三者中任一处未同步,./mvnw clean test 立即红(守卫的 info.version 断言就是为此设计的)。⇒ 红与不红只取决于 api 侧是否原子提交。

推荐顺序(延续 T3.5-07 实证路径:doc 5f02909 先,api 3cd8005 后):

第 0 步 — 前置(契约先行纪律) docs/api/index.md:36 明文要求「契约变更须先改本文件目录下的 OpenAPI,再改实现」。故 M4 的端点实现落地前,先在 doc 侧定型契约(可以是草案报告,正式升版在实现定型后)。

第 1 步 — doc 仓:升正典,单独提交

cd $WS/patbond-doc
# 改 openapi.yaml 的 version + description、index.md 的版本与计数、冻结纪律行
python3 -c "import yaml;yaml.safe_load(open('docs/api/openapi.yaml'))"   # 解析必须通过
mkdocs build --strict -d /tmp/site                                        # exit 0 零 warning
sh scripts/check-secrets.sh --all                                         # exit 0
git add docs/api/openapi.yaml docs/api/index.md
git commit -m "docs(api): M4 契约冻结 v1.5.0——AI 创作域(T4-xx"
git push origin main        # doc CI 只跑 mkdocs strict + secret scan,与 api 无关

补充校验(沿 T3.5-07 §7.2 惯例,建议保留):全部 $ref 可解析、operationId 无重复无缺失、零未引用 schema、tags 声明与使用双向闭合。

第 2 步 — 记录正典 md5(后续所有比对的基准)

md5sum $WS/patbond-doc/docs/api/openapi.yaml

第 3 步 — api 仓:一次性改完 5~8 项,本地全绿后才提交(⚠️ 不要中途 commit

cd $WS/patbond-api
CANON=$WS/patbond-doc/docs/api/openapi.yaml
for m in auth pet user community; do
  cp "$CANON" patbond-$m/src/test/resources/contract/openapi-v1.5.0.yaml
  git rm -q patbond-$m/src/test/resources/contract/openapi-v1.4.0.yaml
done
# 改 4 个 RESOURCE 常量、4×4 计数断言、新增操作矩阵(见 §3.1 的 6/7/8)

提交必须过的三道校验:

# (a) 五路字节全等 —— 唯一能防跨仓漂移的检查,无 CI 兜底
md5sum "$CANON" patbond-{auth,pet,user,community}/src/test/resources/contract/openapi-v1.5.0.yaml |
  awk '{print $1}' | sort -u | wc -l      # 必须为 1

# (b) 全量测试(守卫 + 矩阵)
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test   # BUILD SUCCESS0 失败

# (c) 防泄漏
sh scripts/check-secrets.sh --all                          # exit 0

# (d) 确认没有残留旧快照
git ls-files '*openapi-v*' | sort   # 只应出现 4 个 v1.5.0

三道全过再一次性提交:

git commit -m "test(contract): v1.5.0 快照四模块同步 + AI 创作矩阵入场(T4-xx)

- 四模块快照 openapi-v1.4.0.yaml → openapi-v1.5.0.yaml,删旧文件(守卫只认一份)
- 四份守卫期望升版:1.4.0/32/45/75 → 1.5.0/<路径>/<操作>/<schema>
- 矩阵 181 → <新格数> 格
- md5 与正典五路逐一比对一致 <md5 值>
- 门禁:mvnw clean test BUILD SUCCESS<N> 测试全绿;check-secrets --all exit 0"
git push origin dev

第 4 步 — 等 dev 的 (push) CI 转绿,确认 CI / backend-test (push) 为 success 再继续后续工单:

SHA=$(git rev-parse HEAD)
curl -sk "https://132.232.242.77/api/v1/repos/zhaoyuxi/patbond-api/commits/$SHA/status" |
  python3 -m json.tool

第 5 步 — flutter 侧(S2,可延后):客户端数据层测试对齐新操作数,单独提交。

3.3 反模式(会红 CI,明确禁止)

反模式 后果
cp 新快照就 commit,守卫常量下一提交再改 守卫仍指向已被 git rm 的 v1.4.0 → 契约快照缺失 抛错,dev 全红
只改守卫版本断言,没换快照文件 info.version 断言 1.5.0 vs 快照 1.4.0 → 四模块全红
只同步 2~3 个模块 未同步模块红。四个模块必须同批auth / pet / user / community
手改 api 侧快照而非从正典 cp 字节漂移,md5 不等;CI 不会发现(§1.6),漂移静默进库 —— 最危险的一种
新增操作只升计数不进矩阵 计数断言绿,但「未声明字段即漂移」类断言红(M3.5 曾一次性红 11 格)
保留旧快照「以防万一」 违反 T3-19 纪律;且 git ls-files '*openapi-v*' 会暴露死重

3.4 M4 若新增 api 模块:契约成本会从 4 处变 5 处

已核实 iteration-3/08 §5.1 的「新模块 CI 零成本」结论至今仍准确:根 pom <modules> 现为 common/user/auth/pet/community 五个,ci.yml:55 是根反应堆 ./mvnw -B clean test,新增模块只需根 pom 加一行、ci.yml 零改动

契约侧不是零成本:当前四个模块各自持有一份 OpenApiContract.java + 快照(同构副本纪律,与 BearerAuthFilter 同)。若 M4 新建模块也承载契约一致性测试,升版就要同步 5 份快照 + 5 个常量 + 5 组断言,每次升版成本 +25%。

⇒ 建议:新模块默认不复制契约框架,仅当它确实需要对冻结契约做一致性断言时才复制;其端点的契约矩阵可挂在既有模块(/me 系挂 auth 就是先例)。见拍板 D3。

Flyway:现有 V1V5 全部位于 patbond-user/src/main/resources/db/migration/(含 pet_health 与 community 的 schema),即迁移集中由 user 模块承载。M4 若需新表,下一个版本号是 V6__*.sql,仍放该目录(沿 5/5 先例),不要在新模块另起迁移目录(会导致两个 Flyway 位置,历史表版本冲突)。已推送的 V1V5 不可修改(git-workflow.md:33)。

4. v0.5.0 发布 checklist

4.0 先解决「流程该固化到哪」与双处矛盾

问题(Evidence Collector 提出,本报告独立复核确认):发布流程目前没有一份可信的单一来源

已核实的三条事实:

  1. releases.md:4 指向的正典是一份「草案」:原文「按 [发布 checklist](iterations/iteration-3/08-git-workflow-plan.md)(§3.3)执行」,而该节标题至今是「§3.3 发布 checklist 草案(首次发布用,验证后固化进 git-workflow.md」。

  2. 固化从未发生:本报告通读 git-workflow.md 全文 65 行,章节仅 5 个——分支模型(:5) / 提交信息约定(:11) / 禁止事项(:28) / 凭证防泄漏(:36) / 提交前本地门禁(:56)。无任何发布、PR、E2E、checklist 内容

  3. iteration-3/08 §3.3 的 8 步里 4 步已过期

    • 第 2 步原文「跑 M2+M3 两份 E2E 烟囱脚本」→ 实际已是四份M1/M2/M3/M3.5),M4 后为五份;
    • 第 3 步(命名统一 + api main 重建)→ 一次性项,已完成,不再重复;
    • 第 4 步「git checkout main && git merge --ff-only dev && git push origin main」→ 保护启用后该命令必被拒
    • 第 6 步(启用分支保护)→ 一次性项,已完成。
  4. releases.md 自我矛盾:36:56 写「四份」E2E,但 :131(「发布后生效的纪律」的前瞻步骤 1)仍写「完成 checklist 第 1~2 步(三仓 CI 绿 + E2E 双份回归 PASS)」。错在前瞻侧——照 :131 执行会只跑 M2+M3漏跑 M1 的 7 场景 + M3.5 的 10 场景,共 17 个场景(M4 后漏跑 17 + M4 新增份数)。

⇒ 一份「草案」被当正典、修正散落在发布记录的散文里、且该散文自我矛盾。这正是 M3.5「转述被全链路采信」教训的同类结构。

推荐处置(拍板 D2

把发布流程固化为 git-workflow.md 的新增章节「## 发布流程(dev → main)」,理由:

  • git-workflow.md跨迭代常设规范,本就是 iteration-3/08 §3.3 自己指定的固化目标(「验证后固化进 git-workflow.md」),只是没执行;
  • 迭代报告(iteration-3/08)是历史快照,不应承担常设正典职责——它写的是「M3 时的规划」,随时间必然过期;
  • releases.md 定位是「每次发布追加一条记录」(:3),不是流程正典。

配套三个动作(本报告不执行,建议由主会话统一处置):

动作 目标文件 内容
A git-workflow.md 新增「## 发布流程(dev → main)」章节,正文为 §4.1 的 checklist
B iteration-3/08-git-workflow-plan.md §3.3 标题加历史标注:「(已于 M4 固化进 git-workflow.md,本节仅存历史,勿照此执行)」。不删改正文(历史报告不改写)
C releases.md:4 + :131 :4 的链接改指 git-workflow.md 的新章节;:131 的「E2E 双份回归」改为「E2E 全份回归(份数以 git-workflow.md 为准)」——不写死份数,避免再次出现「数字散落多处」

防复发设计:份数只在一处写死git-workflow.md 的发布流程章节),其他所有位置一律表述为「全份 E2E 回归」并链接过去。M4 新增第五份时只改那一处。

4.1 v0.5.0 发布 checklist(可直接执行)

适用前提main 已启用保护、禁直推(§1.2);必需状态检查上下文已按拍板 D1 修正为 (pull_request) 并完成一次性验证(§1.4)。若 D1 未采纳,第 4 步的门禁语义仍是错位的,见 §5.1。 变量WS=<你的工作区>GITEA=https://132.232.242.77(自签证书,curl-k)。

第 1 步 — 冻结与本地门禁(三仓)

# api
cd $WS/patbond-api && git status --porcelain    # 必须为空
JAVA_HOME=/usr/lib/jvm/java-17-openjdk ./mvnw clean test   # BUILD SUCCESS0 失败
sh scripts/check-secrets.sh --all                          # exit 0
# flutter
cd $WS/patbond-flutter
dart format --output=none --set-exit-if-changed lib test   # 0 changed
flutter analyze                                            # No issues
flutter test                                               # All tests passed
sh scripts/check-secrets.sh --all                          # exit 0
# doc
cd $WS/patbond-doc && mkdocs build --strict -d /tmp/site    # exit 0 零 warning
sh scripts/check-secrets.sh --all                          # exit 0

记录实跑输出的测试数api / flutter 各一个数字),后续发布记录一律用这两个数,不得照抄上游文档(§1.8(4) 的 379/381 就是照抄事故)。

第 2 步 — 三仓 CI 绿(查 API,不看网页印象)

for r in patbond-api patbond-flutter patbond-doc; do
  cd $WS/$r; SHA=$(git rev-parse HEAD)
  echo "=== $r $SHA ==="
  curl -sk "$GITEA/api/v1/repos/zhaoyuxi/$r/commits/$SHA/status" |
    python3 -c "import sys,json;d=json.load(sys.stdin);print(' state:',d['state']);[print('  ',s['status'],repr(s['context'])) for s in d['statuses']]"
done

放行标准:三仓 state: success必须是当前 HEAD 的状态——若 dev 在此期间又有提交推入,重跑本步(releases.md:57 的教训:不要用旧的绿色状态放行)。

第 3 步 — E2E 五份全份回归(发布门禁核心)

cd $WS/patbond-api && docker compose up -d --build     # 六容器
docker compose logs postgres | grep -i flyway          # 记录 Current version(零迁移/新迁移均需取证)
cd $WS/patbond-flutter
dart run test_e2e_manual.dart          # M1  7 场景
dart run test_e2e_m2_manual.dart       # M2  11 场景
dart run test_e2e_m3_manual.dart       # M3  14 场景
dart run test_e2e_m35_manual.dart      # M3.5 10 场景
dart run test_e2e_m4_manual.dart       # M4  <N> 场景 ← 本迭代新增,见 §4.2

放行标准:五份全 PASS,场景数 42 + M4 的 N,零失败;契约偏差 0(逐场景对照 v1.5.0)。同环境串行跑,证据入迭代报告。

⚠️ 门禁是五份,不是两份也不是四份releases.md:131 的「双份」表述已过期,勿照执行(§4.0)。

第 4 步 — 建 PRapi + flutter 各一个)

在 Gitea 网页建 dev → main PR

  • 标题:v0.5.0 M4 AI 创作
  • 正文:贴门禁证据链接(测试数、五份 E2E 报告、契约 v1.5.0 纯增量比对结论)
  • 必需状态检查转绿。按 D1 修正后应为 CI / backend-test (pull_request) / CI / flutter-gates (pull_request)
  • 合并方式必须选 Fast-forward onlyv0.4.0 先例:merge_commit_sha == head sha,零合并提交、历史线性)。
  • 合并后核验:
for r in patbond-api patbond-flutter; do
  cd $WS/$r && git fetch -q origin
  echo "$r main=$(git rev-parse origin/main) dev=$(git rev-parse origin/dev)"
done   # 两个 sha 必须相等

第 5 步 — 打 tag(三仓,annotated

cd $WS/patbond-api    && git tag -a v0.5.0 -m "M4 AI 创作" && git push origin v0.5.0
cd $WS/patbond-flutter && git tag -a v0.5.0 -m "M4 AI 创作" && git push origin v0.5.0
cd $WS/patbond-doc     && git tag -a v0.5.0 -m "M4 AI 创作" && git push origin v0.5.0

延续惯例:annotated tag,正文写完整发布说明(api 的 v0.4.0 tag 正文即模板:版本内容分条 + 契约版本 + 测试数 + 门禁结论)。doc 仓在发布记录提交之后打,使 tag 指向含本次记录的提交。

第 6 步 — 发布记录入 doc

releases.md 顶部追加 v0.5.0 段落(最新在最上,:4 约定):三仓 tag 与哈希、版本内容增量、门禁证据表(测试数用第 1 步实跑值、E2E 写「五份 / 场景数 / 断言数」)、已知遗留、checklist 变更。

第 7 步 — 发布后核验(三仓一致性)

for r in patbond-api patbond-flutter patbond-doc; do
  echo "=== $r ==="; cd $WS/$r
  git ls-remote origin | grep -E 'refs/heads/(dev|main)$|refs/tags/v0.5.0'
done

放行:api/flutter 的 dev == main == v0.5.0^{}doc 的 main == v0.5.0^{}

已废止的步骤(勿执行):iteration-3/08 §3.3 的第 3 步与第 6 步(一次性项,已完成);第 4 步的本地 checkout main && merge --ff-only && push保护启用后必被拒)。

4.2 第五份 E2E 脚本的落位与命名约定

约定
路径 patbond-flutter/ 仓根(与既有四份并列,不进 test/——flutter test 不应收集它,它需要活的后端)
文件名 test_e2e_m4_manual.dart(延续 m2/m3/m35 口径)
运行 dart run test_e2e_m4_manual.dart;前置 api 仓 docker compose up -d --build
头注释 test_e2e_m35_manual.dart:1-40 模板:shebang、// ignore_for_file: avoid_print、前置条件、运行方式、与前四份的分工声明、冻结契约版本(v1.5.0 + 路径/操作/schema 计数)、场景清单编号、脱敏说明、library;
覆盖范围 只覆盖 M4 增量对外面,回归由前四份承担(M3.5 脚本明文如此声明,延续)
必过门禁 flutter analyze(分析器覆盖仓根,analysis_options.yaml 无 exclude)⇒ 必须加 // ignore_for_file: avoid_print,否则 CI 红
不受门禁 dart format 只覆盖 lib testci.yml:56)⇒ 仓根脚本不被格式门禁拦,但仍建议手动 dart format
脱敏纪律 token 截断、预签名 URL 签名 query 抹为 <SIGNATURE_REDACTED>、Idempotency-Key 占位(M3.5 脚本既有纪律)。M4 新增:AI provider 的 API key 与 prompt/响应中的用户内容同样必须脱敏,且脚本本身会被 check-secrets.sh --all 扫(ALLOW 白名单含 redacted,用 <...REDACTED> 形态即可放行)
提交 单独一个提交,信息延续 新增:M4 E2E 烟囱脚本——<主题>N 场景(v0.5.0 发布门禁)
登记 场景数与断言数写进迭代报告;份数写进 git-workflow.md 的发布流程章节(唯一写死处,§4.0 防复发设计)

5. 风险与预案

5.1 PR 出现非快进(无法 ff

成因判别先于动作devmain 分叉只有三种可能,处置完全不同:

cd $WS/<仓> && git fetch origin
git rev-list --left-right --count origin/main...origin/dev   # 输出「A<TAB>B」
# A=0 → main 无独有提交,可 ff(正常)
# A>0 → main 上有 dev 没有的提交 ⇒ 分叉,先查明
git log --oneline origin/dev..origin/main    # 列出 main 独有的提交,逐个看作者与时间
成因 判别特征 处置
(a) 有人绕过 dev 直接改了 main main 独有提交的作者/时间可查;理论上被保护阻止,但管理员可临时关保护 不要用合并提交掩盖releases.md:138 明文)。查明该提交内容,把它 cherry-pick 到 dev,然后 main 重新 ff。同时查为什么保护被绕过
(b) 上次发布用了非 ff 的合并方式 main 独有提交是一个 merge commit 历史已污染但可接受,本次仍可 ff(merge commit 是 dev 的后代?若否则同 (a))。下次严格选 Fast-forward only
(c) 误在 main 上打了 hotfix 见 (a) 同 (a),且暴露 §1.4 推论二:hotfix 想走 PR 会死锁,所以有人图省事直推了 main

明确禁止git push --force origin dev:main(破「不 force push 共享分支」戒律,git-workflow.md:32);merge --allow-unrelated-historiesiteration-3/08 §3.1 选项 C 已判定不推荐)。

若确实必须做非 ff 合并(例如 main 上有必须保留的独有提交且无法 cherry-pick):在 Gitea PR 里选 Create merge commit,并在发布记录中显式登记为例外 + 写清原因。历史线性性让位于可追溯性,但必须留痕。

5.2 CI 状态检查上下文名变化 —— 最高杀伤力的低概率风险

上下文字符串 = <workflow name> / <job key> (<event>),三段任一改动即使必需上下文永久缺失PR 永久不可合并(不是变红,是「检查从未上报」)。触发改名的动作:

动作 后果
ci.ymlname: CI 全部上下文改名
改 job keybackend-test / flutter-gates 该仓上下文改名
增删 job(如把 backend-test 拆成 unit + integration 旧上下文消失
on: 触发器(如给 pull_request 加分支过滤) 对应 event 的上下文可能不再产生

预案(顺序执行)

  1. 改名前先改保护配置,不要反过来。顺序:Gitea 保护里先追加新上下文名(此时新旧并存,required 是「都要绿」)→ 推 ci.yml 改名 → 确认新上下文上报成功 → 再从保护里删掉旧上下文名。这样任一时刻都不会出现「required 上下文无人上报」。
  2. M4 期间尽量不动 ci.yml 的 name/job key。若 M4 新增 api 模块,根反应堆 ./mvnw -B clean testci.yml:55)自动覆盖,无需新增 job ⇒ 天然规避(§3.4)。
  3. 卡死时的解锁手段(记录下来,免得临场慌):仓库 admin 在 Gitea → Settings → Branches → main → 暂时取消勾选「Enable Status Check」或删掉失效上下文 → 合并 → 立即改回正确上下文名。每次这样操作都要在发布记录里留痕。
  4. 改名后必须做一次空 PR 验证(同 §1.4 的一次性验证要求),不要拿正式发布当试验场。

取真名的唯一可靠方法(不要猜、不要凭记忆):

SHA=$(cd $WS/patbond-api && git rev-parse origin/dev)
curl -sk "$GITEA/api/v1/repos/zhaoyuxi/patbond-api/commits/$SHA/statuses?limit=50" |
  python3 -c "import sys,json;[print(repr(s['context']),s['status']) for s in json.load(sys.stdin)]"

5.3 doc 仓不受保护带来的风险

doc 仓 main 未启用保护、可直推、且是唯一分支(§1.2)。四条实际风险与处置:

风险 现状评估 处置
契约正典可被无门禁直推 ⚠️ 这是最实质的风险docs/api/openapi.yaml 是 api 四份快照的唯一上游(§1.6),改它没有任何 CI 门禁能验证下游一致性。一次误推正典 → api 侧 md5 静默不等 → 漂移进库 不靠保护,靠流程:契约升版严格按 §3.2 顺序,api 侧提交正文必须贴 md5(可事后审计)。中期方案见拍板 D5(在 api CI 里加跨仓 md5 校验 step
构建产物误入库 ⚠️ .gitignore 完全不存在(§1.8(3)),site/ 仅靠 ~/.gitignore_global:183 屏蔽 一行修复:doc 仓新建 .gitignoresite/(拍板 D8
误推可直接改写发布记录 中等:releases.md 是发布事实的唯一记录,无审核 依赖 tag 作为交叉对照(三仓同名 tag 互证,releases.md:5 既有设计)。可接受
doc CI 红也能推上去 低:push 触发器覆盖 mainci.yml:7),红了看得见,但不阻塞 保持现状;提交前跑 mkdocs build --strict 是既有纪律(git-workflow.md:62

是否给 doc 仓 main 启用保护?推荐不启用(拍板 D10):doc 是日常直推分支,启用保护意味着每条文档改动都要开 PR,成本远超收益;且 doc 无 dev 分支,启用后连日常提交都要建分支。替代方案是把关键风险(契约正典)用 CI 校验兜住(D5),而不是用分支保护。

5.4 tag 打错的回退方式

三种错法,处置不同。前提v0.5.0 tag 一旦被他人 fetch,改动就是历史改写——但本项目是单人 + 三仓自托管,回退窗口实际很宽。

(a) tag 打在错误的提交上(最常见)

cd $WS/<仓>
git tag -d v0.5.0                          # 删本地
git push origin :refs/tags/v0.5.0          # 删远端(冒号前缀 = 删除 ref)
git tag -a v0.5.0 <正确的sha> -m "M4 AI 创作"
git push origin v0.5.0

⚠️ 删远端 tag 后务必立即重打并推,不要留「三仓 tag 不齐」的中间态。三仓必须同批处理——若只有 api 的 tag 错了,也只需改 api,但要重新核验 §4.1 第 7 步的三仓一致性。

(b) tag 名写错(如打成 v0.5 / V0.5.0

git tag v0.5.0 v0.5          # 从旧 tag 建正确名(annotated 会退化为 lightweight
# 更稳:直接按 (a) 用 -a 重建,保留完整发布说明正文
git tag -d v0.5 && git push origin :refs/tags/v0.5

本项目三仓全部 v0.3.0/v0.4.0 均为 annotated,重建时必须用 -a 并补回说明正文,否则 tag 类型漂移(git cat-file -t 会从 tagcommit)。

(c) tag 说明写错但指向正确

git tag -a v0.5.0 -f -m "<修正后的说明>"   # -f 覆盖本地
git push origin -f v0.5.0                  # 强推该 tag

这是本项目唯一被接受的 force push 形态——git-workflow.md:32 禁的是共享分支,tag 不在其列。但仍建议在发布记录里留一句「tag 说明于 <时间> 修正」。

预防:打 tag 前先核对指向(git rev-parse origin/main 与将要打 tag 的 sha 一致),并按 §4.1 第 7 步一次性核验三仓。

5.5 M4 特有风险:AI provider 凭证扫描规则零覆盖

已核实scripts/check-secrets.sh:28-37 的规则表共 7 条

AK-AWS         AKIA[0-9A-Z]{16}
AK-QCLOUD      AKID[0-9A-Za-z]{16,}
AK-ALIYUN      LTAI[0-9A-Za-z]{12,}
MINIO-DEFAULT  minio[-_.]?admin
PRIVATE-KEY    ^\s*-----BEGIN [A-Z ]*PRIVATE KEY-----\s*$
KEY-ASSIGN     (access[-_]?key(_?id)?|secret[-_]?(access[-_]?)?key)["']?\s*[:=]\s*["']?[A-Za-z0-9+/=_-]{8,}
JWT-SECRET     (jwt[-_.]?secret|signing[-_]?key|token[-_]?secret|hmac[-_]?(key|secret))["']?\s*[:=]\s*["']?[A-Za-z0-9+/=_-]{8,}
DB-PASSWORD    (password|passwd|pwd)... (仅限 .ya?ml|properties|toml|conf|ini 文件)

缺口分析

  • 无任何规则匹配 sk- 前缀族OpenAI sk- / sk-proj-、Anthropic sk-ant-、DeepSeek / Moonshot / 阿里百炼 DashScope 均为 sk- 形态)。
  • KEY-ASSIGN 不覆盖 api_key:其模式只认 access_key / secret_key不含通用 api[-_]?key。⇒ ANTHROPIC_API_KEY=sk-ant-xxxopenai_api_key: sk-xxxdashscope_api_key: sk-xxx 全部漏网
  • 文件名黑名单(:24 DENY_NAME)覆盖 .env / credentials* / *AccessKey*.csv / rootkey.csv,对 AI provider 尚可(凭证通常进 .env),但内容层无兜底。

这与 M3 的处境完全同构iteration-3/08 §4.2 当年把「对象存储凭证进入任何开发机之前必须上线 CI 兜底」定为 M3 第一波必做。M4 的 AI provider key 风险更高——它是第三方控制台签发的长期凭证、直接绑计费、泄漏即可被外部调用刷额度,且 AI 辅助开发下 key 从配置流向「示例代码 / 测试 / 报告 / prompt 样例」的路径比对象存储更多。

处置(拍板 D4,M4 第一波、先于任何 AI 凭证落地):向规则表增补,三仓同批提交git-workflow.md:38 要求副本同步,当前三仓 md5 全等需保持):

AK-LLM-SK      -   -   \bsk-(ant-)?[A-Za-z0-9_-]{20,}
API-KEY-ASSIGN i   -   (api[-_]?key|apikey)["']?[[:space:]]*[:=][[:space:]]*["']?[A-Za-z0-9+/=_-]{16,}

注意两点:(1) 现有 ALLOW 白名单(:18)含 example|sample|dummy|fake|redacted|placeholder|your[-_]…|\$\{…\},文档里写 sk-ant-your-key-heresk-xxx-example 会被放行 ⇒ 文档与 E2E 脚本里的示例 key 一律用这些占位形态。(2) 增补后必须跑 sh scripts/check-secrets.sh --all 三仓验证无误报(历史文档里可能有形似字符串),有误报则收紧模式而非放宽白名单。

配套:同时启用第一层 pre-commit(§1.8(2),三仓 core.hooksPath 全未设置)——AI 凭证场景下「拦在 commit 之前」比「push 后 CI 才红」价值大得多,因为后者意味着凭证已经在远端历史里了。

若真泄漏:第一动作是去 AI provider 控制台吊销/轮换该 key,之后才是清理历史(git-workflow.md:54check-secrets.sh:12 双处明文)。M4 需在 server-exposure.md 增登记 AI provider 凭证条目。

6. 待拍板决策清单

10 项。标 的三项最关键(前两项建议在 M4 开工第一波就定,不要拖到发布前)。

# 事项 推荐 理由摘要
D1 修正 main 的必需状态检查上下文CI / backend-test (push)CI / backend-test (pull_request)flutter 同理 flutter-gates替换而非追加。改完做一次空 PR 验证 采纳 当前门禁由「推 dev」满足而非 PR 自身,(pull_request) 红也能合;且非 dev 分支 → main 的 PR 永久不可合,hotfix 路径是死的。上下文真名已从 CI 实跑记录取到,非猜测 §1.4
D2 发布流程固化到 git-workflow.md 新增章节「## 发布流程(dev → main)」;iteration-3/08 §3.3 标题加「已固化,仅存历史,勿照执行」标注;releases.md:4 改指新章节、:131 的「E2E 双份回归」改为「全份回归」并链接过去。E2E 份数只在一处写死 采纳 现正典是一份「草案」,固化从未发生(git-workflow.md 65 行无发布内容),8 步里 4 步过期,且 releases.md 自我矛盾(:36/:56 四份 vs :131 双份)。照 :131 执行会漏跑 17 个场景 §4.0
D4 check-secrets.sh 增补 AI provider 凭证规则sk- / sk-ant- 前缀族 + 通用 api_key 赋值),三仓同批提交;M4 第一波、先于任何 AI 凭证落地 采纳 现有 7 条规则对 sk- 零覆盖,KEY-ASSIGN 只认 access_key/secret_key 不认 api_keyANTHROPIC_API_KEY=sk-ant-… 全漏网。风险高于 M3 的对象存储凭证(第三方签发、绑计费、泄漏即可外部刷额度)。沿 iteration-3/08 §4.2 先例 §5.5
D3 M4 若新增 api 模块,默认不复制契约框架OpenApiContract.java + 快照);新端点的契约矩阵挂既有模块 不复制 复制会使契约升版成本从 4 处变 5 处(+25%/次)。/me 守卫挂 auth 而实现在 user 已是先例。模块本身接 CI 是零成本(根 pom 加一行,ci.yml 零改) §3.4
D5 在 api CI 增加跨仓 md5 校验 step(拉 doc 仓正典比对四份快照) 本迭代不做,登记为技术债 缺口真实存在(api CI 完全不校验与 doc 正典的字节一致性,漂移会静默进库)。但实现要在 api CI 里 clone doc 仓,触发归属含糊(沿 iteration-3/08 §5.2 对 E2E 进 CI 的同类判断)。M4 先用「提交正文贴 md5」做可审计留痕,验证纪律有效性后再自动化 §1.6 / §5.3
D6 提交前缀规范收口git-workflow.md:13 增补 style 前缀与可选 (scope) 形态;M4 起新提交统一用英文前缀;历史不改写 采纳(改规范 + 改新实践,不改历史) 三方不一致:规范只认 6 个英文前缀;flutter 仓 25/43 用中文前缀(新增/修复/重构);api/doc 已在用规范未覆盖的 test(contract):/docs(api):。反向方案(规范承认中文前缀)会让 api 与 flutter 永久分裂 §1.8(1)
D7 M1 的 E2E 脚本改名 test_e2e_manual.darttest_e2e_m1_manual.dartgit mv,同步 4 处文档引用) 采纳 回归清单变五份后,一个不带版本号的名字最易被误读成「总入口脚本」,进而漏跑。纯 rename,零逻辑风险。若嫌动文档,可推迟到 M4 收官波 §1.7
D8 doc 仓新建 .gitignore(至少含 site/ 采纳,M4 第一波 doc 仓完全没有 .gitignoresite/ 仅靠本机 ~/.gitignore_global:183 屏蔽。换机器或新 clone 时一次 git add -A 即提交上百个构建产物。一行修复 §1.8(3)
D9 三仓启用 pre-commit 第一层:各执行一次 git config core.hooksPath scripts/hooks 采纳,与 D4 同批 钩子脚本已入库但三仓 core.hooksPath 全部 unset ⇒ 第一层完全未生效,当前只有 CI 兜底。AI 凭证场景下「拦在 commit 前」远优于「push 后 CI 才红」(后者凭证已进远端历史) §1.8(2)
D10 doc 仓 main 不启用分支保护(延续现状) 不启用 doc 是日常直推分支且无 dev,启用后每条文档改动都要建分支+PR,成本远超收益。关键风险(契约正典无门禁)用 D5 的 CI 校验兜,而非用分支保护兜 §5.3

6.1 采纳后需落实的改动(本报告均未执行)

归属 动作
Gitea 平台(需 admin D1 改两仓必需上下文 + 一次性空 PR 验证;补做 §1.2 的 branch_protections 带 token 复核并把响应贴回本报告
patbond-doc D2 三处文档改动(git-workflow.md 新增章节、iteration-3/08 §3.3 加标注、releases.md:4/:131 修正);D6 规范增补;D8 新建 .gitignore;本报告挂入 mkdocs.yml 导航(本次未动 mkdocs.yml,8 agent 并发中,由主会话统一处置)
三仓同批 D4 规则表增补 + md5sum 三仓校验 + --all 无误报验证;D9 各执行一次 git config
patbond-flutter D7 git mv 改名 + 4 处文档引用同步
PM / RC §1.8(4) 的数字口径统一(api 测试 381 非 379、断言 226 非 234、真机挂起 10 项、埋点白名单 41 非 42),v0.5.0 发布记录一律用当次实跑值

6.2 本报告的取证边界(明确未证实项)

诚实登记,避免本报告本身成为「被转述采信」的下一个源头:

状态 缺什么
main「禁直接推送」的具体机制 ⚠️ 未直接证实 GET /branch_protections 返 401(本机无 token)。已证实的是 protected=true + v0.4.0 确实被迫走 PR。复核命令见 §1.2
api 测试数 381 / flutter 597 ⚠️ 本报告未运行测试 仅记录来源冲突(iteration-3.5/04 §7.2 = 381 vs releases.md:17 = 379),未自行跑 mvnw clean test
E2E 断言数 226 ⚠️ 未复核 采信 Evidence Collector 的机械计数,本报告未独立数
真机挂起 10 项 / 埋点白名单 41 ⚠️ 未复核 非本角色职责域,采信 EC/RC 口径
(pull_request) 作为必需上下文能否正常 gate ⚠️ 未验证(配置尚未改) 必须按 D1 做一次性空 PR 验证。不要拿 v0.5.0 正式发布当试验场
M4 契约的路径/操作/schema 计数 未知 M4 契约尚未定型,§3.1 的计数留占位

已证实的核心结论一句话:分支保护配置本身与描述完全一致,但必需状态检查填的是 (push) 而非 (pull_request)——门禁语义错位、hotfix 路径死锁;同时发布流程的「正典」是一份从未固化的草案且自我矛盾。这两条是 M4 开工前应优先修掉的。


本报告只写不提交(随波末统一入档);未修改 mkdocs.ymlreleases.mdgit-workflow.md 及任何 checklist —— 改动建议见 §6.1,由主会话统一处置。