#!/usr/bin/env bash # # Mac / Linux 版一键部署,行为与 deploy.ps1 一致。 # # ./deploy/deploy.sh api 只部署服务端 # ./deploy/deploy.sh admin 只部署管理端 # ./deploy/deploy.sh 两个都部署 # ./deploy/deploy.sh api --dry-run 只打包、只显示会上传什么,不真传 # ./deploy/deploy.sh api --with-install 顺带装依赖(只有改了 package.json 才需要) # # 用白名单打包,.env / node_modules / dist / .next 结构上不可能被传上去。 set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(dirname "$script_dir")" config_file="$script_dir/deploy.config.sh" target='all' dry_run=0 with_install=0 # bash 3.2(macOS 自带)在 set -u 下对无参数的 "$@" 会报 unbound variable,所以要加这层保护 for arg in ${@+"$@"}; do case "$arg" in api|admin|all) target="$arg" ;; -n|--dry-run) dry_run=1 ;; --with-install) with_install=1 ;; -h|--help) sed -n '2,13p' "$0"; exit 0 ;; # ${arg} 必须带花括号:紧跟中文时 bash 会把多字节字符合进变量名 *) echo "未知参数:${arg}(可用:api | admin | all | --dry-run | --with-install)" >&2; exit 2 ;; esac done if [ ! -f "$config_file" ]; then echo 'ERROR: 找不到 deploy.config.sh' >&2 echo '把 deploy/deploy.config.example.sh 复制成 deploy/deploy.config.sh,填好服务器信息再跑。' >&2 exit 1 fi # shellcheck disable=SC1090 . "$config_file" for cmd in ssh scp tar; do command -v "$cmd" >/dev/null 2>&1 || { echo "缺少命令:$cmd" >&2; exit 1; } done ssh_target="$DEPLOY_USER@$DEPLOY_HOST" # 读配置里的某一项,如 val API_LABEL val() { local out eval "out=\${$1:-}" printf '%s' "$out" } deploy_app() { local key="$1" local up label local_path remote_path paths excludes clean local_build local install build restart start health nginx_cache nginx_bin up="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')" label="$(val "${up}_LABEL")" local_path="$repo_root/$(val "${up}_LOCAL")" remote_path="$(val "${up}_REMOTE")" paths="$(val "${up}_PATHS")" excludes="$(val "${up}_EXCLUDES")" clean="$(val "${up}_CLEAN")" local_build="$(val "${up}_LOCAL_BUILD")" install="$(val "${up}_INSTALL")" build="$(val "${up}_BUILD")" restart="$(val "${up}_RESTART")" start="$(val "${up}_START")" health="$(val "${up}_HEALTH")" [ -d "$local_path" ] || { echo "本地目录不存在:$local_path" >&2; exit 1; } echo '' echo "=== $label ===" # ---- 本地构建(默认关,配了 $X_LOCAL_BUILD 才跑)---- if [ -n "$local_build" ]; then echo "[1/5] Building locally: $local_build" # 国内拉 Prisma 引擎很慢,指向镜像 ( cd "$local_path" && PRISMA_ENGINES_MIRROR='https://registry.npmmirror.com/-/binary/prisma' bash -c "$local_build" ) else echo '[1/5] Skipping local build' fi # ---- 打包:只收白名单里存在的路径 ---- echo "[2/5] Packing $(val "${up}_LOCAL")" local stamp tar_file remote_tar items stamp="$(date +%Y%m%d-%H%M%S)" tar_file="${TMPDIR:-/tmp}/yelay-${key}-${stamp}.tar.gz" remote_tar="/tmp/$(basename "$tar_file")" local -a tar_args=() for p in $excludes; do tar_args+=("--exclude=$p"); done local -a file_args=() for p in $paths; do [ -e "$local_path/$p" ] && file_args+=("$p") done [ ${#file_args[@]} -gt 0 ] || { echo "白名单里没有任何存在的路径:$paths" >&2; exit 1; } ( cd "$local_path" && tar -czf "$tar_file" ${tar_args[@]+"${tar_args[@]}"} "${file_args[@]}" ) local size_kb size_kb=$(( $(wc -c < "$tar_file") / 1024 )) echo " ${size_kb} KB ($(printf '%s, ' "${file_args[@]}" | sed 's/, $//'))" if [ "$dry_run" = 1 ]; then echo ' DryRun: nothing uploaded.' rm -f "$tar_file" return 0 fi echo "[3/5] Uploading to $remote_path" scp -P "$DEPLOY_PORT" -q "$tar_file" "$ssh_target:$remote_tar" # ---- 拼远端脚本 ---- local clean_cmd='' for p in $clean; do clean_cmd="$clean_cmd rm -rf '$remote_path/$p'"; done local install_cmd='' [ "$with_install" = 1 ] && install_cmd="$install" local start_cmd='' # 先 sleep 2 让刚被杀的进程彻底放开端口,否则新进程会 EADDRINUSE [ -n "$start" ] && start_cmd="sleep 2 $start" # 探活:30 秒还没起来就让部署直接失败,免得「显示成功、线上 502」 local health_cmd='' if [ -n "$health" ]; then health_cmd="for i in {1..30}; do curl -sf -o /dev/null '$health' && break; sleep 1; done curl -sf -o /dev/null '$health' || { echo 'ERROR: service did not come up: $health'; exit 1; }" fi nginx_cache="$(val NGINX_CACHE_DIR)" nginx_bin="$(val NGINX_BIN)" local nginx_cmd='' if [ -n "$nginx_cache" ]; then nginx_cmd="find '$nginx_cache' -mindepth 1 -delete 2>/dev/null || true" [ -n "$nginx_bin" ] && nginx_cmd="$nginx_cmd '$nginx_bin' -s reload 2>/dev/null || true" fi # 远端脚本保持 ASCII 注释,避免 SSH 传输编码问题 local remote_script remote_script="$(cat <