From 9d74bd2d517c6785ff74612f8bccf4d0407abc09 Mon Sep 17 00:00:00 2001 From: wangziqi Date: Sun, 9 Aug 2026 22:04:21 +0800 Subject: [PATCH] =?UTF-8?q?ci(deploy):=20deploy.sh=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E7=9B=B4=E8=B7=91=E6=A8=A1=E5=BC=8F=20+=20wo?= =?UTF-8?q?rkflow=20=E5=81=A5=E5=BA=B7=E6=A3=80=E6=9F=A5=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=20.env=20PORT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - deploy.sh 默认本地直跑(./deploy.sh 或 ./deploy.sh local),不再需要 SSH;传 ssh_host 才走远程版 - workflow 健康检查从 .env 读取 PORT(默认 3000),适配 PORT=3002 等自定义端口 --- .gitea/workflows/deploy.yml | 5 +- deploy.sh | 108 +++++++++++++++++++++++++----------- 2 files changed, 80 insertions(+), 33 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 561b896..584d33f 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -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 diff --git a/deploy.sh b/deploy.sh index 1da249c..d8ac911 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,12 +1,84 @@ #!/usr/bin/env bash # 恭学教育 — PM2 部署脚本 -# 用法: ./deploy.sh [ssh_host] +# 用法: +# ./deploy.sh # 本地直跑(默认,适合 runner/代码就在生产服务器上,不需要 SSH) +# ./deploy.sh local # 同上 +# ./deploy.sh # 远程部署(构建后 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 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' +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 "部署完成!"