From 088735b948d46896b8af30efcb0a2dc5d362b97f Mon Sep 17 00:00:00 2001 From: "Somhairle H. Marisol" Date: Fri, 18 Sep 2026 08:27:41 +0800 Subject: docs(release): 全周期交接文档入库(含 ui-shadcn 迁移交付说明) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [变更性质] 纯文档提交,无运行时逻辑。 [文档内容] 补齐此前各轮未入库的交接/验收文档:backend-auth/backend-domain/ domain-authorization-user(认证与授权域)、etf-recovery-release- handoff(ETF 修复 + ops 演练定稿与生产部署命令)、recovery-* 系列、 frontend/parent-ui-findings(UI 迁移上下文)、worker/integration 等, 以及本轮 docs/ui-shadcn-handoff.md(shadcn-svelte 迁移交接,含 Chart.svelte 契约、runes $state 踩坑记录与 375/768/1440 验证证据)。 [更新方案] 按主题分文;每份文档只记录可复现的命令、验证结果与语义边界, 不导出密钥或生产敏感路径。 [影响范围] 文档渠道:后续 leader/client 审阅入口;与代码提交一一对应便于回溯。 --- docs/etf-recovery-release-handoff.md | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/etf-recovery-release-handoff.md (limited to 'docs/etf-recovery-release-handoff.md') diff --git a/docs/etf-recovery-release-handoff.md b/docs/etf-recovery-release-handoff.md new file mode 100644 index 0000000..70b14d8 --- /dev/null +++ b/docs/etf-recovery-release-handoff.md @@ -0,0 +1,100 @@ +# ETF Recovery Release Handoff — v4(2026-09-17) +状态:**READY_FOR_LEADER_REVIEW**(仅覆盖 ops 发布阻断 ASSET;应用代码自 v2 验收后已冻结)。 +本版取代 v3;v3 中 leader 实际重跑 `ops-drill2.sh`(43+ PASS)与应用全部测试均已独立通过 +(pytest 61 / cargo 58 / frontend 56,共 175 项)+ v3 演练 66 PASS 0 FAIL。 + +## 0. 仅剩一条真实发布阻断(前轮已修;本轮再加修两处执行路径缺陷) +前轮已修:旧 v2 场景 D 的 INJECT=rsync 在 backup 阶段就失败且不恢复旧服务(BD1–BD3 / D / G 断言,见第 2 节)。 + +本轮(v4)针对 leader 指出的两处执行路径缺陷,均在 `ops/deploy.sh` 内最小修复,脚本未重写: +- **A — restore 停服失败仍会写文件**。旧行为:`restore_release_id()` 中 stop 失败仅 note 后继续替换 + runtime.env / release(运行中的二进制被覆盖)。修复(`deploy.sh:210-221`):stop 失败后必须再次 + `is-active` 确认;**无法确认服务已停止即拒绝写任何文件**(含手动 restore 与自动回滚路径),文件 + 保持可恢复原状,die 明确报错退出非零;若 stop 失败但核实服务确已 inactive(常见于"早已停止"), + 允许继续。 +- **B — 停服后 DB 检查深层 exit 绕过恢复分支**。旧行为:`precheck_stopped()` 调用的 + `check_no_inflight()` 在 DB 查询失败、DB 文件缺失或 runtime.env 缺 DB_PATH 时直接 `die`(深层 + exit),主流程的 rc 捕获取不到,旧服务停在停机状态。修复(`deploy.sh:83-113` + 主流程 + `deploy.sh:262-279`):`check_no_inflight` 改为**返回错误码而非 exit**(0=无任务 / 1=有任务 / + 2=DB 或环境错误),`db_scalar` 失败不再 die;主流程把 rc=2 与竞态 rc=1 同样处理:**先恢复旧服务 + (start),不替换任何文件**,_RB1/RB2 断言旧服务 active 且 env/binary/dist 无 swap_。 +- 另修演练真实性:场景 D 的退出码断言此前形同虚设(输出非"0"的条件恒真),现真正捕获 deploy 退出码 + (`rc_d=$?` 断言非零),不以管道/tail 掩盖。 + +## 1. 对象与固定 ID +- 数据集:国泰自由现金流 ETF 159399(market=cn,provider 手动修复数据集 id=`1f9e066a-e785-4ba5-bc9c-ef4742cd02d8`)。 +- 候选 worker 镜像(**immutable ID,只按 ID 使用,禁止 retag**): + `strategy-lab-worker:etf-sina-candidate-r3` = `sha256:34a61c22c5135b6cc8453e5e3621674caf12284c5aa5dbfba1cda15299a656e3` +- 生产在线镜像:`strategy-lab-worker:local`(`bd9e9f06d56b…`,未动、未 retag)。 +- 生产事实(只读核实):unit `strategy-lab-production`(UMask=0077)、环境由 + `runtime.env`(BIND=127.0.0.1:8789 等)、发布目录 `/home/somhairle/.local/share/strategy-lab-production/release`。 + +## 2. v3 + 本轮(v4)相对 v2 的实际变更(全部仅在 `artifacts/etf-recovery-candidate/`) +### 2.1 `ops/deploy.sh`(共 329 行;v4 仅修改 A/B 两处,未重写) +| 位置(行号,v4 当前) | 变更 | +|---|---| +| `deploy.sh:51-53` | `fin_stage()`(manifest 先写 tmp 再原子 mv);注入钩子 `CP_CMD`/`MV_CMD`(默认 cp/mv,行为不变)。 | +| `deploy.sh:137-182` | `backup()`: 任一失败(env cp / release rsync / manifest 写 / finalize)都会退(备份目录改名 `.broken.`)后 `die`;写入 complete 校验(`runtime.env`、`release/strategy-lab-server`、manifest `previous_server_sha256`)全部成功才创建 `${BACKUP_DIR}/complete` 标记(0600)。 | +| `deploy.sh:260-273` | 主流程:**备份在 stop 之前进行**(phase 2/6)。备份失败 = 服务仍在运行,绝不转化为停机。 | +| `deploy.sh:274-303` | stop 后硬重检;rc=1(停后新任务)与 **rc=2(停后 DB/环境不可读)** 都走恢复分支:重启旧服务、什么都不替换、退出非零;备份目录仅供 inspection。 | +| `deploy.sh:83-116` | v4-B:`check_no_inflight()` 返回错误码不深层 exit(0 无任务 / 1 有任务 / 2 DB/env 错误);`db_scalar` 失败与缺 DB_PATH、DB 文件缺失一律 return 2。 | +| `deploy.sh:209-247` | `restore_release_id()`: complete/runtime.env/release 校验、deploy.lock;v4-A:stop 失败必须再次确认服务状态,**无法确认已停即拒绝写任何文件**(明确报错、退出非零、文件保持可恢复),确认 inactive 后继续。 | +| `deploy.sh:184-188` | `swap_release()`: `install ... || return 1; rsync -a --delete --checksum ... || return 1`(R1 根因修复已保留)。 | +| `deploy.sh:249-258` | `verify_restored()`: manifest 的 `previous_worker_image` 与 `previous_server_sha256` 逐一比对(损坏/伪造的 manifest 直接 `die`)。 | +| `deploy.sh:305-321` | swap/env(SED)/start-service/health 各自独立 rc-check,失败一律 `DEPLOY_LOCK_HELD=1 restore_release_id` 自动回滚(stop→替换→start→health 验证)。 | + +### 2.2 `ops/rollback.sh`(共 103 行,重写) +- `rollback.sh:54`:无 `complete` 标记 → 拒绝恢复。`rollback.sh:45`:`list`命令显示 `COMPLETE`。 +- `rollback.sh:62-66`:in-flight runs>0 → 拒绝(不当陷入)。 +- `rollback.sh:72`:**先 `systemctl stop` 服务**,再 cp runtime.env / rsync release,替代任何在线文件被运行中进程写入。 +- `rollback.sh:76-82`:文件替换失败时显式 `start`(尽力而为)并 FATAL exit 1。 +- `rollback.sh:83-86`:启动后 `sleep 2` + is-active 失败 FATAL;`rollback.sh:88-90` health JSON `status=="ok"` 失败 FATAL;`rollback.sh:92-97` sha256 与 env(manifest `previous_worker_image`)不匹配 FATAL。 +- 复审条目映射(与 deploy.sh 共享 lock `deploy.lock`,`rollback.sh:59-60`)。 + +### 2.3 `ops-drill2.sh` — 最终 **PASSED=84 FAILED=0 exit 0**(TDD:先加 RA/RB1/RB2 看其失败,修复后全绿) +- `fake-systemctl` 增加 `.audit.log`(stop/start/restart 顺序审计)。 +- `FAKE_START_FAIL` 是一次性的(deploy 阶段 5 的首次 start 失败;restore/回滚路径的 start 不受影响)。 +- `DEPLOY_PHASE=backup/swap/restore` 使注入只影响指定阶段。 +- 既有场景(v3 均保留,全部仍通过): + - **BD1/BD2/BD3**(backup-cp / backup-rsync / backup-manifest 失败):断言 ① 旧服务 ACTIVE(零停机)② env/二进制/前端 sha256 逐字节不变 ③ 退出码非零 ④ 不允许存在带 complete 标记的备份。 + - **D**(rsync 失败只在 swap 阶段一次性注入):备份先完整完成;自动回滚真实触发,断言服务 active、发布物/env 恢复逐字节一致、**真实捕获的非零退出码**。 + - **G**(手动 rollback):audit 首行 == `stop`、末行 == `start`(先停服再写文件,服务最后启动),且最终服务 ACTIVE。 +- 本轮新增场景: + - **RA**(restore 中 stop 失败且服务仍 active):断言明确拒绝信息、退出码非零、env/binary/dist 逐字节不变、服务仍 active(备份 intact 可恢复)。 + - **RB1**(stop 后 DB 文件被删 → 查询错误):断言恢复旧服务 active、无任何 swap(env/binary/dist 不变)、退出码非零。 + - **RB2**(stop 后 runtime.env 缺 DB_PATH):同上断言;runtime.env 其余键逐字节不变。 +## 3. 最终可执行命令(仅指令,生产由 leader 执行) +```bash +# —— 只重跑合并后的 drill(隔离替身): +bash artifacts/etf-recovery-candidate/ops-drill2.sh # 期望:PASSED=84 FAILED=0 | exit 0 + +# —— 手动回滚(leader 生产会话执行): +artifacts/etf-recovery-candidate/ops/rollback.sh list # 只读,带 COMPLETE 标记 +artifacts/etf-recovery-candidate/ops/rollback.sh # stop -> 替换 -> start -> health/sha 验证 +# (deploy.sh 自动回滚路径等价于 deploy.sh restore ) +``` + +部署命令(生产): +```bash +cd /home/somhairle/projects/strategy-lab/artifacts/etf-recovery-candidate/ops +IMAGE_ID=sha256:34a61c22c5135b6cc8453e5e3621674caf12284c5aa5dbfba1cda15299a656e3 \ + STATE_DIR=/home/somhairle/.local/share/strategy-lab-production \ + REPO_ROOT=/home/somhairle/projects/strategy-lab \ + SERVICE=strategy-lab-production \ + ./deploy.sh deploy +``` +(`deploy.sh` 默认值即上述 STATE_DIR/REPO_ROOT/SERVICE;显式列出仅供 review。IMAGE_ID 必须是唯一的不可变 ID。) +回滚(二选一):`./ops/rollback.sh ` 或 `./ops/deploy.sh restore `;deploy 各失败路径会自动调用 restore。 + +## 4. 语义边界(去除过度承诺,本轮未变) +- **曾写 “重启后所有任务状态保持原样”——该过度承诺已删除**。真实语义:重启时正在执行的任务(dataset=running/pending、run=running/queued)不会被自动续上;重启可能把 running 置为 failed 或使其停留为 stale。恢复/重试由应用侧承担(DatasetCard 重试入口、server 调度),不属本 ops 脚本职责。 +- 回滚目标 = pre-deploy 状态:`WORKER_IMAGE=strategy-lab-worker:local` + 旧二进制/旧 dist(manifest 记录 `previous_worker_image` 与 `previous_server_sha256`,恢复后强校验)。生产 `:local` 标签未被动过,回滚安全。 +- **备份使用 0700 目录(runtime.env chmod 600)**;旧的备份目录(v2 期间产生的 `release-backups/20260917-*`),若 leader 未删除可能存在;脚本对现有备份目录不覆盖(同 RID 冲突 → 拒绝)。 +- **有限保证**:`restore_release_id` 的 stop 失败拒绝路径保证"不写文件",但不保证 deploy 此前已换过的 env/binary 自动还原(该场景下文件保留原状 + 备份 intact,需 leader 手动 restore);停后 DB 错误分支只做到"恢复旧服务 + 不替换",DB 本身的损坏/丢失不在脚本的修复范围。 + +## 5. 遗留与上下文 +- **应用冻结**:pytest/cargo/frontend 测试全部通过(pytest 61 / cargo 58 / frontend 56),leader 独立重跑通过,共 175 项(61+58+56)。qa2(隔离替身实测)之后未再改动应用代码。 +- **qa2 独立验收未变**:`artifacts/etf-recovery-candidate/qa2/`(登录、数据集 ds_cn/ds_sz ready、预览/回测 succeeded `b4339ff5-`、6 步浏览器重试通过)。 +- 禁止 export/输出真实密钥/生产路径;禁止一切生产改动(本会话从未执行生产 systemctl/docker/rsync)。 +- 复审条目映射:R1 = swap/自动回滚路径独立 rc-check(deploy.sh:184-188 与 305-321);R2 = 停后硬重检 + 恢复分支(deploy.sh:274-303,v4 扩展 rc=2);R3 = restore 锁+health+hash(deploy.sh:209-258 / rollback.sh 锁+health+hash);R4 = drill 注入矩阵 + exit 语义(FAILED>0 → exit 1,v4 加 RA/RB1/RB2)。 +- leader 审查入口:重跑 `ops-drill2.sh`(命令见第 3 节);生产动作一律由 leader 执行;改动仅涉及 `ops/deploy.sh`、`ops-drill2.sh` 与本文档,未触应用代码,未提交任何 git commit。 -- cgit v1.2.3