#!/usr/bin/env bash # deploy-client.sh — deploy the Flutter web app, desktop/mobile installers, # and version.yaml. Does NOT restart the backend (version.yaml is read # per-request) nor touch nginx / the marketing site. Target is EC2 by default; # set DEPLOY_HOST/DEPLOY_USER/DEPLOY_SSH_KEY to retarget the Ali box (migration). set -euo pipefail # shellcheck source=scripts/ci/lib-forgejo.sh . "$(dirname "$0")/lib-forgejo.sh" TAG="$1" TARGET_HOST="${DEPLOY_HOST:-$EC2_HOST}" TARGET_USER="${DEPLOY_USER:-$EC2_USER}" echo "==> deploy-client: tag=${TAG} host=${TARGET_HOST}" if [ ! -f dist/web.tar.gz ] || [ ! -f dist/version.yaml ]; then download_release_assets "$TAG" fi echo "==> deploy-client: dist/ contents:" ls -lh dist/ # Prefer the version.yaml released for this tag (carries bumped build_number + # changelog); fall back to the checkout copy only if the asset is absent. VERSION_YAML="dist/version.yaml" [ -f "$VERSION_YAML" ] || VERSION_YAML="backend/config/version.yaml" rm -rf /tmp/jiu-web-new mkdir -p /tmp/jiu-web-new tar -xzf dist/web.tar.gz -C /tmp/jiu-web-new --strip-components=1 # --- 版本一致性校验(发版前,本地产物)-------------------------------------- # 部署错版事故的通用防线:确认将要上线的产物版本 == 本次发版 tag 版本。 # ① version.yaml 的 version:(后端 /version 与官网下载页实时读它) # ② Flutter web 产物 version.json 的 version(自动更新客户端据此判断新版) # 任一不符即中止发版(exit 1),绝不把错版切上线。 TAG_VER="$(ver_from_tag "$TAG")" # 解析器均把路径经 argv 传入、用 chr(34)/chr(39) 规避引号字面量,免嵌套引号踩坑。 YAML_VER="$(python3 -c 'import sys for line in open(sys.argv[1], encoding="utf-8"): if line.startswith("version:"): print(line.split(":", 1)[1].strip().strip(chr(34)).strip(chr(39))); break' "$VERSION_YAML")" WEB_VER="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1], encoding="utf-8")).get("version", ""))' /tmp/jiu-web-new/version.json 2>/dev/null || echo '')" echo "==> deploy-client: 版本校验 tag=${TAG_VER} version.yaml=${YAML_VER} web=${WEB_VER}" if [ "$YAML_VER" != "$TAG_VER" ]; then echo "ERROR: version.yaml 版本(${YAML_VER}) != 发版 tag(${TAG_VER}),中止发版。" >&2 exit 1 fi if [ -n "$WEB_VER" ] && [ "$WEB_VER" != "$TAG_VER" ]; then echo "ERROR: Flutter web 产物版本(${WEB_VER}) != 发版 tag(${TAG_VER}),中止发版。" >&2 exit 1 fi setup_ssh RSYNC_SSH="ssh -i ~/.ssh/ec2_deploy.pem -o StrictHostKeyChecking=no" # 经隧道推文件到 ali 偶发中断,大安装包尤甚(见 client-v1.1.10 run #362 的 # Deploy → Ali 失败:android 83M scp 传到一半连接 reset,整个 job 挂 14min)。 # 用 rsync --partial(断点续传)+ 重试替代 scp:断了从断点续传、不整段重来。 # 单文件推送,重试 5 次;安装包已压缩(apk/zip/exe)故不加 -z。 push_file() { # push_file local src="$1" dst="$2" i for i in 1 2 3 4 5; do if rsync -av --partial --timeout=120 -e "$RSYNC_SSH" \ "$src" "${TARGET_USER}@${TARGET_HOST}:${dst}"; then return 0 fi echo "!! push_file: ${src} 传输中断(第 ${i}/5 次),5s 后 --partial 续传重试…" >&2 sleep 5 done return 1 } echo "==> deploy-client: uploading files to ${TARGET_HOST}" # version.yaml 承载版本号/更新日志,必达;5 次仍失败则 set -e 中止发版。 push_file "$VERSION_YAML" /tmp/version.yaml # Flutter web 目录同步(--delete 保持一致;--partial 续传;重试 3 次)。web 是 # 发版核心产物,3 次仍失败即中止(避免把不完整目录切上线)。 web_ok=0 for i in 1 2 3; do if rsync -avz --delete --partial --timeout=120 -e "$RSYNC_SSH" \ /tmp/jiu-web-new/ "${TARGET_USER}@${TARGET_HOST}:/tmp/jiu-web-new/"; then web_ok=1; break fi echo "!! web 同步中断(第 ${i}/3 次),5s 后 --partial 续传重试…" >&2; sleep 5 done [ "$web_ok" = 1 ] || { echo "ERROR: web 同步 3 次均失败,中止发版。" >&2; exit 1; } # Desktop / mobile installers(nginx 从 /downloads/ 提供)。文件缺失可跳过 # (部分手动发版);传输 5 次仍失败仅告警不阻断 web/version 切换(下次发版补)。 [ -f dist/jiu-windows-x64-setup.exe ] && { push_file dist/jiu-windows-x64-setup.exe /tmp/jiu-windows-x64-setup.exe || echo "!! windows 安装包 5 次仍失败,跳过。" >&2; } || true [ -f dist/jiu-macos-x64.zip ] && { push_file dist/jiu-macos-x64.zip /tmp/jiu-macos-x64.zip || echo "!! macos 安装包 5 次仍失败,跳过。" >&2; } || true [ -f dist/jiu-android.apk ] && { push_file dist/jiu-android.apk /tmp/jiu-android.apk || echo "!! android 安装包 5 次仍失败,跳过。" >&2; } || true ${SSH} "${TARGET_USER}@${TARGET_HOST}" << 'ENDSSH' set -e # Update version config (WorkingDirectory=/opt/jiu reads config/version.yaml). # Backend re-reads it per request — no restart needed. mkdir -p /opt/jiu/config cp /tmp/version.yaml /opt/jiu/config/version.yaml # Swap Flutter web app (atomic) rm -rf /opt/jiu/web-old mv /opt/jiu/web /opt/jiu/web-old 2>/dev/null || true mv /tmp/jiu-web-new /opt/jiu/web # Publish installers to the nginx-served downloads dir; keep only the latest. mkdir -p /opt/jiu/downloads rm -f /opt/jiu/downloads/jiu-windows-* /opt/jiu/downloads/jiu-macos-* /opt/jiu/downloads/jiu-android-* /opt/jiu/downloads/jiu-android.apk 2>/dev/null || true [ -f /tmp/jiu-windows-x64-setup.exe ] && mv -f /tmp/jiu-windows-x64-setup.exe /opt/jiu/downloads/ || true [ -f /tmp/jiu-macos-x64.zip ] && mv -f /tmp/jiu-macos-x64.zip /opt/jiu/downloads/ || true [ -f /tmp/jiu-android.apk ] && mv -f /tmp/jiu-android.apk /opt/jiu/downloads/ || true echo "Client deploy complete!" ENDSSH # --- 版本一致性校验(发版后,线上实测)-------------------------------------- # 切换完成后实测线上 /version(后端每请求实时读 version.yaml),确认真正上线的 # 版本 == 发版 tag。不符即判定部署失败(exit 1)。域名 fronts 当前 active 机器。 VERIFY_URL="${VERIFY_URL:-https://jiu.51yanmei.com}" LIVE_VER="" for i in 1 2 3 4 5; do LIVE_VER="$(curl -fsS --max-time 15 "${VERIFY_URL}/version" 2>/dev/null \ | python3 -c "import json,sys; print(json.load(sys.stdin).get('version',''))" 2>/dev/null || echo '')" [ "$LIVE_VER" = "$TAG_VER" ] && break echo "!! 线上 /version=${LIVE_VER:-<空>} 尚未更新到 ${TAG_VER}(第 ${i}/5 次),3s 后重试…" >&2 sleep 3 done if [ "$LIVE_VER" != "$TAG_VER" ]; then echo "ERROR: 部署后线上 ${VERIFY_URL}/version 版本(${LIVE_VER:-<空>}) != 发版 tag(${TAG_VER}),部署失败。" >&2 teardown_ssh exit 1 fi echo "==> deploy-client: 线上版本校验通过 /version=${LIVE_VER}" teardown_ssh rm -rf /tmp/jiu-web-new echo "==> deploy-client: done"