Files
codex-app-server/deploy/deploy.sh
T
host 89a918b543 Ops batch: deploy automation, backup restore drill, monitoring, multi-user verification
Deploy: deploy.sh builds tarball, backs up current code (keep 3),
deploys, restarts, health-gates 60s (ok/backend=postgres/db_ok),
auto-rolls back on failure; rollback.sh restores latest backup.
Rollback is code-only, DB migrations are additive-only so old
code can still read new columns.
Backup: restore_postgres refuses to restore into the production
DB name unless explicitly allowed (drill never allows it);
restore-drill.sh backs up fresh, restores to a test DB, compares
tasks/projects/users/audit row counts, drops the test DB.
Monitoring: worker writes heartbeat file every loop (atomic
replace, failure only logged); GET /health adds
worker_last_seen_seconds_ago + worker_ok (180s default, env
tunable, still HTTP 200 when worker is stale); monitor.sh
checks server/worker/health/pg_isready/docker-postgres/disk/
app dir as one JSON line, exit non-zero on any fail, plus
systemd timer every 5 minutes. Push alerting to chat still
needs a separate channel decision (SSH key or webhook URL).
Multi-user: verify-multiuser.py creates test users/projects
in memory-key only, checks RBAC via real HTTP API (owner/
maintainer/contributor/viewer, cross-project isolation,
merge 200/403/409, no-key 401), test tasks carry a never-
satisfiable dependency so the production worker never claims
them, cleanup deletes only multiuser-test- rows (audit log
is append-only and keeps test entries by design).
Tests: host 27 pass, cloud 298 pass (8 skipped are
environment-dependent real-Postgres tests).
2026-10-26 04:37:40 +08:00

