Files
fangke/deploy/deploy.sh
T

171 lines
6.6 KiB
Bash
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.
#!/usr/bin/env bash
# 客迹一键部署——固化标准部署链(spec: docs/superpowers/specs/2026-09-29-git-and-deploy-automation-design.md)
# 用法: bash deploy/deploy.sh <server|admin|sdk|all> [--skip-tests] [--allow-dirty]
# 红线: 只操作 /opt/cms-cj 与 tracker-web 容器,绝不触碰服务器其他项目
set -euo pipefail
SECONDS=0
# ---- 配置(环境变量可覆盖)----
JUMP_HOST="${JUMP_HOST:-root@8.152.213.143}"
TARGET_HOST="${TARGET_HOST:-root@47.254.176.10}"
SSH_KEY="${SSH_KEY:-$HOME/.ssh/id_ed25519}"
REMOTE_DIR="${REMOTE_DIR:-/opt/cms-cj}"
SITE_URL="${SITE_URL:-https://tracker.12418113.xyz}"
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
LOG_DIR="$REPO_DIR/deploy/logs"
STAMP="$(date +%Y%m%d-%H%M%S)"
LOG_FILE="$LOG_DIR/deploy-$STAMP.log"
SSH_OPTS=(-i "$SSH_KEY" -o ConnectTimeout=15 -o BatchMode=yes)
SSH=(ssh "${SSH_OPTS[@]}" -J "$JUMP_HOST" "$TARGET_HOST")
SCP=(scp "${SSH_OPTS[@]}" -J "$JUMP_HOST")
log() { echo "[$(date +%H:%M:%S)] $*" | tee -a "$LOG_FILE"; }
die() { log "✗ 失败: $*(部署中止,此前已完成的目标保持已部署状态)"; exit 1; }
ok() { log "✓ $*"; }
mkdir -p "$LOG_DIR" # 必须先于参数解析——早期 die() 也要能写日志
# ---- 参数解析 ----
TARGET_CURRENT="preflight"; STEP="-"
[ $# -ge 1 ] || die "用法: bash deploy/deploy.sh <server|admin|sdk|all> [--skip-tests] [--allow-dirty]"
DEPLOY_TARGET="$1"; shift || true
SKIP_TESTS=0; ALLOW_DIRTY=0
for arg in "$@"; do
case "$arg" in
--skip-tests) SKIP_TESTS=1 ;;
--allow-dirty) ALLOW_DIRTY=1 ;;
*) die "未知参数: $arg" ;;
esac
done
case "$DEPLOY_TARGET" in
server|admin|sdk|all) ;;
*) die "目标必须是 server|admin|sdk|all,收到: $DEPLOY_TARGET" ;;
esac
GIT_HASH="$(git -C "$REPO_DIR" rev-parse --short HEAD)"
log "===== 部署开始 target=$DEPLOY_TARGET git=$GIT_HASH ====="
# ---- 前置检查(所有目标必经)----
preflight() {
STEP="前置检查"
if [ "$ALLOW_DIRTY" -eq 0 ] && [ -n "$(git -C "$REPO_DIR" status --porcelain)" ]; then
die "git 工作区不干净(--allow-dirty 可跳过;强烈建议先提交)"
fi
"${SSH[@]}" 'echo ok' >/dev/null 2>&1 || die "跳板/目标服务器 SSH 不可达($JUMP_HOST → $TARGET_HOST)"
ok "工作区干净,SSH 链路可达"
}
# ---- 各目标实现 ----
server_tests() {
[ "$SKIP_TESTS" -eq 1 ] && return 0
STEP="server 测试"
(cd "$REPO_DIR/server" && python -m pytest tracker/tests/ -q >>"$LOG_FILE" 2>&1) \
|| die "pytest 未全过(详见 $LOG_FILE)"
ok "pytest 全过"
}
sdk_tests() {
[ "$SKIP_TESTS" -eq 1 ] && return 0
STEP="sdk 测试"
(cd "$REPO_DIR/sdk" && npx vitest run >>"$LOG_FILE" 2>&1) || die "vitest 未全过"
(cd "$REPO_DIR/sdk" && npx tsc --noEmit >>"$LOG_FILE" 2>&1) || die "tsc 报错"
ok "sdk vitest+tsc 全过"
}
build_admin() {
STEP="admin 构建"
(cd "$REPO_DIR/admin" && NODE_OPTIONS=--max-old-space-size=6144 pnpm build >>"$LOG_FILE" 2>&1) \
|| die "admin 构建失败(本机 OOM 则确认 NODE_OPTIONS 已带)"
ok "admin 构建完成: $(grep -oE 'index-[A-Za-z0-9_-]+\.js' "$REPO_DIR/admin/dist/index.html" | head -1)"
}
build_sdk() {
STEP="sdk 构建"
(cd "$REPO_DIR/sdk" && node build.mjs >>"$LOG_FILE" 2>&1) || die "sdk 构建失败"
local gz; gz=$(gzip -c "$REPO_DIR/sdk/dist/v1.js" | wc -c)
ok "sdk 构建完成: build_id=$(grep -oE '[0-9a-f]{7,9}-2026[0-9]{8}' "$REPO_DIR/sdk/dist/v1.js" | head -1) gzip=${gz}B"
}
transfer() { # $1=包内路径列表(空格分隔,相对 REPO_DIR) $2=远端包名
STEP="传输与落位"
local tarball="/tmp/$2"
(cd "$REPO_DIR" && tar -czf "$tarball" $1) || die "tar 打包失败: $1"
"${SCP[@]}" "$tarball" "$TARGET_HOST:/tmp/" >/dev/null 2>&1 || die "scp 传输失败(经 $JUMP_HOST)"
rm -f "$tarball"
# tar 按真实路径解压——直接落位挂载点(原地合并,无 cp 无暂存)
"${SSH[@]}" "cd $REMOTE_DIR && tar -xzf /tmp/$2 && rm -f /tmp/$2" >/dev/null 2>&1 \
|| die "远端解压失败($REMOTE_DIR)"
ok "已落位: $1"
}
rebuild_web() {
STEP="server 容器重建"
"${SSH[@]}" "cd $REMOTE_DIR/deploy && docker compose up -d --build web" >>"$LOG_FILE" 2>&1 \
|| die "docker compose 重建失败"
sleep 5
local status; status="$("${SSH[@]}" "docker ps --filter name=tracker-web --format '{{.Status}}'")" \
|| die "容器状态查询失败"
echo "$status" | grep -q "^Up" || die "tracker-web 容器状态异常: $status"
ok "tracker-web: $status"
}
verify_site_up() {
STEP="站点探活"
curl -sf -o /dev/null --max-time 20 "$SITE_URL" || die "$SITE_URL 探活失败"
ok "站点 200"
}
verify_admin_hash() {
STEP="admin 哈希验证"
local local_h online_h
local_h=$(sha256sum "$REPO_DIR/admin/dist/index.html" | cut -d' ' -f1)
online_h=$(curl -sf --max-time 20 "$SITE_URL/" | sha256sum | cut -d' ' -f1) \
|| die "线上 index.html 拉取失败"
[ "$local_h" = "$online_h" ] || die "admin index.html 线上哈希与本机产物不一致(部署未生效或缓存)"
ok "admin 哈希一致"
}
verify_sdk_bid() {
STEP="sdk build_id 验证"
local bid; bid="$(grep -oE '[0-9a-f]{7,9}-2026[0-9]{8}' "$REPO_DIR/sdk/dist/v1.js" | head -1)"
[ -n "$bid" ] || die "本机产物无 build_id"
curl -sf --max-time 20 "$SITE_URL/sdk/v1.js" | grep -q "$bid" \
|| die "线上 v1.js 不含新 build_id $bid"
ok "sdk 线上已含 build_id $bid"
}
deploy_server() {
TARGET_CURRENT=server
server_tests
STEP="server 打包"
# 排除清单镜像 server/.dockerignore 本意:Windows venv(97MB)/缓存/本地xdb 绝不上服务器
(cd "$REPO_DIR" && tar -czf /tmp/keji-server.tgz \
--exclude='__pycache__' --exclude='.pytest_cache' --exclude='db.sqlite3' \
--exclude='.venv' --exclude='.cache' --exclude='tracker/data/*.xdb' server/) \
|| die "server 打包失败"
"${SCP[@]}" /tmp/keji-server.tgz "$TARGET_HOST:/tmp/" >/dev/null 2>&1 || die "scp 失败"
rm -f /tmp/keji-server.tgz
"${SSH[@]}" "cd $REMOTE_DIR && tar -xzf /tmp/keji-server.tgz && rm -f /tmp/keji-server.tgz" >/dev/null 2>&1 \
|| die "远端解压失败"
ok "server 源码落位"
rebuild_web
verify_site_up
}
deploy_admin() {
TARGET_CURRENT=admin
build_admin
transfer "admin/dist" "keji-admin.tgz"
verify_admin_hash
}
deploy_sdk() {
TARGET_CURRENT=sdk
sdk_tests
build_sdk
transfer "sdk/dist" "keji-sdk.tgz"
verify_sdk_bid
}
# ---- 编排(all 按契约依赖序: server → admin → sdk)----
preflight
case "$DEPLOY_TARGET" in
server) deploy_server ;;
admin) deploy_admin ;;
sdk) deploy_sdk ;;
all) deploy_server; deploy_admin; deploy_sdk ;;
esac
log "===== 部署完成 target=$DEPLOY_TARGET git=$GIT_HASH 用时 $SECONDS 秒 ====="