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_HOST: gitea.fse-media.group DEPLOY_PATH: /data/opt/byw-voicehub IMAGE_NAME: byw-voicehub jobs: preflight: name: Preflight 语法与构建检查 runs-on: ubuntu-latest timeout-minutes: 15 container: image: node:22-bookworm-slim steps: - name: 检出代码 uses: actions/checkout@v4 with: fetch-depth: 1 - name: 运行时环境自检 shell: bash run: | set -euxo pipefail node -v npm -v which node npm cat /etc/os-release 2>/dev/null | head -4 || true - name: 安装依赖(npm ci) run: | set -euxo pipefail npm config set registry https://registry.npmmirror.com || true npm config set fund false npm config set audit false npm ci --legacy-peer-deps - 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 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 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: 检出代码 uses: actions/checkout@v4 with: fetch-depth: 1 - name: 计算镜像标签 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 "Image tag : ${DATE_TAG}" echo "Full image : ${IMG_FULL}" echo "Latest image : ${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 容器仓库 uses: docker/login-action@v3 with: registry: ${{ env.GITEA_HOST }} username: ${{ secrets.REGISTRY_USER || github.actor }} password: ${{ secrets.REGISTRY_TOKEN || github.token }} - name: 构建并推送镜像 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 "Image size: %.2f MB\n", $1/1024/1024}' || true docker history --no-trunc "${{ steps.meta.outputs.full }}" 2>/dev/null | head -20 || true deploy: name: 部署到生产服务器 needs: build-image 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: 校验部署所需密钥 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 仓库 -> 设置 -> 工作流 -> 密钥 页面添加" exit 1 fi echo "部署密钥配置完整" - 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 }} command_timeout: 30m script_stop: true script: | set -euxo pipefail 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 "Deploy image : ${IMAGE_NEW}" echo "Deploy dir : ${DEPLOY_DIR}" echo "Executor : $(whoami)" echo "==============================================" mkdir -p "${DEPLOY_DIR}" cd "${DEPLOY_DIR}" 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 echo "Pulling new image ${IMAGE_NEW} ..." docker pull "${IMAGE_NEW}" docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}" echo "New image pulled and tagged as latest" cat > docker-compose.override.yml <' > docker-compose.override.yml" echo " docker compose up -d" echo "================================================================="