From 9848e2f7354ef0b1c6db5aac615076a1c82b81c0 Mon Sep 17 00:00:00 2001 From: xcs Date: Tue, 25 Aug 2026 18:23:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=AC=A0=E8=B4=A6:CD=20=E6=8E=A5=E4=B8=8A?= =?UTF-8?q?=E4=BA=86,=E4=BA=94=E5=A4=84=E3=80=8C=E4=BF=A1=E5=8F=B7?= =?UTF-8?q?=E5=85=A8=E7=BB=BF=E8=80=8C=E4=BA=8B=E6=83=85=E6=B2=A1=E5=8A=9E?= =?UTF-8?q?=E6=88=90=E3=80=8D=E5=90=84=E9=85=8D=E4=BA=86=E4=B8=80=E9=81=93?= =?UTF-8?q?=E6=9C=BA=E6=A3=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 入口守卫的字符串比较、install-activities 的 continue、savePackage 的 onConflictDoNothing、 drizzle 只认 max(created_at) 的水位线、/api/health 不传 status —— 五个会静默成功的机制各配机检。 另记六条还欠(无版本可见性 / 无 graceful shutdown / 换版空窗 / activities:install 的样例副作用 / reset-runs 被打进生产 dist / 每版一份 node_modules)。 --- docs/debt.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/debt.md b/docs/debt.md index ca1e349..20d763a 100644 --- a/docs/debt.md +++ b/docs/debt.md @@ -1057,3 +1057,46 @@ dubious ownership 之类),`catch` 里印一句「非 git 仓库,跳过比对」 `db/init/` 挂进 `/docker-entrypoint-initdb.d`、在**首次起卷**时灌的 —— 换任何一条不经 compose 的路子建库,都得自己先灌一遍。`tools/ci.mjs db-create` 已经这么做(灌的是 `db/init/` 那份文件本身,不另抄一份)。 + +## 2026-08-25 · CD 接上了,五处「信号全绿而事情没办成」各配了一道机检 + +**已接**(`.gitea/workflows/ci.yml` 后半段 14 步 + `tools/cd.mjs` 八个子命令 + `docs/部署.md`): +门禁全绿 → 前置断言 → 备份 → 确保库在 → 发布到 `releases/` → 迁移 → 迁移到位断言 → +判首次/升级 → 播种或装活动包 → 回查 → 发前端 → pm2 重启 → 探活 → 收尾。 +部署在 runner 本机(host 模式),路径类配置从 Gitea Variables 读,机密从 Secrets 读。 + +接的过程中逐条对上了五个**会静默成功**的机制,每个都配了一道机检: + +| 机制 | 它会怎么骗人 | 机检 | +|---|---|---| +| `main.ts` 的入口守卫是 `import.meta.url === \`file://${process.argv[1]}\`` 字符串比较 | 经软链启动两边不等 → `boot()` 永不执行 → **进程零输出 exit 0**(实测过) | pm2 一律用 `releases/` 真实路径起;探活兜底 | +| `install-activities` 所有失败路径都是「打印一行然后 continue」 | 一个包都没装成也 exit 0 | 装完回查 `sys.activity_package`(`activities-verify`) | +| `savePackage` 是 `onConflictDoNothing`,主键 `(activity_id, version)` | 版本没升就什么都不更新,日志照说「已装」 | 版本没变但产物文本变了 → **当场红**(`activities-plan`) | +| drizzle 只取 `max(created_at)` 当水位线,`hash` 列只写不读 | 新迁移的 `when` ≤ 水位线会被永久静默跳过,而 migrate 照样打印「完成」 | 已应用条数 == journal 条数(`migrate-verify`) | +| `/api/health` 用 `c.json()` 没传 status | 库挂了照样返 200,只是 body 里 `db:"down"` | 探活验 body(`health`) | + +**还欠**: + +① **无版本可见性**。`/api/health` 的 `version` 是写死的 `CONTRACTS_VERSION = '0.1.0-m1'`, +从 M1 至今没动过;全仓没有任何地方注入 git sha(`tsup.config.ts` 无 `define`,`vite.config.ts` 也没有)。 +部署完只能靠 `releases//RELEASE` 文件与 `sys.activity_package.installed_at` 间接确认「新版真上去了」。 +要补的话最小侵入是 tsup 加一个 `define: { __GIT_SHA__ }` 再塞进 health 的出参 —— +但那是契约变更(`/api/health` 还是**免鉴权**的,暴露 commit sha 要先想清楚)。 + +② **`main.ts` 没有 graceful shutdown**。`serve()` 的返回值没被接住,HTTP server 从不 `close()`; +SIGTERM 直接 `process.exit(0)` —— 在途请求与**所有 SSE 长连接**硬切。 +换版本时必然掉一次连接(前端 EventSource 会自己重连,所以影响小,但不是零)。 + +③ **换版本有几秒空窗**。pm2 是 `delete` + `start`(脚本路径每版都变,`restart` 会留在旧路径), +中间那几秒 502。要做到零停机得上两个端口 + nginx upstream 切换,MVP 阶段先不做。 + +④ **升级时 `activities:install` 的副作用还在**:它会按主键 upsert 覆盖 8 行样例主数据 +(含 `CU-0142` 的 `coverage_score`),并发出 8 个 data 事件 —— 生产库里若有启用的 data 触发器, +会凭空起 8 张单。当前是靠「版本没变就不装」把它挡在门外,而不是根治。 +根治要一个「只装包不写样例」的入口,那是改代码。 + +⑤ **`reset-runs.js` 会被 tsup 打进 dist 一起发到生产机**(`tsup.config.ts` 里它是显式 entry)。 +没人调它,但它就在 `apps/api/dist/reset-runs.js` 躺着,一条命令就能 TRUNCATE 33 张表。 + +⑥ **每版一份 node_modules**(`pnpm install --prod` 在发布目录里跑,实测 214M)。 +pnpm 硬链到全局 store,真实占盘远小于这个数,但 `du` 看着吓人。留最近 5 版。