今天 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
10 KiB
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. 硬红线
- 只改
backend-quality-gate.yml中 dispatchdeploy-staging的那一段(约 528–541 行,payload=到Dispatched Deploy staging run之间)。其余步骤、其余 workflow 一行不许动。 - 不得修改
migrate-staging-database.yml与deploy-staging.yml的任何校验。本单只是在它们前面加一次触发与等待。 - 迁移失败必须阻断部署,并且 gate 这一步要失败(不能默默跳过)。
- 不得改生产两个 workflow,不得放宽
restore_verified。 - 回滚部署不触发迁移(§3.4)。
- 等待迁移完成必须有超时,不得无限等;超时按失败处理,日志要写清楚是超时还是迁移真的失败了。
- 不得破坏
concurrency.group: staging-mutation,也不得把 gate 自己塞进那个组(会自锁)。 - 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. 让步顺序
- 若 Gitea 这个版本的 dispatch 拿不到
workflow_run_id(return_run_details不支持),改用"按 workflow 文件名 + head_sha 轮询最近的 run"——gate 里查门禁 run 已经是这个模式,照搬即可。 - 若轮询等待在 gate 里实现困难,退到:gate 只 dispatch 迁移,由迁移 workflow 成功后自己 dispatch deploy。链路终点一样,只是把等待挪到了迁移那边。但这会改到
migrate-staging-database.yml,超出 §4.1 的授权范围——走这条必须先回来跟产品确认。 - 任务 2 做不了不阻塞任务 1。
- 绝不让步:迁移失败必须阻断部署;回滚不自动迁移;生产不碰;§3.3 的纪律必须同轮写进
AGENTS.md——不写就等于给自己埋一颗破坏性迁移的雷。
7. 开工前置命令
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. 产品侧验证(改完之后)
- 正常推一次代码,什么都不用点。
- 在
backend-quality-gate的日志里看到:触发迁移 → 迁移结论 → 开始部署。 https://staging.jyotisha.chat/api/health的deployment.gitCommit追上 staging head。
以后你只需要在要回滚、或者要单独跑一次数据修补时,才去点那个按钮。