gaoluyang
10 小时以前 4495503c48973f7554df7606dc2aa801346d4460
宁夏英泽需求:新增 macOS 可用的前端部署脚本 YZFX.sh

YZFX.bat 只能跑在 Windows。本次按同样步骤转写一份 macOS / Linux 版本,只针对英泽
(COMPANY=YZFX,42.63.70.231:/home/vue/dist),其余客户端的 .bat 不动。

与 YZFX.bat 的差别:
- 不再依赖 sshpass.exe:有 sshpass 就用 sshpass,没有就退回 macOS 自带的
/usr/bin/expect 喂密码,本机无需安装任何依赖即可运行。
- 脚本开头 cd 到自身目录,从任何位置调用都能正确构建和上传。
- 结尾的 pause 仅在交互式终端生效,被脚本/管道调用不会卡住。
- 「比较文件大小」按该方法本来标题的原意真正做了比对(.bat 只打印不比对):
在远端执行 test "$(stat -c %s tmp)" -eq <本地字节数>,只取退出码,不做输出解析。

实现要点(这些坑已实测踩过并修掉):
- 不能用 $(ssh_remote ...) 截取远端输出:expect 会把整段交互回显(spawn 命令行、
密码提示)一并写进 stdout,截出来的不是命令结果。
- expect 程序必须写成文件用 -f 调用:`expect -c 脚本 参数...` 不会填充 $argv,
会把额外参数当成脚本文件名。
- 变量一律写成 ${VAR}:bash 3.2 会把紧跟变量的全角字符(如「(」)并入变量名,
"$ZIP_FILE(..." 会被解析成名为 ZIP_FILE( 的变量,配 set -u 直接 unbound variable。

验证方式:在 /tmp 的隔离沙箱里用模拟 ssh/scp(会真的校验密码、按远端 test 返回退出码)
跑通,覆盖 5 条分支:正常走完(exit 0)、远端大小不符、密码错误、远端缺 unzip、scp 失败
——后四条均按预期打印精确错误并 exit 1。沙箱外的真实服务器未做任何连接。

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加1个文件
462 ■■■■■ 文件已修改
YZFX.sh 462 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
YZFX.sh
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,462 @@
#!/usr/bin/env bash
# ============================================================================
#  Vue å‰ç«¯éƒ¨ç½²è„šæœ¬ï¼ˆmacOS / Linux ç‰ˆï¼‰â€”—  å®å¤è‹±æ³½é˜²é”ˆï¼ˆYZFX)
#
#  ç”±åŒç›®å½•çš„ YZFX.bat è½¬å†™è€Œæ¥ï¼šæ­¥éª¤ã€æç¤ºè¯­ã€æœåŠ¡å™¨ã€è´¦å·ã€è¿œç«¯ç›®å½•ã€
#  é‡è¯•/超时参数与 .bat å®Œå…¨ä¸€è‡´ï¼ŒWindows ä¸Šæ€Žä¹ˆç”¨ï¼Œè¿™é‡Œå°±æ€Žä¹ˆç”¨ã€‚
#
#  ä¸Ž .bat çš„全部差别:
#    1. ä¸ä¾èµ– sshpass.exe。系统里有 sshpass å°±ç”¨ sshpass,没有就退回 macOS
#       è‡ªå¸¦çš„ /usr/bin/expect æ¥å–‚密码 â€”— æ‰€ä»¥è¿™å° Mac ä¸ç”¨è£…任何东西也能跑。
#    2. è„šæœ¬å¼€å¤´ä¼š cd åˆ°è‡ªå·±æ‰€åœ¨ç›®å½•,从任何位置调用都能正确构建和上传。
#    3. ç»“尾的 pause åªåœ¨äº¤äº’式终端里生效,被其它脚本/管道调用时不会卡住。
#    4. ã€Œæ¯”较文件大小」这一步按标题的原意真正做了比对(.bat åªæ‰“印不比对),
#       å¤§å°ä¸ä¸€è‡´ç›´æŽ¥æŠ¥é”™ï¼Œé¿å…æŠŠä¸€ä¸ªä¸Šä¼ æ®‹ç¼ºçš„包解压到线上目录。
#
#  ç”¨æ³•:
#      ./YZFX.sh                 # é¦–次可能需要 chmod +x YZFX.sh
#      bash YZFX.sh
#
#  å¯é€‰çŽ¯å¢ƒå˜é‡ï¼ˆä¸€èˆ¬ç”¨ä¸åˆ°ï¼‰ï¼š
#      YZFX_TRANSPORT=sshpass|expect    å¼ºåˆ¶æŒ‡å®šå–‚密码的方式
# ============================================================================
set -uo pipefail
# =========================
# é…ç½®ï¼ˆä¸Ž YZFX.bat ä¸€å­—不差)
# =========================
readonly COMPANY='YZFX'
readonly REMOTE_USER='root'
readonly REMOTE_HOST='42.63.70.231'
readonly REMOTE_PORT='22'
readonly PASSWORD='Nxyzfxxcl1@'
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="${YZFX_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}/yzfx-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(YZFX_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
    YZFX_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
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