P2-ops: migration alignment, deploy test list, monitor alerts/single-host, backup/rollback closure, Caddy template

- migrations: MIGRATION_TABLES now equals POSTGRES_SCHEMA tables
  (uploads, task_attachments, task_followups, environments added);
  SEQUENCE_TABLES matches IDENTITY tables; task_followups
  sender_task_id column ensured before insert; UNMIGRATED_TABLES
  and migration_coverage() report tables with no PG schema instead
  of silently skipping them; CLI prints the coverage report.
- deploy: DEFAULT_TEST_SUITES gains deploy/tests (discover -s only,
  never package path); regression tests pin the default list.
- monitor: MONITOR_SINGLE_HOST=1 makes the server check single-host;
  default stays dual to avoid silent downgrade; server_mode is in
  the JSON. Optional fail-only alert outlet (webhook or mail,
  default off, alert body is pass/fail JSON only, no secrets).
- backup/rollback: README and restore-drill document what is
  recoverable (DB, code) vs not (worktrees, traces, uploads, host
  configs); rollback.sh shares the server-unit list and dual-port
  health success standard with host_deploy.sh (sync test guards it).
- public entry: Caddyfile uses native {$DOMAIN}/{$ACME_EMAIL}
  variables plus a caddy.service template; README documents the
  steps and the current BT-nginx production status.
This commit is contained in:
host committed 2026-10-27 16:37:28 +00:00
1 parent ef532a38c2
commit 64995fafab
14 files changed
+953 -44

No files matched your search

