#!/usr/bin/env bash
|
# ============================================================================
|
# Vue 前端部署脚本(macOS / Linux 版)—— 银川九鸿化工(JHHG)
|
#
|
# 由同目录的 JHHG.bat 转写而来:步骤、提示语、服务器、账号、远端目录、
|
# 超时参数与 .bat 完全一致,Windows 上怎么用,这里就怎么用。
|
#
|
# 与 .bat 的全部差别:
|
# 1. 不依赖 sshpass.exe。系统里有 sshpass 就用 sshpass,没有就退回 macOS
|
# 自带的 /usr/bin/expect 来喂密码 —— 所以这台 Mac 不用装任何东西也能跑。
|
# 2. 脚本开头会 cd 到自己所在目录,从任何位置调用都能正确构建和上传。
|
# 3. 结尾的 pause 只在交互式终端里生效,被其它脚本/管道调用时不会卡住。
|
# 4. 「比较文件大小」这一步按标题的原意真正做了比对(.bat 只打印不比对),
|
# 大小不一致直接报错,避免把一个上传残缺的包解压到线上目录。
|
#
|
# 用法:
|
# ./JHHG.sh # 首次可能需要 chmod +x JHHG.sh
|
# bash JHHG.sh
|
#
|
# 可选环境变量(一般用不到):
|
# JHHG_TRANSPORT=sshpass|expect 强制指定喂密码的方式
|
# ============================================================================
|
|
set -uo pipefail
|
|
# =========================
|
# 配置(与 JHHG.bat 一字不差)
|
# =========================
|
|
readonly COMPANY='JHHG'
|
readonly REMOTE_USER='root'
|
readonly REMOTE_HOST='42.63.70.90'
|
readonly REMOTE_PORT='22'
|
# 密码含 ! 和 #:必须用单引号;不要 source 到交互式 shell 里跑(那里会做 ! 历史展开),
|
# 用 ./JHHG.sh 或 bash JHHG.sh 执行。
|
# 2026-10-12 实测过:服务器接受的就是这个完整字符串(去掉 !@##@! 的 Nxjhhg 会被拒)。
|
readonly PASSWORD='Nxjhhg!@##@!'
|
readonly ZIP_FILE='dist.zip'
|
readonly REMOTE_DIR='/home/vue/dist'
|
readonly SSH_OPTS=(-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3)
|
|
readonly REMOTE_TARGET="${REMOTE_USER}@${REMOTE_HOST}"
|
readonly REMOTE_TMP="${REMOTE_DIR}/.dist.zip.uploading"
|
|
# 脚本自身所在目录 = 项目根目录
|
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# =========================
|
# 输出helper(与 .bat 的提示格式一致)
|
# =========================
|
|
log() { printf '%s\n' "$*"; }
|
ok() { printf '[OK] %s\n' "$*"; }
|
info() { printf '[INFO] %s\n' "$*"; }
|
warn() { printf '[WARN] %s\n' "$*"; }
|
err() { printf '[ERROR] %s\n' "$*"; }
|
|
# 与 .bat 的 :END 一样,交互式终端里停一下再退出
|
pause_if_interactive() {
|
if [[ -t 0 ]]; then
|
printf '\n按回车键退出...'
|
read -r _ || true
|
printf '\n'
|
fi
|
}
|
|
fail() {
|
log ''
|
log '============================================================'
|
log ' DEPLOY FAILED'
|
log '============================================================'
|
log ''
|
log 'Please check the error message above.'
|
log ''
|
pause_if_interactive
|
exit 1
|
}
|
|
# 没被显式处理就失败的命令,也走到 DEPLOY FAILED,不留半截输出
|
on_unexpected_error() {
|
err "命令失败(退出码 $1,脚本第 $2 行)。"
|
fail
|
}
|
|
trap 'on_unexpected_error $? $LINENO' ERR
|
|
# =========================
|
# 传输层:把密码喂给 ssh/scp
|
# =========================
|
|
# 优先 sshpass(和 .bat 一样),没有就用 macOS 自带的 expect
|
TRANSPORT="${JHHG_TRANSPORT:-}"
|
if [[ -z "$TRANSPORT" ]]; then
|
if command -v sshpass >/dev/null 2>&1; then
|
TRANSPORT='sshpass'
|
elif command -v expect >/dev/null 2>&1; then
|
TRANSPORT='expect'
|
fi
|
fi
|
|
# 用当前 TRANSPORT 执行一条 ssh/scp 命令,参数原样透传
|
# 密码通过环境变量传递,不出现在命令行参数里(不会进 ps 输出)
|
run_remote() {
|
if [[ "$TRANSPORT" == 'sshpass' ]]; then
|
SSHPASS="$PASSWORD" sshpass -e "$@"
|
return $?
|
fi
|
# expect 分支:程序必须写成文件再用 -f 调用,因为 `expect -c 脚本 参数...`
|
# 不会把参数放进 $argv(会把它当脚本文件名)。
|
local exp_file rc
|
exp_file="$(mktemp "${TMPDIR:-/tmp}/jhhg-deploy.XXXXXX")" || {
|
err 'temp file creation failed.'
|
return 1
|
}
|
cat >"$exp_file" <<'EXPECT_PROGRAM'
|
set timeout 300
|
log_user 1
|
if {[catch {spawn {*}$argv} spawn_err]} {
|
puts stderr "spawn failed: $spawn_err"
|
exit 127
|
}
|
expect {
|
-re "(?i)are you sure you want to continue connecting" { send "yes\r"; exp_continue }
|
-re "(?i)password:" { send "$env(JHHG_PW)\r"; exp_continue }
|
eof
|
}
|
# 把子进程的退出码原样带出去(密码错时 ssh 返回 255,必须让上层感知到)
|
set wait_result {}
|
catch wait wait_result
|
set rc [lindex $wait_result 3]
|
if {![string is integer -strict $rc]} { set rc 1 }
|
exit $rc
|
EXPECT_PROGRAM
|
JHHG_PW="$PASSWORD" expect -f "$exp_file" "$@"
|
rc=$?
|
rm -f "$exp_file"
|
return $rc
|
}
|
|
ssh_remote() { # ssh_remote "远端命令"
|
run_remote ssh -p "$REMOTE_PORT" "${SSH_OPTS[@]}" "$REMOTE_TARGET" "$1"
|
}
|
|
scp_up() { # scp_up 本地文件 远端路径
|
run_remote scp -P "$REMOTE_PORT" "${SSH_OPTS[@]}" "$1" "${REMOTE_TARGET}:$2"
|
}
|
|
# =========================
|
# 开始
|
# =========================
|
|
log ''
|
log '============================================================'
|
log ' Vue Dist Auto Deploy'
|
log '============================================================'
|
log ''
|
log "Company : $COMPANY"
|
log "Server : $REMOTE_USER@$REMOTE_HOST"
|
log "Port : $REMOTE_PORT"
|
log "Remote Dir : $REMOTE_DIR"
|
log "Package : $ZIP_FILE"
|
log ''
|
|
# =========================
|
# [1/7] 密码工具
|
# =========================
|
|
log '[1/7] Checking password tool...'
|
|
if [[ -z "$TRANSPORT" ]]; then
|
err 'sshpass 和 expect 都没有,无法自动输入密码。'
|
info 'macOS 一般自带 /usr/bin/expect,正常不会走到这里;'
|
info '如果确实没有,装 sshpass 即可(可能需先加第三方 tap):brew install sshpass'
|
info 'Linux: apt install sshpass / yum install sshpass'
|
fail
|
fi
|
|
if [[ "$TRANSPORT" == 'sshpass' ]]; then
|
ok "sshpass found: $(command -v sshpass)"
|
else
|
ok "expect found: $(command -v expect)(macOS 自带,无需安装 sshpass)"
|
fi
|
|
# =========================
|
# [2/7] pnpm
|
# =========================
|
|
log ''
|
log '[2/7] Checking pnpm...'
|
|
if ! command -v pnpm >/dev/null 2>&1; then
|
err 'pnpm not found.'
|
info 'HINT: npm install -g pnpm'
|
fail
|
fi
|
|
if ! pnpm --version; then
|
err 'pnpm execution failed.'
|
fail
|
fi
|
|
# =========================
|
# [3/7] node
|
# =========================
|
|
log ''
|
log '[3/7] Checking Node.js...'
|
|
if ! command -v node >/dev/null 2>&1; then
|
err 'Node.js not found.'
|
fail
|
fi
|
|
if ! node --version; then
|
err 'Node.js execution failed.'
|
fail
|
fi
|
|
# =========================
|
# node_modules
|
# =========================
|
|
if [[ ! -d node_modules ]]; then
|
log ''
|
info 'node_modules not found.'
|
info 'Running pnpm install...'
|
if ! pnpm install; then
|
err 'pnpm install failed.'
|
fail
|
fi
|
ok 'pnpm install completed.'
|
else
|
ok 'node_modules exists.'
|
fi
|
|
# =========================
|
# [4/7] 构建
|
# =========================
|
|
log ''
|
log '[4/7] Building project...'
|
|
SKIP_BUILD=''
|
if [[ -f "$ZIP_FILE" ]]; then
|
log ''
|
warn "$ZIP_FILE already exists."
|
printf 'Rebuild? Y/N (default Y): '
|
read -r REBUILD_ANSWER || true
|
case "$(printf '%s' "${REBUILD_ANSWER:-}" | tr '[:lower:]' '[:upper:]')" in
|
N) info 'Skip build.'; SKIP_BUILD='1' ;;
|
esac
|
fi
|
|
if [[ -z "$SKIP_BUILD" ]]; then
|
info "Company: $COMPANY"
|
info 'Command: pnpm run build'
|
log ''
|
export VITE_COMPANY="$COMPANY"
|
if ! pnpm run build; then
|
log ''
|
err 'Build failed.'
|
fail
|
fi
|
log ''
|
ok 'Build completed.'
|
fi
|
|
# =========================
|
# [5/7] 检查包
|
# =========================
|
|
log ''
|
log '[5/7] Checking package...'
|
|
if [[ ! -f "$ZIP_FILE" ]]; then
|
err "$ZIP_FILE not found."
|
info 'HINT: Please check whether your build generates dist.zip.'
|
fail
|
fi
|
|
LOCAL_ZIP_BYTES="$(wc -c < "$ZIP_FILE" | tr -d ' ')"
|
LOCAL_ZIP_MB="$(awk -v b="$LOCAL_ZIP_BYTES" 'BEGIN { printf "%.2f", b / 1048576 }')"
|
|
ok 'Package found.'
|
info "File size: $LOCAL_ZIP_BYTES bytes"
|
info "File size: $LOCAL_ZIP_MB MB"
|
|
# =========================
|
# [6/7] 测试 SSH
|
# =========================
|
|
log ''
|
log '[6/7] Testing SSH connection...'
|
|
if ! ssh_remote 'echo SSH_OK'; then
|
err 'SSH connection failed.'
|
fail
|
fi
|
|
ok 'SSH connection successful.'
|
|
# =========================
|
# 建远端目录
|
# =========================
|
|
log ''
|
info 'Creating remote directory...'
|
|
if ! ssh_remote "mkdir -p '$REMOTE_DIR'"; then
|
err 'Failed to create remote directory.'
|
fail
|
fi
|
|
ok 'Remote directory ready.'
|
|
# =========================
|
# 上传临时文件
|
# =========================
|
|
log ''
|
info 'Uploading package...'
|
|
ssh_remote "rm -f '$REMOTE_TMP'" || true
|
|
# 远端路径不能加引号:ssh 那条的单引号是给远端 shell 剥的,而 scp 的参数不经 shell,
|
# 单引号会变成文件名的真实字符(远端会去开 "'/home/vue/dist/..." 这个相对路径)
|
if ! scp_up "$ZIP_FILE" "$REMOTE_TMP"; then
|
err 'Upload failed.'
|
fail
|
fi
|
|
ok 'Upload completed.'
|
|
# =========================
|
# 校验远端文件
|
# =========================
|
|
log ''
|
info 'Checking remote package...'
|
|
if ! ssh_remote "test -f '$REMOTE_TMP'"; then
|
err 'Remote package does not exist.'
|
fail
|
fi
|
|
ok 'Remote package exists.'
|
|
# =========================
|
# 比较文件大小
|
# =========================
|
|
log ''
|
info 'Checking package size...'
|
log ''
|
|
# 大小比对放在远端做、只取退出码。
|
# 不要用 $(run_remote ...) 去截取远端输出:expect 会把整段交互回显(spawn 命令行、
|
# 密码提示)一起写进 stdout,截出来的不是命令结果,比对必然失败。
|
if ! ssh_remote "test \"\$(stat -c %s '$REMOTE_TMP')\" -eq $LOCAL_ZIP_BYTES"; then
|
# 变量一律加 ${}:bash 3.2 会把紧跟其后的全角字符(如「(」)当成变量名的一部分,
|
# 于是 $ZIP_FILE( 会被解析成名为 "ZIP_FILE(" 的变量,配 set -u 直接报 unbound variable
|
err "包大小不一致:远端 ${REMOTE_TMP} 与本地 ${ZIP_FILE}(${LOCAL_ZIP_BYTES} 字节)不符,上传可能被截断。"
|
info "远端临时文件已保留。可在服务器上执行 stat -c %s '${REMOTE_TMP}' 核对,确认后删除。"
|
fail
|
fi
|
|
ok "包大小一致:${LOCAL_ZIP_BYTES} 字节。"
|
|
# =========================
|
# 改名生效
|
# =========================
|
|
log ''
|
info 'Activating package...'
|
|
if ! ssh_remote "mv -f '$REMOTE_TMP' '$REMOTE_DIR/$ZIP_FILE'"; then
|
err 'Failed to activate package.'
|
fail
|
fi
|
|
ok 'Package activated.'
|
|
# =========================
|
# 检查 unzip
|
# =========================
|
|
log ''
|
info 'Checking unzip...'
|
|
if ! ssh_remote 'command -v unzip'; then
|
err 'unzip is not installed on server.'
|
log ''
|
log 'CentOS/RHEL:'
|
log 'yum install -y unzip'
|
log ''
|
log 'Ubuntu/Debian:'
|
log 'apt install -y unzip'
|
fail
|
fi
|
|
ok 'unzip found.'
|
|
# =========================
|
# 测试 ZIP
|
# =========================
|
|
log ''
|
info 'Testing remote ZIP...'
|
|
if ! ssh_remote "cd '$REMOTE_DIR' && unzip -t '$ZIP_FILE'"; then
|
log ''
|
err 'ZIP test failed.'
|
info 'Remote ZIP has been kept.'
|
fail
|
fi
|
|
ok 'ZIP test passed.'
|
|
# =========================
|
# 解压
|
# =========================
|
|
log ''
|
info 'Extracting package...'
|
|
if ! ssh_remote "cd '$REMOTE_DIR' && unzip -o '$ZIP_FILE'"; then
|
log ''
|
err 'Remote unzip failed.'
|
info 'Remote ZIP has been kept.'
|
fail
|
fi
|
|
log ''
|
ok 'Extract completed.'
|
|
# =========================
|
# 删除远端 ZIP
|
# =========================
|
|
log ''
|
info 'Removing remote ZIP...'
|
|
if ssh_remote "rm -f '$REMOTE_DIR/$ZIP_FILE'"; then
|
ok 'Remote ZIP removed.'
|
else
|
warn 'Failed to remove remote ZIP.'
|
fi
|
|
# =========================
|
# [7/7] 完成
|
# =========================
|
|
log ''
|
log '============================================================'
|
log ' DEPLOY SUCCESS'
|
log '============================================================'
|
log ''
|
log "Company : $COMPANY"
|
log "Server : $REMOTE_HOST:$REMOTE_PORT"
|
log "Remote Dir : $REMOTE_DIR"
|
log "Package : $ZIP_FILE"
|
log "Size : $LOCAL_ZIP_MB MB"
|
log ''
|
log 'Deployment completed successfully.'
|
log ''
|
|
pause_if_interactive
|