# 启优学官网 · GitLab CI/CD 单目录覆盖部署
# 流程：停止应用 → 覆盖 current → 安装与构建 → 启动并检查 → 更新 Nginx
# 触发：push 到 main 分支
#
# GitLab 版本：11.7
# 仅使用 stages、only、script 等基础语法；服务器部署锁用于拒绝并发部署。

variables:
  DEPLOY_ROOT: "/root/qiyouxue"
  CURRENT_DIR: "/root/qiyouxue/current"
  SHARED_DIR: "/root/qiyouxue/shared"
  SHARED_ENV: "/root/qiyouxue/shared/.env"
  DEPLOY_LOCK: "/root/qiyouxue/shared/deploy.lock"
  APP_NAME: "qiyouxue"
  SITE_URL: "http://127.0.0.1:8793/"
  PM2_BIN: "/usr/bin/pm2"
  PM2_HOME_DIR: "/root/.pm2"
  PM2_USE_SUDO: "1"
  PUBLIC_HOST: "qiyouxueedu.com"

stages:
  - deploy

deploy:
  stage: deploy
  tags:
    - qiyouxue-prod
  only:
    - main
  script:
    - |
      set -Eeuo pipefail

      echo "[deploy] 提交：$CI_COMMIT_SHA"

      case "$DEPLOY_ROOT" in
        /*) ;;
        *) echo "DEPLOY_ROOT 必须是绝对路径" >&2; exit 2 ;;
      esac
      case "$PM2_BIN" in
        /*) ;;
        *) echo "PM2_BIN 必须是绝对路径" >&2; exit 2 ;;
      esac
      case "$PM2_HOME_DIR" in
        /*) ;;
        *) echo "PM2_HOME_DIR 必须是绝对路径" >&2; exit 2 ;;
      esac
      if [ "$CURRENT_DIR" != "$DEPLOY_ROOT/current" ] || [ "$SHARED_DIR" != "$DEPLOY_ROOT/shared" ]; then
        echo "current 或 shared 目录不属于部署根目录" >&2
        exit 2
      fi
      if [ "$SHARED_ENV" != "$SHARED_DIR/.env" ] || [ "$DEPLOY_LOCK" != "$SHARED_DIR/deploy.lock" ]; then
        echo "生产配置或部署锁路径不属于 shared 目录" >&2
        exit 2
      fi
      case "$APP_NAME" in
        *[!A-Za-z0-9_.-]*|'') echo "PM2 应用名包含不安全字符" >&2; exit 2 ;;
      esac
      case "$PM2_USE_SUDO" in
        0|1) ;;
        *) echo "PM2_USE_SUDO 只能是 0 或 1" >&2; exit 2 ;;
      esac
      test -n "$CI_COMMIT_SHA" || (echo "CI_COMMIT_SHA 缺失" >&2 && exit 2)
      test -x "$PM2_BIN" || (echo "PM2 不存在或不可执行：$PM2_BIN" >&2 && exit 2)
      command -v flock >/dev/null || (echo "服务器缺少 flock" >&2 && exit 2)
      command -v curl >/dev/null || (echo "服务器缺少 curl" >&2 && exit 2)
      command -v node >/dev/null || (echo "服务器缺少 Node.js" >&2 && exit 2)

      if [ -L "$DEPLOY_ROOT" ] || { [ -e "$DEPLOY_ROOT" ] && [ ! -d "$DEPLOY_ROOT" ]; }; then
        echo "DEPLOY_ROOT 必须是非软链接实体目录：$DEPLOY_ROOT" >&2
        exit 2
      fi
      mkdir -p "$DEPLOY_ROOT"
      if [ "$(readlink -f "$DEPLOY_ROOT")" != "$DEPLOY_ROOT" ] || [ -L "$DEPLOY_ROOT" ]; then
        echo "DEPLOY_ROOT 必须是非软链接实体目录：$DEPLOY_ROOT" >&2
        exit 2
      fi
      if [ -L "$SHARED_DIR" ] || { [ -e "$SHARED_DIR" ] && [ ! -d "$SHARED_DIR" ]; }; then
        echo "shared 必须是非软链接实体目录：$SHARED_DIR" >&2
        exit 2
      fi
      mkdir -p "$SHARED_DIR"
      if [ "$(readlink -f "$SHARED_DIR")" != "$SHARED_DIR" ]; then
        echo "shared 必须位于指定部署目录：$SHARED_DIR" >&2
        exit 2
      fi
      test -f "$SHARED_ENV" || (echo "缺少生产配置：$SHARED_ENV" >&2 && exit 2)

      # GitLab 11.7 没有 resource_group。非阻塞锁让并发 pipeline 直接失败，
      # 避免较早的 pipeline 排队后覆盖较新的提交。
      exec 9>"$DEPLOY_LOCK"
      if ! flock -n 9; then
        echo "已有部署正在进行，本次 pipeline 不进入等待队列" >&2
        exit 3
      fi

      pm2_command() {
        if [ "$PM2_USE_SUDO" = "1" ]; then
          sudo -n env PM2_HOME="$PM2_HOME_DIR" "$PM2_BIN" "$@"
        else
          env PM2_HOME="$PM2_HOME_DIR" "$PM2_BIN" "$@"
        fi
      }

      # 先停止且只停止本项目进程。单目录部署失败时不会自动回滚。
      if pm2_command describe "$APP_NAME" >/dev/null 2>&1; then
        echo "[deploy] 停止本项目 PM2 进程：$APP_NAME"
        pm2_command delete "$APP_NAME"
      fi

      # 首次迁移时只移除 current 软链接本身，不触碰它原来指向的 release。
      if [ -L "$CURRENT_DIR" ]; then
        old_current_target=$(readlink -f "$CURRENT_DIR" 2>/dev/null || true)
        echo "[deploy] 移除旧 current 软链接（原目标保留）：${old_current_target:-未知}"
        unlink "$CURRENT_DIR"
      elif [ -e "$CURRENT_DIR" ] && [ ! -d "$CURRENT_DIR" ]; then
        echo "current 存在但不是目录，拒绝覆盖：$CURRENT_DIR" >&2
        exit 2
      fi

      mkdir -p "$CURRENT_DIR"
      if [ -L "$CURRENT_DIR" ] || [ "$(readlink -f "$CURRENT_DIR")" != "$CURRENT_DIR" ]; then
        echo "current 必须是指定位置的实体目录：$CURRENT_DIR" >&2
        exit 2
      fi

      echo "[deploy] 清空 current"
      find "$CURRENT_DIR" -mindepth 1 -maxdepth 1 -exec rm -rf -- {} +

      echo "[deploy] 导出提交到 current"
      git -c safe.directory="$CI_PROJECT_DIR" -C "$CI_PROJECT_DIR" cat-file -e "$CI_COMMIT_SHA^{commit}"
      git -c safe.directory="$CI_PROJECT_DIR" -C "$CI_PROJECT_DIR" archive "$CI_COMMIT_SHA" | tar -x -C "$CURRENT_DIR"
      ln -s "$SHARED_ENV" "$CURRENT_DIR/.env"

      cd "$CURRENT_DIR"
      echo "[deploy] 安装依赖"
      npm ci

      echo "[deploy] 使用 shared 中的 CMS 数据和构建锁进行构建"
      node --env-file="$SHARED_ENV" -e '
        const path = require("node:path");
        const shared = path.resolve(process.argv[1]);
        for (const name of ["CMS_DATA_DIR", "CMS_BUILD_LOCK"]) {
          const value = process.env[name];
          if (!value) throw new Error(`${name} 未配置`);
          const resolved = path.resolve(value);
          if (resolved === shared || !resolved.startsWith(`${shared}${path.sep}`)) {
            throw new Error(`${name} 必须位于 ${shared}/ 下`);
          }
        }
      ' "$SHARED_DIR"
      node --env-file="$SHARED_ENV" scripts/run-with-site-lock.mjs -- npm run build:inside-lock

      echo "[deploy] 启动本项目 PM2 进程"
      pm2_command start "$CURRENT_DIR/server.mjs" \
        --name "$APP_NAME" \
        --cwd "$CURRENT_DIR" \
        --node-args="--env-file=$SHARED_ENV"

      healthy=0
      for attempt in $(seq 1 30); do
        if curl --fail --silent --show-error --max-time 3 "$SITE_URL" >/dev/null; then
          healthy=1
          break
        fi
        echo "[deploy] 等待服务启动... ($attempt/30)"
        sleep 1
      done
      if [ "$healthy" != "1" ]; then
        echo "健康检查失败：$SITE_URL" >&2
        pm2_command logs "$APP_NAME" --lines 30 --nostream || true
        exit 1
      fi

      echo "[deploy] 更新并验证 Nginx"
      test -f "$CURRENT_DIR/deploy/nginx/qiyouxue.conf" || (echo "缺少 Nginx 配置" >&2 && exit 2)
      sudo -n /usr/bin/install -o root -g root -m 0644 "$CURRENT_DIR/deploy/nginx/qiyouxue.conf" /etc/nginx/conf.d/qiyouxue.conf
      sudo -n /usr/sbin/nginx -t
      sudo -n /usr/bin/systemctl reload nginx
      curl --fail --silent --show-error --max-time 5 \
        --noproxy "*" \
        --resolve "$PUBLIC_HOST:443:127.0.0.1" \
        "https://$PUBLIC_HOST/" >/dev/null

      echo "[deploy] 部署完成：$CURRENT_DIR"
      echo "[deploy] 旧 releases 目录未被读取或清理；确认无用后请人工删除"
