Files
Jyotisha/docs/operations/production-release-runbook-20261014.md
T
jesse-uxandGrok (agent loop) 09906cdccc fix(release): keep owners and grants in production recovery points
Co-Authored-By: Grok (agent loop)
2026-10-14 21:01:52 +08:00

10 KiB
Raw Blame History

生产首发步骤(2026-10-14)

给产品负责人照着点。本单合入测试环境并部署完成之后再做。执行人不要把出生资料、姓名、邮箱、会话、密钥写进仓库或评论。

发布用两个提交:

  • H:当时的测试环境分支头。main 要快进到这一个,不能合并出新提交。
  • R:测试环境上最近一次「代码门禁成功」的提交。H 可以比 R 更靠前,但中间只能是文档。公网健康检查里的版本号必须等于 R。

四个按钮都在 Gitea Actions 里,要从 main 点。迁移演练没有第五个按钮。产品在生产主机上自己跑脚本,看下面第 5 步。

0. 前一天

冻结代码推送。文档可以继续推。确认测试环境健康检查的版本号仍是 R,并且从 R 到当前测试环境头没有代码改动。

1. 核对版本

打开 https://staging.jyotisha.chat/api/health。deployment.gitCommit 等于 R。

在仓库里执行 deploy/is-docs-only-range.sh R <当前测试环境头>。退出码 0 才继续。退出码 1 或 2 都停,不要发布。

2. 把 main 快进到测试环境头

用快进把 main 推到 H。不要做 merge。快进之后 main 和测试环境是同一个提交。

通过:Gitea 上两个分支指向同一个 40 位提交。

3. Release Quality Gate

Gitea → Jyotish Release Quality Gate → Use workflow from:main。不需要填发布提交。

通过:这一步成功,摘要里能看到 release_sha(这就是 R)和 head_sha(这就是 H),并写着相对 R 的后续改动只是文档。

失败:不要点后面的按钮。看失败那一行的原文,把原文交给验收,不要附带环境文件。

4. Create Production Recovery Point

Gitea → Create Production Recovery Point。Use workflow from:main。deploy_sha 填 R。

通过:成功结束,记下三样东西:recovery_reference、recovery_created_at、以及它已经写明还原验证通过。主机上的 /opt/jyotisha-production/.state/recovery-baseline.env 里必须有 RECOVERY_FORMAT=2。没有这一行,或者数字小于 2,都不要往下走:先再点一次创建恢复点。8 月以来不带权限的旧备份不能再拿来回退。加密备份在生产主机的备份目录里,不要打开、不要把密钥贴出来。

失败:停。不要迁移。

5. 在主机上演练迁移

没有单独的 Gitea 按钮。用部署账号在生产主机上跑。主机上如果还没有这份代码,只读检出 R,不要在主机上构建镜像。备份文件要自己填进 RECOVERY_DUMP,脚本不会猜哪一个是最新的。

先看计划,不连数据库:

DEPLOY_PATH=/opt/jyotisha-production JYOTISHA_REHEARSAL_PLAN=1 bash deploy/rehearse-production-migrations.sh

通过:输出里 stop_in_before=no,并且一次性库的名字以 rehearse_ 开头。

再正式跑。把刚才那个备份文件路径放进 RECOVERY_DUMP,文件必须在 /opt/jyotisha-production/backups/ 下面,并且是 .dump.enc:

DEPLOY_PATH=/opt/jyotisha-production RECOVERY_DUMP=/opt/jyotisha-production/backups/<刚才那个备份文件名> bash deploy/rehearse-production-migrations.sh

脚本只还原到一次性库,不会改名为 jyotisha 的生产库。新库带不走原来的建库权限,所以脚本只在这个一次性库上,把连接和建对象的权限交给迁移角色。生产库不动。磁盘使用率要低于 70%。另一项生产变更正在进行时会停。跑完会删掉一次性库。

通过,要同时满足:

  • 输出有 other_person_accepted_rows=0。这是 20260925030000 之前的人数。不是 0 就停,只把这个数字发给产品,不要继续,也不要写出任何一行的内容。
  • archived_sessions_before / archived_sessions_after:归档标记还在的会话数。迁移后应为 0。旧备份里还没有这一列时,前面那个数是 absent。
  • self_chart_rows_before / self_chart_rows_after:人物表里「本人」那一行。迁移后应为 0。还没有 role 列时,前面那个数是 absent。
  • notice_lines:两批迁移日志里 notice 的行数。标题修补和自动标题回填的数字在这些行里。生产上标题修补预期是 0;不是 0 就停,只报数字。
  • revoked_grants=0。这是迁移前后授权的差:迁移不能收回备份里已经有的表权限或列权限。不是 0 就停,不要点生产迁移。
  • chart_profiles_service_role_select_missing=0。这是服务角色对人物表每一列的读取权,缺几列这里就是几。不是 0 时脚本退出码是 4,并停住。不要点生产迁移。
  • 最后一行是 rehearsal_ok=1,并且命令的退出码是 0。只看见这一行但退出码不是 0,不算通过。

演练结束后,在 postgres 容器里执行 \l rehearse_*,确认没有名字以 rehearse_ 开头的库留下来。演练库删不掉时,脚本退出码不是 0,并写出「演练库未删除,请手动 DROP DATABASE rehearse_…」。只删它写出的那一个名字,不要删名为 jyotisha 的生产库。生产库还没动。把退出码和它打印的那一句原因留下来。不要复制数据库口令或备份密钥。

