diff --git a/docs/tasks/README.md b/docs/tasks/README.md index a024b252..37ad66aa 100644 --- a/docs/tasks/README.md +++ b/docs/tasks/README.md @@ -230,6 +230,7 @@ | `TASK-chart-page-blocking-open-20260915.md` | `PROGRESS-chart-page-blocking-open-20260915.md` | **P1**:星盘页开一次要等很久且常常只给一句「过一会儿再打开」。实测引擎五个调用合计 0.75 秒、mapper 13 种形态零抛出——瓶颈在 `/chart` 是动态路由 + 侧栏改成硬文档跳转,整页 SSR 等完 1 串 4 并才开始画,白屏最长 45 秒(BUG-716);`postEngine` 把 429/500/超时/坏 JSON 全碾成 `null` 且零日志,两种性质相反的故障共用一句文案(BUG-715);开页并行打两个重计算限流端点(配额 2)、无缓存,且「打开即有」印在失败页上(BUG-717)。**串行在 readonly-pages-fix 之后** | 待验收 | `codex/chart-page-blocking-open-20260915` | | `TASK-rectification-title-repair-migration-20260915.md` | — | BUG-699 / 704 的数据修补写成了 Node 脚本(要 `SCHEMA_DATABASE_URL`),但 `Migrate Staging Database` 只跑 `migrator` 应用 SQL 迁移、不执行任意脚本——产品没有任何按钮能修自己那批错名字的会话。脚本里本来就是纯 SQL,搬进一次性迁移即可复用现成按钮。生产停在 `7b620c7a`(无 `use-rectification-surface.ts`),where 自然匹配 0 行,是 no-op | 待领取 | `codex/rectification-title-repair-migration-20260915` | | `TASK-staging-dispatch-autofill-sha-20260915.md` | — | `Migrate Staging Database` 每次都要手抄 40 位 SHA,而那个值恰恰是「最新一个过门禁的 staging 提交」——机器能自己算,查询代码那一步里就有。改成留空自动解析、填了仍走原路径(回滚用),三条安全属性一条不丢。**产品 2026-09-15 明确授权修改该 workflow,执行方不得以 AGENTS.md §2.7 拒改**;生产两个按钮保持手填,那是护栏不是麻烦 | 待领取 | `codex/staging-dispatch-autofill-sha-20260915` | +| `TASK-staging-auto-migrate-on-deploy-20260915.md` | — | 门禁通过后自动先跑 staging 迁移再部署,不再手点(迁移幂等、无挂起时是 no-op,`db-migrate.mjs --check` 挂起返 3 可用于日志)。今天 `deploy-staging.yml` 完全不提迁移,忘点就让新代码跑在旧 schema 上且无人拦。**产品再次授权改 workflow,范围限 `backend-quality-gate.yml` 的 dispatch 段**;迁移失败必须阻断部署;回滚不自动迁移;生产完全不动。⚠️ 同轮必须把「迁移须对已部署代码向后兼容、破坏性变更拆两轮」写进 AGENTS.md §7.6 | 待领取 | `codex/staging-auto-migrate-on-deploy-20260915` | | `TASK-api-server-decomposition-20260916.md` | `PROGRESS-api-server-decomposition-20260916.md` | **重构单(串行在 qizheng 单之后)**:把业务逻辑搬出 `JyotishAPIHandler`。核心不是行数,是全仓 3 处靠 `JyotishAPIHandler.__new__` 伪造空壳 handler 借方法(`consultation_workflow_service` ×2、`capture_report_blocked_repairs_golden`、`local_accuracy_report`,MCP 也走这条),依赖方向反了、handler 没有 `headers`/`wfile` 随时可炸。四阶段:拆 `__new__` 后门 → 抽 ≥150 行业务方法 → `do_POST`/`do_GET` 改路由表 → 重新冻结行数 baseline(余量 300→50)。纯搬运不改行为,`test_api_server_security.py` 3841 行断言一条不许改。预计 11,314 → 约 9,230 行。BUG 段 710+ | 待领取 | — | ## 命名与归档 diff --git a/docs/tasks/TASK-staging-auto-migrate-on-deploy-20260915.md b/docs/tasks/TASK-staging-auto-migrate-on-deploy-20260915.md new file mode 100644 index 00000000..47254dfa --- /dev/null +++ b/docs/tasks/TASK-staging-auto-migrate-on-deploy-20260915.md @@ -0,0 +1,189 @@ +# TASK · staging 部署自动带上数据库迁移,不再手动触发 + +- 日期:2026-09-15 +- 基线 commit:`origin/staging` @ `de47c06d` +- 执行分支:`codex/staging-auto-migrate-on-deploy-20260915` +- 影响面:`.gitea/workflows/backend-quality-gate.yml`(dispatch 段) +- **取代关系**:本单落地后,`TASK-staging-dispatch-autofill-sha-20260915.md` 的手填问题基本消失(那个按钮退化成回滚/补修专用)。两单若同轮做,先做本单;只做本单也可以。 + +--- + +## 1. 产品诉求(原话) + +> 那能不能把这个改成自动部署过程中如果需要就自动触发,别手动触发了。 + +--- + +## 2. 现状与可行性 + +### 2.1 今天的链路 + +``` +push staging → backend-quality-gate → (通过) → 自动 dispatch deploy-staging + ↑ + Migrate Staging Database ← 完全手动,没有任何自动触发 +``` + +`.gitea/workflows/migrate-staging-database.yml` 的标题就叫 **"(manual only)"**。而 `deploy-staging.yml` 里**一次都没有提到迁移**——`grep -n "migration\|schema_migrations\|db:migrate" .gitea/workflows/deploy-staging.yml` 无命中。 + +**后果不只是麻烦**:今天发一版需要新表结构的代码,如果忘了先点迁移,新代码会直接跑在旧 schema 上,而且**没有任何东西会拦住**。这已经踩过——`PROGRESS-consultation-context-and-cache-20260906.md` 里写着「部署前须先 Migrate Staging Database」,那是靠人记住的。 + +### 2.2 「如果需要」有现成判据 + +`frontend/scripts/db-migrate.mjs` 已经有 check 模式(`npm run db:migrate:check`): + +- 列出所有未应用的迁移文件 +- **有挂起 → 退出码 3;没有挂起 → 退出码 0** +- 顺便检测 checksum 漂移(已应用的文件被改过 → 抛 `migration checksum mismatch`) + +这就是为这件事造的判据,不需要新发明。 + +### 2.3 而且迁移本身是幂等的 + +`db-migrate.mjs` 用 `migration.schema_migrations` 台账跳过已应用的文件,并持 `pg_advisory_lock`。**所以"每次部署前都跑一遍迁移"在没有挂起时天然是 no-op。** + +这带来一个更简单的设计:**不必先检测再决定跑不跑,直接每次都跑**,`--check` 只用来在日志里说清楚这次到底有没有东西要应用。少一个分支,少一类 bug。 + +### 2.4 并发已经排好队 + +`deploy-staging` 与 `migrate-staging-database` 共用 `concurrency.group: staging-mutation`(`cancel-in-progress: false`、`queue: max`),两者本来就串行。链式触发不会互相踩。 + +--- + +## 3. 决策记录 + +### 3.1 产品授权修改 workflow(第二次) + +`AGENTS.md` §2.7「不改 `.gitea/workflows/**`」由产品负责人于 2026-09-15 再次明确授权,**范围仅限 `backend-quality-gate.yml` 的 dispatch 段**。执行方不得以 §2.7 拒改。 + +### 3.2 顺序:先迁移,后部署 + +新链路: + +``` +gate 通过 → dispatch Migrate Staging Database(等它成功)→ dispatch deploy-staging + ↑ 无挂起时自身是 no-op +``` + +**迁移失败则不部署。** 宁可停在旧版本,也不要让新代码跑在没迁完的库上。 + +### 3.3 ⚠️ 这条顺序引入一个必须写死的新纪律 + +先迁移后部署意味着:**在迁移完成到镜像替换之间,当前正在运行的旧代码会跑在新 schema 上**(几十秒到几分钟)。 + +因此从本单起,**staging 的迁移必须对"当前已部署的那一版代码"向后兼容**: + +- 加列、加表、加索引、加触发器、加函数 —— **可以** +- 删列、删表、重命名、收紧 `NOT NULL` / `CHECK`、改类型 —— **不可以在同一轮里做**,必须拆成"先加新的、发代码、再删旧的"两轮 + +这条以前没写过,因为迁移是人手动在部署前点的,窗口由人控制。自动化之后它变成硬约束,必须进 `AGENTS.md`(见任务 3)。 + +### 3.4 回滚部署不自动迁移 + +`allow_rollback = true` 的部署是**往回退**,此时自动跑迁移毫无意义且危险(迁移不会因为镜像回退而撤销)。回滚路径保持现状:不触发迁移。 + +### 3.5 生产完全不动 + +`Deploy Production` 与 `Migrate Production Database` 保持手动。后者要求 `recovery_reference` / `recovery_created_at` / `restore_verified` —— **`restore_verified` 是一个人对"我验证过这个恢复点能还原"的担保,本质上不可自动化**。`AGENTS.md` §1.4 也把生产定为手动流程。本单一行都不碰生产。 + +### 3.6 手动按钮保留 + +`Migrate Staging Database` 仍然保留并可手动运行——回滚后补迁移、单独跑数据修补、排查时都要用。 + +--- + +## 4. 硬红线 + +1. **只改 `backend-quality-gate.yml` 中 dispatch `deploy-staging` 的那一段**(约 528–541 行,`payload=` 到 `Dispatched Deploy staging run` 之间)。其余步骤、其余 workflow 一行不许动。 +2. **不得修改 `migrate-staging-database.yml` 与 `deploy-staging.yml` 的任何校验**。本单只是在它们前面加一次触发与等待。 +3. **迁移失败必须阻断部署**,并且 gate 这一步要失败(不能默默跳过)。 +4. **不得改生产两个 workflow**,不得放宽 `restore_verified`。 +5. **回滚部署不触发迁移**(§3.4)。 +6. 等待迁移完成必须有**超时**,不得无限等;超时按失败处理,日志要写清楚是超时还是迁移真的失败了。 +7. 不得破坏 `concurrency.group: staging-mutation`,也不得把 gate 自己塞进那个组(会自锁)。 +8. YAML 必须能被 Gitea 正确解析——语法错会让按钮和自动链路一起消失。 + +--- + +## 5. 任务分解 + +### 任务 1 · gate 在部署前先触发迁移 + +**1.1** 在现有 dispatch `deploy-staging` 之前插入一步:用同样的 `curl … /actions/workflows/migrate-staging-database.yml/dispatches?return_run_details=true` 触发迁移,`inputs.deploy_sha` 传 gate 刚验过的那个 SHA(gate 手里已经有,不需要解析)。 + +**1.2** 取回 `workflow_run_id`,**轮询直到 `status == "completed"`**: + +- `conclusion == "success"` → 继续 dispatch deploy +- 其它 → 这一步失败,**不 dispatch deploy**,日志写明迁移 run id 与结论 +- 超过超时(建议 20 分钟,与 migrate workflow 的 `timeout-minutes: 20` 对齐)→ 失败,写明是超时 + +轮询的写法可以照抄 gate 里已有的 run 查询模式(`actions/runs/`),**不要引入新依赖**。 + +**1.3** 日志要说人话,至少三行:触发了哪个 run、迁移结论是什么、这次有没有实际应用迁移文件(后者由任务 2 提供)。 + +**验收标准** + +- 无挂起迁移时推一个代码提交:链路走完,迁移 run 成功且日志显示"无待应用迁移",部署照常。 +- 有挂起迁移时推一个提交:迁移先跑并应用,成功后才开始部署。 +- 人为让迁移失败(例如一条坏 SQL):**部署没有被触发**,gate 这一步红。 + +### 任务 2 · 让日志说清楚"这次有没有迁移" + +**2.1** `deploy/run-staging-migration.sh` 已经在最后 `select filename from migration.schema_migrations order by filename`。在应用之前先跑一次 `--check` 等价查询,把**本次待应用的文件名**打出来;一条都没有就打印一行明确的"无待应用迁移"。 + +**2.2** 不改这个脚本的任何校验或 compose 调用,只加日志输出。 + +**验收标准**:两种情况(有/无挂起)在 Gitea 日志里一眼能分清。 + +### 任务 3 · 把 §3.3 的纪律写进约束文件 + +**3.1** `AGENTS.md` §7.6 现在是: + +> 不改数据库结构的轮次不得顺带动迁移;动表的轮次必须真跑 `npm run test:db` + +在其后补一条,写明:**staging 迁移在部署之前自动应用,因此迁移必须对当前已部署的代码向后兼容**;破坏性变更(删列 / 删表 / 重命名 / 收紧约束 / 改类型)必须拆成两轮。给出"加 → 发代码 → 删"的标准顺序。 + +**3.2** `deploy/README.md`:把 staging 那一节改成「推 staging 后,门禁通过会自动迁移再部署,不用手点」;写明手动按钮仍保留用于回滚后补迁移与单独跑数据修补;写明**生产仍然手动**及其理由。 + +**3.3** `docs/tasks/PROGRESS-staging-auto-migrate-on-deploy-20260915.md`。 + +**3.4** 不新增 BUG 编号(当前最大 BUG-717,这是流程改动不是缺陷)。`CHANGELOG.md` 不动。 + +--- + +## 6. 让步顺序 + +1. 若 Gitea 这个版本的 dispatch 拿不到 `workflow_run_id`(`return_run_details` 不支持),**改用"按 workflow 文件名 + head_sha 轮询最近的 run"**——gate 里查门禁 run 已经是这个模式,照搬即可。 +2. 若轮询等待在 gate 里实现困难,**退到:gate 只 dispatch 迁移,由迁移 workflow 成功后自己 dispatch deploy**。链路终点一样,只是把等待挪到了迁移那边。**但这会改到 `migrate-staging-database.yml`,超出 §4.1 的授权范围——走这条必须先回来跟产品确认。** +3. 任务 2 做不了不阻塞任务 1。 +4. **绝不让步**:迁移失败必须阻断部署;回滚不自动迁移;生产不碰;§3.3 的纪律必须同轮写进 `AGENTS.md`——**不写就等于给自己埋一颗破坏性迁移的雷**。 + +--- + +## 7. 开工前置命令 + +```bash +cd /workspace/Jyotisha +git status -sb | head -1 +git fetch origin --prune +git worktree add -b codex/staging-auto-migrate-on-deploy-20260915 \ + .worktrees/staging-auto-migrate-on-deploy-20260915 origin/staging +cd .worktrees/staging-auto-migrate-on-deploy-20260915 +sed -n '500,545p' .gitea/workflows/backend-quality-gate.yml # 要改的 dispatch 段 +sed -n '1,40p' .gitea/workflows/migrate-staging-database.yml # 被触发方的输入契约 +python3 -c "import yaml;yaml.safe_load(open('.gitea/workflows/backend-quality-gate.yml'));print('yaml ok')" +``` + +⚠️ **这一单改的是部署管线本身。** 推完必须确认:Gitea 上三个 workflow 都还在、门禁仍能被 push 触发、`Migrate Staging Database` 的手动表单仍能打开。 + +交付:`git push origin HEAD:staging`,推完核对远端 SHA,并**观察这次推送自己触发的那条链路**是否按新顺序走完——这单的最好证据就是它自己的第一次运行。 + +--- + +## 8. 产品侧验证(改完之后) + +1. 正常推一次代码,什么都不用点。 +2. 在 `backend-quality-gate` 的日志里看到:触发迁移 → 迁移结论 → 开始部署。 +3. `https://staging.jyotisha.chat/api/health` 的 `deployment.gitCommit` 追上 staging head。 + +**以后你只需要在要回滚、或者要单独跑一次数据修补时,才去点那个按钮。**