fix(release): keep owners and grants in production recovery points

Co-Authored-By: Grok (agent loop)
This commit is contained in:
jesse-ux
2026-10-14 21:01:52 +08:00
co-authored by Grok (agent loop)
parent d13250f40a
commit 09906cdccc
13 changed files with 381 additions and 145 deletions
@@ -37,7 +37,7 @@ Gitea → Jyotish Release Quality Gate → Use workflow from:`main`。不需
Gitea → Create Production Recovery Point。Use workflow from:`main`。`deploy_sha` 填 `R`。
通过:成功结束,记下三样东西:`recovery_reference`、`recovery_created_at`、以及它已经写明还原验证通过。加密备份在生产主机的备份目录里,不要打开、不要把密钥贴出来。
通过:成功结束,记下三样东西:`recovery_reference`、`recovery_created_at`、以及它已经写明还原验证通过。主机上的 `/opt/jyotisha-production/.state/recovery-baseline.env` 里必须有 `RECOVERY_FORMAT=2`。没有这一行,或者数字小于 2,都不要往下走:先再点一次创建恢复点。8 月以来不带权限的旧备份不能再拿来回退。加密备份在生产主机的备份目录里,不要打开、不要把密钥贴出来。
失败:停。不要迁移。
@@ -59,7 +59,7 @@ DEPLOY_PATH=/opt/jyotisha-production JYOTISHA_REHEARSAL_PLAN=1 bash deploy/rehea
DEPLOY_PATH=/opt/jyotisha-production RECOVERY_DUMP=/opt/jyotisha-production/backups/<刚才那个备份文件名> bash deploy/rehearse-production-migrations.sh
```
脚本只还原到一次性库,不会改名为 `jyotisha` 的生产库。磁盘使用率要低于 70%。另一项生产变更正在进行时会停。跑完会删掉一次性库。
脚本只还原到一次性库,不会改名为 `jyotisha` 的生产库。新库带不走原来的建库权限,所以脚本只在这个一次性库上,把连接和建对象的权限交给迁移角色。生产库不动。磁盘使用率要低于 70%。另一项生产变更正在进行时会停。跑完会删掉一次性库。
通过,要同时满足:
@@ -67,10 +67,11 @@ DEPLOY_PATH=/opt/jyotisha-production RECOVERY_DUMP=/opt/jyotisha-production/back
- `archived_sessions_before` / `archived_sessions_after`:归档标记还在的会话数。迁移后应为 0。旧备份里还没有这一列时,前面那个数是 `absent`。
- `self_chart_rows_before` / `self_chart_rows_after`:人物表里「本人」那一行。迁移后应为 0。还没有 `role` 列时,前面那个数是 `absent`。
- `notice_lines`:两批迁移日志里 `notice` 的行数。标题修补和自动标题回填的数字在这些行里。生产上标题修补预期是 0;不是 0 就停,只报数字。
- 权限冒烟三行都是 `true`:网页角色能读账户资料、能读会话列表,服务角色能读人物表。这是演练库里的授权(备份里原来的授权,加上这次迁移新给的),不是去问正在服务的那一个生产库。
- 最后一行是 `rehearsal_ok=1`。
- `revoked_grants=0`。这是迁移前后授权的差:迁移不能收回备份里已经有的表权限或列权限。不是 0 就停,不要点生产迁移。
- `chart_profiles_service_role_select_missing=0`。这是服务角色对人物表每一列的读取权,缺几列这里就是几。不是 0 时脚本退出码是 4,并停住。不要点生产迁移。
- 最后一行是 `rehearsal_ok=1`,并且命令的退出码是 0。只看见这一行但退出码不是 0,不算通过。
服务角色读人物表若仍是 `false`,脚本退出码 4,并停住。不要点生产迁移。演练库删不掉时,脚本不再只是警告:退出码不是 0,并写出「演练库未删除,请手动 DROP DATABASE rehearse_…」。生产库还没动。把退出码和它打印的那一句原因留下来。不要复制数据库口令或备份密钥。
演练结束后,在 postgres 容器里执行 `\l rehearse_*`,确认没有名字以 `rehearse_` 开头的库留下来。演练库删不掉时,脚本退出码不是 0,并写出「演练库未删除,请手动 DROP DATABASE rehearse_…」。只删它写出的那一个名字,不要删名为 `jyotisha` 的生产库。生产库还没动。把退出码和它打印的那一句原因留下来。不要复制数据库口令或备份密钥。
## 6. Migrate Production Database
@@ -103,16 +104,41 @@ Gitea → Deploy production。Use workflow from:`main`。
旧代码读不了新的表结构。所以不能只把程序退回去。两件事都要做,都由产品负责人点,不要让执行代理代点。
1. 整库还原。没有单独按钮。第 4 步的 `recovery_reference` 斜杠后面就是备份文件名,例如 `production-pre-migration-20261014T120000Z.dump.enc`。文件在 `/opt/jyotisha-production/backups/` 下面。先 `sha256sum` 这个文件,和 `/opt/jyotisha-production/.state/recovery-baseline.env` 里的 `RECOVERY_SHA256` 对上,再继续。工具和创建恢复点时一样,都在 `deploy/run-production-recovery.sh` 里:`openssl` 解密,再在 postgres 容器里 `pg_restore`。这一步会清掉现在的生产库 `jyotisha` 再装回备份。先停 `api` 和 `web`,不要停 `postgres`。在 postgres 容器里删除并重建名为 `jyotisha` 的库(只动这一个),然后:
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`。口令和密钥只用主机上已有的环境文件,不要贴出来。
```text
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容器> \
pg_restore -U "$POSTGRES_USER" -d jyotisha --no-owner --no-acl --exit-on-error
| 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;"
```
口令和密钥只用主机上已有的环境文件,不要贴出来。还原后数这四样,要和备份旁边的 `*-restore-verify.json` 里 `checks` 一致:`identity.users`、`public.profiles`、`public.credit_transactions`、`public` 里的表数量。对不上就停,不要部署。
整库导出带不走数据库本身的权限。上面这一句只补回迁移角色在这个库上的连接和建对象权限,和主机最初的引导一致。不要删掉空的 `public`,备份会改它的属主,先删会让还原停住。
2. 再点 Deploy production,`allow_rollback` 选 true,目标用发布前的生产版本 `7b620c7a`。测试环境头如果只多了文档、自己没有门禁记录,回滚用的控制器仍然是最近一次门禁通过的修订,不再因为这个被拒绝。目标版本仍必须在已经审查过的 main 历史里。
还原后数这四样,要和备份旁边的 `*-restore-verify.json` 里 `checks` 一致:`identity.users`、`public.profiles`、`public.credit_transactions`、`public` 里的表数量。同一份 `checks` 里 `table_grants_match`、`column_privileges_match`、`table_owners_match` 必须都是 true。这三样是创建恢复点时,源库和还原库的表权限、列权限、表属主已经对过。对不上就停,不要部署。
然后再用网站账号实际读一行。没有登录身份时,行级安全会把行藏起来,看起来像没读到,但那不是权限不够。先把身份设成库里已有的一条账户,再切换角色。下面这条必须成功(退出码 0),并且能读到一行。权限不够会直接失败,失败就停,不要部署:
```text
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;"
```
2. 再点 Deploy production,`allow_rollback` 选 true,目标用发布前的生产版本 `7b620c7a`。点之前先在 Gitea 的 Actions 运行列表里确认:这个版本有一次成功的手动发布门禁(Release Quality Gate)。查不到就停,问产品负责人,不要点。测试环境头如果只多了文档、自己没有门禁记录,回滚用的控制器仍然是最近一次门禁通过的修订,不再因为这个被拒绝。目标版本仍必须在已经审查过的 main 历史里。回滚如果提示恢复点格式低于 2,同样先回到第 4 步创建新恢复点,不要改用旧备份。
还原并且部署成功之后,再按核对清单看健康检查。版本号应回到 `7b620c7a`。