6. Migrate Production Database

只在第 5 步 rehearsal_ok=1 之后点。

Gitea → Migrate Production Database。Use workflow from:main。

  • deploy_sha:R
  • recovery_reference:第 4 步记下的引用
  • recovery_created_at:第 4 步记下的时间,24 小时内
  • restore_verified:true

通过:工作流成功,并且没有「恢复点超过 24 小时」或「版本对不上」的失败句。

失败:不要部署新代码。按下面的「失败后」整库还原。

7. Deploy production

Gitea → Deploy production。Use workflow from:main。

  • deploy_sha:R
  • allow_rollback:false
  • verification_mode:public

通过:工作流成功。然后做 docs/testing/production-first-release-20261014.md。

失败后

接受整库还原到第 4 步那个恢复点。恢复点之后新写入的数据会丢掉。内测还没开放时,这个窗口里通常还没有新的用户数据。

旧代码读不了新的表结构。所以不能只把程序退回去。两件事都要做,都由产品负责人点,不要让执行代理代点。

  1. 整库还原。没有单独按钮。只允许 RECOVERY_FORMAT=2 的恢复点。先看 /opt/jyotisha-production/.state/recovery-baseline.env:没有 RECOVERY_FORMAT=2 就停,回到第 4 步再点一次创建恢复点。8 月的旧备份不能用。第 4 步的 recovery_reference 斜杠后面就是备份文件名,例如 production-pre-migration-20261014T120000Z.dump.enc。文件在 /opt/jyotisha-production/backups/ 下面。先 sha256sum 这个文件,和同一份 recovery-baseline.env 里的 RECOVERY_SHA256 对上,再继续。

先停 api 和 web,不要停 postgres。口令和密钥只用主机上已有的环境文件,不要贴出来。

api_id=$(sudo -n docker ps -q --filter label=com.docker.compose.project=jyotisha-production --filter label=com.docker.compose.service=api)
web_id=$(sudo -n docker ps -q --filter label=com.docker.compose.project=jyotisha-production --filter label=com.docker.compose.service=web)
sudo -n docker stop $api_id $web_id
postgres_id=$(sudo -n docker ps -q --filter label=com.docker.compose.project=jyotisha-production --filter label=com.docker.compose.service=postgres)
sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_id" \
  psql -U "$POSTGRES_USER" -d postgres -v ON_ERROR_STOP=1 \
  -c "DROP DATABASE jyotisha WITH (FORCE);"
sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_id" \
  psql -U "$POSTGRES_USER" -d postgres -v ON_ERROR_STOP=1 \
  -c "CREATE DATABASE jyotisha;"
openssl enc -d -aes-256-cbc -pbkdf2 -pass env:PRODUCTION_BACKUP_ENCRYPTION_KEY -in <备份文件> \
  | sudo -n docker exec -i -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_id" \
      pg_restore -U "$POSTGRES_USER" -d jyotisha --exit-on-error
sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_id" \
  psql -U "$POSTGRES_USER" -d jyotisha -v ON_ERROR_STOP=1 \
  -c "GRANT CONNECT, CREATE ON DATABASE jyotisha TO schema_owner;"

整库导出带不走数据库本身的权限。上面这一句只补回迁移角色在这个库上的连接和建对象权限,和主机最初的引导一致。不要删掉空的 public,备份会改它的属主,先删会让还原停住。

还原后数这四样,要和备份旁边的 *-restore-verify.json 里 checks 一致:identity.users、public.profiles、public.credit_transactions、public 里的表数量。同一份 checks 里 table_grants_match、column_privileges_match、table_owners_match 必须都是 true。这三样是创建恢复点时,源库和还原库的表权限、列权限、表属主已经对过。对不上就停,不要部署。

然后再用网站账号实际读一行。没有登录身份时,行级安全会把行藏起来,看起来像没读到,但那不是权限不够。先把身份设成库里已有的一条账户,再切换角色。下面这条必须成功(退出码 0),并且能读到一行。权限不够会直接失败,失败就停,不要部署:

sudo -n docker exec -e PGPASSWORD="$POSTGRES_PASSWORD" "$postgres_id" \
  psql -U "$POSTGRES_USER" -d jyotisha -v ON_ERROR_STOP=1 \
  -c "select set_config('request.jwt.claim.sub', (select id::text from public.profiles limit 1), false); set role authenticated; select 1 from public.profiles limit 1;"
  1. 再点 Deploy production,allow_rollback 选 true,目标用发布前的生产版本 7b620c7a。点之前先在 Gitea 的 Actions 运行列表里确认:这个版本有一次成功的手动发布门禁(Release Quality Gate)。查不到就停,问产品负责人,不要点。测试环境头如果只多了文档、自己没有门禁记录,回滚用的控制器仍然是最近一次门禁通过的修订,不再因为这个被拒绝。目标版本仍必须在已经审查过的 main 历史里。回滚如果提示恢复点格式低于 2,同样先回到第 4 步创建新恢复点,不要改用旧备份。

还原并且部署成功之后,再按核对清单看健康检查。版本号应回到 7b620c7a。