name: 构建 & 部署 BYW VoiceHub run-name: ${{ github.actor }} 触发部署 · Run ${{ github.run_number }} on: push: branches: [main, master] workflow_dispatch: inputs: deploy_target: description: 构建完成后是否自动部署到服务器 type: boolean default: true required: false skip_build_cache: description: 跳过 BuildKit 分层缓存,强制重新构建(排错用) type: boolean default: false required: false # 同一时刻只允许一个部署在跑,避免冲突;如果新提交进来,取消前一个未完成的 concurrency: group: deploy-production cancel-in-progress: true env: # ============================================================ # 👇👇👇 这里改成你自己的 Gitea 域名(不要 https:// 前缀,不要端口默认 443/80) # 例:git.baoyiwai.com 或 gitea.example.com:3000 GITEA_HOST: gitea.example.com # 👆👆👆 # ============================================================ IMAGE_NAME: byw-voicehub DEPLOY_PATH: /opt/byw-voicehub jobs: # ============================================================ # Job 1: 校验构建环境 & 打包产物(npm ci + build,不依赖 Docker) # ============================================================ preflight: name: Preflight 语法 & 构建检查 runs-on: ubuntu-latest timeout-minutes: 15 steps: - name: 📥 Checkout 代码 uses: actions/checkout@v4 with: fetch-depth: 1 - name: 🔧 安装 Node.js 22 uses: actions/setup-node@v4 with: node-version: "22" cache: "npm" - name: 🧪 安装依赖(npm ci,严格按锁文件) run: | set -euxo pipefail # 国内加速镜像(Runner 在国外可以删掉这两行) npm config set registry https://registry.npmmirror.com || true npm config set fund false npm config set audit false npm ci - name: 🔎 检查 package.json 没有 pnpm 残留 run: | set -euo pipefail if grep -qE '"packageManager"\s*:\s*"pnpm' package.json; then echo "❌ package.json 里有 pnpm 包管理器声明,和用户要求冲突" exit 1 fi echo "✅ package.json 未发现 pnpm 声明" - name: 🏗️ 本地 nuxt build(独立于 Docker 先验证代码是可构建的) run: | set -euxo pipefail NODE_OPTIONS="--max-old-space-size=2048" npm run build echo "✅ .output/ 生成成功:" ls -la .output/ 2>/dev/null || true du -sh .output 2>/dev/null || true # ============================================================ # Job 2: 构建 Docker 镜像 → 推送到 Gitea 自带容器仓库 # ============================================================ build-image: name: 构建并推送镜像 needs: preflight runs-on: ubuntu-latest timeout-minutes: 40 outputs: image_tag: ${{ steps.meta.outputs.tag }} image_full: ${{ steps.meta.outputs.full }} image_latest: ${{ steps.meta.outputs.latest }} steps: - name: 📥 Checkout 代码 uses: actions/checkout@v4 with: fetch-depth: 1 - name: 🏷️ 计算镜像 tag(日期 + 短 SHA) id: meta shell: bash run: | set -euo pipefail SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}" SHORT_SHA="${SHA:0:7}" DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}" IMG_FULL="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:${DATE_TAG}" IMG_LATEST="${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:latest" echo "tag=${DATE_TAG}" >> "$GITHUB_OUTPUT" echo "full=${IMG_FULL}" >> "$GITHUB_OUTPUT" echo "latest=${IMG_LATEST}" >> "$GITHUB_OUTPUT" echo "======================================" echo "🪪 镜像版本 tag : ${DATE_TAG}" echo "🪪 镜像完整名 : ${IMG_FULL}" echo "🪪 镜像 latest : ${IMG_LATEST}" echo "======================================" - name: 🔧 启用 QEMU(跨架构小概率用得到,保险) uses: docker/setup-qemu-action@v3 with: platforms: linux/amd64 - name: 🔧 启用 Docker Buildx uses: docker/setup-buildx-action@v3 with: install: true - name: 🔐 登录 Gitea 容器仓库(Packages) uses: docker/login-action@v3 with: registry: ${{ env.GITEA_HOST }} # Secrets 里 REGISTRY_USER 没填就默认用触发构建的用户 username: ${{ secrets.REGISTRY_USER || github.actor }} # Secrets 里 REGISTRY_TOKEN 优先,没填就用 Gitea Actions 自动注入的仓库级 token password: ${{ secrets.REGISTRY_TOKEN || github.token }} - name: 🐳 构建 & 推送(BuildKit 分层缓存,skip_build_cache=true 时禁用) id: build_push uses: docker/build-push-action@v6 env: CACHE_REF: ${{ env.GITEA_HOST }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache with: context: . file: ./Dockerfile platforms: linux/amd64 push: true provenance: false tags: | ${{ steps.meta.outputs.full }} ${{ steps.meta.outputs.latest }} build-args: | BUILDKIT_INLINE_CACHE=1 NODE_ENV=production cache-from: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0}', env.CACHE_REF) }} cache-to: ${{ inputs.skip_build_cache == true && '""' || format('type=registry,ref={0},mode=max', env.CACHE_REF) }} labels: | org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }} org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.created=${{ github.event.head_commit.timestamp }} - name: 🧮 镜像体积 & 层信息(日志里看一下,避免镜像越来越大) run: | set -euxo pipefail docker pull "${{ steps.meta.outputs.full }}" || true docker image inspect "${{ steps.meta.outputs.full }}" --format='{{.Size}}' 2>/dev/null \ | awk '{printf "✅ 镜像体积:%.2f MB\n", $1/1024/1024}' || true docker history --no-trunc "${{ steps.meta.outputs.full }}" 2>/dev/null | head -20 || true # ============================================================ # Job 3: SSH 到服务器 → 拉新镜像 → 滚动重启 # ============================================================ deploy: name: 部署到生产服务器 needs: build-image # push 事件默认自动部署;手动触发时看 checkbox if: ${{ !cancelled() && !failure() && (github.event_name == 'push' || inputs.deploy_target == true) }} runs-on: ubuntu-latest timeout-minutes: 25 environment: name: production url: ${{ secrets.NUXT_PUBLIC_SITE_URL }} steps: - name: ⚠️ 检查部署所需 Secrets 是否都配置了 shell: bash run: | set -euo pipefail MISSING=() [ -n "${{ secrets.DEPLOY_HOST }}" ] || MISSING+=("DEPLOY_HOST") [ -n "${{ secrets.DEPLOY_USER }}" ] || MISSING+=("DEPLOY_USER") [ -n "${{ secrets.DEPLOY_SSH_KEY }}" ] || MISSING+=("DEPLOY_SSH_KEY") if [ ${#MISSING[@]} -gt 0 ]; then echo "❌ 缺少以下 Repository Secrets:${MISSING[*]}" echo "👉 去 Gitea 仓库 → Settings → Secrets and variables → Actions 里添加" exit 1 fi echo "✅ 部署 Secrets 配置完整" - name: 🚀 一键滚动部署(SSH) uses: appleboy/ssh-action@v1.2.0 with: host: ${{ secrets.DEPLOY_HOST }} port: ${{ secrets.DEPLOY_PORT || 22 }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} # 部署可能慢,给足 30 分钟 command_timeout: 30m # 出错立刻停,不继续往下跑 script_stop: true script: | set -euxo pipefail # ---------- 0. 基本参数 ---------- IMAGE_NEW="${{ needs.build-image.outputs.image_full }}" IMAGE_LATEST="${{ needs.build-image.outputs.image_latest }}" DEPLOY_DIR="${DEPLOY_PATH}" COMPOSE="docker compose" echo "==============================================" echo "🚢 本次部署镜像: ${IMAGE_NEW}" echo "🗂️ 部署目录 : ${DEPLOY_DIR}" echo "👤 执行用户 : $(whoami)" echo "==============================================" # ---------- 1. 确保部署目录存在 ---------- mkdir -p "${DEPLOY_DIR}" cd "${DEPLOY_DIR}" # ---------- 2. 登录 Gitea 容器仓库(用 pull token,最小权限) ---------- if [ -n "${{ secrets.REGISTRY_PULL_TOKEN }}" ] && [ -n "${{ secrets.REGISTRY_PULL_USER }}" ]; then echo "🔐 使用专用 pull 凭证登录 Gitea Container Registry" echo "${{ secrets.REGISTRY_PULL_TOKEN }}" \ | docker login "${GITEA_HOST}" \ -u "${{ secrets.REGISTRY_PULL_USER }}" \ --password-stdin 2>/dev/null || true fi # ---------- 3. 先拉镜像(提前下好,后面 up 不会卡) ---------- echo "⬇️ 开始拉取新镜像 ${IMAGE_NEW} ..." docker pull "${IMAGE_NEW}" docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}" echo "✅ 新镜像拉取完成,并已打 latest tag" # ---------- 4. 生成 / 更新 docker-compose.override.yml ---------- # 指向新的精确 tag(不是 latest,出问题能快速回滚) cat > docker-compose.override.yml <' > docker-compose.override.yml" echo " docker compose up -d" echo "================================================================="