name: 构建并部署 VoiceHub run-name: ${{ gitea.actor }} 触发部署 #${{{{ gitea.run_number }}}} on: push: branches: - main - master # 允许在 Gitea 页面手动点「运行工作流」 workflow_dispatch: inputs: deploy_target: description: 是否在构建后自动部署到服务器 type: boolean default: true required: false # 同一时刻只跑一个部署,避免部署竞态 concurrency: group: deploy-production cancel-in-progress: false env: # ====== 按需修改(改成你的 Gitea 域名,不要 http:// 前缀) ====== GITEA_REGISTRY: gitea.example.com # ====== 一般不改 ====== IMAGE_NAME: byw-voicehub DEPLOY_PATH: /opt/byw-voicehub NODE_VERSION: "22" jobs: # ============================================================ # Job 1: 构建 & 推送 Docker 镜像到 Gitea 自带容器仓库 # ============================================================ build-image: name: 构建 Docker 镜像 runs-on: ubuntu-latest outputs: image_tag: ${{ steps.meta.outputs.tag }} image_full: ${{ steps.meta.outputs.full }} steps: - name: 📥 拉取代码 uses: actions/checkout@v4 with: fetch-depth: 1 - name: 🔧 设置 Node.js uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} registry-url: https://registry.npmmirror.com # 国内加速,不需要可删 - name: 🧪 环境校验 run: | set -euxo pipefail node -v npm -v npm ci --no-audit --no-fund - name: 🔐 登录 Gitea 容器仓库 uses: docker/login-action@v3 with: registry: ${{ env.GITEA_REGISTRY }} username: ${{ gitea.repository_owner }} password: ${{ secrets.REGISTRY_TOKEN || gitea.token }} - name: 🏷️ 生成镜像标签 id: meta run: | set -euo pipefail SHORT_SHA="${GITEA_SHA:0:7}" DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}" FULL="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:${DATE_TAG}" LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest" echo "tag=${DATE_TAG}" >> "$GITEA_OUTPUT" echo "full=${FULL}" >> "$GITEA_OUTPUT" echo "latest=${LATEST}" >> "$GITEA_OUTPUT" echo "✅ 镜像 tag: ${DATE_TAG}" echo "✅ 镜像完整: ${FULL}" - name: 🐳 构建并推送镜像(BuildKit 多阶段 + 缓存) uses: docker/build-push-action@v6 with: context: . file: ./Dockerfile push: true tags: | ${{ steps.meta.outputs.full }} ${{ steps.meta.outputs.latest }} cache-from: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache cache-to: type=registry,ref=${{ env.GITEA_REGISTRY }}/${{ gitea.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache,mode=max build-args: | BUILDKIT_INLINE_CACHE=1 labels: | org.opencontainers.image.source=${{ gitea.server_url }}/${{ gitea.repository }} org.opencontainers.image.revision=${{ gitea.sha }} # ============================================================ # Job 2: SSH 到服务器拉取新镜像并重启 # ============================================================ deploy: name: 部署到服务器 needs: build-image if: ${{ !cancelled() && !failure() && (inputs.deploy_target == true || github.event_name == 'push') }} runs-on: ubuntu-latest environment: production steps: - name: 🌐 SSH 部署到生产服务器 uses: https://github.com/appleboy/ssh-action@v1.2.0 with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} port: ${{ secrets.DEPLOY_PORT || 22 }} script_stop: true command_timeout: 30m script: | set -euxo pipefail # 1) 首次部署时自动创建目录 & 空的 docker-compose.yml/.env mkdir -p "${DEPLOY_PATH}" cd "${DEPLOY_PATH}" # 2) 登录 Gitea 镜像仓库(用一个仅 pull 权限的 token 更安全) echo "${{ secrets.REGISTRY_PULL_TOKEN || gitea.token }}" \ | docker login "${GITEA_REGISTRY}" -u "${{ secrets.REGISTRY_PULL_USER || gitea.repository_owner }}" --password-stdin 2>/dev/null || true # 3) 拉最新镜像 IMAGE_LATEST="${GITEA_REGISTRY}/${GITEA_REPO_OWNER}/${IMAGE_NAME}:latest" docker pull "$IMAGE_LATEST" # 4) 写一个最小的 docker-compose.override.yml,指向刚拉的镜像 # (避免改动服务器上已配置好的 docker-compose.yml 和 .env) cat > docker-compose.override.yml <<'YAML' services: voicehub: image: __REPLACE_WITH_IMAGE__ YAML sed -i "s|__REPLACE_WITH_IMAGE__|${IMAGE_LATEST}|g" docker-compose.override.yml # 5) 启动(如果没有 docker-compose.yml 就自动从你的仓库默认模板里下载一份) if [ ! -f docker-compose.yml ]; then echo "ℹ️ 首次部署:写入默认 docker-compose.yml 模板(请稍后在服务器上编辑 .env)" curl -fsSL "${{ gitea.server_url }}/${{ gitea.repository }}/raw/branch/main/docker-compose.yml" \ -o docker-compose.yml || echo "下载失败,跳过(你需要手动上传 docker-compose.yml)" fi # 6) 健康拉取 + 滚动重启(先拉好再 up,downtime 最小) docker compose pull --quiet voicehub 2>/dev/null || true docker compose up -d --remove-orphans # 7) 等健康检查通过,最多等 120s for i in $(seq 1 24); do if docker compose ps --format json 2>/dev/null | grep -q '"Health":"healthy"'; then echo "✅ 容器健康" break fi sleep 5 done # 8) 清理旧镜像(只留最近 2 个 tag,省磁盘) docker image prune -af --filter "until=72h" || true echo "🎉 部署完成!镜像=${{ needs.build-image.outputs.image_full }}"