八角色并行开工分析,合计 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>
64 KiB
M4「AI 创作」Git 工作流与发布流程规划
角色:Git Workflow Master | 日期:2026-09-14 | 性质:只读调研 + 规划,本报告未执行任何 commit / push / tag / 分支创建 / 远端配置变更。 前置:v0.4.0 已发布(三仓 tag 齐)。本报告是 iteration-3/08 的 M4 续篇,并接管其 §3.3「发布 checklist 草案」的固化职责。
结论摘要
- 用户交付的 6 条分支保护描述,逐条核实全部为真(Gitea API 原样贴在 §1.2):api/flutter 的
main均protected=true、enable_status_check=true、上下文显式填写、required_approvals=0;两仓dev均未保护;doc 仓main未保护。 - 但填的是错的触发器——这是本报告最重要的发现(§1.4)。必需上下文是
CI / backend-test (push),而push触发器被限定在branches: [dev]。推论有两个后果:门禁实际由「推 dev」满足而非 PR 自身;且任何非 dev 分支 → main 的 PR 将永久无法合并,与「hotfix 走短命分支 + PR」的既定纪律直接冲突。修复方向与真实上下文字符串见 §1.4 / 拍板 D1。 - 发布 checklist 从未固化:
releases.md:4指向的正典是 iteration-3/08 §3.3,标题至今仍是「草案(验证后固化进 git-workflow.md)」,而git-workflow.md全文 65 行无任何发布/PR/E2E 章节。所有修正只以散文形式活在releases.md,且该文件自我矛盾。处置见 §4.0。 - M4 分支策略推荐:继续 dev 直推,不引入常态 feature 分支——理由之一是 CI 的
push触发器不覆盖feat/**,开分支等于自断快速反馈(§2)。 - 契约 v1.4.0 → v1.5.0 需同步 9 处(doc 2 处 + api 4 份快照 + 4 个守卫常量 + 4×4 计数断言),且字节级一致性无 CI 保障、全靠人工 md5 —— 操作顺序见 §3。
- M4 必须新增第五份 E2E 脚本
test_e2e_m4_manual.dart,发布回归门禁由四份变五份(§4)。 - 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 |
46afe8a(annotated)→ 3cd8005 |
clean |
| patbond-flutter | dev @ fbcd734 |
fbcd734 |
fbcd734 |
c963a0a(annotated)→ fbcd734 |
clean |
| patbond-doc | main @ 5cc6361 |
—(无 dev) | 5cc6361 |
08fda68(annotated)→ 5cc6361 |
clean |
三仓 dev == main == tag,历史线性,符合描述。三个 v0.4.0 均为 annotated tag(git cat-file -t = tag),带完整发布说明正文——延续此惯例。
补充事实(用户未提及,非差异但需知悉):
- v0.4.0 的 tag 实际打于 2026-09-14 11:17(
taggerdate:api/flutter11:17:35~36、doc11:19:17),taggerLixi20。v0.3.0 打于2026-09-10 15:47。即发布动作发生在今天上午,而非 M3.5 收官日 09-11——安全事件导致的 CI 中断修复与发布挤在同一天。 - api 仓存在本地私有 ref
refs/backup/old-main-ff876bc→ff876bc(v0.3.0 重建 main 时保留的孤儿提交备份)。ls-remote无此 ref ⇒ 仅存于本机。若换机器或本仓重克隆,该备份即消失。 - 分支拓扑不对称:api 本地只有
dev一个分支;flutter 本地另有main@030b11f,落后 origin/main(fbcd734)。这是个陷阱:flutter 仓若有人git checkout main会拿到陈旧的 main。M4 期间建议直接删掉 flutter 本地 main(发布走 PR,本地根本不需要 main)。 - doc 仓远端确实只有 main(
GET /branches/dev→not found),不参与发布分支语义 —— 符合描述。
1.2 分支保护实配 —— 6 条描述全部为真
取证方式:GET /api/v1/repos/zhaoyuxi/{repo}/branches/{branch}(Gitea 1.26.4,https://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 权限的 token(Gitea → 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_push(false = 禁一切直推)、enable_push_whitelist/push_whitelist_usernames、enable_merge_whitelist、block_on_official_review_requests、required_approvals、status_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 是 dev,doc 是 main);pull_request 无分支过滤,任何 PR 都会跑。
门禁命令与本地门禁同源(git-workflow.md:58-62):api = ./mvnw -B clean test(ci.yml:55);flutter = dart format --output=none --set-exit-if-changed lib test / flutter analyze / flutter test(ci.yml:56-58);doc = mkdocs build --strict -d /tmp/site。三仓首个 step 均为 sh scripts/check-secrets.sh --all(api ci.yml:41-42、flutter :31-32、doc :22-24)。
三仓 concurrency: group: ci-${{ github.ref }} + cancel-in-progress: true:push 与 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:44(doc 无 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) 单值,而非追加。理由:
- 语义正确:PR 的合并门禁应由 PR 自己的流水线把关。
- 解锁 hotfix 路径:
pull_request触发器无分支过滤(ci.yml:18),任何分支的 PR 都会跑 ⇒feat/*/hotfix/*→ main 的 PR 可正常合并。 - 不重新引入空集漏洞:仍是显式命名,只是名字改对。
- 不损失保证:有人会担心失去「dev tip 自身 push 绿」的冗余。但 ff-only 合并下 PR head ≡ dev tip,两条流水线跑的是同一棵树、同一套命令 ⇒ 该保证是重复的,不是额外的。
- 若改为「两者都要」:严格性更高,但 hotfix 死锁依然存在(
(push)仍缺失)⇒ 不解决推论二,不推荐。
变更后必做的一次性验证(否则等于换一个未验证的配置):改配置后在任一仓开一个空改动的试验 PR(或就用 v0.5.0 的正式 PR),确认 Gitea 的 PR 页面把 (pull_request) 显示为 required 且它红时合并按钮确实禁用。M3 的教训是「配置改了但语义没验」——这次要在 v0.5.0 发布之前验完,不要拿正式发布当试验场。
注意上下文名的脆弱性:字符串由 name: CI(workflow 名)+ job key(backend-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 体验补齐 |
main ← dev |
true | 3cd8005 |
2026-09-14T11:14:55+08:00 |
| patbond-flutter | #1 | v0.4.0 M3.5 体验补齐 |
main ← dev |
true | fbcd734 |
2026-09-14T11:15:05+08:00 |
merge_commit_sha 恰等于 head sha ⇒ Fast-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 全部 success(api 两个、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.yaml(info.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-404 → 1.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 新脚本必须遵守):
- 不被
flutter test执行:脚本在仓根而非test/⇒flutter test(ci.yml:58)不会收集它们。这是有意的(需要活的后端),保持现状。 - 被
flutter analyze检查:analysis_options.yaml只include: package:flutter_lints/flutter.yaml,无任何 exclude ⇒ 分析器覆盖整包含仓根.dart文件。故新脚本必须过 analyze,且需照既有惯例在文件头加// ignore_for_file: avoid_print(test_e2e_m35_manual.dart:3原文即如此)。 - 不被格式门禁检查:
dart format --output=none --set-exit-if-changed lib test(ci.yml:56)只覆盖lib与test,仓根脚本不在范围内。⇒ 新脚本格式跑偏不会红 CI,但建议仍手动dart format保持一致。
另有 4 个 integration_test/*.dart(client_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(:1target/、:10-11本地application.yml+.sample例外、:14.env),flutter 仓有.gitignore(build/.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 测试数 | 379(releases.md:17 / :35、feature-checklist.md:5) |
381(iteration-3.5/04 §7.2 原文「BUILD SUCCESS,381 测试全绿」;该报告同时说明测试 379→381 因新增 2 个矩阵方法) | 381 应为准;379 是冻结前口径,被下游照抄 |
| E2E 断言数 | 234(releases.md:36) |
226(Evidence 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 直推为默认,仅在两种情形开短命分支。理由按证据强度排序:
- CI 的
push触发器不覆盖 feature 分支(ci.yml:16-17branches: [dev])⇒ 推到feat/xxx不会跑任何 CI。开分支等于自断快速反馈,除非每个分支都立刻开 PR 换pull_request触发(ci.yml:18无分支过滤,这条是通的)。这是本项目特有的、比通用最佳实践更强的约束。 - 既有纪律本就如此:
git-workflow.md:9把 feature 分支限定为「跨多天 / 有破坏性风险 / 多人并行同仓」三种情形,M1–M3.5 四个迭代全程 dev 直推,43(flutter)/ 数十(api)提交零合并提交、历史线性。没有出现需要分支的问题。 - 单人开发: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/style(style已在 flutter 实用,补进白名单)。 - scope 可选,正式承认
test(contract):/docs(api):形态。 - 主题用中文一句话;引用 M4 工单号(
T4-xx)与 ADR 编号。 - 正文必须带验收证据(
git-workflow.md:16)——测试数、门禁命令结论。这是 M4 数字口径不再漂移的第一道防线(§1.8(4))。 - 不改写已推送历史(
git-workflow.md:34);不 force pushdev/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 + flutter(PR)→ 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.0 → 1.5.0 |
| 2 | 同文件 :5+ info.description |
追加 1.5.0 M4 冻结段落 + 重申「对 v1.4.0 纯增量」承诺 |
| 3 | patbond-doc/docs/api/index.md:3 |
「(OpenAPI 3,v1.4.0),当前 32 路径 / 45 操作」→ 新版本与新计数 |
| 4 | 同文件 :36 |
冻结纪律行追加「AI 创作域已冻结(1.5.0)」 |
| 5 | api ×4 快照文件 | 新增 openapi-v1.5.0.yaml(正典字节副本)、git rm 旧 openapi-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 SUCCESS,0 失败
# (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 全部位于 V5 不可修改(patbond-user/src/main/resources/db/migration/(含 pet_health 与 community 的 schema),即迁移集中由 user 模块承载。M4 若需新表,下一个版本号是 V6__*.sql,仍放该目录(沿 5/5 先例),不要在新模块另起迁移目录(会导致两个 Flyway 位置,历史表版本冲突)。已推送的 V1git-workflow.md:33)。
4. v0.5.0 发布 checklist
4.0 先解决「流程该固化到哪」与双处矛盾
问题(Evidence Collector 提出,本报告独立复核确认):发布流程目前没有一份可信的单一来源。
已核实的三条事实:
-
releases.md:4指向的正典是一份「草案」:原文「按[发布 checklist](iterations/iteration-3/08-git-workflow-plan.md)(§3.3)执行」,而该节标题至今是「§3.3 发布 checklist 草案(首次发布用,验证后固化进 git-workflow.md)」。 -
固化从未发生:本报告通读
git-workflow.md全文 65 行,章节仅 5 个——分支模型(:5) / 提交信息约定(:11) / 禁止事项(:28) / 凭证防泄漏(:36) / 提交前本地门禁(:56)。无任何发布、PR、E2E、checklist 内容。 -
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 步(启用分支保护)→ 一次性项,已完成。
-
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 SUCCESS,0 失败
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 步 — 建 PR(api + 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 only(v0.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 test(ci.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)
成因判别先于动作。dev 与 main 分叉只有三种可能,处置完全不同:
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-histories(iteration-3/08 §3.1 选项 C 已判定不推荐)。
若确实必须做非 ff 合并(例如 main 上有必须保留的独有提交且无法 cherry-pick):在 Gitea PR 里选 Create merge commit,并在发布记录中显式登记为例外 + 写清原因。历史线性性让位于可追溯性,但必须留痕。
5.2 CI 状态检查上下文名变化 —— 最高杀伤力的低概率风险
上下文字符串 = <workflow name> / <job key> (<event>),三段任一改动即使必需上下文永久缺失,PR 永久不可合并(不是变红,是「检查从未上报」)。触发改名的动作:
| 动作 | 后果 |
|---|---|
改 ci.yml 的 name: CI |
全部上下文改名 |
改 job key(backend-test / flutter-gates) |
该仓上下文改名 |
增删 job(如把 backend-test 拆成 unit + integration) |
旧上下文消失 |
改 on: 触发器(如给 pull_request 加分支过滤) |
对应 event 的上下文可能不再产生 |
预案(顺序执行):
- 改名前先改保护配置,不要反过来。顺序:Gitea 保护里先追加新上下文名(此时新旧并存,required 是「都要绿」)→ 推
ci.yml改名 → 确认新上下文上报成功 → 再从保护里删掉旧上下文名。这样任一时刻都不会出现「required 上下文无人上报」。 - M4 期间尽量不动
ci.yml的 name/job key。若 M4 新增 api 模块,根反应堆./mvnw -B clean test(ci.yml:55)自动覆盖,无需新增 job ⇒ 天然规避(§3.4)。 - 卡死时的解锁手段(记录下来,免得临场慌):仓库 admin 在 Gitea → Settings → Branches → main → 暂时取消勾选「Enable Status Check」或删掉失效上下文 → 合并 → 立即改回正确上下文名。每次这样操作都要在发布记录里留痕。
- 改名后必须做一次空 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 仓新建 .gitignore 含 site/(拍板 D8) |
| 误推可直接改写发布记录 | 中等:releases.md 是发布事实的唯一记录,无审核 |
依赖 tag 作为交叉对照(三仓同名 tag 互证,releases.md:5 既有设计)。可接受 |
| doc CI 红也能推上去 | 低:push 触发器覆盖 main(ci.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 会从 tag 变 commit)。
(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-前缀族(OpenAIsk-/sk-proj-、Anthropicsk-ant-、DeepSeek / Moonshot / 阿里百炼 DashScope 均为sk-形态)。 KEY-ASSIGN不覆盖api_key:其模式只认access_key/secret_key,不含通用api[-_]?key。⇒ANTHROPIC_API_KEY=sk-ant-xxx、openai_api_key: sk-xxx、dashscope_api_key: sk-xxx全部漏网。- 文件名黑名单(
:24DENY_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-here 或 sk-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:54 与 check-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_key ⇒ ANTHROPIC_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.dart → test_e2e_m1_manual.dart(git mv,同步 4 处文档引用) |
采纳 | 回归清单变五份后,一个不带版本号的名字最易被误读成「总入口脚本」,进而漏跑。纯 rename,零逻辑风险。若嫌动文档,可推迟到 M4 收官波 | §1.7 |
| D8 | doc 仓新建 .gitignore(至少含 site/) |
采纳,M4 第一波 | doc 仓完全没有 .gitignore,site/ 仅靠本机 ~/.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.yml、releases.md、git-workflow.md及任何 checklist —— 改动建议见 §6.1,由主会话统一处置。