+103 -11
View File
@@ -104,6 +104,16 @@ deploy/rollback.sh <host> [user]
恢复后同样重启并验 health(60 秒),成功/失败都明说;主机上没有备份包
时直接报错退出,不编造。
### 回滚成功标准:清单里每个端口都通过(P2-ops)
rollback.sh 与 host_deploy.sh 共用同一份 unit 清单约定:server-1 必含,
server-2 以 `systemctl cat` 探测为准(存在才纳入停/起与健康检查),worker
是 autoscaler active 时用 autoscaler、否则用 cloud-worker。两脚本改动清单
时必须同步改,`deploy/tests/test_p2_ops.py` 会双向核对(只改一处就变红)。
回滚成功 = 8765 通过,且 server-2 存在时 8766 也通过。只通 8765 不算成功:
LB 上游的 server-2 若仍跑故障代码,回滚等于没回完。
### 回滚边界:只回代码,不回数据库
数据库迁移是只加列的幂等操作(例如 projects.repo_url、tasks 的 6 列),
@@ -128,6 +138,9 @@ sh /opt/codex-app-server/deploy/restore-drill.sh
audit_log 行数(行数相等即通过,只比行数,不比对具体内容,不打印任何
行内容)→ 比对完立即删掉测试库(trap 保证中途失败也删)。
范围(P2-ops):演练 PASS 只证明数据库备份可恢复,不证明 worktree /
链路 / 上传可恢复——那些不在备份里,见“备份制度”一节。
安全红线:恢复目标永远是新生成的测试库名,绝不等于生产库名;
`restore_postgres` 本身也会拒绝目标库名等于 `CLOUD_DB_NAME`
(`allow_production` 绝不在本脚本里设为 True),双重保险。本脚本绝不
@@ -165,14 +178,38 @@ journalctl -u monitor.service -f # 看每次检查的输出
timer 每 5 分钟跑一次,日志进 journal。
### 监控边界:本轮只有“主机侧检查 + journal 日志”
### 监控边界:默认只有“检查 + journal 日志”,告警出口可选(P2-ops)
monitor.sh 本身不会发消息给用户。推送到 Muse 聊天的告警仍需用户另行
决定通道(例如 SSH key 常驻通道让外部 cron 拉 journal,或者一个
webhook URL 让 monitor 失败时 POST 过去),在通道定下来之前,不要假装
monitor.sh 会通知任何人。手工验证步骤:主机上跑一次 monitor.sh 看 JSON
与退出码,再跑一次 --self-test 看 SELF-TEST PASS,最后
`systemctl list-timers | grep monitor` 确认 timer 已启用。
默认不配告警出口时,monitor.sh 仍然只做检查 + journal 日志,不会发消息
给任何人。需要 fail 时通知时,在 monitor.service 的 `Environment=`(或
monitor.timer 的运行环境)里二选一配置,默认关闭:
```ini
# 二选一,同时配了只用 webhook:
Environment=MONITOR_ALERT_URL=https://example.invalid/hook
# 或:
Environment=MONITOR_ALERT_MAILTO=ops@example.invalid
```
- 只在 overall=fail 时发,pass 时不发;故障持续期间 timer 每 5 分钟跑
一次就会重发,接收端需自行去重(本机不做去重状态文件,免得状态文件
本身成为新的故障点)。
- 告警内容只有 JSON 检查结果(pass/fail 与 server_mode),不含 health
正文、不含任何环境变量、不含密钥;URL 与收件人只走环境变量。
- 邮件路径依赖本机 `mail` 命令,没有它时会记一条“告警未发出”到 journal,
不会静默假装已通知。
### 单机模式:MONITOR_SINGLE_HOST=1(P2-ops)
默认 server 检查要求 server-1 与 server-2 双 active。确认是单机部署时,
在 monitor 运行环境里设 `MONITOR_SINGLE_HOST=1`,server 检查只查
server-1;否则单机主机的 server 检查会恒 fail,恒 fail 会掩盖真故障。
双机主机上绝不要设它(设了就是静默降级)。输出 JSON 的 `server_mode`
字段写明本次是 single 还是 dual,降级可见。
手工验证步骤:主机上跑一次 monitor.sh 看 JSON 与退出码,再跑一次
--self-test 看 SELF-TEST PASS,最后 `systemctl list-timers | grep monitor`
确认 timer 已启用。--self-test 时内层检查会清掉告警出口,不会误发告警。
## worker 心跳与 health 的 worker 字段
@@ -225,17 +262,72 @@ completed 测试任务是直接在 DB 里把测试任务置 completed,不跑
- 审计日志是 append-only 的:验证过程的审计条目会留在 audit_log 里,
脚本不删审计表——审计就该留痕,这一点不是清理遗漏。
## 公网入口与 webhook secret(已有文件,不属于本轮新增)
## 备份制度:什么可恢复、什么不可恢复(P2-ops,二选一里选了“写清区分”)
- `caddy/Caddyfile`:只暴露 `/webhooks/*` 与 `/oauth/*`,任务/项目/管理
API 对外一律 403;域名与邮箱占位符需部署时替换。
不要谎称已做全量备份。本节写清现状;worktree / 链路 / 上传目录纳入
备份范围是另一单(需先定量:租户目录大小、备份窗口、恢复演练抽查),
不在本批做。
| 内容 | 在备份里吗 | 恢复手段 |
| --- | --- | --- |
| 数据库(Postgres 全库) | 是 | pg-backup 每天一次(留 7 份)+ restore-drill 恢复到测试库比对行数 |
| 代码(/opt/codex-app-server,不含 work/) | 是 | 每次部署前打备份 tar(留 5 份)+ rollback.sh 恢复 |
| 任务 worktree(work/、tenants/ 下各任务目录) | 否 | 无。主机丢了就丢了;数据库里的任务行还在,但 worktree 没了,续跑/链路/附件都会缺 |
| 链路文件(host-traces/)与上传文件(附件包) | 否 | 无。链路是过程记录,丢了只影响可观测性;上传附件丢了则任务附件不可下载 |
| 运行配置(/etc/codex-app-server/env、unit 文件、nginx/Caddy 配置) | 否(不在代码 tar 与数据库备份里) | 换机时从备份清单手动重贴,见“公网入口”与 new-host-checklist.md |
后果要说透:只靠现有备份,数据库能恢复、代码能回滚,但一台主机彻底丢了,
在跑任务的工作目录与历史链路是回不来的;恢复后需要把受影响的任务标记为
失败/重建,而不是假装它们还能续跑。restore-drill.sh 的 PASS 只证明数据库
备份可恢复,不证明整机可恢复(脚本头注释同样写明)。
## 公网入口与 webhook secret
- `caddy/Caddyfile`(P2-ops:占位符改为 Caddy 原生变量模板):只暴露
`/webhooks/*` 与 `/oauth/*`,任务/项目/管理 API 对外一律 403;自动证书
走 Caddy(本批二选一里选了 Caddy,certbot 路径未做成模板、只是备选,
不要假装两条路都已就绪)。
- `setup-webhook-secret.sh`:在主机上生成 webhook secret 并写入 env 文件,
绝不打印 secret;已设则不动,避免轮换后 GitHub 端旧 secret 失效。
### Caddy 模板部署步骤(新机可复现,不申请真实证书也能备好文件)
```sh
# 1. 主机上装 Caddy(包管理器或官方二进制均可),建运行目录:
mkdir -p /etc/caddy
cp deploy/caddy/Caddyfile /etc/caddy/Caddyfile
cp deploy/caddy/caddy.service /etc/systemd/system/caddy.service
# 2. 写变量文件(域名与邮箱不是密钥,但也不要写进仓库):
cat > /etc/caddy/env <<'EOF'
DOMAIN=example.invalid
ACME_EMAIL=ops@example.invalid
EOF
chmod 600 /etc/caddy/env
# 3. Caddyfile 里的 {$DOMAIN} / {$ACME_EMAIL} 由 Caddy 运行时从环境变量
# 展开;变量未设时 Caddy 启动即报错(fail-fast,不会静默跑成空域名)。
systemctl daemon-reload
systemctl enable --now caddy
# 4. 验证:curl -v https://$DOMAIN/webhooks/github 应被 Caddy 转发
# (控制面本身仍只监听 127.0.0.1:8765,不直接对外)。
```
### 当前生产现状(换机需重贴,模板不等于已上线)
当前生产走宝塔 nginx(api.diante.me 等 vhost 由 BT 面板管理),不是
Caddy;BT 若重生成 vhost 会吃掉 location,需从备份贴回(见
new-host-checklist.md 第 8 节)。仓库内 Caddy 模板是“新机可复现”的选项,
不是“生产已切 Caddy”的声明。备份清单必须另含:BT vhost 配置备份、
/etc/caddy/Caddyfile(如已用 Caddy)、caddy.service、unit 文件
(codex-cloud-server*.service 等不在 deploy tar 里,改动后须单独 scp,
见 codex-cloud-server-2.service 头注释)——这些都不在代码 tar 与数据库
备份里,换机时不重贴就等于没备份。
## 仍未做(不要假装已做)
- 推送告警通道:monitor 失败时通知到 Muse 聊天的通道还没定(见监控一节)。
- 回滚不回数据库:见回滚边界一节;改类型/删列的迁移需要单独评估。
- 恢复演练不碰生产库:演练只比行数,不比对具体内容;恢复到生产库的
真实灾难恢复流程(停服务、恢复、重启、验 health)还没演练过。
- worktree / 链路 / 上传目录未纳入备份:见“备份制度”一节;主机彻底丢了
这些回不来,需要另单定量后做。
- 生产仍走宝塔 nginx,未切 Caddy:Caddy 模板只是仓库内可部署选项。
- CloudExecutor、GUI、Slack 执行、推送实发:与本轮无关,仍是 0 或占位。