Files
2026-09-30 11:02:58 +08:00

194 lines
6.0 KiB
Bash
Executable File
Raw Permalink 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
#
# 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 <<REMOTE
set -e
export PATH=${NODE_BIN}:\$PATH
mkdir -p '$remote_path'
cd '$remote_path'
$clean_cmd
# macOS 打的包带 com.apple.provenance 之类的 xattr 头,GNU tar 每条警告一行,刷屏且无用
tar --warning=no-unknown-keyword -xzf '$remote_tar'
rm -f '$remote_tar'
$install_cmd
$build
$restart
$start_cmd
$health_cmd
$nginx_cmd
echo '__DEPLOY_DONE__'
REMOTE
)"
echo '[4/5] Running on server'
ssh -p "$DEPLOY_PORT" "$ssh_target" 'bash -s' <<< "$remote_script"
rm -f "$tar_file"
if [ -n "$restart" ]; then
echo '[5/5] Restarted'
else
echo '[5/5] Done'
echo " NOTE: 去宝塔面板手动重启 $label"
fi
[ -n "$start" ] && echo " Started, health check passed: $health"
[ -n "$nginx_cmd" ] && echo ' Nginx proxy cache cleared and reloaded'
}
started_at=$(date +%s)
case "$target" in
api) deploy_app api ;;
admin) deploy_app admin ;;
all) deploy_app api; deploy_app admin ;;
esac
[ "$dry_run" = 1 ] || echo "
All done in $(( $(date +%s) - started_at ))s"