From efa1b7852640e67e340e6de8c66ccd4fcc0411d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Tue, 29 Sep 2026 17:10:48 +0800 Subject: [PATCH] =?UTF-8?q?feat(deploy):=20=E4=B8=80=E9=94=AE=E9=83=A8?= =?UTF-8?q?=E7=BD=B2=E8=84=9A=E6=9C=AC=E2=80=94=E2=80=94tar=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E8=B7=AF=E5=BE=84=E7=9B=B4=E8=90=BD=E6=8C=82=E8=BD=BD?= =?UTF-8?q?=E7=82=B9/=E5=A4=B1=E8=B4=A5=E5=8D=B3=E5=81=9C/=E5=93=88?= =?UTF-8?q?=E5=B8=8C=E9=AA=8C=E8=AF=81/=E9=83=A8=E7=BD=B2=E6=97=A5?= =?UTF-8?q?=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- deploy/deploy.sh | 170 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 deploy/deploy.sh diff --git a/deploy/deploy.sh b/deploy/deploy.sh new file mode 100644 index 0000000..230cef7 --- /dev/null +++ b/deploy/deploy.sh @@ -0,0 +1,170 @@ +#!/usr/bin/env bash +# 客迹一键部署——固化标准部署链(spec: docs/superpowers/specs/2026-09-29-git-and-deploy-automation-design.md) +# 用法: bash deploy/deploy.sh [--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 [--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 秒 ====="