Files
codex-app-server/deploy/deploy.sh
host 1f4463209b deploy: fix host backup tar exclude patterns
The host backup tar runs with -C /opt codex-app-server, so its
members are named codex-app-server/..., and the old patterns
./work and ./worker-heartbeat.json never matched anything.
The live work/ dir (host-state, worktrees rewritten by running
workers) was therefore included, tar hit 'file changed as we
read it', exited 1, and set -e aborted the deploy. Patterns
are now codex-app-server/work and
codex-app-server/worker-heartbeat.json, which actually match.
2026-10-26 07:00:24 +08:00

171 lines
7.0 KiB
Bash
Executable File
Raw Permalink 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='codex-app-server/work' --exclude='codex-app-server/worker-heartbeat.json' -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. 先清掉旧代码(保留 work/ 工作区,里面是任务 worktree),再解包新代码。
# 只做覆盖式解包会留下新版新增的文件,回滚时旧版文件还在,造成“回滚不干净”
# (2026-10-26 实测:回滚后 deploy/ 新脚本仍残留)。
find "$APP_DIR" -mindepth 1 -maxdepth 1 ! -name work -exec rm -rf {} +
tar -xzf /tmp/codex-app-server-code.tar.gz -C "$APP_DIR"
chown -R codexsvc:codexsvc "$APP_DIR"
# 3. 重启并验 health
# autoscaler 接管后,worker 由它拉起(autoscale-* 实例),
# 静态 codex-cloud-worker 必须保持停用,否则两边同时派任务。
# 这里按 autoscaler 是否在跑决定重启哪一边;回滚路径同理。
if systemctl is-active -q codex-autoscaler; then
WORKER_UNIT=codex-autoscaler
else
WORKER_UNIT=codex-cloud-worker
fi
systemctl restart codex-cloud-server $WORKER_UNIT
if wait_health; then
echo "部署成功,health 通过"
exit 0
fi
# 4. 失败:恢复备份并重启,再验一次
echo "新代码 health 未通过,开始回滚到备份:$BACKUP_FILE"
systemctl stop $WORKER_UNIT codex-cloud-server || true
find "$APP_DIR" -mindepth 1 -maxdepth 1 ! -name work -exec rm -rf {} +
tar -xzf "$BACKUP_FILE" -C /opt
chown -R codexsvc:codexsvc "$APP_DIR"
systemctl start codex-cloud-server $WORKER_UNIT
if wait_health; then
echo "部署失败,已回滚到备份,health 已恢复(新代码未上线)" >&2
exit 1
fi
echo "已回滚但 health 仍失败,需人工介入" >&2
exit 1
HOST_SCRIPT
echo "部署完成,代码 commit:$COMMIT"