Compare commits

2 Commits

Author SHA1 Message Date
9d74bd2d51 ci(deploy): deploy.sh 支持本地直跑模式 + workflow 健康检查读取 .env PORT
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 等自定义端口
2026-08-09 22:04:21 +08:00
f4072ce93a fix(db): 迁移 CLI 默认读取仓库根目录 .env
- datasource.ts 优先加载根目录 .env,再回退 apps/server/.env
- 生产只需维护根目录一份 .env,迁移脚本不再报 root 空密码
2026-08-09 22:02:00 +08:00
3 changed files with 92 additions and 36 deletions

View File

@@ -75,7 +75,10 @@ jobs:
run: |
cd ${{ vars.REMOTE_DIR || '/opt/gongxue' }}
sleep 3
code="$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3000/api/dashboard/stats || true)"
# 从 .env 读 PORT默认 3000避免硬编码端口与后端不一致
PORT="$(grep '^PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true)"
PORT="${PORT:-3000}"
code="$(curl -s -o /dev/null -w '%{http_code}' "http://127.0.0.1:${PORT}/api/dashboard/stats" || true)"
echo "后端 HTTP 状态: ${code}401=正常,接口需登录)"
if [ "${code}" != "401" ] && [ "${code}" != "200" ]; then
echo "健康检查失败:后端未按预期响应" >&2

View File

@@ -1,9 +1,18 @@
import { DataSource } from 'typeorm';
import { config } from 'dotenv';
import { join } from 'path';
import { basename, join, resolve } from 'path';
// TypeORM CLI runs from apps/server/
const root = process.cwd();
// TypeORM CLI 默认在 apps/server/ 下运行,但生产环境的 .env 通常放在仓库根目录。
// 这里先加载仓库根目录的 .env默认再加载当前目录apps/server/.env作为补充/覆盖,
// 这样无论 .env 放哪一份,迁移 CLI 都能读到数据库配置。
const root = resolve(process.cwd());
const repoRoot =
basename(root) === 'server' && basename(join(root, '..')) === 'apps'
? resolve(root, '../..')
: root;
// 先根目录后当前目录dotenv 默认不覆盖已存在变量,所以根目录优先级更高)
config({ path: join(repoRoot, '.env') });
config({ path: join(root, '.env') });
export default new DataSource({

108
deploy.sh
View File

@@ -1,12 +1,84 @@
#!/usr/bin/env bash
# 恭学教育 — PM2 部署脚本
# 用法: ./deploy.sh [ssh_host]
# 用法:
# ./deploy.sh # 本地直跑(默认,适合 runner/代码就在生产服务器上,不需要 SSH
# ./deploy.sh local # 同上
# ./deploy.sh <ssh_host> # 远程部署(构建后 rsync 到远端,需要 SSH 免密/key
# 前提: 服务器 MySQL 已在运行PM2 已安装
set -euo pipefail
SSH_HOST="${1:-tencent}"
MODE="${1:-local}"
# 校验 SSH_HOST非空、不以 - 开头(防被 ssh/rsync 当作选项)、仅允许安全字符(防命令注入)
# 调用锁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
@@ -21,23 +93,6 @@ if [[ ! "${SSH_HOST}" =~ ^[A-Za-z0-9._:@%+=,-]+$ ]]; then
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 \
@@ -66,18 +121,11 @@ ssh -o BatchMode=yes -o ConnectTimeout=10 "${SSH_HOST}" "
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'
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
@@ -106,10 +154,6 @@ if [ "${HEALTHY}" -ne 1 ]; then
fi
echo "健康检查通过gongxue-backend 在线"
REMOTE_HEALTH
then
echo "错误:健康检查失败,部署中止" >&2
exit 1
fi
echo ""
echo "部署完成!"