159 lines
6.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# 部署自动化:VM 侧运行,把本地代码同步到外部主机并重启验证,失败自动回滚。
#
# 用法:
# deploy/deploy.sh <host> [user] [--dry-run]
# host:主机 IP 或域名(必填);user:SSH 用户(默认 root)。
# --dry-run:只打印将要做的步骤,不 scp、不 ssh、不改主机。
#
# 密码:本脚本绝不接受密码参数、不读任何 env 文件、不打印任何密钥。
# SSH 密码由 ssh/scp 交互提示输入,或由调用方自己的 SSH 通道提供
# (例如已配好的 key)。需要经代理时,用环境变量 SSH_PROXY_CMD
# 传入 ProxyCommand 内容(ssh 会自行展开 %h %p),未设则直连:
# SSH_PROXY_CMD="python3 /tmp/proxy_tunnel.py %h %p" deploy/deploy.sh <host>
#
# 步骤:
# 1. 本机打代码 tar 包(排除 bin/、work/、.git、__pycache__、state、
# host-config-real.json,与之前手工同步的排除规则一致)。
# 2. scp 到主机 /tmp/codex-app-server-code.tar.gz。
# 3. ssh 到主机:先把当前 /opt/codex-app-server 打备份包到
# /opt/codex-app-server-backups/(时间戳命名,只留最近 3 份,
# 备份排除 work/ 目录以免过大);解包新代码并 chown codexsvc;
# 重启 server+worker;循环 curl localhost:8765/health 最多 60 秒,
# 要求 ok=true 且 backend=postgres 且 db_ok=true。
# 4. 健康检查失败时自动恢复刚打的备份包、重启、再验 health;回滚后
# health 恢复也算部署失败(退出非零),因为新代码没上去;回滚后
# health 仍失败则退出非零并明说“已回滚但 health 仍失败,需人工介入”。
# 5. 成功时只打印新代码的 git commit(从本机 git 取,不编造版本号)。
#
# 回滚边界:回滚只回代码,不回数据库。数据库迁移是只加列的幂等操作
# (projects.repo_url、tasks 的 6 列等),回滚代码后老代码仍能读新列,
# 所以只回代码是安全的;数据库本身绝不会被本脚本动到。
set -eu
HOST=${1:-}
USER_ARG=${2:-root}
DRY_RUN=0
for a in "$@"; do
if [ "$a" = "--dry-run" ]; then DRY_RUN=1; fi
done
if [ -z "$HOST" ] || [ "$HOST" = "--dry-run" ]; then
echo "用法:deploy/deploy.sh <host> [user] [--dry-run]" >&2
exit 2
fi
# user 位置可能是 --dry-run,处理一下
if [ "$USER_ARG" = "--dry-run" ]; then USER_ARG=root; fi
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
REPO_ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
cd "$REPO_ROOT"
COMMIT=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
TARBALL="$REPO_ROOT/work/external-install/codex-app-server-code.tar.gz"
REMOTE_TARBALL="/tmp/codex-app-server-code.tar.gz"
# ssh/scp 命令构造:有 SSH_PROXY_CMD 时走代理,否则直连。
ssh_cmd() {
if [ -n "${SSH_PROXY_CMD:-}" ]; then
ssh -o "ProxyCommand $SSH_PROXY_CMD" -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null "$@"
else
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$@"
fi
}
scp_cmd() {
if [ -n "${SSH_PROXY_CMD:-}" ]; then
scp -o "ProxyCommand $SSH_PROXY_CMD" -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null "$@"
else
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$@"
fi
}
echo "部署目标:$USER_ARG@$HOST,代码 commit:$COMMIT"
if [ "$DRY_RUN" = "1" ]; then
echo "[dry-run] 将打 tar 包:$TARBALL(排除 bin/ work/ .git __pycache__ state host-config-real.json)"
echo "[dry-run] 将 scp 到 $USER_ARG@$HOST:$REMOTE_TARBALL"
echo "[dry-run] 将在主机上备份 /opt/codex-app-server 到 /opt/codex-app-server-backups/(留 3 份),解包、chown codexsvc、重启 server+worker、验 health 60 秒,失败自动回滚"
exit 0
fi
mkdir -p "$REPO_ROOT/work/external-install"
tar --exclude='./bin' --exclude='./work' --exclude='./.git' \
--exclude='__pycache__' --exclude='*.pyc' \
--exclude='host-state*.json*' --exclude='host-daemon.*' \
--exclude='host-config-real.json' --exclude='host-run.log' \
-czf "$TARBALL" .
echo "tar 包已生成:$TARBALL"
scp_cmd "$TARBALL" "$USER_ARG@$HOST:$REMOTE_TARBALL"
echo "tar 包已传到主机"
# 主机侧部署脚本:备份 -> 解包 -> 重启 -> 验 health -> 失败回滚。
# 注意:这里用单引号 heredoc 传过去,主机侧变量不被本机展开;
# 健康判断用 python3 解析 JSON,不依赖 jq(主机未必装 jq)。
ssh_cmd "$USER_ARG@$HOST" 'sh -s' <<'HOST_SCRIPT'
set -eu
APP_DIR=/opt/codex-app-server
BACKUP_DIR=/opt/codex-app-server-backups
STAMP=$(date +%Y%m%d%H%M%S)
mkdir -p "$BACKUP_DIR"
BACKUP_FILE="$BACKUP_DIR/codex-app-server-$STAMP.tar.gz"
check_health() {
# 要求 ok=true 且 backend=postgres 且 db_ok=true,满足返回 0。
OUT=$(curl -s --max-time 5 http://127.0.0.1:8765/health || true)
[ -n "$OUT" ] && printf '%s' "$OUT" | python3 -c '
import json, sys
try:
d = json.load(sys.stdin)
except Exception:
sys.exit(1)
sys.exit(0 if (d.get("ok") is True and d.get("backend") == "postgres"
and d.get("db_ok") is True) else 1)
'
}
wait_health() {
i=0
while [ "$i" -lt 12 ]; do
if check_health; then return 0; fi
sleep 5
i=$((i + 1))
done
return 1
}
# 1. 备份当前代码(排除 work/,只留最近 3 份)
tar --exclude='./work' -czf "$BACKUP_FILE" -C /opt codex-app-server
echo "已备份当前代码到:$BACKUP_FILE"
ls -t "$BACKUP_DIR"/codex-app-server-*.tar.gz 2>/dev/null \
| tail -n +4 | xargs -r rm -f
# 2. 解包新代码并改属主
tar -xzf /tmp/codex-app-server-code.tar.gz -C "$APP_DIR"
chown -R codexsvc:codexsvc "$APP_DIR"
# 3. 重启并验 health
systemctl restart codex-cloud-server codex-cloud-worker
if wait_health; then
echo "部署成功,health 通过"
exit 0
fi
# 4. 失败:恢复备份并重启,再验一次
echo "新代码 health 未通过,开始回滚到备份:$BACKUP_FILE"
systemctl stop codex-cloud-worker codex-cloud-server || true
tar -xzf "$BACKUP_FILE" -C /opt
chown -R codexsvc:codexsvc "$APP_DIR"
systemctl start codex-cloud-server codex-cloud-worker
if wait_health; then
echo "部署失败,已回滚到备份,health 已恢复(新代码未上线)" >&2
exit 1
fi
echo "已回滚但 health 仍失败,需人工介入" >&2
exit 1
HOST_SCRIPT
echo "部署完成,代码 commit:$COMMIT"