Files
gongxue-base/deploy.sh
wangziqi 3bcad138a1 fix(ops): 加固部署/代理/迁移脚本,生产环境安全默认
由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复:
- serve-proxy:API_TARGET 生效、SPA 404 语义、流式静态文件、hop-by-hop/超时/断连/穿越防护
- migrate.sh:密码不进 argv/不泄漏 pm2、原子锁防重入、DML-only 事务说明、就绪诊断
- deploy.sh/CI:保护 .env、总是 npm ci(迁移依赖 devDeps)、健康检查、并发锁
- ecosystem/oxlint/.env.example:优雅停机、React 版本对齐、TRUST_PROXY 说明

Reviewed-by: OCR (open-codereview.ai)
2026-08-09 21:29:22 +08:00

117 lines
3.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 恭学教育 — PM2 部署脚本
# 用法: ./deploy.sh [ssh_host]
# 前提: 服务器 MySQL 已在运行PM2 已安装
set -euo pipefail
SSH_HOST="${1:-tencent}"
# 校验 SSH_HOST非空、不以 - 开头(防被 ssh/rsync 当作选项)、仅允许安全字符(防命令注入)
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"
# 调用锁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/5 本地构建后端 ==="
npm run build -w @gongxue/server
echo "=== 2/5 本地构建前端 ==="
npm run build -w @gongxue/admin
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-pathsdevDependencies不能 --omit=dev
npm ci
echo '执行数据库迁移...'
npm run migration:run -w @gongxue/server
echo 'PM2 重载...'
pm2 startOrReload ecosystem.config.cjs --update-env
pm2 save
echo '=== PM2 状态 ==='
pm2 status
"
echo "=== 5/5 健康检查 ==="
# 后端未提供 /api/health 等专用健康端点,改用 pm2 jlistJSON按 name 查找 gongxue-backend
# 判断 status === 'online' 且 unstable_restarts === 0。
# 说明:
# - restart_time 是累计重启次数(正常滚动/长期运行也会累计),不适合做健康阈值,改用 unstable_restarts。
# - online ≠ healthyonline 只代表 PM2 认为进程存活;若后续后端提供健康端点,应优先 curl 探测。
# 带重试以覆盖 PM2 reload 的 listen_timeout8s窗口应用不在线则 exit 1部署失败。
if ! 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
then
echo "错误:健康检查失败,部署中止" >&2
exit 1
fi
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')"