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)
This commit is contained in:
92
deploy.sh
92
deploy.sh
@@ -5,40 +5,112 @@
|
||||
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"
|
||||
|
||||
echo "=== 1/4 本地构建后端 ==="
|
||||
# 调用锁: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/4 本地构建前端 ==="
|
||||
echo "=== 2/5 本地构建前端 ==="
|
||||
npm run build -w @gongxue/admin
|
||||
|
||||
echo "=== 3/4 同步到 ${SSH_HOST} ==="
|
||||
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/4 安装依赖 → 迁移 → PM2 重载 ==="
|
||||
ssh "${SSH_HOST}" "
|
||||
echo "=== 4/5 安装依赖 → 迁移 → PM2 重载 ==="
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" "
|
||||
set -e
|
||||
cd ${REMOTE_DIR}
|
||||
mkdir -p logs
|
||||
if [ ! -d node_modules ]; then
|
||||
echo '首次部署,安装依赖...'
|
||||
npm ci --omit=dev
|
||||
fi
|
||||
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
|
||||
echo '=== PM2 状态 ==='
|
||||
pm2 status
|
||||
"
|
||||
|
||||
echo "=== 5/5 健康检查 ==="
|
||||
# 后端未提供 /api/health 等专用健康端点,改用 pm2 jlist(JSON)按 name 查找 gongxue-backend:
|
||||
# 判断 status === 'online' 且 unstable_restarts === 0。
|
||||
# 说明:
|
||||
# - restart_time 是累计重启次数(正常滚动/长期运行也会累计),不适合做健康阈值,改用 unstable_restarts。
|
||||
# - online ≠ healthy:online 只代表 PM2 认为进程存活;若后续后端提供健康端点,应优先 curl 探测。
|
||||
# 带重试以覆盖 PM2 reload 的 listen_timeout(8s)窗口;应用不在线则 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 "${SSH_HOST}" 'hostname -I 2>/dev/null | awk "{print \$1}" || curl -s ifconfig.me')"
|
||||
echo "访问: http://$(ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" 'hostname -I 2>/dev/null | awk "{print \$1}" || curl -s ifconfig.me')"
|
||||
|
||||
Reference in New Issue
Block a user