gaoluyang
4 天以前 e4994fccfb49ebfb108ba62cbd82aea4e004f6d8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
#!/usr/bin/env bash
# ============================================================
# 宁夏万通新材 —— 前端部署脚本(macOS / Linux 版)
# 对应仓库里的 WTXC.bat,只适用于本分支(万通新材)。
#
#   ./WTXC.sh                 # 构建 + 上传 + 解压(等价于 WTXC.bat)
#   ./WTXC.sh --dry-run       # 只构建和本地检查,不连服务器、不上传
#   ./WTXC.sh --skip-build    # 不构建,直接上传当前目录已有的 dist.zip
#
# 流程与 WTXC.bat 完全一致:pnpm build(带 VITE_COMPANY=WTXC)→ 生成 dist.zip
#   → scp 上传到 <远端目录>/.dist.zip.uploading → 校验大小 → 改名
#   → unzip -t 校验 → unzip -o 解压 → 删除远端 zip
#
# 部署目标:root@42.63.71.140:/home/vue/dist
#
# 环境变量覆盖:DEPLOY_PASSWORD、DEPLOY_HOST、DEPLOY_DIR、DEPLOY_PORT
#   (改密码或临时指到别的目录时用,避免把密码写进命令行历史)
#
# 说明:WTXC.bat 走的是 bin\sshpass.exe(Windows 专用)。macOS 上没有 sshpass
#   时本脚本自动改用系统自带的 expect 送密码,因此无需额外安装;若已装
#   sshpass(brew install hudochenkov/sshpass/sshpass)则优先使用它。
# ============================================================
set -euo pipefail
 
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
 
# ------------------------------------------------------------------
# 1. 参数与目标配置(对应 WTXC.bat 顶部的 Configuration 段)
# ------------------------------------------------------------------
SKIP_BUILD=0
DRY_RUN=0
 
for arg in "$@"; do
  case "$arg" in
    --skip-build) SKIP_BUILD=1 ;;
    --dry-run) DRY_RUN=1 ;;
    -h | --help)
      # 打印文件头部的注释说明(第一行 shebang 之后到 set 之前)
      awk 'NR == 1 { next } /^set / { exit } { print }' "${BASH_SOURCE[0]}"
      exit 0
      ;;
    *)
      echo "[ERROR] 未知参数:${arg}(可用:--skip-build / --dry-run / -h)" >&2
      exit 1
      ;;
  esac
done
 
COMPANY="WTXC"
EXPECT_BRANCH="dev_宁夏_万通新材料"
REMOTE_USER="root"
REMOTE_HOST="42.63.71.140"
PORT="22"
PASSWORD="Nxwtxc123."
REMOTE_DIR="/home/vue/dist"
ZIP_FILE="dist.zip"
 
# 允许用环境变量覆盖
REMOTE_HOST="${DEPLOY_HOST:-$REMOTE_HOST}"
REMOTE_DIR="${DEPLOY_DIR:-$REMOTE_DIR}"
PASSWORD="${DEPLOY_PASSWORD:-$PASSWORD}"
PORT="${DEPLOY_PORT:-$PORT}"
 
SSH_OPTS=(-o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 -o NumberOfPasswordPrompts=1)
 
# ------------------------------------------------------------------
# 2. 前置检查:只能在本客户分支上构建部署
#    包里写死了万通的名称/Logo/接口地址,用别的分支构建会把别客户的前端传上来。
# ------------------------------------------------------------------
CURRENT_BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '')"
if [ -n "$CURRENT_BRANCH" ] && [ "$CURRENT_BRANCH" != "$EXPECT_BRANCH" ]; then
  if [ "${ALLOW_BRANCH_MISMATCH:-0}" != "1" ]; then
    echo "[ERROR] 当前分支是 ${CURRENT_BRANCH},不是万通分支 ${EXPECT_BRANCH}。" >&2
    echo "        本脚本会把构建结果部署到万通生产服务器,请先切回万通分支:" >&2
    echo "        git checkout ${EXPECT_BRANCH}" >&2
    echo "        确实要在该分支上部署时,可加 ALLOW_BRANCH_MISMATCH=1 跳过此校验。" >&2
    exit 1
  fi
  echo "[WARN] 分支不是 ${EXPECT_BRANCH}(当前 ${CURRENT_BRANCH}),按 ALLOW_BRANCH_MISMATCH=1 继续。" >&2
fi
 
