Some checks failed
CI / check (pull_request) Failing after 5m3s
- deploy.sh 默认本地直跑(./deploy.sh 或 ./deploy.sh local),不再需要 SSH;传 ssh_host 才走远程版 - workflow 健康检查从 .env 读取 PORT(默认 3000),适配 PORT=3002 等自定义端口
161 lines
5.0 KiB
Bash
Executable File
161 lines
5.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 恭学教育 — PM2 部署脚本
|
||
# 用法:
|
||
# ./deploy.sh # 本地直跑(默认,适合 runner/代码就在生产服务器上,不需要 SSH)
|
||
# ./deploy.sh local # 同上
|
||
# ./deploy.sh <ssh_host> # 远程部署(构建后 rsync 到远端,需要 SSH 免密/key)
|
||
# 前提: 服务器 MySQL 已在运行,PM2 已安装
|
||
set -euo pipefail
|
||
|
||
MODE="${1:-local}"
|
||
|
||
# 调用锁:mkdir 原子创建,已存在即代表已有部署在跑,直接退出
|
||
LOCK_DIR="/tmp/gongxue-deploy.lock"
|
||
if ! mkdir "$LOCK_DIR" 2>/dev/null; then
|
||
echo "检测到 $LOCK_DIR,已有部署在运行。若确认没有其他部署,请手动删除该锁目录。" >&2
|
||
exit 1
|
||
fi
|
||
release_deploy_lock() {
|
||
rmdir "$LOCK_DIR" 2>/dev/null || true
|
||
}
|
||
trap release_deploy_lock EXIT
|
||
|
||
echo "=== 1/4 本地构建后端 ==="
|
||
npm run build -w @gongxue/server
|
||
|
||
echo "=== 2/4 本地构建前端 ==="
|
||
npm run build -w @gongxue/admin
|
||
|
||
# ── 健康检查(本地/远程共用逻辑,通过 pm2 jlist 按 name 判断 online 且无 unstable_restarts)──
|
||
# online ≠ healthy:若后续后端提供健康端点,应优先 curl 探测。
|
||
run_health_check() {
|
||
set -euo pipefail
|
||
local healthy=0
|
||
local i
|
||
for i in $(seq 1 15); do
|
||
if pm2 jlist 2>/dev/null | node -e '
|
||
let data = "";
|
||
process.stdin.on("data", (c) => (data += c));
|
||
process.stdin.on("end", () => {
|
||
const apps = JSON.parse(data || "[]");
|
||
const app = apps.find((a) => a && a.name === "gongxue-backend");
|
||
const env = (app || {}).pm2_env || {};
|
||
if (env.status === "online" && Number(env.unstable_restarts || 0) === 0) {
|
||
process.exit(0);
|
||
}
|
||
process.exit(1);
|
||
});
|
||
'; then
|
||
healthy=1
|
||
break
|
||
fi
|
||
sleep 2
|
||
done
|
||
if [ "${healthy}" -ne 1 ]; then
|
||
echo "错误:后端进程未就绪(gongxue-backend 非 online 或存在不稳定重启)" >&2
|
||
pm2 status >&2
|
||
return 1
|
||
fi
|
||
echo "健康检查通过:gongxue-backend 在线"
|
||
}
|
||
|
||
if [ "${MODE}" = "local" ]; then
|
||
echo "=== 3/4 安装依赖 → 迁移 → PM2 重载(本机) ==="
|
||
npm ci
|
||
echo '执行数据库迁移...'
|
||
npm run migration:run -w @gongxue/server
|
||
echo 'PM2 重载...'
|
||
pm2 startOrReload ecosystem.config.cjs --update-env
|
||
pm2 save
|
||
pm2 status
|
||
|
||
echo "=== 4/4 健康检查 ==="
|
||
run_health_check
|
||
echo ""
|
||
echo "部署完成!后端: http://127.0.0.1:$(grep '^PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || echo 3000)/api"
|
||
exit 0
|
||
fi
|
||
|
||
# ── 远程模式(保留,用于部署到其他主机)──
|
||
SSH_HOST="${MODE}"
|
||
# 校验 SSH_HOST:非空、不以 - 开头、仅允许安全字符(防注入)
|
||
if [[ -z "${SSH_HOST}" ]]; then
|
||
echo "错误:SSH_HOST 不能为空" >&2
|
||
exit 1
|
||
fi
|
||
if [[ "${SSH_HOST}" == -* ]]; then
|
||
echo "错误:SSH_HOST 不能以 - 开头(会被 ssh/rsync 当作命令行选项)" >&2
|
||
exit 1
|
||
fi
|
||
if [[ ! "${SSH_HOST}" =~ ^[A-Za-z0-9._:@%+=,-]+$ ]]; then
|
||
echo "错误:SSH_HOST 含空白或 shell 元字符(仅允许字母、数字及 . _ : @ % + = , -)" >&2
|
||
exit 1
|
||
fi
|
||
REMOTE_DIR="/opt/gongxue"
|
||
|
||
echo "=== 3/5 同步到 ${SSH_HOST} ==="
|
||
# --delete 保留,但必须排除服务器端独有文件(.env、数据、上传目录),防止被清掉
|
||
rsync -avz --delete \
|
||
-e "ssh -o BatchMode=yes -o ConnectTimeout=10" \
|
||
--exclude='node_modules' \
|
||
--exclude='.git' \
|
||
--exclude='*.db' \
|
||
--exclude='.DS_Store' \
|
||
--exclude='logs/' \
|
||
--exclude='.turbo/' \
|
||
--exclude='.env*' \
|
||
--exclude='data.sql' \
|
||
--exclude='uploads/' \
|
||
./ "${SSH_HOST}:${REMOTE_DIR}/"
|
||
|
||
echo "=== 4/5 安装依赖 → 迁移 → PM2 重载 ==="
|
||
ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" "
|
||
set -e
|
||
cd ${REMOTE_DIR}
|
||
mkdir -p logs
|
||
echo '安装依赖...'
|
||
# 注意:migration:run 依赖 ts-node/tsconfig-paths(devDependencies),不能 --omit=dev
|
||
npm ci
|
||
echo '执行数据库迁移...'
|
||
npm run migration:run -w @gongxue/server
|
||
echo 'PM2 重载...'
|
||
pm2 startOrReload ecosystem.config.cjs --update-env
|
||
pm2 save
|
||
pm2 status
|
||
"
|
||
|
||
echo "=== 5/5 健康检查 ==="
|
||
ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" bash -s <<'REMOTE_HEALTH'
|
||
set -euo pipefail
|
||
HEALTHY=0
|
||
for _i in $(seq 1 15); do
|
||
if pm2 jlist 2>/dev/null | node -e '
|
||
let data = "";
|
||
process.stdin.on("data", (c) => (data += c));
|
||
process.stdin.on("end", () => {
|
||
const apps = JSON.parse(data || "[]");
|
||
const app = apps.find((a) => a && a.name === "gongxue-backend");
|
||
const env = (app || {}).pm2_env || {};
|
||
if (env.status === "online" && Number(env.unstable_restarts || 0) === 0) {
|
||
process.exit(0);
|
||
}
|
||
process.exit(1);
|
||
});
|
||
'; then
|
||
HEALTHY=1
|
||
break
|
||
fi
|
||
sleep 2
|
||
done
|
||
if [ "${HEALTHY}" -ne 1 ]; then
|
||
echo "错误:后端进程未就绪(gongxue-backend 非 online 或存在不稳定重启)" >&2
|
||
pm2 status >&2
|
||
exit 1
|
||
fi
|
||
echo "健康检查通过:gongxue-backend 在线"
|
||
REMOTE_HEALTH
|
||
|
||
echo ""
|
||
echo "部署完成!"
|
||
echo "访问: http://$(ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" 'hostname -I 2>/dev/null | awk "{print \$1}" || curl -s ifconfig.me')"
|