Files
BYW-VoiceHub/.gitea/workflows/deploy.yml
T
Henry_Du 082b5c854c
构建 & 部署 BYW VoiceHub / 构建并推送镜像 (push) Canceled after 0s
构建 & 部署 BYW VoiceHub / 部署到生产服务器 (push) Canceled after 0s
构建 & 部署 BYW VoiceHub / Preflight 语法 & 构建检查 (push) Canceled after 3m44s
fix(ci): deploy.yml 改为用户真实 GITEA_HOST 和 DEPLOY_PATH(/data/opt/byw-voicehub)
2026-09-06 10:21:48 +08:00

323 lines
14 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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:
# ============================================================
# ⚠️ 用户真实配置(部署 BYW VoiceHub 专用,不要改回 example)
# ============================================================
# Gitea 实例域名(不含 https://,如果是非标准端口要加 :端口号)
GITEA_HOST: gitea.fse-media.group
# 生产服务器 B 上的实际部署目录(之前 /opt/byw-voicehub 已废弃,以用户实际为准)
DEPLOY_PATH: /data/opt/byw-voicehub
# ============================================================
IMAGE_NAME: 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 <<YAML
services:
voicehub:
image: ${IMAGE_NEW}
YAML
echo "📝 docker-compose.override.yml 已写入,指定镜像=${IMAGE_NEW}"
# ---------- 5. 没有 docker-compose.yml? 自动从仓库下一份模板 ----------
if [ ! -f docker-compose.yml ]; then
echo "ℹ️ 未发现 docker-compose.yml,自动从仓库模板下载"
TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY:-${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
echo " ↓ URL=${TEMPLATE_URL}"
curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
|| (
echo "⚠️ 自动下载模板失败,跳过(你需要手动上传 docker-compose.yml 和 .env)"
# 不 exit,因为用户可能已经有一套自定义部署
)
fi
# ---------- 6. 没有 .env? 给一份占位(方便首次部署发现问题) ----------
if [ ! -f .env ]; then
echo "================================================================="
echo "⚠️ WARNING:${DEPLOY_DIR}/.env 不存在!!!"
echo "👉 第一次部署请先在服务器上创建 .env(至少填 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
echo "👉 参考仓库根的 .env.example 或部署文档"
echo "================================================================="
# 不退出,因为 compose 会用 environment 默认值尝试启动(只是大概率连不上 DB)
fi
# ---------- 7. 滚动启动(如果只有 voicehub 单服务,不会动 postgres/redis) ----------
echo "🔄 docker compose up -d(只重建变更过的 voicehub)..."
${COMPOSE} up -d --remove-orphans
# ---------- 8. 等待应用变 healthy,最多 2 分钟 ----------
MAX_WAIT=24 # 24 × 5s = 2min
WAITED=0
echo "⏳ 等待容器 healthcheck 通过(最多 120s)..."
while [ ${WAITED} -lt ${MAX_WAIT} ]; do
STATUS="$(${COMPOSE} ps --format json 2>/dev/null || true)"
if echo "${STATUS}" | grep -q '"Health":"healthy"'; then
echo "✅ voicehub 容器健康检查通过"
break
fi
# 同时也检查是否已经 running 但健康检查还没跑完(防止 healthcheck 写太严误判)
if echo "${STATUS}" | grep -q '"State":"running"'; then
:
else
# 连 running 都不是 → 直接打印日志末尾 60 行帮忙排错
echo "⚠️ voicehub 还没 running,当前状态:"
${COMPOSE} ps || true
fi
sleep 5
WAITED=$((WAITED + 1))
done
if [ ${WAITED} -ge ${MAX_WAIT} ]; then
echo "⚠️ 等待超时,下面是最近 120 行 voicehub 日志:"
${COMPOSE} logs --tail=120 voicehub || true
exit 1
fi
# ---------- 9. 清理旧镜像(保留最近 2 个 tag + latest) ----------
echo "🧹 清理 3 天前的旧镜像 / 悬空层,防止磁盘爆"
docker image prune -af --filter "until=72h" || true
docker builder prune -af || true
# ---------- 10. 回滚指引 ----------
echo ""
echo "================================================================="
echo "🎉 部署成功!访问: ${{ secrets.NUXT_PUBLIC_SITE_URL || '请查看你的站点 URL' }}"
echo "🔁 如需紧急回滚到上一个版本,在服务器执行:"
echo " cd ${DEPLOY_DIR}"
echo " # 1) 先看有哪些 tag:"
echo " docker images | grep ${IMAGE_NAME} | head -5"
echo " # 2) 选一个老 tag 填到 override:"
echo " echo 'services:\n voicehub:\n image: ${GITEA_HOST}/${GITHUB_REPOSITORY_OWNER}/${IMAGE_NAME}:<旧TAG>' > docker-compose.override.yml"
echo " docker compose up -d"
echo "================================================================="