docs(tasks): auto-migrate staging before deploy instead of a manual button

今天 backend-quality-gate 通过后自动 dispatch deploy-staging,但
deploy-staging.yml 里一次都没提到迁移——忘点 Migrate Staging Database 就让
新代码跑在旧 schema 上,没有任何东西会拦。这已经靠人记着(见
PROGRESS-consultation-context-and-cache-20260906「部署前须先 Migrate」)。

判据是现成的:db-migrate.mjs 的 check 模式有挂起返 3、无挂起返 0,还顺带查
checksum 漂移。而且迁移本身靠 migration.schema_migrations 台账幂等,无挂起时
天然 no-op——所以不必先检测再决定跑不跑,直接每次都跑,少一个分支。两个
workflow 又共用 concurrency group staging-mutation,链式触发不会互相踩。

新顺序:gate 通过 → 触发迁移并等它成功 → 才 dispatch 部署。迁移失败不部署。

这条顺序引入一个以前不存在的纪律:迁移完成到镜像替换之间,旧代码会短暂跑在
新 schema 上。所以 staging 迁移必须对当前已部署的代码向后兼容,破坏性变更要
拆成"先加、发代码、再删"两轮。以前迁移窗口由人控制所以不需要这条,自动化后
它是硬约束,必须同轮写进 AGENTS.md §7.6——不写就是埋雷。

回滚部署不自动迁移(迁移不会因镜像回退而撤销)。生产两个 workflow 一行不碰:
restore_verified 本质上是人对恢复点的担保,不可自动化。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0155nFCgCHtoA7jhSDGmZmMu
This commit is contained in:
Jesse_Chen
2026-09-15 15:14:21 +00:00
co-authored by Claude Fable 5
parent de47c06d97
commit 9bf13df023
2 changed files with 190 additions and 0 deletions
+1
View File
@@ -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+ | 待领取 | — |
## 命名与归档
@@ -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` 的那一段**(约 528541 行,`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/<id>`),**不要引入新依赖**。
**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。
**以后你只需要在要回滚、或者要单独跑一次数据修补时,才去点那个按钮。**