echo
echo "============================================================"
echo "         Vue Dist Auto Deploy (万通新材 / macOS)"
echo "============================================================"
echo
echo "Company    : $COMPANY"
echo "Server     : $REMOTE_USER@$REMOTE_HOST:$PORT"
echo "Remote Dir : $REMOTE_DIR"
echo "Package    : $ZIP_FILE"
if [ -n "$(git status --porcelain 2>/dev/null || true)" ]; then
  echo "Git        : ⚠ 工作区有未提交改动(部署的是工作区内容,不是提交内容)"
else
  echo "Git        : ${CURRENT_BRANCH} @ $(git rev-parse --short HEAD 2>/dev/null || echo '-')"
fi
echo
 
# ------------------------------------------------------------------
# 3. 依赖检查(对应 WTXC.bat 的 [1/7]~[3/7])
# ------------------------------------------------------------------
echo "[1/4] 检查 pnpm / node ..."
command -v pnpm >/dev/null 2>&1 || {
  echo "[ERROR] 找不到 pnpm,请先 npm install -g pnpm" >&2
  exit 1
}
command -v node >/dev/null 2>&1 || {
  echo "[ERROR] 找不到 node" >&2
  exit 1
}
echo "      pnpm $(pnpm --version) / node $(node --version)"
 
if [ ! -d node_modules ]; then
  echo "[INFO] node_modules 不存在,执行 pnpm install ..."
  pnpm install
else
  echo "      node_modules 已存在"
fi
 
# ------------------------------------------------------------------
# 4. 构建(对应 WTXC.bat 的 [4/7])
#    注意:archiver 插件是在 closeBundle 里异步写 dist.zip,
#    所以构建完要等它落地;同时先删掉旧 zip,避免把旧包当新包传上去。
#    默认每次重新构建(WTXC.bat 也是如此),只有显式 --skip-build 才复用旧包。
# ------------------------------------------------------------------
if [ "$SKIP_BUILD" -eq 0 ]; then
  echo
  echo "[2/4] 构建中(VITE_COMPANY=${COMPANY},pnpm run build)..."
  rm -f "$ZIP_FILE"
  VITE_COMPANY="$COMPANY" pnpm run build
  for _ in $(seq 1 30); do
    [ -f "$ZIP_FILE" ] && break
    sleep 1
  done
else
  echo
  echo "[2/4] 跳过构建(--skip-build),复用已有的 ${ZIP_FILE}"
  echo "[WARN] 请确认该包就是用 VITE_COMPANY=${COMPANY} 构建的:" >&2
  echo "       包里已写死客户的名称/Logo/接口地址,用错包会把别客户的前端传到本服务器。" >&2
fi
 
if [ ! -f "$ZIP_FILE" ]; then
  echo "[ERROR] 构建结束但没有生成 $ZIP_FILE" >&2
  exit 1
fi
 
ZIP_BYTES="$(wc -c < "$ZIP_FILE" | tr -d ' ')"
echo "[OK] 打包完成:${ZIP_FILE}(${ZIP_BYTES} 字节)"
 
if [ "$DRY_RUN" -eq 1 ]; then
  echo "[INFO] --dry-run:本地检查通过,未连服务器、未上传。"
  exit 0
fi
 
# ------------------------------------------------------------------
# 5. 远端操作(对应 WTXC.bat 的 [6/7][7/7])
# ------------------------------------------------------------------
EXPECT_HELPER="$(mktemp -t wtxc-sshpass)"
cleanup() { rm -f "$EXPECT_HELPER"; }
trap cleanup EXIT
 
cat > "$EXPECT_HELPER" <<'EXPECT'
#!/usr/bin/expect -f
# 用法: sshpass-helper <password> <command...>
set timeout 3600
set pw  [lindex $argv 0]
set cmd [lrange $argv 1 end]
eval spawn -noecho $cmd
expect {
  -re "(?i)are you sure you want to continue connecting" { send "yes\r"; exp_continue }
  -re "(?i)password:"                                   { send "$pw\r"; exp_continue }
  -re "(?i)permission denied"                           { exit 5 }
  # 超时必须显式退出,否则 expect 会挂在后面的 wait 上不返回
  timeout                                               { puts stderr "SSH 超时"; exit 9 }
  eof
}
catch wait result
exit [lindex $result 3]
EXPECT
chmod +x "$EXPECT_HELPER"
 
HAS_SSHPASS=0
command -v sshpass >/dev/null 2>&1 && HAS_SSHPASS=1
if [ "$HAS_SSHPASS" -eq 0 ] && ! command -v expect >/dev/null 2>&1; then
  echo "[ERROR] 既没有 sshpass 也没有 expect,无法在脚本里自动送密码。" >&2
  echo "        可执行:brew install hudochenkov/sshpass/sshpass,或手工 scp 上传。" >&2
  exit 1
