#!/usr/bin/env bash set -Eeuo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/shihezi}" DEPLOY_HOST="${DEPLOY_HOST:-47.111.103.66}" DEPLOY_USER="${DEPLOY_USER:-root}" DEPLOY_SSH_PORT="${DEPLOY_SSH_PORT:-22}" DEPLOY_PATH="${DEPLOY_PATH:-/opt/service/project/shz-admin/dist}" YARN_CMD="${YARN_CMD:-yarn}" SKIP_INSTALL=0 DRY_RUN=0 info() { printf '\033[1;32m%s\033[0m\n' "$*" } error() { printf '\033[1;31m%s\033[0m\n' "$*" >&2 } die() { error "$*" exit 1 } usage() { cat <<'EOF' 用法: ./scripts/deploy-frontend.sh DEPLOY_USER=deploy DEPLOY_PATH=/var/www/shz-admin ./scripts/deploy-frontend.sh 功能: 1. 在本地安装依赖并执行 yarn build 2. 使用 rsync 将本地构建产物上传到服务器 3. 校验服务器上的 index.html 是否存在 默认配置(均可通过环境变量覆盖): DEPLOY_USER SSH 用户名,默认 root DEPLOY_PATH 服务器上的前端静态文件目录,默认 /opt/service/project/shz-admin/dist DEPLOY_HOST SSH 主机,默认 47.111.103.66 DEPLOY_SSH_PORT SSH 端口,默认 22 BUILD_DIR 本地构建目录,默认 ./shihezi(由 config/config.ts 的 outputPath 决定) YARN_CMD Yarn 命令,默认 yarn 参数: --skip-install 跳过本地依赖安装,直接构建 --dry-run 只构建和检查,不连接服务器、不上传文件 -h, --help 显示帮助 说明: 脚本使用当前用户的 SSH key/agent,不在代码中保存密码。 如果服务器 SSH 端口不是 22,请设置 DEPLOY_SSH_PORT。 EOF } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --skip-install) SKIP_INSTALL=1 ;; --dry-run) DRY_RUN=1 ;; -h|--help) usage exit 0 ;; *) die "未知参数:$1(使用 --help 查看用法)" ;; esac shift done } validate_config() { [[ -n "$DEPLOY_USER" ]] || die "缺少 DEPLOY_USER,例如:DEPLOY_USER=root" [[ -n "$DEPLOY_PATH" ]] || die "缺少 DEPLOY_PATH,例如:DEPLOY_PATH=/var/www/shz-admin" [[ "$DEPLOY_SSH_PORT" =~ ^[0-9]+$ ]] || die "DEPLOY_SSH_PORT 必须是数字" [[ "$DEPLOY_PATH" =~ ^/[A-Za-z0-9._/-]+$ ]] || die "DEPLOY_PATH 必须是安全的绝对路径:$DEPLOY_PATH" case "$DEPLOY_PATH" in /|/opt|/opt/service|/var|/var/www|/usr|/usr/share|/usr/share/nginx|/usr/share/nginx/html) die "拒绝使用过于宽泛的 DEPLOY_PATH:$DEPLOY_PATH" ;; esac } install_dependencies() { if [[ "$SKIP_INSTALL" == "1" ]]; then info "跳过依赖安装" return fi if command -v "$YARN_CMD" >/dev/null 2>&1; then info "安装前端依赖:$YARN_CMD install --frozen-lockfile" "$YARN_CMD" install --frozen-lockfile elif command -v npm >/dev/null 2>&1; then info "未找到 Yarn,使用 npm 安装依赖" if [[ -f package-lock.json ]]; then npm ci else npm install --no-package-lock fi else die "未找到 yarn 或 npm" fi } build_frontend() { if command -v "$YARN_CMD" >/dev/null 2>&1; then info "开始本地构建前端:$YARN_CMD build" "$YARN_CMD" build elif command -v npm >/dev/null 2>&1; then info "未找到 Yarn,使用 npm run build 构建前端" npm run build else die "未找到 yarn 或 npm" fi [[ -f "${BUILD_DIR}/index.html" ]] || die "构建完成但未找到 ${BUILD_DIR}/index.html" info "本地构建完成:${BUILD_DIR}" } upload_frontend() { local target="${DEPLOY_USER}@${DEPLOY_HOST}" local ssh_command="ssh -p ${DEPLOY_SSH_PORT}" if [[ "$DRY_RUN" == "1" ]]; then info "DRY RUN:不会连接服务器或上传文件" info "目标:${target}:${DEPLOY_PATH}/" return fi command -v ssh >/dev/null 2>&1 || die "未找到 ssh 命令" command -v rsync >/dev/null 2>&1 || die "未找到 rsync 命令" info "检查远程目录:${target}:${DEPLOY_PATH}/" ssh -p "$DEPLOY_SSH_PORT" "$target" "mkdir -p '${DEPLOY_PATH}'" info "上传构建产物:${BUILD_DIR}/ -> ${target}:${DEPLOY_PATH}/" rsync -az --delete --delay-updates \ -e "$ssh_command" \ "${BUILD_DIR}/" "${target}:${DEPLOY_PATH}/" info "校验远程 index.html" ssh -p "$DEPLOY_SSH_PORT" "$target" "test -s '${DEPLOY_PATH}/index.html'" info "前端上传完成:${target}:${DEPLOY_PATH}/" } main() { parse_args "$@" cd "$ROOT_DIR" validate_config install_dependencies build_frontend upload_frontend } main "$@"