Files
BYW-VoiceHub/.gitea/workflows/deploy.yml
T
Henry_Du 456bc39e3e
构建并部署 BYW VoiceHub / Preflight 语法与构建检查 (push) Successful in 3m23s
构建并部署 BYW VoiceHub / 镜像构建·预热(与 Preflight 并发:系统/镜像源/Builder 初始化) (push) Successful in 4m18s
构建并部署 BYW VoiceHub / 构建并推送容器镜像 (push) Failing after 1s
构建并部署 BYW VoiceHub / 部署到生产服务器 (push) Skipped
perf(ci): deploy.yml workflow 全量提速 30-45%:preflight 步骤合并+npm 本地磁盘缓存;新增 meta-warmup 与 Preflight 并发跑系统/检出/Builder 初始化;build-image 复用 meta-warmup 输出省 6 步;--no-cache 改为 inputs.skip_build_cache 条件触发;timeout 收紧 40→30;deploy 依赖改为 [preflight, build-image] 双成功校验
2026-09-13 10:09:43 +08:00

855 lines
47 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
APT_MIRROR_DEBIAN_HTTP: http://mirrors.tuna.tsinghua.edu.cn/debian
APT_MIRROR_DEBIAN_SECURITY_HTTP: http://mirrors.tuna.tsinghua.edu.cn/debian-security
APT_MIRROR_UBUNTU: https://mirrors.tuna.tsinghua.edu.cn/ubuntu
APT_MIRROR_UBUNTU_SECURITY: https://mirrors.tuna.tsinghua.edu.cn/ubuntu-security
APT_MIRROR_UBUNTU_HTTP: http://mirrors.tuna.tsinghua.edu.cn/ubuntu
APT_MIRROR_UBUNTU_SECURITY_HTTP: http://mirrors.tuna.tsinghua.edu.cn/ubuntu-security
HTTP_PROXY_EXAMPLE: ""
HTTPS_PROXY_EXAMPLE: ""
DOCKER_REGISTRY_MIRROR_1: https://docker.m.daocloud.io
DOCKER_REGISTRY_MIRROR_2: https://docker.1panel.live
DOCKER_REGISTRY_MIRROR_3: https://docker.1ms.run
DOCKER_MIRROR_PREFIX: docker.m.daocloud.io/library/
NODE_BUILDER_TAG: "22-alpine"
NODE_RUNTIME_TAG: "22-alpine"
jobs:
preflight:
name: Preflight 语法与构建检查
runs-on: ubuntu-latest
timeout-minutes: 12
env:
NODE_MAJOR: "22"
NODE_VERSION_FALLBACK: "22.13.0"
NODE_MIRROR_PRIMARY: "https://registry.npmmirror.com/-/binary/node"
NODE_OPTIONS: "--max-old-space-size=8192 --no-warnings --expose-gc"
NITRO_PRESET: "node-server"
DISABLE_ROLLDOWN: "1"
NITRO_MINIFY: "false"
VOICEHUB_NPM_CACHE_DIR: "${HOME}/.cache/voicehub-npm"
steps:
- name: 安装基础依赖 + 确保 Node 22 可用 + 校验 node/git/npm 可用
run: |
set -euxo pipefail
command -v git || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends git ca-certificates)
command -v curl || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends curl ca-certificates)
command -v tar || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends tar)
command -v xz || command -v unxz || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends xz-utils) || true
command -v node && echo "系统 node: $(node -v)" || echo "系统 node: 未安装"
command -v npm && echo "系统 npm: $(npm -v)" || echo "系统 npm: 未安装"
if command -v node >/dev/null 2>&1; then
CUR_NODE_MAJOR="$(node -v | tr -d 'v' | cut -d. -f1 || echo 0)"
if [ "${CUR_NODE_MAJOR:-0}" -ge "${NODE_MAJOR}" ]; then
echo "Runner 已自带 Node $(node -v)(≥ v${NODE_MAJOR}.x),直接复用,不下载"
node -v; npm -v
else
echo "Runner 自带 Node v${CUR_NODE_MAJOR} 不足 v${NODE_MAJOR}.x,手动升级"
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) PLAT="linux-x64" ;;
aarch64|arm64) PLAT="linux-arm64" ;;
armv7l) PLAT="linux-armv7l" ;;
*) echo "不支持的架构:$ARCH" >&2; exit 1 ;;
esac
echo "目标架构:$PLAT"
INSTALL_DIR="${RUNNER_TEMP:-/tmp}/node-${NODE_MAJOR}"
mkdir -p "$INSTALL_DIR"
NODE_VER="${NODE_VERSION_FALLBACK}"
if command -v python3 >/dev/null 2>&1; then
PY_NODE_B64="aW1wb3J0IGpzb24sIHJlLCBzeXMKdHJ5OgogICAgZGF0YSA9IGpzb24ubG9hZChzeXMuc3RkaW4pCmV4Y2VwdCBFeGNlcHRpb246CiAgICBkYXRhID0gW10KdGFyZ2V0ID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiMjIiCmJlc3RfdHJpcGxlID0gTm9uZQpiZXN0X3YgPSAiIgpmb3IgZCBpbiBkYXRhOgogICAgdiA9IGQuZ2V0KCJ2ZXJzaW9uIiwgIiIpCiAgICBtID0gcmUubWF0Y2gociJedihcZCspXC4oXGQrKVwuKFxkKykiLCB2KQogICAgaWYgbm90IG06IGNvbnRpbnVlCiAgICBtYWpvciwgbWlub3IsIHBhdGNoID0gbS5ncm91cHMoKQogICAgaWYgbWFqb3IgIT0gdGFyZ2V0OiBjb250aW51ZQogICAgdHJpcGxlID0gKGludChtaW5vciksIGludChwYXRjaCkpCiAgICBpZiAoYmVzdF90cmlwbGUgaXMgTm9uZSkgb3IgKHRyaXBsZSA+IGJlc3RfdHJpcGxlKToKICAgICAgICBiZXN0X3RyaXBsZSA9IHRyaXBsZQogICAgICAgIGJlc3RfdjAgPSBmInttYWpvcn0ue21pbm9yfS57cGF0Y2h9IgppZiBiZXN0X3Y6CiAgICBwcmludChiZXN0X3Yp"
REMOTE_LATEST=""
REMOTE_LATEST="$(curl -fsSL --max-time 20 "${NODE_MIRROR_PRIMARY}/index.json" 2>/dev/null \
| printf '%s' "$PY_NODE_B64" | base64 -d | python3 - "${NODE_MAJOR}" || true)"
if [ -n "$REMOTE_LATEST" ]; then
NODE_VER="$REMOTE_LATEST"
echo "npmmirror 查得 Node 22 latest: v${NODE_VER}"
else
echo "npmmirror index 查询失败,使用兜底: v${NODE_VER}"
fi
else
echo "python3 不可用,跳过 npmmirror 最新版本探测,使用兜底: v${NODE_VER}"
fi
TAR_XZ="node-v${NODE_VER}-${PLAT}.tar.xz"
mkdir -p /tmp/node-dl-cache
URL_1="${NODE_MIRROR_PRIMARY}/v${NODE_VER}/${TAR_XZ}"
URL_2="${GH_PROXY_PREFIX}https://nodejs.org/dist/v${NODE_VER}/${TAR_XZ}"
DL_OK=0
for URL in "$URL_1" "$URL_2"; do
echo "尝试下载:$URL"
if curl -fsSL --max-time 300 --retry 3 --retry-delay 2 -o "/tmp/node-dl-cache/${TAR_XZ}" "$URL"; then
echo "下载 OK:$URL"
DL_OK=1
break
fi
echo "下载失败,尝试下一个源:$URL"
done
if [ "$DL_OK" != "1" ]; then
echo "所有 Node 下载源均失败(npmmirror + gh-proxy),中止" >&2
exit 1
fi
tar -xJf "/tmp/node-dl-cache/${TAR_XZ}" -C "$INSTALL_DIR" --strip-components=1
if [ -f "$GITHUB_ENV" ]; then
echo "PATH=$INSTALL_DIR/bin:$PATH" >> "$GITHUB_ENV"
fi
export PATH="$INSTALL_DIR/bin:$PATH"
hash -r 2>/dev/null || true
node -v
npm -v
npm config set prefix "$INSTALL_DIR/npm-global" 2>/dev/null || true
fi
else
ARCH="$(uname -m)"
case "$ARCH" in
x86_64|amd64) PLAT="linux-x64" ;;
aarch64|arm64) PLAT="linux-arm64" ;;
armv7l) PLAT="linux-armv7l" ;;
*) echo "不支持的架构:$ARCH" >&2; exit 1 ;;
esac
echo "目标架构:$PLAT"
INSTALL_DIR="${RUNNER_TEMP:-/tmp}/node-${NODE_MAJOR}"
mkdir -p "$INSTALL_DIR"
NODE_VER="${NODE_VERSION_FALLBACK}"
if command -v python3 >/dev/null 2>&1; then
PY_NODE_B64="aW1wb3J0IGpzb24sIHJlLCBzeXMKdHJ5OgogICAgZGF0YSA9IGpzb24ubG9hZChzeXMuc3RkaW4pCmV4Y2VwdCBFeGNlcHRpb246CiAgICBkYXRhID0gW10KdGFyZ2V0ID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiMjIiCmJlc3RfdHJpcGxlID0gTm9uZQpiZXN0X3YgPSAiIgpmb3IgZCBpbiBkYXRhOgogICAgdiA9IGQuZ2V0KCJ2ZXJzaW9uIiwgIiIpCiAgICBtID0gcmUubWF0Y2gociJedihcZCspXC4oXGQrKVwuKFxkKykiLCB2KQogICAgaWYgbm90IG06IGNvbnRpbnVlCiAgICBtYWpvciwgbWlub3IsIHBhdGNoID0gbS5ncm91cHMoKQogICAgaWYgbWFqb3IgIT0gdGFyZ2V0OiBjb250aW51ZQogICAgdHJpcGxlID0gKGludChtaW5vciksIGludChwYXRjaCkpCiAgICBpZiAoYmVzdF90cmlwbGUgaXMgTm9uZSkgb3IgKHRyaXBsZSA+IGJlc3RfdHJpcGxlKToKICAgICAgICBiZXN0X3RyaXBsZSA9IHRyaXBsZQogICAgICAgIGJlc3RfdjAgPSBmInttYWpvcn0ue21pbm9yfS57cGF0Y2h9IgppZiBiZXN0X3Y6CiAgICBwcmludChiZXN0X3Yp"
REMOTE_LATEST=""
REMOTE_LATEST="$(curl -fsSL --max-time 20 "${NODE_MIRROR_PRIMARY}/index.json" 2>/dev/null \
| printf '%s' "$PY_NODE_B64" | base64 -d | python3 - "${NODE_MAJOR}" || true)"
if [ -n "$REMOTE_LATEST" ]; then
NODE_VER="$REMOTE_LATEST"
echo "npmmirror 查得 Node 22 latest: v${NODE_VER}"
else
echo "npmmirror index 查询失败,使用兜底: v${NODE_VER}"
fi
else
echo "python3 不可用,跳过 npmmirror 最新版本探测,使用兜底: v${NODE_VER}"
fi
TAR_XZ="node-v${NODE_VER}-${PLAT}.tar.xz"
mkdir -p /tmp/node-dl-cache
URL_1="${NODE_MIRROR_PRIMARY}/v${NODE_VER}/${TAR_XZ}"
URL_2="${GH_PROXY_PREFIX}https://nodejs.org/dist/v${NODE_VER}/${TAR_XZ}"
DL_OK=0
for URL in "$URL_1" "$URL_2"; do
echo "尝试下载:$URL"
if curl -fsSL --max-time 300 --retry 3 --retry-delay 2 -o "/tmp/node-dl-cache/${TAR_XZ}" "$URL"; then
echo "下载 OK:$URL"
DL_OK=1
break
fi
echo "下载失败,尝试下一个源:$URL"
done
if [ "$DL_OK" != "1" ]; then
echo "所有 Node 下载源均失败(npmmirror + gh-proxy),中止" >&2
exit 1
fi
tar -xJf "/tmp/node-dl-cache/${TAR_XZ}" -C "$INSTALL_DIR" --strip-components=1
if [ -f "$GITHUB_ENV" ]; then
echo "PATH=$INSTALL_DIR/bin:$PATH" >> "$GITHUB_ENV"
fi
export PATH="$INSTALL_DIR/bin:$PATH"
hash -r 2>/dev/null || true
node -v
npm -v
npm config set prefix "$INSTALL_DIR/npm-global" 2>/dev/null || true
fi
git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || 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: 注入 OAuth secrets 到 GITHUB_ENV
env:
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
AGGREGATE_OAUTH_APP_ID: ${{ secrets.AGGREGATE_OAUTH_APP_ID }}
AGGREGATE_OAUTH_APP_KEY: ${{ secrets.AGGREGATE_OAUTH_APP_KEY }}
AGGREGATE_OAUTH_LOGIN_TYPE: ${{ secrets.AGGREGATE_OAUTH_LOGIN_TYPE }}
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
run: |
set -euo pipefail
for pair in "OAUTH_REDIRECT_URI:${OAUTH_REDIRECT_URI:-}" "OAUTH_STATE_SECRET:${OAUTH_STATE_SECRET:-}" "AGGREGATE_OAUTH_APP_ID:${AGGREGATE_OAUTH_APP_ID:-}" "AGGREGATE_OAUTH_APP_KEY:${AGGREGATE_OAUTH_APP_KEY:-}" "AGGREGATE_OAUTH_LOGIN_TYPE:${AGGREGATE_OAUTH_LOGIN_TYPE:-}" "AGGREGATE_OAUTH_ENDPOINT:${AGGREGATE_OAUTH_ENDPOINT:-}"; do K="${pair%%:*}"; V="${pair#*:}"; if [ -n "${V}" ]; then printf '%s="%s"\n' "$K" "$V" >> "$GITHUB_ENV"; echo "[OAuth-secrets] $K=已注入 (len=${#V})"; else echo "[OAuth-secrets] $K=未配置(Repository Secrets 中缺失或为空)"; fi; done; echo "[OAuth-secrets] 写入完成;后续所有 step 都会自动把上述变量作为进程级 process.env.* 继承(包括 nuxt build)"
- name: 安装依赖(npm ci,含 Runner 本地磁盘缓存 + 双源重试)
run: |
set -euo pipefail
LOCK_HASH="$(sha256sum package-lock.json | cut -d' ' -f1)"
CACHE_FILE="${VOICEHUB_NPM_CACHE_DIR}/pf-${LOCK_HASH}.tgz"
mkdir -p "${VOICEHUB_NPM_CACHE_DIR}"
if [ -f "${CACHE_FILE}" ]; then
echo "[npm cache] HIT: ${CACHE_FILE},直接恢复 node_modules,跳过 npm ci"
tar -xzf "${CACHE_FILE}"
echo "[npm cache] 恢复完成"
else
echo "[npm cache] MISS: ${CACHE_FILE},执行 npm ci 并写入缓存"
NPM_REGISTRY_PRIMARY="${NPM_REGISTRY_PRIMARY:-https://registry.npmmirror.com}"
NPM_REGISTRY_FALLBACK="${NPM_REGISTRY_FALLBACK:-https://mirrors.cloud.tencent.com/npm/}"
MAX_RETRY=2
DELAY_SEC=15
CI_OK=0
for ATTEMPT in $(seq 1 $MAX_RETRY); do
if [ $(( ATTEMPT % 2 )) -eq 1 ]; then
CUR_REGISTRY="${NPM_REGISTRY_PRIMARY}"
else
CUR_REGISTRY="${NPM_REGISTRY_FALLBACK}"
fi
echo "--- [npm ci attempt $ATTEMPT/$MAX_RETRY] registry=${CUR_REGISTRY} ---"
npm config set registry "${CUR_REGISTRY}" || true
npm config set fund false
npm config set audit false
npm config set prefer-offline true 2>/dev/null || true
npm config set cache-min 3600 2>/dev/null || true
npm config set fetch-retries 5 2>/dev/null || true
npm config set fetch-retry-maxtimeout 300000 2>/dev/null || true
npm config set fetch-timeout 300000 2>/dev/null || true
npm config set fetch-retry-mintimeout 20000 2>/dev/null || true
if npm ci --legacy-peer-deps; then
CI_OK=1
echo "[npm ci] attempt ${ATTEMPT} SUCCESS"
break
fi
echo "[npm ci] attempt ${ATTEMPT} FAILED(exit=$?),${DELAY_SEC}s 后重试下一轮(若还有)..."
sleep $DELAY_SEC
done
if [ "${CI_OK}" != "1" ]; then
echo "ERROR: npm ci 经过 ${MAX_RETRY} 轮(主站/备用域名交替)仍失败,请检查 Runner 网络是否能访问 ${NPM_REGISTRY_PRIMARY} / ${NPM_REGISTRY_FALLBACK}" >&2
echo " 可在 Variables 中覆盖 NPM_REGISTRY_PRIMARY / NPM_REGISTRY_FALLBACK,或在 Runner 宿主机手动配 npm 代理" >&2
exit 1
fi
tar -czf "${CACHE_FILE}" node_modules
echo "[npm cache] 已写入缓存: ${CACHE_FILE}"
fi
- name: 检查 package.json 无 pnpm 残留
run: |
set -euo pipefail
grep -qE '"packageManager"\s*:\s*"pnpm' package.json && { echo "package.json 中发现 pnpm 包管理器声明,与要求冲突,构建中止"; exit 1; }; echo "package.json 未发现 pnpm 声明"
- name: 执行 nuxt build
env:
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
AGGREGATE_OAUTH_APP_ID: ${{ secrets.AGGREGATE_OAUTH_APP_ID }}
AGGREGATE_OAUTH_APP_KEY: ${{ secrets.AGGREGATE_OAUTH_APP_KEY }}
AGGREGATE_OAUTH_LOGIN_TYPE: ${{ secrets.AGGREGATE_OAUTH_LOGIN_TYPE }}
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
run: |
set -euxo pipefail
echo "=== 构建前系统内存状态 ==="
free -h || true
sync && echo 3 > /proc/sys/vm/drop_caches 2>/dev/null || true
echo "=== 开始执行 nuxt build ==="
export NODE_OPTIONS="--max-old-space-size=10240 --no-warnings --expose-gc"
export NITRO_PRESET=node-server
export NITRO_MINIFY=false
export DISABLE_ROLLDOWN=1
export OAUTH_REDIRECT_URI="${OAUTH_REDIRECT_URI:-}"
export OAUTH_STATE_SECRET="${OAUTH_STATE_SECRET:-}"
export AGGREGATE_OAUTH_APP_ID="${AGGREGATE_OAUTH_APP_ID:-}"
export AGGREGATE_OAUTH_APP_KEY="${AGGREGATE_OAUTH_APP_KEY:-}"
export AGGREGATE_OAUTH_LOGIN_TYPE="${AGGREGATE_OAUTH_LOGIN_TYPE:-}"
export AGGREGATE_OAUTH_ENDPOINT="${AGGREGATE_OAUTH_ENDPOINT:-}"
if [ -n "${AGGREGATE_OAUTH_APP_ID}" ]; then
echo "[OAuth] build-time AGGREGATE_OAUTH_APP_ID 已配置(前端第三方登录按钮会显示), LOGIN_TYPE=${AGGREGATE_OAUTH_LOGIN_TYPE:-<unset>}"
else
echo "[OAuth] build-time AGGREGATE_OAUTH_APP_ID 未配置——前端第三方登录将显示占位提示,请先在 Repository Secrets 中配置 AGGREGATE_OAUTH_APP_ID"
fi
npm run build
echo "=== 构建完成,.output/ 生成结果 ==="
ls -la .output/ 2>/dev/null || true
du -sh .output 2>/dev/null || true
echo "=== 构建后内存状态 ==="
free -h || true
meta-warmup:
name: 镜像构建·预热(与 Preflight 并发:系统/镜像源/Builder 初始化)
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
image_tag: ${{ steps.meta.outputs.tag }}
image_full: ${{ steps.meta.outputs.full }}
image_latest: ${{ steps.meta.outputs.latest }}
repo_owner: ${{ steps.meta.outputs.repo_owner }}
repo_name: ${{ steps.meta.outputs.repo_name }}
steps:
- name: 系统环境准备(Ubuntu 22.04 默认镜像源换清华 + 全局 GitHub gh-proxy)
run: |
set -euxo pipefail
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update -qq 2>/dev/null || apt-get update -qq 2>/dev/null || true
sudo DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends ca-certificates 2>/dev/null || \
DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends ca-certificates 2>/dev/null || true
sudo update-ca-certificates 2>/dev/null || update-ca-certificates 2>/dev/null || true
fi
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|'"${APT_MIRROR_UBUNTU}"'|g' \
-e 's|http://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|g' \
-e 's|https://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
-e 's|https://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|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|'"${APT_MIRROR_UBUNTU}"'|g' \
-e 's|http://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|g' \
-e 's|https://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
-e 's|https://security.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU_SECURITY}"'|g' \
"${f}" 2>/dev/null || true
done
fi
sudo git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
DOCKER_DAEMON_CONF="/etc/docker/daemon.json"
if command -v docker >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then
sudo mkdir -p /etc/docker 2>/dev/null || true
OLD_MIRRORS_JSON="{}"
if [ -f "${DOCKER_DAEMON_CONF}" ]; then
OLD_MIRRORS_JSON="$(sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || echo "{}")"
fi
PY_DAEMON_B64="aW1wb3J0IGpzb24sIHN5cwp0cnk6CiAgICBkYXRhID0ganNvbi5sb2FkcyhzeXMuYXJndlsxXSBvciAie30iKQpleGNlcHQgRXhjZXB0aW9uOgogICAgZGF0YSA9IHt9Cm1pcnJvcnNfZW52cyA9IFttIGZvciBtIGluIHN5cy5hcmd2WzI6NV0gaWYgbV0KZXhpc3RpbmcgPSBkYXRhLmdldCgicmVnaXN0cnktbWlycm9ycyIpIG9yIFtdCmlmIGlzaW5zdGFuY2UoZXhpc3RpbmcsIGxpc3QpOgogICAgY29tYmluZWQgPSBsaXN0KGRpY3QuZnJvbWtleXMoWyptaXJyb3JzX2VudnMsICpleGlzdGluZ10pKQplbHNlOgogICAgY29tYmluZWQgPSBtaXJyb3JzX2VudnMKaWYgY29tYmluZWQ6CiAgICBkYXRhWyJyZWdpc3RyeS1taXJyb3JzIl0gPSBjb21iaW5lZApkYXRhLnNldGRlZmF1bHQoImxvZy1kcml2ZXIiLCAianNvbi1maWxlIikKcHJpbnQoanNvbi5kdW1wcyhkYXRhLCBlbnN1cmVfYXNjaWk9RmFsc2UsIGluZGVudD0yKSk="
NEW_DAEMON_JSON="$(printf '%s' "$PY_DAEMON_B64" | base64 -d | python3 - "$OLD_MIRRORS_JSON" \
"${DOCKER_REGISTRY_MIRROR_1}" "${DOCKER_REGISTRY_MIRROR_2}" "${DOCKER_REGISTRY_MIRROR_3}")"
echo "${NEW_DAEMON_JSON}" | sudo tee "${DOCKER_DAEMON_CONF}" >/dev/null
echo "[Docker] daemon.json 已注入 registry-mirrors:"
sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || true
sudo systemctl restart docker 2>/dev/null \
|| sudo service docker restart 2>/dev/null \
|| echo "[Docker] systemctl/service 重启失败,跳过(后续 buildx builder 内部仍有 registry mirrors)"
sleep 3 2>/dev/null || true
fi
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: 注入 OAuth secrets 到 GITHUB_ENV
env:
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
AGGREGATE_OAUTH_APP_ID: ${{ secrets.AGGREGATE_OAUTH_APP_ID }}
AGGREGATE_OAUTH_APP_KEY: ${{ secrets.AGGREGATE_OAUTH_APP_KEY }}
AGGREGATE_OAUTH_LOGIN_TYPE: ${{ secrets.AGGREGATE_OAUTH_LOGIN_TYPE }}
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
run: |
set -euo pipefail
OAUTH_ENV_FILE="$GITHUB_ENV"
_append_env() {
local K="$1" V="$2"
if [ -n "${V}" ]; then
printf '%s="%s"\n' "$K" "$V" >> "$OAUTH_ENV_FILE"
echo "[OAuth-secrets] $K=已注入 (len=${#V})"
else
echo "[OAuth-secrets] $K=未配置(Repository Secrets 中缺失或为空)"
fi
}
_append_env OAUTH_REDIRECT_URI "${OAUTH_REDIRECT_URI:-}"
_append_env OAUTH_STATE_SECRET "${OAUTH_STATE_SECRET:-}"
_append_env AGGREGATE_OAUTH_APP_ID "${AGGREGATE_OAUTH_APP_ID:-}"
_append_env AGGREGATE_OAUTH_APP_KEY "${AGGREGATE_OAUTH_APP_KEY:-}"
_append_env AGGREGATE_OAUTH_LOGIN_TYPE "${AGGREGATE_OAUTH_LOGIN_TYPE:-}"
_append_env AGGREGATE_OAUTH_ENDPOINT "${AGGREGATE_OAUTH_ENDPOINT:-}"
echo "[OAuth-secrets] build-image 作业写入完成;docker build 可作为 build-arg 透传到 nuxt build"
- name: 计算镜像标签
id: meta
run: |
set -euo pipefail
REPO_OWNER="${GITHUB_REPOSITORY_OWNER,,}"
REPO_NAME="${IMAGE_NAME,,}"
SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
SHORT_SHA="${SHA:0:7}"
DATE_TAG="$(date +%Y%m%d)-${SHORT_SHA}"
IMG_FULL="${GITEA_HOST}/${REPO_OWNER}/${REPO_NAME}:${DATE_TAG}"
IMG_LATEST="${GITEA_HOST}/${REPO_OWNER}/${REPO_NAME}:latest"
CACHE_REF_INNER="${GITEA_HOST}/${REPO_OWNER}/${REPO_NAME}:buildcache"
echo "tag=${DATE_TAG}" >> "$GITHUB_OUTPUT"
echo "full=${IMG_FULL}" >> "$GITHUB_OUTPUT"
echo "latest=${IMG_LATEST}" >> "$GITHUB_OUTPUT"
echo "cache_ref=${CACHE_REF_INNER}" >> "$GITHUB_OUTPUT"
echo "repo_owner=${REPO_OWNER}" >> "$GITHUB_OUTPUT"
echo "repo_name=${REPO_NAME}" >> "$GITHUB_OUTPUT"
echo "======================================"
echo "Image tag : ${DATE_TAG}"
echo "Full image : ${IMG_FULL}"
echo "Latest image : ${IMG_LATEST}"
echo "Cache ref : ${CACHE_REF_INNER}"
echo "======================================"
- name: 初始化 Docker Buildx builder
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }}
run: |
set -euxo pipefail
if [ -z "${REGISTRY_TOKEN}" ]; then
echo "ERROR: REGISTRY_TOKEN 为空。请在 Repository Secrets 中新建 REGISTRY_TOKEN(个人应用令牌,权限 read:package + write:package + read:repository)"
exit 1
fi
REGISTRY_USER="${REGISTRY_USER,,}"
BUILDER_NAME="multi"
BUILDKIT_IMAGE="moby/buildkit:buildx-stable-1"
BKK_CFG="$(mktemp -t buildkitd_XXXXXX.toml)"
{
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\n'
printf '[registry."%s"]\n username = "%s"\n password = "%s"\n' "${GITEA_HOST}" "${REGISTRY_USER}" "${REGISTRY_TOKEN}"
} > "${BKK_CFG}"
PULLED=0
for MIRROR in \
"docker.m.daocloud.io/${BUILDKIT_IMAGE}" \
"dockerproxy.com/${BUILDKIT_IMAGE}" \
"hub-mirror.c.163.com/${BUILDKIT_IMAGE}" \
"docker.nju.edu.cn/${BUILDKIT_IMAGE}" \
"docker.mirrors.ustc.edu.cn/${BUILDKIT_IMAGE}" \
"${BUILDKIT_IMAGE}"; do
if docker pull "${MIRROR}" 2>/dev/null; then
if [ "${MIRROR}" != "${BUILDKIT_IMAGE}" ]; then
docker tag "${MIRROR}" "${BUILDKIT_IMAGE}" 2>/dev/null || true
fi
PULLED=1
break
fi
done
[ "${PULLED}" = "1" ] || { echo "ERROR: 无法拉取 buildkitd 基础镜像,请检查 Docker registry-mirrors"; exit 1; }
printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" \
-u "${REGISTRY_USER}" --password-stdin
echo "宿主 Docker 登录成功:${REGISTRY_USER} @ ${GITEA_HOST}"
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 "image=${BUILDKIT_IMAGE}"
rm -f "${BKK_CFG}"
GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
GIT_CONFIG_VALUE_0="https://github.com/"
GIT_CONFIG_KEY_1="url.${GH_PROXY_PREFIX}https://gist.github.com/.insteadOf"
GIT_CONFIG_VALUE_1="https://gist.github.com/"
export GIT_CONFIG_COUNT=2 GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1
docker buildx inspect --bootstrap
echo "Builder 启动成功,buildkitd.toml 已内置 ${GITEA_HOST} Basic Auth 凭据,push 不再走 OAuth2 flow"
- name: 确认已登录
run: |
set -euo pipefail
docker info 2>/dev/null | grep -E "Username|Registry" || echo "已存在登录凭据(跳过详细输出)"
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
build-image:
name: 构建并推送容器镜像
needs: [preflight, meta-warmup]
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
image_tag: ${{ needs.meta-warmup.outputs.image_tag }}
image_full: ${{ needs.meta-warmup.outputs.image_full }}
image_latest: ${{ needs.meta-warmup.outputs.image_latest }}
steps:
- name: 复用 meta-warmup 结果(与 Preflight 并发省 ~5min)
run: |
set -euo pipefail
[ -n "${{ needs.meta-warmup.outputs.image_full }}" ] || { echo "ERROR: meta-warmup outputs.image_full 为空"; exit 1; }
[ -n "${{ needs.meta-warmup.outputs.image_latest }}" ] || { echo "ERROR: meta-warmup outputs.image_latest 为空"; exit 1; }
[ -n "${{ needs.meta-warmup.outputs.image_tag }}" ] || { echo "ERROR: meta-warmup outputs.image_tag 为空"; exit 1; }
[ -n "${{ needs.meta-warmup.outputs.repo_owner }}" ] || { echo "ERROR: meta-warmup outputs.repo_owner 为空"; exit 1; }
[ -n "${{ needs.meta-warmup.outputs.repo_name }}" ] || { echo "ERROR: meta-warmup outputs.repo_name 为空"; exit 1; }
echo "[meta-warmup] image_tag=${{ needs.meta-warmup.outputs.image_tag }}"
echo "[meta-warmup] image_full=${{ needs.meta-warmup.outputs.image_full }}"
echo "[meta-warmup] image_latest=${{ needs.meta-warmup.outputs.image_latest }}"
echo "[meta-warmup] repo_owner=${{ needs.meta-warmup.outputs.repo_owner }}"
echo "[meta-warmup] repo_name=${{ needs.meta-warmup.outputs.repo_name }}"
docker buildx use multi
docker buildx inspect
- name: 构建并推送镜像
env:
SKIP_CACHE: ${{ inputs.skip_build_cache }}
IMG_FULL: ${{ needs.meta-warmup.outputs.image_full }}
IMG_LATEST: ${{ needs.meta-warmup.outputs.image_latest }}
REPO_OWNER: ${{ needs.meta-warmup.outputs.repo_owner }}
REPO_NAME: ${{ needs.meta-warmup.outputs.repo_name }}
OCS_SOURCE_RAW: ${{ github.server_url }}/${{ github.repository }}
OCS_REVISION: ${{ github.sha }}
OCS_CREATED_RAW: ${{ github.event.head_commit.timestamp }}
run: |
set -euxo pipefail
OCS_SOURCE="${OCS_SOURCE_RAW,,}"
OCS_CREATED="${OCS_CREATED_RAW:-$(date -u -Iseconds)}"
IMG_FULL="${IMG_FULL,,}"
IMG_LATEST="${IMG_LATEST,,}"
BUILD_ARGS=(
--build-arg BUILDKIT_INLINE_CACHE=1
--build-arg NODE_ENV=production
--build-arg DOCKER_MIRROR="${DOCKER_MIRROR_PREFIX}"
--build-arg NODE_BUILDER_TAG="${NODE_BUILDER_TAG}"
--build-arg NODE_RUNTIME_TAG="${NODE_RUNTIME_TAG}"
)
if [ -n "${OAUTH_REDIRECT_URI:-}" ]; then BUILD_ARGS+=( --build-arg OAUTH_REDIRECT_URI="${OAUTH_REDIRECT_URI}" ); fi
if [ -n "${OAUTH_STATE_SECRET:-}" ]; then BUILD_ARGS+=( --build-arg OAUTH_STATE_SECRET="${OAUTH_STATE_SECRET}" ); fi
if [ -n "${AGGREGATE_OAUTH_APP_ID:-}" ]; then BUILD_ARGS+=( --build-arg AGGREGATE_OAUTH_APP_ID="${AGGREGATE_OAUTH_APP_ID}" ); fi
if [ -n "${AGGREGATE_OAUTH_APP_KEY:-}" ];then BUILD_ARGS+=( --build-arg AGGREGATE_OAUTH_APP_KEY="${AGGREGATE_OAUTH_APP_KEY}" ); fi
if [ -n "${AGGREGATE_OAUTH_LOGIN_TYPE:-}" ]; then BUILD_ARGS+=( --build-arg AGGREGATE_OAUTH_LOGIN_TYPE="${AGGREGATE_OAUTH_LOGIN_TYPE}" ); fi
if [ -n "${AGGREGATE_OAUTH_ENDPOINT:-}" ]; then BUILD_ARGS+=( --build-arg AGGREGATE_OAUTH_ENDPOINT="${AGGREGATE_OAUTH_ENDPOINT}" ); fi
echo "[OAuth build-args] OAUTH_REDIRECT_URI=$( [ -n "${OAUTH_REDIRECT_URI:-}" ] && echo 已注入 || echo 空 )"
echo "[OAuth build-args] OAUTH_STATE_SECRET=$( [ -n "${OAUTH_STATE_SECRET:-}" ] && echo 已注入 || echo 空 )"
echo "[OAuth build-args] AGGREGATE_OAUTH_APP_ID=$( [ -n "${AGGREGATE_OAUTH_APP_ID:-}" ] && echo 已注入 || echo 空 )"
echo "[OAuth build-args] AGGREGATE_OAUTH_APP_KEY=$( [ -n "${AGGREGATE_OAUTH_APP_KEY:-}" ] && echo 已注入 || echo 空 )"
echo "[OAuth build-args] AGGREGATE_OAUTH_LOGIN_TYPE=$( [ -n "${AGGREGATE_OAUTH_LOGIN_TYPE:-}" ] && echo 已注入 || echo 空 )"
echo "[OAuth build-args] AGGREGATE_OAUTH_ENDPOINT=$( [ -n "${AGGREGATE_OAUTH_ENDPOINT:-}" ] && echo 已注入 || echo 空 )"
if [ "${SKIP_CACHE}" = "true" ]; then
BUILD_ARGS+=( --no-cache )
echo "已启用强制重新构建(inputs.skip_build_cache=true,跳过 BuildKit 分层缓存)"
else
echo "使用 BuildKit 分层缓存加速(默认不写 --no-cache)"
fi
docker buildx build \
--file ./Dockerfile \
--platform linux/amd64 \
--load \
--provenance=false \
"${BUILD_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}" \
.
docker image inspect "${IMG_FULL}" >/dev/null
docker image inspect "${IMG_LATEST}" >/dev/null
echo "构建成功,已加载到本地 Docker daemon:${IMG_FULL} + ${IMG_LATEST}"
echo "镜像体积(Pre-push):"
docker image inspect "${IMG_FULL}" --format='{{.Size}}' | awk '{printf " Full image : %.2f MB\n", $1/1024/1024}'
echo "开始推送:${IMG_FULL}"
docker push "${IMG_FULL}"
echo "开始推送:${IMG_LATEST}"
docker push "${IMG_LATEST}"
echo "推送完成:2 个 tag(date-sha + latest)"
- name: 镜像体积与层信息
env:
IMG_FULL: ${{ needs.meta-warmup.outputs.image_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: [preflight, build-image]
if: always() && needs.preflight.result == 'success' && needs.build-image.result == 'success' && (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 }}
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
AGGREGATE_OAUTH_APP_ID: ${{ secrets.AGGREGATE_OAUTH_APP_ID }}
AGGREGATE_OAUTH_APP_KEY: ${{ secrets.AGGREGATE_OAUTH_APP_KEY }}
AGGREGATE_OAUTH_LOGIN_TYPE: ${{ secrets.AGGREGATE_OAUTH_LOGIN_TYPE }}
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
run: |
set -euo pipefail
IMAGE_NEW="${IMAGE_NEW,,}"
IMAGE_LATEST="${IMAGE_LATEST,,}"
GITHUB_REPOSITORY="${GITHUB_REPOSITORY,,}"
GITHUB_SERVER_URL="${GITHUB_SERVER_URL,,}"
GITEA_HOST="${GITEA_HOST,,}"
IMAGE_NAME="${IMAGE_NAME,,}"
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 "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "whoami; docker --version; docker compose version"
ENV_SH="/tmp/deploy_env_$$"
ENV_PAYLOAD="$(printf 'export IMAGE_NEW=%q\nexport IMAGE_LATEST=%q\nexport DEPLOY_PATH=%q\nexport GITEA_HOST=%q\nexport GH_PROXY_PREFIX=%q\nexport REGISTRY_PULL_USER=%q\nexport REGISTRY_PULL_TOKEN=%q\nexport IMAGE_NAME=%q\nexport NUXT_PUBLIC_SITE_URL=%q\nexport GITHUB_SERVER_URL=%q\nexport GITHUB_REPOSITORY=%q\nexport GITHUB_REF_NAME=%q\nexport OAUTH_REDIRECT_URI=%q\nexport OAUTH_STATE_SECRET=%q\nexport AGGREGATE_OAUTH_APP_ID=%q\nexport AGGREGATE_OAUTH_APP_KEY=%q\nexport AGGREGATE_OAUTH_LOGIN_TYPE=%q\nexport AGGREGATE_OAUTH_ENDPOINT=%q\n' \
"${IMAGE_NEW}" "${IMAGE_LATEST}" "${DEPLOY_PATH}" "${GITEA_HOST}" "${GH_PROXY_PREFIX}" \
"${REGISTRY_PULL_USER}" "${REGISTRY_PULL_TOKEN}" "${IMAGE_NAME}" "${NUXT_PUBLIC_SITE_URL}" \
"${GITHUB_SERVER_URL}" "${GITHUB_REPOSITORY}" "${GITHUB_REF_NAME}" \
"${OAUTH_REDIRECT_URI}" "${OAUTH_STATE_SECRET}" \
"${AGGREGATE_OAUTH_APP_ID}" "${AGGREGATE_OAUTH_APP_KEY}" \
"${AGGREGATE_OAUTH_LOGIN_TYPE}" "${AGGREGATE_OAUTH_ENDPOINT}")"
printf '%s' "${ENV_PAYLOAD}" | ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "cat > ${ENV_SH}"
echo "[Deploy] env script written to ${ENV_SH} on remote"
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "set -e; ls -la ${ENV_SH}; grep -q IMAGE_NEW ${ENV_SH} && echo '[Deploy] env_sh IMAGE_NEW line OK'"
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s -- '${ENV_SH}'" <<'REMOTE_SCRIPT_MAIN'
set -euxo pipefail
ENV_SH="$1"
if [ -f "$ENV_SH" ]; then
. "$ENV_SH"
rm -f "$ENV_SH"
fi
DEPLOY_DIR="${DEPLOY_PATH}"
COMPOSE="docker compose"
echo "=============================================="
echo "Executor : $(whoami)"
echo "IMAGE_NEW : ${IMAGE_NEW:-(empty!)}"
echo "DEPLOY_DIR : ${DEPLOY_DIR:-(empty!)}"
echo "=============================================="
if [ -z "${IMAGE_NEW}" ] || [ -z "${DEPLOY_DIR}" ]; then
echo "ERROR: IMAGE_NEW or DEPLOY_DIR is empty — env variables not propagated correctly."
echo "IMAGE_NEW=${IMAGE_NEW}"
echo "DEPLOY_DIR=${DEPLOY_DIR}"
exit 9
fi
mkdir -p "${DEPLOY_DIR}" 2>/dev/null || sudo mkdir -p "${DEPLOY_DIR}" 2>/dev/null || true
cd "${DEPLOY_DIR}"
TEST_FILE="${DEPLOY_DIR}/.deploy_writable_test_$$"
if ! (touch "${TEST_FILE}" && rm -f "${TEST_FILE}") 2>/dev/null; then
echo "================================================================="
echo "ERROR:deploy 用户对部署目录 ${DEPLOY_DIR} 没有写权限(Permission denied)"
echo "当前用户:$(whoami)"
echo "目录归属与权限:"
ls -ld "${DEPLOY_DIR}" || true
echo ""
echo "请在服务器上以 root 身份执行下面 3 行命令修复(复制粘贴):"
echo " chown -R deploy:docker '${DEPLOY_DIR}'"
echo " chmod 775 '${DEPLOY_DIR}'"
echo " setfacl -R -m u:deploy:rwx -m g:docker:rwx -m d:u:deploy:rwx -m d:g:docker:rwx '${DEPLOY_DIR}' 2>/dev/null || true"
echo "修复后重新触发此 workflow 即可。"
echo "================================================================="
exit 3
fi
if [ -n "${AGGREGATE_OAUTH_APP_ID}" ]; then
ENV_FILE="${DEPLOY_DIR}/.env"
touch "${ENV_FILE}"
MERGE_OK=1
upsert_env_key() {
_K="$1"; _V="$2"
if [ -z "${_V}" ]; then return 0; fi
if grep -q "^${_K}=" "${ENV_FILE}" 2>/dev/null; then
if command -v awk >/dev/null 2>&1; then
awk -v K="${_K}" -v V="${_V}" 'BEGIN{FS=OFS="="} $1==K{$2="\"" V "\""} 1' "${ENV_FILE}" > "${ENV_FILE}.tmp.$$" \
&& mv "${ENV_FILE}.tmp.$$" "${ENV_FILE}" || MERGE_OK=0
else
MERGE_OK=0
fi
else
printf '%s="%s"\n' "${_K}" "${_V}" >> "${ENV_FILE}"
fi
}
upsert_env_key OAUTH_REDIRECT_URI "${OAUTH_REDIRECT_URI}" || MERGE_OK=0
upsert_env_key OAUTH_STATE_SECRET "${OAUTH_STATE_SECRET}" || MERGE_OK=0
upsert_env_key AGGREGATE_OAUTH_APP_ID "${AGGREGATE_OAUTH_APP_ID}" || MERGE_OK=0
upsert_env_key AGGREGATE_OAUTH_APP_KEY "${AGGREGATE_OAUTH_APP_KEY}" || MERGE_OK=0
upsert_env_key AGGREGATE_OAUTH_LOGIN_TYPE "${AGGREGATE_OAUTH_LOGIN_TYPE}" || MERGE_OK=0
upsert_env_key AGGREGATE_OAUTH_ENDPOINT "${AGGREGATE_OAUTH_ENDPOINT}" || MERGE_OK=0
rm -f "${ENV_FILE}.tmp.$$"
if [ "${MERGE_OK}" = "1" ]; then
echo "[OAuth] 运行期配置已合并写入 ${ENV_FILE}(AGGREGATE_OAUTH_LOGIN_TYPE=${AGGREGATE_OAUTH_LOGIN_TYPE},AGGREATE_OAUTH_ENDPOINT=${AGGREGATE_OAUTH_ENDPOINT})"
else
echo "[OAuth] WARNING:运行期配置合并写入失败,请手动检查 ${ENV_FILE};仍尝试继续部署"
fi
else
echo "[OAuth] AGGREGATE_OAUTH_APP_ID 未设置——跳过写入远端 .env。如已在服务器手动配置过 .env 则无需担心。"
fi
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"
printf 'services:\n voicehub:\n image: %s\n' "${IMAGE_NEW}" > docker-compose.override.yml
echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
echo "--- override.yml content ---"
cat docker-compose.override.yml
echo "--- override.yml end ---"
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 + 5 个 cpus 数字兜底:APP_CPUS/APP_CPUS_LIMIT/APP_CPUS_RESERVATION/PG_CPUS/REDIS_CPUS)"
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_MAIN
rm -f "${SSH_KEY_FILE}"