fi
 
# 在远端执行命令,返回其退出码;stdout 由调用方捕获(失败时输出打到 stderr)
remote_run() {
  local out rc=0
  if [ "$HAS_SSHPASS" -eq 1 ]; then
    out="$(SSHPASS="$PASSWORD" sshpass -e ssh -p "$PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$1" 2>&1)" || rc=$?
  else
    out="$("$EXPECT_HELPER" "$PASSWORD" ssh -p "$PORT" "${SSH_OPTS[@]}" "$REMOTE_USER@$REMOTE_HOST" "$1" 2>&1)" || rc=$?
  fi
  if [ "$rc" -ne 0 ]; then
    echo "[ERROR] 远端命令执行失败(exit $rc):$1" >&2
    [ -n "$out" ] && printf '%s\n' "$out" >&2
  fi
  printf '%s' "$out"
  return "$rc"
}
 
# 上传一个文件:remote_copy <本地文件> <远端绝对路径>(保留进度条)
remote_copy() {
  if [ "$HAS_SSHPASS" -eq 1 ]; then
    SSHPASS="$PASSWORD" sshpass -e scp -P "$PORT" "${SSH_OPTS[@]}" "$1" "$REMOTE_USER@$REMOTE_HOST:$2"
  else
    "$EXPECT_HELPER" "$PASSWORD" scp -P "$PORT" "${SSH_OPTS[@]}" "$1" "$REMOTE_USER@$REMOTE_HOST:$2"
  fi
}
 
echo
echo "[3/4] 连接服务器 ..."
remote_run "echo SSH_OK" >/dev/null
echo "[OK] SSH 连接成功"
 
remote_run "mkdir -p '$REMOTE_DIR'" >/dev/null
echo "[OK] 远端目录就绪"
 
REMOTE_TMP="$REMOTE_DIR/.dist.zip.uploading"
echo
echo "[4/4] 上传并解压 ..."
remote_run "rm -f '$REMOTE_TMP'" >/dev/null
remote_copy "$ZIP_FILE" "$REMOTE_TMP"
echo "[OK] 上传完成"
 
# 远端命令的输出里会混有 expect 回显的密码提示,命令本身的 stdout 总在最后,
# 取最后一个字段即为所需数值(stat 的 stderr 也会走到这里,故再做一次空值判断)。
REMOTE_BYTES="$(remote_run "stat -c %s '$REMOTE_TMP'" | tr -d '\r' | awk 'END { print $NF }')"
REMOTE_BYTES="$(printf '%s' "$REMOTE_BYTES" | tr -cd '0-9')"
if [ -z "$REMOTE_BYTES" ] || [ "$REMOTE_BYTES" != "$ZIP_BYTES" ]; then
  echo "[ERROR] 远端文件大小($REMOTE_BYTES)与本地($ZIP_BYTES)不一致,已中止;远端临时文件保留在 $REMOTE_TMP" >&2
  exit 1
fi
echo "[OK] 大小校验一致($REMOTE_BYTES 字节)"
 
remote_run "mv -f '$REMOTE_TMP' '$REMOTE_DIR/$ZIP_FILE'" >/dev/null
echo "[OK] 包已就位"
 
remote_run "command -v unzip" >/dev/null || {
  echo "[ERROR] 远端没有 unzip:CentOS 用 yum install -y unzip / Ubuntu 用 apt install -y unzip" >&2
  exit 1
}
 
remote_run "cd '$REMOTE_DIR' && unzip -tq '$ZIP_FILE'" >/dev/null
echo "[OK] 远端 zip 校验通过"
 
remote_run "cd '$REMOTE_DIR' && unzip -oq '$ZIP_FILE'" >/dev/null
echo "[OK] 解压完成"
 
remote_run "rm -f '$REMOTE_DIR/$ZIP_FILE'" >/dev/null || echo "[WARN] 远端 zip 删除失败(不影响使用)"
 
echo
echo "============================================================"
echo "                 DEPLOY SUCCESS"
echo "============================================================"
echo "Company    : $COMPANY"
echo "Server     : $REMOTE_HOST:$PORT"
echo "Remote Dir : $REMOTE_DIR"
echo "Package    : ${ZIP_FILE}(${ZIP_BYTES} 字节)"
echo "提示:前端是静态资源,浏览器 Ctrl/Cmd+Shift+R 强刷后再验证。"
echo