Files
BYW-VoiceHub/.gitea/workflows/deploy.yml
T
Henry_Du 10117fc7e2
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Failing after 8s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Skipped
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Failing after 4s
perf(ci): 所有 Job Debian/Ubuntu 换清华源 + GitHub 访问统一走 v4.gh-proxy.org 前缀代理 + Buildx registry-mirrors
2026-09-06 11:32:31 +08:00

450 lines
20 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:
GITEA_HOST: gitea.fse-media.group
DEPLOY_PATH: /data/opt/byw-voicehub
IMAGE_NAME: byw-voicehub
GH_PROXY_PREFIX: https://v4.gh-proxy.org/
APT_MIRROR_DEBIAN: https://mirrors.tuna.tsinghua.edu.cn/debian
APT_MIRROR_DEBIAN_SECURITY: https://mirrors.tuna.tsinghua.edu.cn/debian-security
HTTP_PROXY_EXAMPLE: ""
HTTPS_PROXY_EXAMPLE: ""
jobs:
preflight:
name: Preflight 语法与构建检查
runs-on: ubuntu-latest
timeout-minutes: 15
container:
image: node:22-bookworm-slim
steps:
- name: 安装基础运行依赖(git/ca-certificates,已换清华 TUNA 源)
run: |
set -euxo pipefail
# Debian 12 (bookworm) 换清华 TUNA 国内镜像(避免 deb.debian.org 国外源速度 <200KB/s)
SOURCES_FILE="/etc/apt/sources.list.d/debian.sources"
if [ -f "${SOURCES_FILE}" ]; then
sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
sed -i "s|http://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" "${SOURCES_FILE}"
sed -i "s|https://deb.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" "${SOURCES_FILE}"
else
# 兜底旧格式 /etc/apt/sources.list
if [ -f /etc/apt/sources.list ]; then
sed -i "s|http://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
sed -i "s|https://deb.debian.org/debian|${APT_MIRROR_DEBIAN}|g" /etc/apt/sources.list
sed -i "s|http://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
sed -i "s|https://security.debian.org/debian-security|${APT_MIRROR_DEBIAN_SECURITY}|g" /etc/apt/sources.list
fi
fi
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends \
git ca-certificates curl
rm -rf /var/lib/apt/lists/*
# 全局 git 代理:任何 git clone/fetch/pull https://github.com/... 自动走 gh-proxy.com 前缀
git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
git --version
node -v
npm -v
- name: 检出代码
run: |
set -euxo pipefail
REPO_URL="https://${GITEA_HOST}/${GITHUB_REPOSITORY}.git"
BRANCH="${GITHUB_REF_NAME:-main}"
echo "Clone from: ${REPO_URL} (${BRANCH})"
rm -rf * .[^.]* 2>/dev/null || true
git clone --depth 1 -b "${BRANCH}" "${REPO_URL}" .
git rev-parse --short HEAD
- 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: 系统环境准备(Ubuntu 22.04 默认镜像源换清华 + 全局 GitHub gh-proxy)
run: |
set -euxo pipefail
# Ubuntu 22.04 默认是 http://archive.ubuntu.com/ubuntu 和 http://security.ubuntu.com/ubuntu
if command -v apt-get >/dev/null 2>&1 && [ -f /etc/apt/sources.list ]; then
sudo sed -i \
-e 's|http://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
-e 's|http://security.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security|g' \
-e 's|https://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
/etc/apt/sources.list
fi
if command -v apt-get >/dev/null 2>&1 && ls /etc/apt/sources.list.d/*.sources 1>/dev/null 2>&1; then
for f in /etc/apt/sources.list.d/*.sources; do
sudo sed -i \
-e 's|http://archive.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu|g' \
-e 's|http://security.ubuntu.com/ubuntu|https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security|g' \
"${f}" 2>/dev/null || true
done
fi
# 全局 git:所有 https://github.com/ 自动走 gh-proxy 前缀
sudo git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
# npm:强制用 npmmirror(与 preflight 对齐)
npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
echo "环境准备完成"
cat /etc/os-release 2>/dev/null | head -4 || true
docker --version
docker buildx version || echo "buildx 未安装"
- name: 检出代码
run: |
set -euxo pipefail
REPO_URL="https://${GITEA_HOST}/${GITHUB_REPOSITORY}.git"
BRANCH="${GITHUB_REF_NAME:-main}"
echo "Clone from: ${REPO_URL} (${BRANCH})"
rm -rf * .[^.]* 2>/dev/null || true
git clone --depth 1 -b "${BRANCH}" "${REPO_URL}" .
git rev-parse --short HEAD
docker version
docker buildx version || echo "buildx 未安装,将使用 docker build --push"
- name: 计算镜像标签
id: meta
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: 初始化 Docker Buildx builder(带国内 registry-mirrors + gh-proxy 环境)
run: |
set -euxo pipefail
BUILDER_NAME="multi"
BKK_CFG="$(mktemp -t buildkitd_XXXXXX.toml)"
# 用 printf 逐行写 buildkitd 配置,避免 YAML parser 把 heredoc 内部的 [...] 误判为 YAML key
{
printf '[worker.oci]\n max-parallelism = 2\n\n'
printf '[registry."docker.io"]\n mirrors = ["docker.m.daocloud.io","dockerproxy.com","hub-mirror.c.163.com","docker.nju.edu.cn","docker.mirrors.ustc.edu.cn"]\n\n'
printf '[registry."ghcr.io"]\n mirrors = ["ghcr.nju.edu.cn","ghcr.dockerproxy.com"]\n\n'
printf '[registry."k8s.gcr.io"]\n mirrors = ["k8s-gcr.m.daocloud.io","registry.cn-hangzhou.aliyuncs.com/google_containers"]\n'
} > "${BKK_CFG}"
docker buildx rm "${BUILDER_NAME}" 2>/dev/null || true
docker buildx create \
--name "${BUILDER_NAME}" \
--driver docker-container \
--use \
--config "${BKK_CFG}" \
--buildkitd-flags '--allow-insecure-entitlement security.insecure --allow-insecure-entitlement network.host' \
--driver-opt network=host \
--driver-opt "env=HTTP_PROXY=${HTTP_PROXY_EXAMPLE}" \
--driver-opt "env=HTTPS_PROXY=${HTTPS_PROXY_EXAMPLE}"
rm -f "${BKK_CFG}"
export GIT_CONFIG_COUNT=2
export GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
export GIT_CONFIG_VALUE_0="https://github.com/"
export GIT_CONFIG_KEY_1="url.${GH_PROXY_PREFIX}https://gist.github.com/.insteadOf"
export GIT_CONFIG_VALUE_1="https://gist.github.com/"
docker buildx inspect --bootstrap
- name: 登录 Gitea 容器仓库
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }}
run: |
set -euo pipefail
if [ -z "${REGISTRY_TOKEN}" ]; then
echo "ERROR: REGISTRY_TOKEN 为空。请在 Repository Secrets 中新建 REGISTRY_TOKEN(权限:read:package + write:package)"
exit 1
fi
printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" \
-u "${REGISTRY_USER}" --password-stdin
echo "登录成功:${REGISTRY_USER} @ ${GITEA_HOST}"
- name: 构建并推送镜像
env:
CACHE_REF: ${{ env.GITEA_HOST }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:buildcache
SKIP_CACHE: ${{ inputs.skip_build_cache }}
IMG_FULL: ${{ steps.meta.outputs.full }}
IMG_LATEST: ${{ steps.meta.outputs.latest }}
OCS_SOURCE: ${{ github.server_url }}/${{ github.repository }}
OCS_REVISION: ${{ github.sha }}
OCS_CREATED: ${{ github.event.head_commit.timestamp }}
run: |
set -euxo pipefail
BUILD_ARGS=(
--build-arg BUILDKIT_INLINE_CACHE=1
--build-arg NODE_ENV=production
)
CACHE_ARGS=()
if [ "${SKIP_CACHE}" != "true" ]; then
CACHE_ARGS=(
--cache-from "type=registry,ref=${CACHE_REF}"
--cache-to "type=registry,ref=${CACHE_REF},mode=max"
)
else
echo "已启用 skip_build_cache,不使用分层缓存,强制重新构建"
BUILD_ARGS+=( --no-cache )
fi
docker buildx build \
--context . \
--file ./Dockerfile \
--platform linux/amd64 \
--push \
--provenance=false \
"${BUILD_ARGS[@]}" \
"${CACHE_ARGS[@]}" \
--tag "${IMG_FULL}" \
--tag "${IMG_LATEST}" \
--label "org.opencontainers.image.source=${OCS_SOURCE}" \
--label "org.opencontainers.image.revision=${OCS_REVISION}" \
--label "org.opencontainers.image.created=${OCS_CREATED}"
- name: 镜像体积与层信息
env:
IMG_FULL: ${{ steps.meta.outputs.full }}
run: |
set -euxo pipefail
docker pull "${IMG_FULL}" || true
docker image inspect "${IMG_FULL}" --format='{{.Size}}' 2>/dev/null \
| awk '{printf "Image size: %.2f MB\n", $1/1024/1024}' || true
docker history --no-trunc "${IMG_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: 校验部署所需密钥
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
run: |
set -euo pipefail
MISSING=()
[ -n "${DEPLOY_HOST}" ] || MISSING+=("DEPLOY_HOST")
[ -n "${DEPLOY_USER}" ] || MISSING+=("DEPLOY_USER")
[ -n "${DEPLOY_SSH_KEY}" ] || MISSING+=("DEPLOY_SSH_KEY")
if [ ${#MISSING[@]} -gt 0 ]; then
echo "缺少以下 Repository Secrets:${MISSING[*]}"
echo "请在 Gitea 仓库 -> 设置 -> 工作流 -> 密钥 页面添加(注意是 Repository Secrets,不是 Deploy Keys)"
exit 1
fi
echo "部署密钥配置完整(DEPLOY_HOST=${DEPLOY_HOST},DEPLOY_USER=${DEPLOY_USER})"
- name: SSH 滚动部署
env:
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.DEPLOY_PORT || 22 }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
REGISTRY_PULL_USER: ${{ secrets.REGISTRY_PULL_USER }}
REGISTRY_PULL_TOKEN: ${{ secrets.REGISTRY_PULL_TOKEN }}
NUXT_PUBLIC_SITE_URL: ${{ secrets.NUXT_PUBLIC_SITE_URL }}
IMAGE_NEW: ${{ needs.build-image.outputs.image_full }}
IMAGE_LATEST: ${{ needs.build-image.outputs.image_latest }}
GITHUB_SERVER_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
GITHUB_REF_NAME: ${{ github.ref_name }}
run: |
set -euo pipefail
# 写私钥到临时文件(避免 stdin 转义问题)
SSH_KEY_FILE="$(mktemp -t deploy_ssh_key.XXXXXX)"
printf '%s\n' "${DEPLOY_SSH_KEY}" > "${SSH_KEY_FILE}"
chmod 600 "${SSH_KEY_FILE}"
SSH_OPTS=(
-i "${SSH_KEY_FILE}"
-o StrictHostKeyChecking=no
-o UserKnownHostsFile=/dev/null
-o ConnectTimeout=10
-o ServerAliveInterval=30
-o ServerAliveCountMax=8
-p "${DEPLOY_PORT}"
)
echo "=============================================="
echo "Deploy target: ${DEPLOY_USER}@${DEPLOY_HOST}:${DEPLOY_PORT}"
echo "Deploy image : ${IMAGE_NEW}"
echo "Deploy dir : ${DEPLOY_PATH}"
echo "=============================================="
# 本地预检查:ssh 先连上跑 whoami 确认连通,避免 20 分钟后才发现连不上
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "whoami; docker --version; docker compose version"
# 把变量作为 heredoc 脚本的输入传过去,远端展开我们需要的值
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" \
IMAGE_NEW="${IMAGE_NEW}" \
IMAGE_LATEST="${IMAGE_LATEST}" \
DEPLOY_PATH="${DEPLOY_PATH}" \
GITEA_HOST="${GITEA_HOST}" \
GH_PROXY_PREFIX="${GH_PROXY_PREFIX}" \
REGISTRY_PULL_USER="${REGISTRY_PULL_USER}" \
REGISTRY_PULL_TOKEN="${REGISTRY_PULL_TOKEN}" \
IMAGE_NAME="${IMAGE_NAME}" \
NUXT_PUBLIC_SITE_URL="${NUXT_PUBLIC_SITE_URL}" \
GITHUB_SERVER_URL="${GITHUB_SERVER_URL}" \
GITHUB_REPOSITORY="${GITHUB_REPOSITORY}" \
GITHUB_REF_NAME="${GITHUB_REF_NAME}" \
bash <<'REMOTE_SCRIPT'
set -euxo pipefail
DEPLOY_DIR="${DEPLOY_PATH}"
COMPOSE="docker compose"
echo "=============================================="
echo "Executor : $(whoami)"
echo "Deploy image : ${IMAGE_NEW}"
echo "Deploy dir : ${DEPLOY_DIR}"
echo "=============================================="
mkdir -p "${DEPLOY_DIR}"
cd "${DEPLOY_DIR}"
if [ -n "${REGISTRY_PULL_TOKEN}" ] && [ -n "${REGISTRY_PULL_USER}" ]; then
echo "使用专用 pull 凭证登录 Gitea Container Registry"
printf '%s' "${REGISTRY_PULL_TOKEN}" \
| docker login "${GITEA_HOST}" \
-u "${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 <<YAML
services:
voicehub:
image: ${IMAGE_NEW}
YAML
echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
if [ ! -f docker-compose.yml ]; then
echo "docker-compose.yml 不存在,自动从仓库模板下载(如访问 GitHub 会自动加 gh-proxy 前缀)"
TEMPLATE_URL="${GITHUB_SERVER_URL:-https://${GITEA_HOST}}/${GITHUB_REPOSITORY}/raw/branch/${GITHUB_REF_NAME:-main}/docker-compose.yml"
case "${TEMPLATE_URL}" in
*"://github.com/"*) TEMPLATE_URL="${GH_PROXY_PREFIX}${TEMPLATE_URL}" ;;
esac
echo "URL=${TEMPLATE_URL}"
curl -fsSL --max-time 30 "${TEMPLATE_URL}" -o docker-compose.yml \
|| echo "自动下载模板失败,请手动上传 docker-compose.yml 和 .env(参考仓库根目录 .env.example)"
fi
if [ ! -f .env ]; then
echo "================================================================="
echo "WARNING:${DEPLOY_DIR}/.env 不存在"
echo "首次部署请在服务器上创建 .env(至少填写 POSTGRES_PASSWORD / JWT_SECRET / NUXT_PUBLIC_SITE_URL)"
echo "参考仓库根目录的 .env.example"
echo "================================================================="
fi
echo "Executing docker compose up -d ..."
${COMPOSE} up -d --remove-orphans
MAX_WAIT=24
WAITED=0
echo "Waiting for container healthcheck (max 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 healthcheck passed"
break
fi
if echo "${STATUS}" | grep -q '"State":"running"'; then
:
else
echo "voicehub not running, current status:"
${COMPOSE} ps || true
fi
sleep 5
WAITED=$((WAITED + 1))
done
if [ ${WAITED} -ge ${MAX_WAIT} ]; then
echo "Healthcheck timeout. Last 120 lines of voicehub logs:"
${COMPOSE} logs --tail=120 voicehub || true
exit 1
fi
echo "Pruning images older than 72 hours..."
docker image prune -af --filter "until=72h" || true
docker builder prune -af || true
echo ""
echo "================================================================="
echo "Deploy succeeded. Visit: ${NUXT_PUBLIC_SITE_URL:-请自行查看你的站点 URL}"
echo "Rollback instructions (on server):"
echo " cd ${DEPLOY_DIR}"
echo " docker images | grep ${IMAGE_NAME} | head -5"
echo " printf 'services:\\n voicehub:\\n image: <OLD_TAG_FROM_ABOVE>\\n' > docker-compose.override.yml"
echo " docker compose up -d"
echo "================================================================="
REMOTE_SCRIPT
# 清理私钥临时文件
rm -f "${SSH_KEY_FILE}"