调查时间:2026-09-17
调查范围:origin/test / origin/main 全部真实 git 历史 + sudo db-prod 只读查生产库 +
scripts/release_main.py status/plan 只读子命令真跑(不写 Git/Codeup)
调查人:执行层(sonnet)· 只调查整理,未执行任何发版/写操作
v2 修订说明:长官补了三条要求 —— ①清单只放 feature/store-mobile-refractive-archive
自己的 MR,root 的 #698 单独标"依赖项"不混进清单,但要 verify 会不会带别人提交 ②对照历史发版
经验逐条排查 ③把 root 9-16 口信要点对进清单。本版已按此重写第1、4节,新增第5、6两节。
feature/store-mobile-refractive-archive 自己这条线)查法:git log --merges origin/test --grep="store-mobile-refractive" -i,命中 11 条,其中 1 条是
Merge remote-tracking branch 'origin/test' into feature/...(把 test 合回功能分支的反向合并,
不是 MR,已排除),剩 10 个真实 MR,按合并时间升序:
| MR 编号 | 合并时间(UTC+8) | 合并提交 | 标题 | 含哪些 commit(简述) |
|---|---|---|---|---|
| #693 | 09-16 04:30:55 | 5cf89cd5 |
手机端视光档案(批0-批7b) | 批0地基/批1儿童列表/批2档案详情+绑定+趋势图/批2.5补端点/批3录入页骨架+AI识别/批3b录入页收尾/批4查看页三Tab/批7a已读回执后端(含 batch7a_conclusion_read_receipt 迁移)/批7b未读徽章/查看页历次对比色误用修复 |
| #694 | 09-16 13:11:24 | 46a888d7 |
趋势图三指标+AI进度面板+测试补齐+两处缺陷修复 | 趋势图三指标切换+验光方法字段/AI分析分步进度面板/按风险缺口补7用例/解绑失败兜底+重复建档预警 |
| #695 | 09-16 15:38:47 | ca6530c0 |
长官真机反馈8处修复(徽章/Y轴刻度/头像/年份/已推送内容/折行/单位/负号) | 时间线卡徽章文案改「家长未读」/Y轴固定步长+列表页4处对齐/时间线卡徽章基线对齐/查看页4处缺陷 |
| #697 | 09-16 16:59:53 | ad3bfdfd |
徽章压矮+智脑分析五处+趋势图对齐平板端+示范档案姓名订正 | 时间线卡降高/智脑分析与历次对比5处/demo_profile_data_fix2 迁移(订正示范档案姓名)/回退卡片降高压徽章/趋势图对齐平板端 |
| #700 | 09-16 18:23:00 | 2040dd1e |
录入页两高危+门店结论布局+AI重试+列表贴边+绑定搜索三路+迁移链修正 | 徽章对齐/列表贴边+趋势图悬浮框/demo_profile_data_fix3 迁移(订正推送快照旧姓名)/绑定候选席位搜索三路(席位号/昵称/手机号)/录入页7项修复(含2高危)/迁移链修正:demo_profile_data_fix3 改挂 phase81_001(按 root 9-16 口信) |
| #701 | 09-16 19:15:07 | 340af781 |
tooltip接线+曲线动画恢复+重试按钮对齐平板端 | tooltip真接线+恢复曲线动画+重试按钮对齐 |
| #702 | 09-16 20:00:47 | 609138d2 |
列表贴边ml-auto+眼位记法+孤行换行修复 | 列表贴边改ml-auto+眼位记法+两处孤行换行 |
| #703 | 09-16 22:31:32 | d9d37d75 |
视功能档位选择器+本次未做对齐+日期范围+推送状态块定稿 | 视功能档位选择器+两处行为对齐平板端+推送状态块定稿 |
| #704 | 09-16 23:10:21 | 9e5ac47e |
已读回执补齐家长端接线+录入保存后跳查看页 | 已读回执补齐家长端接线+录入保存后跳查看页对齐平板端 |
| #705 | 09-16 23:58:27 | 61dcf7e5 |
编辑保存报错修复+三处破坏性操作补二次确认 | 编辑保存报错修复+三处破坏性操作补二次确认 |
这 10 个是本次发版清单的全部主体,共 34 个非 merge commit(release_main.py plan 实跑核实,
见第5节)。
MR #698(root · feat/phase81-refractive-4l-grounding 分支 · 09-16 17:41:54 合并 502764dd,
标题「Phase 81.8 示范档案 4L 新口径冻结迁移 + 平板 UAT 截图存证」)——只带一个纯数据迁移
phase81_001_demo_profile_refresh.py(冻结示范档案6条AI分析文案为4L新口径)+ 文档/截图,
不含任何业务代码改动。它被夹在我们的 #697 和 #700 之间,原因是我们 #700 里的
demo_profile_data_fix3 迁移的 down_revision 改挂到了 phase81_001(见 #700 那行的"迁移链修正")
—— 这条依赖关系是我们自己代码里写死的,不是 root 强加的。
时间窗内 root 同一分支还有 #692(09-16 00:54)和 #696(09-16 16:36)两个 MR,均无迁移文件、
纯代码/prompt 改动 —— 这两个不在依赖链上,不需要跟着发(第6节详述 root 口信落点)。
git merge-base --is-ancestor 验证,非凭印象)先物理确认迁移链的 revision/down_revision(注意 revision 标识与文件名不完全一致,root 9-16 提醒
过这个坑:文件名 batch7a_conclusion_read_receipt.py 但 revision 是 batch7a_read_receipt):
migrations/versions/batch7a_conclusion_read_receipt.py revision=batch7a_read_receipt down_revision=demo_profile_data_fix1
migrations/versions/demo_profile_data_fix2.py revision=demo_profile_data_fix2 down_revision=batch7a_read_receipt
migrations/versions/phase81_001_demo_profile_refresh.py revision=phase81_001 down_revision=demo_profile_data_fix2
migrations/versions/demo_profile_data_fix3.py revision=demo_profile_data_fix3 down_revision=phase81_001
链是单线不分叉(已用 grep -rl 'down_revision = "xxx"' 逐个反查,每个 revision 只有唯一下游,
demo_profile_data_fix3 是当前链尾,无人以它为 down_revision)。
用 git merge-base --is-ancestor <引入迁移的 commit> <各 MR 合并提交> 逐个验证「首次落在哪个 MR」:
| 迁移 revision | 引入 commit | 首次落在 | 验证方式 |
|---|---|---|---|
batch7a_read_receipt |
5e99e8b2 |
#693(本线) | is-ancestor 对 #693/#694/#695/#697/#698/#700-705 全为真,#693 是最早一个 |
demo_profile_data_fix2 |
e21ea8b3 |
#697(本线) | is-ancestor 对 #697 起为真,#693/#694/#695 为假 |
phase81_001 |
c41a1042 |
#698(root · 依赖项) | is-ancestor 对 #698 起为真,#693-#697 为假 |
demo_profile_data_fix3 |
f3f60460 |
#700(本线) | is-ancestor 对 #700 起为真,#693-#698 为假 |
origin/main查法:git grep -l '^revision = "<id>"' origin/main -- migrations/(锚定行首,避免误命中其他文件的
down_revision 引用同名字符串——root 明确提醒过"别用文件名 cat-file 猜"这个坑)。
对照组(证明查法有效):demo_profile_data_fix1 已知在 main(它是上一轮 MR#687 发布内容),
查询返回 origin/main:migrations/versions/demo_profile_data_fix1.py,命中 → 查法有效。
| revision | 在 main 上? |
|---|---|
demo_profile_data_fix1(对照组) |
✅ 命中 |
batch7a_read_receipt |
❌ 未命中 |
demo_profile_data_fix2 |
❌ 未命中 |
phase81_001 |
❌ 未命中 |
demo_profile_data_fix3 |
❌ 未命中 |
sudo db-prod 只读查)SELECT version_num FROM alembic_version;
version_num
------------------------
demo_profile_data_fix1
(1 row)
与 git 层面核对完全一致:生产真实停在 demo_profile_data_fix1,下一环正是
batch7a_read_receipt,链条无缺环、无需要跳过的中间版本。这也印证了 root 口信里
"代码新、数据旧"的说法(见第6节)——生产的 alembic 版本号确实还没往前走一步。
phase81_001_demo_profile_refresh.py 及另外三条迁移全部只用 op.execute 操作物理表,
grep -n "^import app\|^from app" 对四个文件均 0 命中——不依赖任何业务代码运行时。
这意味着即便 root 那条分支的代码改动(#692/#696)是否已完整上生产存疑,也不影响
phase81_001 这条纯数据迁移的可发布性——它只改 6 行示范档案的 ai_analysis_text,
幂等 + 三重 WHERE 限定(固定 UUID + is_sample=true + 值不同才更新)。
--mr 693 --mr 694 --mr 695 --mr 697 --mr 698 --mr 700 --mr 701 --mr 702 --mr 703 --mr 704 --mr 705
共 11 个 MR(本线10个「见第1节」 + root 依赖项 #698「见第1节末尾」)。
结论先行:用真实 release_main.py plan(只读,不写 Git/Codeup)对这 11 个 MR 号跑了一遍,
逐 MR 的 commit_ids 与 source_branch 精确对应各自的分支和提交,没有出现属于第三方分支的
提交或文件。理由分三层:
① 时间窗地毯式核查:git log --merges origin/test --since="2026-09-15 23:00" --until="2026-09-17 01:00"
把这个时间段内所有合入 test 的 merge 全部列出(不限定分支关键词),结果只有两条分支活跃:
我们的 feature/store-mobile-refractive-archive(10个MR)和 root 的
feat/phase81-refractive-4l-grounding(#692/#696/#698 三个MR)。没有第三个会话的 MR 夹在这个窗口里。
② 工具机制本身按 MR 隔离,不按时间/分支整体拉平:读了 scripts/release_main.py 源码
(build_release_plan 函数),它对每个点名 MR 只取
git rev-list --no-merges <该MR合并提交的第一父>..<该MR合并提交的第二父>——即“合并前的 test 状态”
到“功能分支尖端”之间的非合并提交,天然排除了已经在 test_parent 里的所有其他人的提交。
且工具显式检测“嵌套合并”(功能分支中途合回 test 解冲突产生的 merge):只有当该嵌套合并的所有
非首父提交都是 test_parent 的祖先时才放行展开,否则直接 ReleaseGuardError 中止——这道判定专门
防"合并回 test 时顺带夹带了外来提交"。我们的 #700 范围内确实有一个
Merge remote-tracking branch 'origin/test' into feature/store-mobile-refractive-archive
(67593559,09-16 18:16),已验证它合入的 origin/test 内容(含 #698)在 #700 合并时已经是
test_parent 的祖先,没有触发 foreign_merges 报错。
③ 真跑 plan 子命令拿到的实证(只读,build_release_plan 不写 Git/Codeup):
$ python3 scripts/release_main.py plan --repo <worktree> --test-ref origin/test --main-ref origin/main \
--mr 693 --mr 694 --mr 695 --mr 697 --mr 698 --mr 700 --mr 701 --mr 702 --mr 703 --mr 704 --mr 705 --json
mr count: 11
commit count: 39
changed files count: 47
migration files: [batch7a_conclusion_read_receipt.py, demo_profile_data_fix2.py,
demo_profile_data_fix3.py, phase81_001_demo_profile_refresh.py]
693 → feature/store-mobile-refractive-archive
694 → feature/store-mobile-refractive-archive
695 → feature/store-mobile-refractive-archive
697 → feature/store-mobile-refractive-archive
698 → feat/phase81-refractive-4l-grounding ← 与 root 说法一致,且工具自动认出这不是同一分支
700 → feature/store-mobile-refractive-archive
701 → feature/store-mobile-refractive-archive
702 → feature/store-mobile-refractive-archive
703 → feature/store-mobile-refractive-archive
704 → feature/store-mobile-refractive-archive
705 → feature/store-mobile-refractive-archive
旁证(证明"漏掉依赖项会被拦,不是理论推演"):把 #698 从点名列表里去掉重跑 plan,
结果 仍然 exit 0(plan 本身不校验迁移祖先链,只是纯计算),但 migration files 变成只有
3 个(phase81_001_demo_profile_refresh.py 消失),而 demo_profile_data_fix3.py
(down_revision = phase81_001)依然在列——这正是 assert_migration_ancestors_on_main
设计要拦的场景。该守门只在 publish 阶段(建 release 分支/MR 之前)才会触发,plan/status
阶段不会报错,发布前必须靠人工核对迁移链(已在第2节做完),不能只跑 plan 看 exit code 就放心。
main..test 比对法或裸内容 diff(两者都被 memory 标记为易误判),sudo db-prod 查真实 alembic_version」双重交叉验证 —— 详见第3节。git show --name-only <merge> 判断合并提交带了什么文件(该写法对 merge commit 返回假0),^1..^2 区间或 plan 工具输出。--branch"的阈值,建议真正发版时主动带--branch release/store-mobile-vf-20260917(或更短命名),不要等 500 才加。#698 不属于本功能线分支,必须与本线 10 个 MR 一并点名,否则 demo_profile_data_fix3
建分支阶段会被 assert_migration_ancestors_on_main 拦下(已用第4.2节的"去掉#698重跑"实测坐实)。
root 同一分支的 #692、#696 不在本清单里,原因见第6节——它们与本次迁移链无依赖关系,
是否需要跟着发属于另一条决策,不能默认"发了我们这11个就等于把 root 那条线发全了"。
git log origin/main 的实际 HEAD(d0428e78,MR#699"release: MR 696 选择性晋级 main",
合并时间 09-16 18:09:56)与本任务开始时的 worktree 快照信息里的旧 HEAD(b282c143,MR#687)
不一致 —— origin/main 已经比对话开始时的快照更新,发版前请以 git fetch 后的 origin/main
为准。
MR#699 用的是"选择性晋级"(cherry-pick 到新 release/* 分支),产生的是新 commit SHA,
不是原 origin/test 上 96638b64 的祖先 —— 用 git merge-base --is-ancestor 直接查原始
合并提交会误判为"不在 main"(本次已验证到这个坑,改用文件内容 diff 比对绕过,见第6节)。
生产部署每轮跑 alembic upgrade heads(复数),会把 main 上所有头待跑的迁移一并执行 ——
本次点名的 11 个 MR 只带 4 个迁移且构成单线链(第2节已验证无分叉),没有夹带其他游离头的风险。
| 来源 memory | 坑/经验 | 本次是否适用 | 怎么避 / 已怎么避 |
|---|---|---|---|
prod-release-ledger.md |
含 migration 的 MR 要单独标红报长官 | ✅ 适用 | 4 个迁移已在第1/2节单独标出,均为纯 op.execute,无 nginx conf 改动、无需 reload |
prod-release-ledger.md |
合并 MR = 立即发生产,合并前是最后检查点 | ✅ 适用 | 本报告就是"合并前检查",严禁在本任务内跑发版命令 |
release-verify-methodology.md §2 |
断言远端状态前必先 git fetch |
✅ 已做 | 开工先 git fetch origin --quiet |
release-verify-methodology.md §3 |
run 号必须实查不许顺延猜 | ⏸ 不适用(本任务不发版) | 留给真正执行发版的会话,发完按此条验证 |
release-verify-methodology.md §7 |
有迁移的发版要真库排练 + 读流水线 alembic 输出 | ⏸ 部分适用 | 本次已用 sudo db-prod 查证生产真实版本号 = 迁移链上一环,链路对得上;流水线侧留给发版会话 |
release-verify-methodology.md §8 |
隔离 cherry-pick 让 main/test SHA 永久分叉是常态,别用"内容不同"就判断没发过 | ✅ 已避坑 | 判"是否已发"用 revision 锚点 grep + db-prod,不用内容 diff |
release-prod-lessons-20260907.md 教训1 |
main..test 比对法误判(merge commit 永远不消失) |
✅ 已避坑 | 全程未用此法;改用 revision 锚点 grep(§3.1)+ db-prod 真查(§3.2) |
release-prod-lessons-20260907.md 教训2 |
功能跨多分支只查一条分支必漏 | ✅ 已避坑 | §4.2① 做了时间窗地毯式扫描(不限分支关键词),确认只有本线+root线两条分支活跃,没漏第三条 |
release-prod-lessons-20260907.md 教训3 |
git show --name-only <merge> 对合并提交返假0 |
✅ 已避坑 | 全程用 ^1..^2 区间或 plan 工具输出取文件/提交列表,没用这个会返假0的写法 |
release-prod-lessons-20260907.md 教训4 |
生产部署跑 alembic upgrade heads(复数),会带上所有头 |
✅ 已核查 | 4 个迁移构成单线链(§2 已验证无分叉),main 上没有额外游离头会被夹带 |
release-prod-lessons-20260907.md §6 |
判断"是否已发生产"三种查法两种错,唯一准的是 main 提交消息里的挑取标记 | ✅ 已避坑 | 本次用 revision 锚点 grep(非 is-ancestor、非内容diff)+ db-prod 双重交叉,规避了这三种查法里错的两种 |
release-prod-lessons-20260907.md §11 |
发版先跑 status 只读子命令拿"待发编号" |
✅ 已真跑 | 见第4.2节③,status 输出 11 个 MR 全部"未发",与 §3 的 git/db 核对完全一致 |
how-to-run-release-to-prod-correctly.md |
真发版要用 --yes --auto-merge,禁止 printf 管道喂 y |
⏸ 不适用(本任务不发版) | 留给真正执行发版的会话 |
pitfall-release-branch-name-and-script-version.md 坑一 |
MR 号超长拼分支名 → Codeup 建 MR API 500,阈值约15个 | ⚠️ 接近阈值 | 本次11个MR,建议真发版时主动带 --branch release/短名,已写入§4.3 |
pitfall-release-branch-name-and-script-version.md 坑二 |
手里的 release_main.py 可能是旧版 |
✅ 已核查,新版 | grep -c _API_MERGE_SUBJECT_RE scripts/release_main.py = 2(新版);worktree 版与可信副本 /usr/local/lib/release/release_main.py md5 均为 fd4d976526d280b4487416c4dcf6396e,逐字节一致、无漂移 |
release-tool-upgrades-20260915.md |
MR 自动按 test 合入顺序排序;空提交不再误判冲突;部署实证加认迁移失败 | ✅ 已知悉 | §4.1 已注明书写顺序不敏感;本次11个MR均为正常提交非空提交,不涉及该分支 |
pitfall-yunxiao-all-pipelines-down-since-0828.md |
判部署成败别看云效流水线颜色,生产走自建 cron 拉取;判据是 sudo db-prod 查 alembic_version + 前端 chunk 指纹 |
⏸ 不适用(本任务不发版) | 留给真正执行发版后的验证会话按此判据核实 |
| root 说的 | 核实结果 | 对清单的影响 |
|---|---|---|
他的代码线 #696 已上生产(main 到 d0428e78,18:25 部署成功) |
origin/main HEAD 确实是 d0428e78(MR#699"release: MR 696 选择性晋级 main",合并时间 09-16 18:09:56,与"18:25 部署成功"时间线吻合)。内容层面抽查:app/services/four_l_summary_prompt.py 与 #696 引入版本 diff 为 0 行,吻合"代码已发"。refractive_record_service.py diff 706 行(未细究原因,可能是后续其他改动叠加),不影响本清单判断 |
不影响本清单——#696 无迁移文件,已确认与本次迁移链无依赖 |
| 数据线 #698 没发 | 已用 revision 锚点 grep 确认 phase81_001 不在 origin/main;sudo db-prod 查生产 alembic_version = demo_profile_data_fix1,链条上还没走到 phase81_001 那一环 |
与本次调查完全吻合,#698 作为依赖项列入待发清单 |
| 生产现在是"代码新、数据旧":新分析走新口径,示范档案6条历史文案还是老的 | 逻辑自洽:代码(#696)已上但数据迁移(phase81_001,#698)没上,示范档案是 _patient_is_sample 短路不会自动重算的静态展示数据,所以文案仍是老口径 |
说明 #698 这条数据迁移发布紧迫性较高(现网示范档案文案与新代码口径不一致,属于可见的用户体验问题) |
迁移链:demo_profile_data_fix2 → phase81_001(他) → demo_profile_data_fix3(我们),另 batch7a_read_receipt 也没进 main |
物理链核实为四环:demo_profile_data_fix1(main已有) → batch7a_read_receipt → demo_profile_data_fix2 → phase81_001 → demo_profile_data_fix3,root 描述的三环是完整链的后三环,batch7a_read_receipt 是他额外提醒的第四环,两者合起来与本报告第2节一致 |
确认清单第2节链条完整,4个迁移一个不漏 |
| "你们这边儿一并发布" = 发布权在我们,发我们时把他的 #698 带上 | 已按此口径处理:清单主体是我们10个MR,#698 作为"依赖项"单独标注,一并点名 | 第1节末尾、第4.1节均已体现 |
判某迁移在不在 main,别用文件名 cat-file 猜(文件名与 revision 不一致),要 git grep -l '^revision = "xxx"' 锚行首 + 对照组 |
本报告第3.1节完全按此方法执行,并带对照组(demo_profile_data_fix1) |
已落实,无偏差 |
# 1. 找本线 MR
git log --merges origin/test --grep="store-mobile-refractive" -i --pretty=format:"%H|%ad|%s" --date=iso
# 2. 时间窗地毯式扫描(不限分支关键词,核实无第三方MR夹带)
git log --merges origin/test --since="2026-09-15 23:00" --until="2026-09-17 01:00" --pretty=format:"%H|%ad|%s" --date=iso
# 3. 迁移落点(以 demo_profile_data_fix2 为例)
git merge-base --is-ancestor e21ea8b3947cce602c2882ec0f74eebe53125de4 ad3bfdfdbd9428f3fc7205ebdabaa7b1fa5b34b1 # 对 #697,exit 0=真
# 4. 生产现状(锚定行首 + 对照组)
git grep -l '^revision = "demo_profile_data_fix1"' origin/main -- migrations/ # 对照组,命中
git grep -l '^revision = "batch7a_read_receipt"' origin/main -- migrations/ # 目标,未命中
# 5. 生产库真实版本
sudo db-prod "SELECT version_num FROM alembic_version"
# 6. 只读发版计划(不写 Git/Codeup)—— 验证"不带别人提交"
python3 scripts/release_main.py plan --repo <worktree> --test-ref origin/test --main-ref origin/main \
--mr 693 --mr 694 --mr 695 --mr 697 --mr 698 --mr 700 --mr 701 --mr 702 --mr 703 --mr 704 --mr 705 --json
# 7. 只读待发状态(不写 Git/Codeup)
python3 scripts/release_main.py status --repo <worktree> --test-ref origin/test --main-ref origin/main \
--mr 693 --mr 694 --mr 695 --mr 697 --mr 698 --mr 700 --mr 701 --mr 702 --mr 703 --mr 704 --mr 705
# 8. 版本/漂移自查
grep -c "_API_MERGE_SUBJECT_RE" scripts/release_main.py # 期望非0 = 新版
md5sum scripts/release_main.py /usr/local/lib/release/release_main.py # 期望一致 = 无漂移
本报告只调查整理,未执行任何 release-to-prod / git 写操作 / 生产库写操作。