Files
Jyotisha/docs/tasks/TASK-staging-auto-migrate-on-deploy-20260915.md
T
Jesse_ChenandClaude Fable 5 9bf13df023 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
2026-09-15 15:14:21 +00:00

10 KiB
Raw Blame History

TASK · staging 部署自动带上数据库迁移,不再手动触发

  • 日期:2026-09-15
  • 基线 commitorigin/staging @ de47c06d
  • 执行分支:codex/staging-auto-migrate-on-deploy-20260915
  • 影响面:.gitea/workflows/backend-quality-gate.ymldispatch 段)
  • 取代关系:本单落地后,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.mjsmigration.schema_migrations 台账跳过已应用的文件,并持 pg_advisory_lock所以"每次部署前都跑一遍迁移"在没有挂起时天然是 no-op。

这带来一个更简单的设计:不必先检测再决定跑不跑,直接每次都跑--check 只用来在日志里说清楚这次到底有没有东西要应用。少一个分支,少一类 bug。

2.4 并发已经排好队

deploy-stagingmigrate-staging-database 共用 concurrency.group: staging-mutationcancel-in-progress: falsequeue: 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 ProductionMigrate 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.ymldeploy-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_idreturn_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. 开工前置命令

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/healthdeployment.gitCommit 追上 staging head。

以后你只需要在要回滚、或者要单独跑一次数据修补时,才去点那个按钮。