#!/bin/sh # 部署自动化:VM 侧运行,把本地代码同步到外部主机并重启验证,失败自动回滚。 # # 用法: # deploy/deploy.sh [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 # # 步骤: # 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 [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"