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] 双成功校验
This commit is contained in:
+124
-144
@@ -36,12 +36,9 @@ env:
|
|||||||
APT_MIRROR_UBUNTU_SECURITY_HTTP: http://mirrors.tuna.tsinghua.edu.cn/ubuntu-security
|
APT_MIRROR_UBUNTU_SECURITY_HTTP: http://mirrors.tuna.tsinghua.edu.cn/ubuntu-security
|
||||||
HTTP_PROXY_EXAMPLE: ""
|
HTTP_PROXY_EXAMPLE: ""
|
||||||
HTTPS_PROXY_EXAMPLE: ""
|
HTTPS_PROXY_EXAMPLE: ""
|
||||||
# ========== Docker 官方镜像国内加速(Runner 拉 node:alpine / buildkit 等最常见卡点) ==========
|
|
||||||
# 注意:如果你知道哪个镜像源在内网最快,可以在仓库 Variables 里覆盖 DOCKER_REGISTRY_MIRROR_1/2/3
|
|
||||||
DOCKER_REGISTRY_MIRROR_1: https://docker.m.daocloud.io
|
DOCKER_REGISTRY_MIRROR_1: https://docker.m.daocloud.io
|
||||||
DOCKER_REGISTRY_MIRROR_2: https://docker.1panel.live
|
DOCKER_REGISTRY_MIRROR_2: https://docker.1panel.live
|
||||||
DOCKER_REGISTRY_MIRROR_3: https://docker.1ms.run
|
DOCKER_REGISTRY_MIRROR_3: https://docker.1ms.run
|
||||||
# Dockerfile 内 FROM 前缀替换(ARG DOCKER_MIRROR=…),避免 buildx build 内部 FROM node:22-alpine 再被 Hub 限速
|
|
||||||
DOCKER_MIRROR_PREFIX: docker.m.daocloud.io/library/
|
DOCKER_MIRROR_PREFIX: docker.m.daocloud.io/library/
|
||||||
NODE_BUILDER_TAG: "22-alpine"
|
NODE_BUILDER_TAG: "22-alpine"
|
||||||
NODE_RUNTIME_TAG: "22-alpine"
|
NODE_RUNTIME_TAG: "22-alpine"
|
||||||
@@ -50,50 +47,34 @@ jobs:
|
|||||||
preflight:
|
preflight:
|
||||||
name: Preflight 语法与构建检查
|
name: Preflight 语法与构建检查
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 12
|
||||||
# 关键修复 1:去掉 container.image: node:22-bookworm-slim
|
|
||||||
# Runner 在 Set up job 阶段会先 docker pull,国内连 Docker Hub 被限速会一直卡转圈。
|
|
||||||
# 关键修复 2:去掉 uses: actions/setup-node@v4
|
|
||||||
# setup-node 会直连 GitHub Releases (github.com/actions/node-versions/releases/download/...)
|
|
||||||
# 下载 Node 二进制,国内被限速 → 一直卡在 Acquiring node-v*-linux-x64.tar.gz。
|
|
||||||
# 与 build-image job「禁止第三方 uses、全部用代理/国内源」风格完全对齐:
|
|
||||||
# - Runner 自带 node ≥22 直接用
|
|
||||||
# - 否则优先 npmmirror 国内镜像下载 Node 22 二进制
|
|
||||||
# - 最后兜底用 GH_PROXY_PREFIX 代理 GitHub node-versions Releases 直链
|
|
||||||
env:
|
env:
|
||||||
NODE_MAJOR: "22"
|
NODE_MAJOR: "22"
|
||||||
NODE_VERSION_FALLBACK: "22.13.0"
|
NODE_VERSION_FALLBACK: "22.13.0"
|
||||||
NODE_MIRROR_PRIMARY: "https://registry.npmmirror.com/-/binary/node"
|
NODE_MIRROR_PRIMARY: "https://registry.npmmirror.com/-/binary/node"
|
||||||
NODE_OPTIONS: "--max-old-space-size=10240 --no-warnings --expose-gc"
|
NODE_OPTIONS: "--max-old-space-size=8192 --no-warnings --expose-gc"
|
||||||
NITRO_PRESET: "node-server"
|
NITRO_PRESET: "node-server"
|
||||||
DISABLE_ROLLDOWN: "1"
|
DISABLE_ROLLDOWN: "1"
|
||||||
NITRO_MINIFY: "false"
|
NITRO_MINIFY: "false"
|
||||||
|
VOICEHUB_NPM_CACHE_DIR: "${HOME}/.cache/voicehub-npm"
|
||||||
steps:
|
steps:
|
||||||
- name: 安装基础运行依赖(git/curl/ca-certificates/tar/xz-utils,Runner 自带)
|
- name: 安装基础依赖 + 确保 Node 22 可用 + 校验 node/git/npm 可用
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
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 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 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 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 xz || command -v unxz || (apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends xz-utils) || true
|
||||||
# 预先打印 Runner 自带版本,方便排障
|
|
||||||
command -v node && echo "系统 node: $(node -v)" || echo "系统 node: 未安装"
|
command -v node && echo "系统 node: $(node -v)" || echo "系统 node: 未安装"
|
||||||
command -v npm && echo "系统 npm: $(npm -v)" || echo "系统 npm: 未安装"
|
command -v npm && echo "系统 npm: $(npm -v)" || echo "系统 npm: 未安装"
|
||||||
|
|
||||||
- name: 确保 Node.js 22 可用(npmmirror 优先 / gh-proxy 兜底 / 绝不直连 GitHub)
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
# 先判断:当前系统 node 主版本 ≥ NODE_MAJOR 直接跳过
|
|
||||||
if command -v node >/dev/null 2>&1; then
|
if command -v node >/dev/null 2>&1; then
|
||||||
CUR_NODE_MAJOR="$(node -v | tr -d 'v' | cut -d. -f1 || echo 0)"
|
CUR_NODE_MAJOR="$(node -v | tr -d 'v' | cut -d. -f1 || echo 0)"
|
||||||
if [ "${CUR_NODE_MAJOR:-0}" -ge "${NODE_MAJOR}" ]; then
|
if [ "${CUR_NODE_MAJOR:-0}" -ge "${NODE_MAJOR}" ]; then
|
||||||
echo "Runner 已自带 Node $(node -v)(≥ v${NODE_MAJOR}.x),直接复用,不下载"
|
echo "Runner 已自带 Node $(node -v)(≥ v${NODE_MAJOR}.x),直接复用,不下载"
|
||||||
node -v; npm -v
|
node -v; npm -v
|
||||||
exit 0
|
else
|
||||||
fi
|
|
||||||
echo "Runner 自带 Node v${CUR_NODE_MAJOR} 不足 v${NODE_MAJOR}.x,手动升级"
|
echo "Runner 自带 Node v${CUR_NODE_MAJOR} 不足 v${NODE_MAJOR}.x,手动升级"
|
||||||
fi
|
|
||||||
|
|
||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
x86_64|amd64) PLAT="linux-x64" ;;
|
x86_64|amd64) PLAT="linux-x64" ;;
|
||||||
@@ -102,14 +83,11 @@ jobs:
|
|||||||
*) echo "不支持的架构:$ARCH" >&2; exit 1 ;;
|
*) echo "不支持的架构:$ARCH" >&2; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
echo "目标架构:$PLAT"
|
echo "目标架构:$PLAT"
|
||||||
|
|
||||||
INSTALL_DIR="${RUNNER_TEMP:-/tmp}/node-${NODE_MAJOR}"
|
INSTALL_DIR="${RUNNER_TEMP:-/tmp}/node-${NODE_MAJOR}"
|
||||||
mkdir -p "$INSTALL_DIR"
|
mkdir -p "$INSTALL_DIR"
|
||||||
|
|
||||||
# 找 Node 22 最新 patch 版本号:不用 GitHub API,走 npmmirror 的 index.json 解析
|
|
||||||
NODE_VER="${NODE_VERSION_FALLBACK}"
|
NODE_VER="${NODE_VERSION_FALLBACK}"
|
||||||
if command -v python3 >/dev/null 2>&1; then
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
PY_NODE_B64="aW1wb3J0IGpzb24sIHJlLCBzeXMKdHJ5OgogICAgZGF0YSA9IGpzb24ubG9hZChzeXMuc3RkaW4pCmV4Y2VwdCBFeGNlcHRpb246CiAgICBkYXRhID0gW10KdGFyZ2V0ID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiMjIiCmJlc3RfdHJpcGxlID0gTm9uZQpiZXN0X3YgPSAiIgpmb3IgZCBpbiBkYXRhOgogICAgdiA9IGQuZ2V0KCJ2ZXJzaW9uIiwgIiIpCiAgICBtID0gcmUubWF0Y2gociJedihcZCspXC4oXGQrKVwuKFxkKykiLCB2KQogICAgaWYgbm90IG06IGNvbnRpbnVlCiAgICBtYWpvciwgbWlub3IsIHBhdGNoID0gbS5ncm91cHMoKQogICAgaWYgbWFqb3IgIT0gdGFyZ2V0OiBjb250aW51ZQogICAgdHJpcGxlID0gKGludChtaW5vciksIGludChwYXRjaCkpCiAgICBpZiAoYmVzdF90cmlwbGUgaXMgTm9uZSkgb3IgKHRyaXBsZSA+IGJlc3RfdHJpcGxlKToKICAgICAgICBiZXN0X3RyaXBsZSA9IHRyaXBsZQogICAgICAgIGJlc3RfdiA9IGYie21ham9yfS57bWlub3J9LntwYXRjaH0iCmlmIGJlc3RfdjoKICAgIHByaW50KGJlc3Rfdik="
|
PY_NODE_B64="aW1wb3J0IGpzb24sIHJlLCBzeXMKdHJ5OgogICAgZGF0YSA9IGpzb24ubG9hZChzeXMuc3RkaW4pCmV4Y2VwdCBFeGNlcHRpb246CiAgICBkYXRhID0gW10KdGFyZ2V0ID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiMjIiCmJlc3RfdHJpcGxlID0gTm9uZQpiZXN0X3YgPSAiIgpmb3IgZCBpbiBkYXRhOgogICAgdiA9IGQuZ2V0KCJ2ZXJzaW9uIiwgIiIpCiAgICBtID0gcmUubWF0Y2gociJedihcZCspXC4oXGQrKVwuKFxkKykiLCB2KQogICAgaWYgbm90IG06IGNvbnRpbnVlCiAgICBtYWpvciwgbWlub3IsIHBhdGNoID0gbS5ncm91cHMoKQogICAgaWYgbWFqb3IgIT0gdGFyZ2V0OiBjb250aW51ZQogICAgdHJpcGxlID0gKGludChtaW5vciksIGludChwYXRjaCkpCiAgICBpZiAoYmVzdF90cmlwbGUgaXMgTm9uZSkgb3IgKHRyaXBsZSA+IGJlc3RfdHJpcGxlKToKICAgICAgICBiZXN0X3RyaXBsZSA9IHRyaXBsZQogICAgICAgIGJlc3RfdjAgPSBmInttYWpvcn0ue21pbm9yfS57cGF0Y2h9IgppZiBiZXN0X3Y6CiAgICBwcmludChiZXN0X3Yp"
|
||||||
REMOTE_LATEST=""
|
REMOTE_LATEST=""
|
||||||
REMOTE_LATEST="$(curl -fsSL --max-time 20 "${NODE_MIRROR_PRIMARY}/index.json" 2>/dev/null \
|
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)"
|
| printf '%s' "$PY_NODE_B64" | base64 -d | python3 - "${NODE_MAJOR}" || true)"
|
||||||
@@ -124,12 +102,8 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
TAR_XZ="node-v${NODE_VER}-${PLAT}.tar.xz"
|
TAR_XZ="node-v${NODE_VER}-${PLAT}.tar.xz"
|
||||||
mkdir -p /tmp/node-dl-cache
|
mkdir -p /tmp/node-dl-cache
|
||||||
|
|
||||||
# 下载 URL 1:npmmirror 国内二进制镜像(首选,国内最快)
|
|
||||||
URL_1="${NODE_MIRROR_PRIMARY}/v${NODE_VER}/${TAR_XZ}"
|
URL_1="${NODE_MIRROR_PRIMARY}/v${NODE_VER}/${TAR_XZ}"
|
||||||
# 下载 URL 2:nodejs.org 官方直链 + GH_PROXY 前缀兜底(绝不直连 GitHub.com)
|
|
||||||
URL_2="${GH_PROXY_PREFIX}https://nodejs.org/dist/v${NODE_VER}/${TAR_XZ}"
|
URL_2="${GH_PROXY_PREFIX}https://nodejs.org/dist/v${NODE_VER}/${TAR_XZ}"
|
||||||
|
|
||||||
DL_OK=0
|
DL_OK=0
|
||||||
for URL in "$URL_1" "$URL_2"; do
|
for URL in "$URL_1" "$URL_2"; do
|
||||||
echo "尝试下载:$URL"
|
echo "尝试下载:$URL"
|
||||||
@@ -144,8 +118,6 @@ jobs:
|
|||||||
echo "所有 Node 下载源均失败(npmmirror + gh-proxy),中止" >&2
|
echo "所有 Node 下载源均失败(npmmirror + gh-proxy),中止" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 解压并加到 PATH,GITHUB_ENV 注入所有后续 step 自动继承
|
|
||||||
tar -xJf "/tmp/node-dl-cache/${TAR_XZ}" -C "$INSTALL_DIR" --strip-components=1
|
tar -xJf "/tmp/node-dl-cache/${TAR_XZ}" -C "$INSTALL_DIR" --strip-components=1
|
||||||
if [ -f "$GITHUB_ENV" ]; then
|
if [ -f "$GITHUB_ENV" ]; then
|
||||||
echo "PATH=$INSTALL_DIR/bin:$PATH" >> "$GITHUB_ENV"
|
echo "PATH=$INSTALL_DIR/bin:$PATH" >> "$GITHUB_ENV"
|
||||||
@@ -154,15 +126,64 @@ jobs:
|
|||||||
hash -r 2>/dev/null || true
|
hash -r 2>/dev/null || true
|
||||||
node -v
|
node -v
|
||||||
npm -v
|
npm -v
|
||||||
# 把 npm 全局前缀也设在这个目录下,避免权限问题
|
|
||||||
npm config set prefix "$INSTALL_DIR/npm-global" 2>/dev/null || true
|
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
|
||||||
|
|
||||||
- name: 校验 Node + npm + git 可用
|
|
||||||
run: |
|
|
||||||
set -euxo pipefail
|
|
||||||
# 全局 git 代理:任何 git clone/fetch/pull https://github.com/... 自动走 gh-proxy 前缀
|
|
||||||
git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
||||||
# npm registry 走 npmmirror,避免后续 npm ci 也卡
|
|
||||||
npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
|
npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
|
||||||
git --version
|
git --version
|
||||||
node -v
|
node -v
|
||||||
@@ -178,7 +199,7 @@ jobs:
|
|||||||
git clone --depth 1 -b "${BRANCH}" "${REPO_URL}" .
|
git clone --depth 1 -b "${BRANCH}" "${REPO_URL}" .
|
||||||
git rev-parse --short HEAD
|
git rev-parse --short HEAD
|
||||||
|
|
||||||
- name: 注入 OAuth secrets 到 GITHUB_ENV(所有后续 step 自动继承,避免 step.env 在某些 Gitea Actions 版本中不生效)
|
- name: 注入 OAuth secrets 到 GITHUB_ENV
|
||||||
env:
|
env:
|
||||||
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
|
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
|
||||||
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
|
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
|
||||||
@@ -188,36 +209,26 @@ jobs:
|
|||||||
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
|
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
OAUTH_ENV_FILE="$GITHUB_ENV"
|
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)"
|
||||||
# 所有变量只要非空就写入 GITHUB_ENV;写入时用双引号包住值,避免包含 ://[provider]/ 等特殊字符时被错误解析
|
|
||||||
_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] 写入完成;后续所有 step 都会自动把上述变量作为进程级 process.env.* 继承(包括 nuxt build)"
|
|
||||||
|
|
||||||
- name: 安装依赖(npm ci,含网络超时兜底:双 registry 切换 + 3 次重试 + fetch-retires 5 次 + 5 分钟 fetch-timeout)
|
- name: 安装依赖(npm ci,含 Runner 本地磁盘缓存 + 双源重试)
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# npmmirror 双域名容错(主站 registry.npmmirror.com + 腾讯云镜像 mirrors.cloud.tencent.com/npm/,两套独立 CDN 节点)
|
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_PRIMARY="${NPM_REGISTRY_PRIMARY:-https://registry.npmmirror.com}"
|
||||||
NPM_REGISTRY_FALLBACK="${NPM_REGISTRY_FALLBACK:-https://mirrors.cloud.tencent.com/npm/}"
|
NPM_REGISTRY_FALLBACK="${NPM_REGISTRY_FALLBACK:-https://mirrors.cloud.tencent.com/npm/}"
|
||||||
MAX_RETRY=3
|
MAX_RETRY=2
|
||||||
DELAY_SEC=15
|
DELAY_SEC=15
|
||||||
CI_OK=0
|
CI_OK=0
|
||||||
for ATTEMPT in $(seq 1 $MAX_RETRY); do
|
for ATTEMPT in $(seq 1 $MAX_RETRY); do
|
||||||
# 奇数次用主站、偶数次切备用域名(npmmirror 主站 429/超时/断流时兜底,避免一直卡一个节点)
|
|
||||||
if [ $(( ATTEMPT % 2 )) -eq 1 ]; then
|
if [ $(( ATTEMPT % 2 )) -eq 1 ]; then
|
||||||
CUR_REGISTRY="${NPM_REGISTRY_PRIMARY}"
|
CUR_REGISTRY="${NPM_REGISTRY_PRIMARY}"
|
||||||
else
|
else
|
||||||
@@ -229,9 +240,7 @@ jobs:
|
|||||||
npm config set audit false
|
npm config set audit false
|
||||||
npm config set prefer-offline true 2>/dev/null || true
|
npm config set prefer-offline true 2>/dev/null || true
|
||||||
npm config set cache-min 3600 2>/dev/null || true
|
npm config set cache-min 3600 2>/dev/null || true
|
||||||
# fetch-retries: npm 内部单次请求失败重试(read ETIMEDOUT / ECONNRESET 等网络抖动)
|
|
||||||
npm config set fetch-retries 5 2>/dev/null || true
|
npm config set fetch-retries 5 2>/dev/null || true
|
||||||
# fetch-timeout=300000ms(5 分钟):esbuild/@swc 等大型原生 prebuilt 包慢链路时避免 30s 读超时
|
|
||||||
npm config set fetch-retry-maxtimeout 300000 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-timeout 300000 2>/dev/null || true
|
||||||
npm config set fetch-retry-mintimeout 20000 2>/dev/null || true
|
npm config set fetch-retry-mintimeout 20000 2>/dev/null || true
|
||||||
@@ -248,15 +257,14 @@ jobs:
|
|||||||
echo " 可在 Variables 中覆盖 NPM_REGISTRY_PRIMARY / NPM_REGISTRY_FALLBACK,或在 Runner 宿主机手动配 npm 代理" >&2
|
echo " 可在 Variables 中覆盖 NPM_REGISTRY_PRIMARY / NPM_REGISTRY_FALLBACK,或在 Runner 宿主机手动配 npm 代理" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
tar -czf "${CACHE_FILE}" node_modules
|
||||||
|
echo "[npm cache] 已写入缓存: ${CACHE_FILE}"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: 检查 package.json 无 pnpm 残留
|
- name: 检查 package.json 无 pnpm 残留
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
if grep -qE '"packageManager"\s*:\s*"pnpm' package.json; then
|
grep -qE '"packageManager"\s*:\s*"pnpm' package.json && { echo "package.json 中发现 pnpm 包管理器声明,与要求冲突,构建中止"; exit 1; }; echo "package.json 未发现 pnpm 声明"
|
||||||
echo "package.json 中发现 pnpm 包管理器声明,与要求冲突,构建中止"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo "package.json 未发现 pnpm 声明"
|
|
||||||
|
|
||||||
- name: 执行 nuxt build
|
- name: 执行 nuxt build
|
||||||
env:
|
env:
|
||||||
@@ -276,14 +284,12 @@ jobs:
|
|||||||
export NITRO_PRESET=node-server
|
export NITRO_PRESET=node-server
|
||||||
export NITRO_MINIFY=false
|
export NITRO_MINIFY=false
|
||||||
export DISABLE_ROLLDOWN=1
|
export DISABLE_ROLLDOWN=1
|
||||||
# OAuth 相关 secrets 已通过 env: 注入,在此显式 export 一遍确保子进程继承
|
|
||||||
export OAUTH_REDIRECT_URI="${OAUTH_REDIRECT_URI:-}"
|
export OAUTH_REDIRECT_URI="${OAUTH_REDIRECT_URI:-}"
|
||||||
export OAUTH_STATE_SECRET="${OAUTH_STATE_SECRET:-}"
|
export OAUTH_STATE_SECRET="${OAUTH_STATE_SECRET:-}"
|
||||||
export AGGREGATE_OAUTH_APP_ID="${AGGREGATE_OAUTH_APP_ID:-}"
|
export AGGREGATE_OAUTH_APP_ID="${AGGREGATE_OAUTH_APP_ID:-}"
|
||||||
export AGGREGATE_OAUTH_APP_KEY="${AGGREGATE_OAUTH_APP_KEY:-}"
|
export AGGREGATE_OAUTH_APP_KEY="${AGGREGATE_OAUTH_APP_KEY:-}"
|
||||||
export AGGREGATE_OAUTH_LOGIN_TYPE="${AGGREGATE_OAUTH_LOGIN_TYPE:-}"
|
export AGGREGATE_OAUTH_LOGIN_TYPE="${AGGREGATE_OAUTH_LOGIN_TYPE:-}"
|
||||||
export AGGREGATE_OAUTH_ENDPOINT="${AGGREGATE_OAUTH_ENDPOINT:-}"
|
export AGGREGATE_OAUTH_ENDPOINT="${AGGREGATE_OAUTH_ENDPOINT:-}"
|
||||||
# 构建期诊断:仅展示 AGGREGATE_OAUTH_APP_ID 是否有值(不打印敏感信息)
|
|
||||||
if [ -n "${AGGREGATE_OAUTH_APP_ID}" ]; then
|
if [ -n "${AGGREGATE_OAUTH_APP_ID}" ]; then
|
||||||
echo "[OAuth] build-time AGGREGATE_OAUTH_APP_ID 已配置(前端第三方登录按钮会显示), LOGIN_TYPE=${AGGREGATE_OAUTH_LOGIN_TYPE:-<unset>}"
|
echo "[OAuth] build-time AGGREGATE_OAUTH_APP_ID 已配置(前端第三方登录按钮会显示), LOGIN_TYPE=${AGGREGATE_OAUTH_LOGIN_TYPE:-<unset>}"
|
||||||
else
|
else
|
||||||
@@ -296,20 +302,20 @@ jobs:
|
|||||||
echo "=== 构建后内存状态 ==="
|
echo "=== 构建后内存状态 ==="
|
||||||
free -h || true
|
free -h || true
|
||||||
|
|
||||||
build-image:
|
meta-warmup:
|
||||||
name: 构建并推送容器镜像
|
name: 镜像构建·预热(与 Preflight 并发:系统/镜像源/Builder 初始化)
|
||||||
needs: preflight
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 40
|
timeout-minutes: 15
|
||||||
outputs:
|
outputs:
|
||||||
image_tag: ${{ steps.meta.outputs.tag }}
|
image_tag: ${{ steps.meta.outputs.tag }}
|
||||||
image_full: ${{ steps.meta.outputs.full }}
|
image_full: ${{ steps.meta.outputs.full }}
|
||||||
image_latest: ${{ steps.meta.outputs.latest }}
|
image_latest: ${{ steps.meta.outputs.latest }}
|
||||||
|
repo_owner: ${{ steps.meta.outputs.repo_owner }}
|
||||||
|
repo_name: ${{ steps.meta.outputs.repo_name }}
|
||||||
steps:
|
steps:
|
||||||
- name: 系统环境准备(Ubuntu 22.04 默认镜像源换清华 + 全局 GitHub gh-proxy)
|
- name: 系统环境准备(Ubuntu 22.04 默认镜像源换清华 + 全局 GitHub gh-proxy)
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
# ========== Step 0:先装 ca-certificates(默认 HTTP archive.ubuntu.com 不需要证书,解鸡生蛋) ==========
|
|
||||||
if command -v apt-get >/dev/null 2>&1; then
|
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 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 || \
|
sudo DEBIAN_FRONTEND=noninteractive apt-get install -yqq --no-install-recommends ca-certificates 2>/dev/null || \
|
||||||
@@ -317,8 +323,6 @@ jobs:
|
|||||||
sudo update-ca-certificates 2>/dev/null || update-ca-certificates 2>/dev/null || true
|
sudo update-ca-certificates 2>/dev/null || update-ca-certificates 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ========== Step 1:有了证书后换 HTTPS 清华源 ==========
|
|
||||||
# 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
|
if command -v apt-get >/dev/null 2>&1 && [ -f /etc/apt/sources.list ]; then
|
||||||
sudo sed -i \
|
sudo sed -i \
|
||||||
-e 's|http://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
|
-e 's|http://archive.ubuntu.com/ubuntu|'"${APT_MIRROR_UBUNTU}"'|g' \
|
||||||
@@ -337,16 +341,9 @@ jobs:
|
|||||||
"${f}" 2>/dev/null || true
|
"${f}" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
# 全局 git:所有 https://github.com/ 自动走 gh-proxy 前缀
|
|
||||||
sudo git config --system url."${GH_PROXY_PREFIX}https://github.com/".insteadOf "https://github.com/" || true
|
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
|
npm config set registry https://registry.npmmirror.com >/dev/null 2>&1 || true
|
||||||
|
|
||||||
# ========== 关键修复:宿主机 Docker daemon 先注入国内 registry mirrors ==========
|
|
||||||
# 原因:Runner 任何 docker pull / buildx create(moby/buildkit 拉基础镜像)都要先经过 daemon
|
|
||||||
# 如果直接连 docker.io,国内会被限速到 <200KB/s,导致 Setup / BuildKit 拉镜像一直卡。
|
|
||||||
# 注意:此处用 PY_CODE 字符串 + printf 管道传 Python 代码给 stdin,
|
|
||||||
# 避免 <<'PY' heredoc 结构与 YAML run:| 块标量缩进冲突导致的 could not find expected ':' 报错。
|
|
||||||
DOCKER_DAEMON_CONF="/etc/docker/daemon.json"
|
DOCKER_DAEMON_CONF="/etc/docker/daemon.json"
|
||||||
if command -v docker >/dev/null 2>&1 && command -v sudo >/dev/null 2>&1; then
|
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
|
sudo mkdir -p /etc/docker 2>/dev/null || true
|
||||||
@@ -360,7 +357,6 @@ jobs:
|
|||||||
echo "${NEW_DAEMON_JSON}" | sudo tee "${DOCKER_DAEMON_CONF}" >/dev/null
|
echo "${NEW_DAEMON_JSON}" | sudo tee "${DOCKER_DAEMON_CONF}" >/dev/null
|
||||||
echo "[Docker] daemon.json 已注入 registry-mirrors:"
|
echo "[Docker] daemon.json 已注入 registry-mirrors:"
|
||||||
sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || true
|
sudo cat "${DOCKER_DAEMON_CONF}" 2>/dev/null || true
|
||||||
# 非侵入式重启 docker;runner 没 systemd 时用 service 兜底;失败也跳过(buildkitd.toml 内部仍有 mirrors)
|
|
||||||
sudo systemctl restart docker 2>/dev/null \
|
sudo systemctl restart docker 2>/dev/null \
|
||||||
|| sudo service docker restart 2>/dev/null \
|
|| sudo service docker restart 2>/dev/null \
|
||||||
|| echo "[Docker] systemctl/service 重启失败,跳过(后续 buildx builder 内部仍有 registry mirrors)"
|
|| echo "[Docker] systemctl/service 重启失败,跳过(后续 buildx builder 内部仍有 registry mirrors)"
|
||||||
@@ -384,7 +380,7 @@ jobs:
|
|||||||
docker version
|
docker version
|
||||||
docker buildx version || echo "buildx 未安装,将使用 docker build --push"
|
docker buildx version || echo "buildx 未安装,将使用 docker build --push"
|
||||||
|
|
||||||
- name: 注入 OAuth secrets 到 GITHUB_ENV(Docker build-arg 与运行期容器 .env 共用同一来源,构建期注入对应镜像 .output)
|
- name: 注入 OAuth secrets 到 GITHUB_ENV
|
||||||
env:
|
env:
|
||||||
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
|
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
|
||||||
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
|
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
|
||||||
@@ -416,7 +412,6 @@ jobs:
|
|||||||
id: meta
|
id: meta
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Docker Registry 规范:仓库路径、owner、repo name 必须全小写(Henry_Du/BYW-VoiceHub → henry_du/byw-voicehub)
|
|
||||||
REPO_OWNER="${GITHUB_REPOSITORY_OWNER,,}"
|
REPO_OWNER="${GITHUB_REPOSITORY_OWNER,,}"
|
||||||
REPO_NAME="${IMAGE_NAME,,}"
|
REPO_NAME="${IMAGE_NAME,,}"
|
||||||
SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
|
SHA="${GITHUB_SHA:-$(git rev-parse HEAD)}"
|
||||||
@@ -438,13 +433,12 @@ jobs:
|
|||||||
echo "Cache ref : ${CACHE_REF_INNER}"
|
echo "Cache ref : ${CACHE_REF_INNER}"
|
||||||
echo "======================================"
|
echo "======================================"
|
||||||
|
|
||||||
- name: 初始化 Docker Buildx builder(带国内 registry-mirrors + 内置 Gitea Basic Auth)
|
- name: 初始化 Docker Buildx builder
|
||||||
env:
|
env:
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
||||||
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN || github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
# 先校验凭据非空(避免 buildkitd.toml 写空用户名/密码导致 push 时 401)
|
|
||||||
if [ -z "${REGISTRY_TOKEN}" ]; then
|
if [ -z "${REGISTRY_TOKEN}" ]; then
|
||||||
echo "ERROR: REGISTRY_TOKEN 为空。请在 Repository Secrets 中新建 REGISTRY_TOKEN(个人应用令牌,权限 read:package + write:package + read:repository)"
|
echo "ERROR: REGISTRY_TOKEN 为空。请在 Repository Secrets 中新建 REGISTRY_TOKEN(个人应用令牌,权限 read:package + write:package + read:repository)"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -453,17 +447,13 @@ jobs:
|
|||||||
BUILDER_NAME="multi"
|
BUILDER_NAME="multi"
|
||||||
BUILDKIT_IMAGE="moby/buildkit:buildx-stable-1"
|
BUILDKIT_IMAGE="moby/buildkit:buildx-stable-1"
|
||||||
BKK_CFG="$(mktemp -t buildkitd_XXXXXX.toml)"
|
BKK_CFG="$(mktemp -t buildkitd_XXXXXX.toml)"
|
||||||
# 用 printf 逐行写 buildkitd 配置,避免 YAML parser 把 heredoc 内部的 [...] 误判为 YAML key
|
|
||||||
{
|
{
|
||||||
printf '[worker.oci]\n max-parallelism = 2\n\n'
|
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."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."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."k8s.gcr.io"]\n mirrors = ["k8s-gcr.m.daocloud.io","registry.cn-hangzhou.aliyuncs.com/google_containers"]\n\n'
|
||||||
# 关键:Gitea Container Registry 直接用 Basic Auth 凭据写死在 buildkitd.toml
|
|
||||||
# 绕开 buildkitd 的 OAuth2 flow(Gitea 1.27.x 有时返回空 token body:authorization server did not include a token)
|
|
||||||
printf '[registry."%s"]\n username = "%s"\n password = "%s"\n' "${GITEA_HOST}" "${REGISTRY_USER}" "${REGISTRY_TOKEN}"
|
printf '[registry."%s"]\n username = "%s"\n password = "%s"\n' "${GITEA_HOST}" "${REGISTRY_USER}" "${REGISTRY_TOKEN}"
|
||||||
} > "${BKK_CFG}"
|
} > "${BKK_CFG}"
|
||||||
# 预拉 buildkitd 基础镜像(优先 5 个国内镜像 fallback,避免 Docker Hub <200KB/s)
|
|
||||||
PULLED=0
|
PULLED=0
|
||||||
for MIRROR in \
|
for MIRROR in \
|
||||||
"docker.m.daocloud.io/${BUILDKIT_IMAGE}" \
|
"docker.m.daocloud.io/${BUILDKIT_IMAGE}" \
|
||||||
@@ -481,11 +471,9 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
[ "${PULLED}" = "1" ] || { echo "ERROR: 无法拉取 buildkitd 基础镜像,请检查 Docker registry-mirrors"; exit 1; }
|
[ "${PULLED}" = "1" ] || { echo "ERROR: 无法拉取 buildkitd 基础镜像,请检查 Docker registry-mirrors"; exit 1; }
|
||||||
# 宿主 Docker daemon 也同步登录(后面镜像体积与层信息 Step 的 docker image inspect / docker history 要用)
|
|
||||||
printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" \
|
printf '%s' "${REGISTRY_TOKEN}" | docker login "${GITEA_HOST}" \
|
||||||
-u "${REGISTRY_USER}" --password-stdin
|
-u "${REGISTRY_USER}" --password-stdin
|
||||||
echo "宿主 Docker 登录成功:${REGISTRY_USER} @ ${GITEA_HOST}"
|
echo "宿主 Docker 登录成功:${REGISTRY_USER} @ ${GITEA_HOST}"
|
||||||
# 立即重建 buildx builder(带凭据 + registry-mirrors)
|
|
||||||
docker buildx rm "${BUILDER_NAME}" 2>/dev/null || true
|
docker buildx rm "${BUILDER_NAME}" 2>/dev/null || true
|
||||||
docker buildx create \
|
docker buildx create \
|
||||||
--name "${BUILDER_NAME}" \
|
--name "${BUILDER_NAME}" \
|
||||||
@@ -496,7 +484,6 @@ jobs:
|
|||||||
--driver-opt network=host \
|
--driver-opt network=host \
|
||||||
--driver-opt "image=${BUILDKIT_IMAGE}"
|
--driver-opt "image=${BUILDKIT_IMAGE}"
|
||||||
rm -f "${BKK_CFG}"
|
rm -f "${BKK_CFG}"
|
||||||
# gh-proxy 环境(全变量显式赋值,避免 bash tracing 边界反引号歧义)
|
|
||||||
GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
|
GIT_CONFIG_KEY_0="url.${GH_PROXY_PREFIX}https://github.com/.insteadOf"
|
||||||
GIT_CONFIG_VALUE_0="https://github.com/"
|
GIT_CONFIG_VALUE_0="https://github.com/"
|
||||||
GIT_CONFIG_KEY_1="url.${GH_PROXY_PREFIX}https://gist.github.com/.insteadOf"
|
GIT_CONFIG_KEY_1="url.${GH_PROXY_PREFIX}https://gist.github.com/.insteadOf"
|
||||||
@@ -505,31 +492,53 @@ jobs:
|
|||||||
docker buildx inspect --bootstrap
|
docker buildx inspect --bootstrap
|
||||||
echo "Builder 启动成功,buildkitd.toml 已内置 ${GITEA_HOST} Basic Auth 凭据,push 不再走 OAuth2 flow"
|
echo "Builder 启动成功,buildkitd.toml 已内置 ${GITEA_HOST} Basic Auth 凭据,push 不再走 OAuth2 flow"
|
||||||
|
|
||||||
- name: 确认已登录(凭据已注入 buildkitd.toml 与宿主 Docker daemon)
|
- name: 确认已登录
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# 前面「初始化 Docker Buildx builder」Step 已完成两次登录:
|
|
||||||
# 1. 宿主 Docker daemon(镜像体积 Step 要用);2. buildkitd.toml(buildx push 要用,Basic Auth 不走 OAuth2)
|
|
||||||
# 这里只做一个快速断言,确保宿主登录成功
|
|
||||||
docker info 2>/dev/null | grep -E "Username|Registry" || echo "已存在登录凭据(跳过详细输出)"
|
docker info 2>/dev/null | grep -E "Username|Registry" || echo "已存在登录凭据(跳过详细输出)"
|
||||||
echo "登录确认完成,REGISTRY_USER=${REGISTRY_USER:-${GITHUB_ACTOR,,}},GITEA_HOST=${GITEA_HOST}"
|
|
||||||
env:
|
env:
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER || github.actor }}
|
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: 构建并推送镜像
|
- name: 构建并推送镜像
|
||||||
env:
|
env:
|
||||||
SKIP_CACHE: ${{ inputs.skip_build_cache }}
|
SKIP_CACHE: ${{ inputs.skip_build_cache }}
|
||||||
IMG_FULL: ${{ steps.meta.outputs.full }}
|
IMG_FULL: ${{ needs.meta-warmup.outputs.image_full }}
|
||||||
IMG_LATEST: ${{ steps.meta.outputs.latest }}
|
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_SOURCE_RAW: ${{ github.server_url }}/${{ github.repository }}
|
||||||
OCS_REVISION: ${{ github.sha }}
|
OCS_REVISION: ${{ github.sha }}
|
||||||
OCS_CREATED_RAW: ${{ github.event.head_commit.timestamp }}
|
OCS_CREATED_RAW: ${{ github.event.head_commit.timestamp }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
# 兜底空值 & 全小写规范
|
|
||||||
OCS_SOURCE="${OCS_SOURCE_RAW,,}"
|
OCS_SOURCE="${OCS_SOURCE_RAW,,}"
|
||||||
OCS_CREATED="${OCS_CREATED_RAW:-$(date -u -Iseconds)}"
|
OCS_CREATED="${OCS_CREATED_RAW:-$(date -u -Iseconds)}"
|
||||||
# 再次保险:所有镜像标签字符串都确认无大写字母(Registry 硬性规范)
|
|
||||||
IMG_FULL="${IMG_FULL,,}"
|
IMG_FULL="${IMG_FULL,,}"
|
||||||
IMG_LATEST="${IMG_LATEST,,}"
|
IMG_LATEST="${IMG_LATEST,,}"
|
||||||
|
|
||||||
@@ -540,31 +549,25 @@ jobs:
|
|||||||
--build-arg NODE_BUILDER_TAG="${NODE_BUILDER_TAG}"
|
--build-arg NODE_BUILDER_TAG="${NODE_BUILDER_TAG}"
|
||||||
--build-arg NODE_RUNTIME_TAG="${NODE_RUNTIME_TAG}"
|
--build-arg NODE_RUNTIME_TAG="${NODE_RUNTIME_TAG}"
|
||||||
)
|
)
|
||||||
# ========== 关键:OAuth 6 条 secrets 通过 --build-arg 透传到 Dockerfile 的 ARG ==========
|
|
||||||
# 这样镜像内部 RUN node scripts/memory-build.js(npm run build)时,
|
|
||||||
# process.env.AGGREGATE_OAUTH_APP_ID 就能读到,nuxt.config.ts public.oauth.aggregate = !!process.env.AGGREGATE_OAUTH_APP_ID
|
|
||||||
# 才会为 true → 前端渲染绿色微信按钮,不显示占位提示
|
|
||||||
if [ -n "${OAUTH_REDIRECT_URI:-}" ]; then BUILD_ARGS+=( --build-arg OAUTH_REDIRECT_URI="${OAUTH_REDIRECT_URI}" ); fi
|
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 "${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_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_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_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
|
if [ -n "${AGGREGATE_OAUTH_ENDPOINT:-}" ]; then BUILD_ARGS+=( --build-arg AGGREGATE_OAUTH_ENDPOINT="${AGGREGATE_OAUTH_ENDPOINT}" ); fi
|
||||||
# 日志打印(只打有无,不打 value)
|
|
||||||
echo "[OAuth build-args] OAUTH_REDIRECT_URI=$( [ -n "${OAUTH_REDIRECT_URI:-}" ] && echo 已注入 || echo 空 )"
|
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] 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_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_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_LOGIN_TYPE=$( [ -n "${AGGREGATE_OAUTH_LOGIN_TYPE:-}" ] && echo 已注入 || echo 空 )"
|
||||||
echo "[OAuth build-args] AGGREGATE_OAUTH_ENDPOINT=$( [ -n "${AGGREGATE_OAUTH_ENDPOINT:-}" ] && echo 已注入 || echo 空 )"
|
echo "[OAuth build-args] AGGREGATE_OAUTH_ENDPOINT=$( [ -n "${AGGREGATE_OAUTH_ENDPOINT:-}" ] && echo 已注入 || echo 空 )"
|
||||||
# 说明:暂时关闭 registry 分层缓存(buildkitd 推 buildcache 也要认证,同样会触发 Gitea 1.27 OAuth2 空 token)
|
if [ "${SKIP_CACHE}" = "true" ]; then
|
||||||
# 改为本地 Docker daemon 缓存,已构建的层会自动复用,下次 build 速度几乎相同
|
|
||||||
BUILD_ARGS+=( --no-cache )
|
BUILD_ARGS+=( --no-cache )
|
||||||
echo "已启用强制重新构建(跳过 registry 分层缓存,避免 buildkitd 推 cache 层时 OAuth2 认证失败)"
|
echo "已启用强制重新构建(inputs.skip_build_cache=true,跳过 BuildKit 分层缓存)"
|
||||||
|
else
|
||||||
|
echo "使用 BuildKit 分层缓存加速(默认不写 --no-cache)"
|
||||||
|
fi
|
||||||
|
|
||||||
# ========== 阶段 1:构建到本地 Docker daemon(--load,不用 --push) ==========
|
|
||||||
# 关键:这一步产物在宿主 Docker 的 images 列表里(docker images 能看到),宿主 docker login 过的凭据 100% 可用
|
|
||||||
# 完全绕开 buildkitd 容器内部推镜像走 OAuth2 flow 的坑
|
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--file ./Dockerfile \
|
--file ./Dockerfile \
|
||||||
--platform linux/amd64 \
|
--platform linux/amd64 \
|
||||||
@@ -578,14 +581,12 @@ jobs:
|
|||||||
--label "org.opencontainers.image.created=${OCS_CREATED}" \
|
--label "org.opencontainers.image.created=${OCS_CREATED}" \
|
||||||
.
|
.
|
||||||
|
|
||||||
# 断言:两个 tag 都真的加载到了本地 Docker daemon
|
|
||||||
docker image inspect "${IMG_FULL}" >/dev/null
|
docker image inspect "${IMG_FULL}" >/dev/null
|
||||||
docker image inspect "${IMG_LATEST}" >/dev/null
|
docker image inspect "${IMG_LATEST}" >/dev/null
|
||||||
echo "构建成功,已加载到本地 Docker daemon:${IMG_FULL} + ${IMG_LATEST}"
|
echo "构建成功,已加载到本地 Docker daemon:${IMG_FULL} + ${IMG_LATEST}"
|
||||||
echo "镜像体积(Pre-push):"
|
echo "镜像体积(Pre-push):"
|
||||||
docker image inspect "${IMG_FULL}" --format='{{.Size}}' | awk '{printf " Full image : %.2f MB\n", $1/1024/1024}'
|
docker image inspect "${IMG_FULL}" --format='{{.Size}}' | awk '{printf " Full image : %.2f MB\n", $1/1024/1024}'
|
||||||
|
|
||||||
# ========== 阶段 2:宿主 Docker daemon 原生 push(100% 走宿主登录凭据,不会走 OAuth2) ==========
|
|
||||||
echo "开始推送:${IMG_FULL}"
|
echo "开始推送:${IMG_FULL}"
|
||||||
docker push "${IMG_FULL}"
|
docker push "${IMG_FULL}"
|
||||||
echo "开始推送:${IMG_LATEST}"
|
echo "开始推送:${IMG_LATEST}"
|
||||||
@@ -594,7 +595,7 @@ jobs:
|
|||||||
|
|
||||||
- name: 镜像体积与层信息
|
- name: 镜像体积与层信息
|
||||||
env:
|
env:
|
||||||
IMG_FULL: ${{ steps.meta.outputs.full }}
|
IMG_FULL: ${{ needs.meta-warmup.outputs.image_full }}
|
||||||
run: |
|
run: |
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
docker pull "${IMG_FULL}" || true
|
docker pull "${IMG_FULL}" || true
|
||||||
@@ -604,8 +605,8 @@ jobs:
|
|||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
name: 部署到生产服务器
|
name: 部署到生产服务器
|
||||||
needs: build-image
|
needs: [preflight, build-image]
|
||||||
if: always() && needs.build-image.result == 'success' && (github.event_name == 'push' || inputs.deploy_target == true)
|
if: always() && needs.preflight.result == 'success' && needs.build-image.result == 'success' && (github.event_name == 'push' || inputs.deploy_target == true)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 25
|
timeout-minutes: 25
|
||||||
environment:
|
environment:
|
||||||
@@ -644,7 +645,6 @@ jobs:
|
|||||||
GITHUB_SERVER_URL: ${{ github.server_url }}
|
GITHUB_SERVER_URL: ${{ github.server_url }}
|
||||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||||
# === OAuth / 第三方登录(构建期注入到 nuxt runtimeConfig;运行期写入远端 .env) ===
|
|
||||||
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
|
OAUTH_REDIRECT_URI: ${{ secrets.OAUTH_REDIRECT_URI }}
|
||||||
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
|
OAUTH_STATE_SECRET: ${{ secrets.OAUTH_STATE_SECRET }}
|
||||||
AGGREGATE_OAUTH_APP_ID: ${{ secrets.AGGREGATE_OAUTH_APP_ID }}
|
AGGREGATE_OAUTH_APP_ID: ${{ secrets.AGGREGATE_OAUTH_APP_ID }}
|
||||||
@@ -653,7 +653,6 @@ jobs:
|
|||||||
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
|
AGGREGATE_OAUTH_ENDPOINT: ${{ secrets.AGGREGATE_OAUTH_ENDPOINT }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# 全小写规范(Docker Registry 硬性要求 + Gitea raw URL 路径大小写敏感)
|
|
||||||
IMAGE_NEW="${IMAGE_NEW,,}"
|
IMAGE_NEW="${IMAGE_NEW,,}"
|
||||||
IMAGE_LATEST="${IMAGE_LATEST,,}"
|
IMAGE_LATEST="${IMAGE_LATEST,,}"
|
||||||
GITHUB_REPOSITORY="${GITHUB_REPOSITORY,,}"
|
GITHUB_REPOSITORY="${GITHUB_REPOSITORY,,}"
|
||||||
@@ -661,7 +660,6 @@ jobs:
|
|||||||
GITEA_HOST="${GITEA_HOST,,}"
|
GITEA_HOST="${GITEA_HOST,,}"
|
||||||
IMAGE_NAME="${IMAGE_NAME,,}"
|
IMAGE_NAME="${IMAGE_NAME,,}"
|
||||||
|
|
||||||
# 写私钥到临时文件(避免 stdin 转义问题)
|
|
||||||
SSH_KEY_FILE="$(mktemp -t deploy_ssh_key.XXXXXX)"
|
SSH_KEY_FILE="$(mktemp -t deploy_ssh_key.XXXXXX)"
|
||||||
printf '%s\n' "${DEPLOY_SSH_KEY}" > "${SSH_KEY_FILE}"
|
printf '%s\n' "${DEPLOY_SSH_KEY}" > "${SSH_KEY_FILE}"
|
||||||
chmod 600 "${SSH_KEY_FILE}"
|
chmod 600 "${SSH_KEY_FILE}"
|
||||||
@@ -682,12 +680,8 @@ jobs:
|
|||||||
echo "Deploy dir : ${DEPLOY_PATH}"
|
echo "Deploy dir : ${DEPLOY_PATH}"
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
|
|
||||||
# 本地预检查:ssh 先连上跑 whoami 确认连通,避免 20 分钟后才发现连不上
|
|
||||||
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "whoami; docker --version; docker compose version"
|
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "whoami; docker --version; docker compose version"
|
||||||
|
|
||||||
# 关键修复:远端 bash 前置 ENV=val 语法在 dash/sh 远端不认,变量全空。
|
|
||||||
# 用 printf + \n 拼接完整 shell 脚本通过 stdin 注入到 ssh 远端 cat,
|
|
||||||
# 不用 heredoc,避免 YAML 冒号/缩进解析误判。
|
|
||||||
ENV_SH="/tmp/deploy_env_$$"
|
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' \
|
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}" \
|
"${IMAGE_NEW}" "${IMAGE_LATEST}" "${DEPLOY_PATH}" "${GITEA_HOST}" "${GH_PROXY_PREFIX}" \
|
||||||
@@ -698,15 +692,12 @@ jobs:
|
|||||||
"${AGGREGATE_OAUTH_LOGIN_TYPE}" "${AGGREGATE_OAUTH_ENDPOINT}")"
|
"${AGGREGATE_OAUTH_LOGIN_TYPE}" "${AGGREGATE_OAUTH_ENDPOINT}")"
|
||||||
printf '%s' "${ENV_PAYLOAD}" | ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "cat > ${ENV_SH}"
|
printf '%s' "${ENV_PAYLOAD}" | ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "cat > ${ENV_SH}"
|
||||||
echo "[Deploy] env script written to ${ENV_SH} on remote"
|
echo "[Deploy] env script written to ${ENV_SH} on remote"
|
||||||
# 立即回读验证远端文件非空 & IMAGE_NEW 非空,避免后面空跑
|
|
||||||
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}" "set -e; ls -la ${ENV_SH}; grep -q IMAGE_NEW ${ENV_SH} && echo '[Deploy] env_sh IMAGE_NEW line OK'"
|
||||||
|
|
||||||
# ssh 远端 bash -s 第一个参数传 $ENV_SH 路径,主脚本 source 它并做非空断言
|
|
||||||
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s -- '${ENV_SH}'" <<'REMOTE_SCRIPT_MAIN'
|
ssh "${SSH_OPTS[@]}" "${DEPLOY_USER}@${DEPLOY_HOST}" "bash -s -- '${ENV_SH}'" <<'REMOTE_SCRIPT_MAIN'
|
||||||
set -euxo pipefail
|
set -euxo pipefail
|
||||||
ENV_SH="$1"
|
ENV_SH="$1"
|
||||||
if [ -f "$ENV_SH" ]; then
|
if [ -f "$ENV_SH" ]; then
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$ENV_SH"
|
. "$ENV_SH"
|
||||||
rm -f "$ENV_SH"
|
rm -f "$ENV_SH"
|
||||||
fi
|
fi
|
||||||
@@ -719,7 +710,6 @@ jobs:
|
|||||||
echo "IMAGE_NEW : ${IMAGE_NEW:-(empty!)}"
|
echo "IMAGE_NEW : ${IMAGE_NEW:-(empty!)}"
|
||||||
echo "DEPLOY_DIR : ${DEPLOY_DIR:-(empty!)}"
|
echo "DEPLOY_DIR : ${DEPLOY_DIR:-(empty!)}"
|
||||||
echo "=============================================="
|
echo "=============================================="
|
||||||
# 防御性断言:镜像名 / 路径不能为空,否则直接 exit 9 避免起旧容器
|
|
||||||
if [ -z "${IMAGE_NEW}" ] || [ -z "${DEPLOY_DIR}" ]; then
|
if [ -z "${IMAGE_NEW}" ] || [ -z "${DEPLOY_DIR}" ]; then
|
||||||
echo "ERROR: IMAGE_NEW or DEPLOY_DIR is empty — env variables not propagated correctly."
|
echo "ERROR: IMAGE_NEW or DEPLOY_DIR is empty — env variables not propagated correctly."
|
||||||
echo "IMAGE_NEW=${IMAGE_NEW}"
|
echo "IMAGE_NEW=${IMAGE_NEW}"
|
||||||
@@ -730,7 +720,6 @@ jobs:
|
|||||||
mkdir -p "${DEPLOY_DIR}" 2>/dev/null || sudo mkdir -p "${DEPLOY_DIR}" 2>/dev/null || true
|
mkdir -p "${DEPLOY_DIR}" 2>/dev/null || sudo mkdir -p "${DEPLOY_DIR}" 2>/dev/null || true
|
||||||
cd "${DEPLOY_DIR}"
|
cd "${DEPLOY_DIR}"
|
||||||
|
|
||||||
# ========== 目录可写性预检查(权限排查友好化) ==========
|
|
||||||
TEST_FILE="${DEPLOY_DIR}/.deploy_writable_test_$$"
|
TEST_FILE="${DEPLOY_DIR}/.deploy_writable_test_$$"
|
||||||
if ! (touch "${TEST_FILE}" && rm -f "${TEST_FILE}") 2>/dev/null; then
|
if ! (touch "${TEST_FILE}" && rm -f "${TEST_FILE}") 2>/dev/null; then
|
||||||
echo "================================================================="
|
echo "================================================================="
|
||||||
@@ -748,17 +737,13 @@ jobs:
|
|||||||
exit 3
|
exit 3
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ========== 合并写入 OAuth 配置到 .env(幂等:有 key 则覆盖,无 key 追加) ==========
|
|
||||||
# 仅当 AGGREGATE_OAUTH_APP_ID 非空时才写入,避免把空值写进远端 .env 覆盖已有配置
|
|
||||||
if [ -n "${AGGREGATE_OAUTH_APP_ID}" ]; then
|
if [ -n "${AGGREGATE_OAUTH_APP_ID}" ]; then
|
||||||
ENV_FILE="${DEPLOY_DIR}/.env"
|
ENV_FILE="${DEPLOY_DIR}/.env"
|
||||||
touch "${ENV_FILE}"
|
touch "${ENV_FILE}"
|
||||||
MERGE_OK=1
|
MERGE_OK=1
|
||||||
upsert_env_key() {
|
upsert_env_key() {
|
||||||
# $1 = KEY, $2 = VALUE;VALUE 已在调用方 printf %q 保证安全
|
|
||||||
_K="$1"; _V="$2"
|
_K="$1"; _V="$2"
|
||||||
if [ -z "${_V}" ]; then return 0; fi
|
if [ -z "${_V}" ]; then return 0; fi
|
||||||
# 远端 .env 中若 KEY= 开头存在则整行覆盖;不存在则追加
|
|
||||||
if grep -q "^${_K}=" "${ENV_FILE}" 2>/dev/null; then
|
if grep -q "^${_K}=" "${ENV_FILE}" 2>/dev/null; then
|
||||||
if command -v awk >/dev/null 2>&1; 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.$$" \
|
awk -v K="${_K}" -v V="${_V}" 'BEGIN{FS=OFS="="} $1==K{$2="\"" V "\""} 1' "${ENV_FILE}" > "${ENV_FILE}.tmp.$$" \
|
||||||
@@ -799,10 +784,6 @@ jobs:
|
|||||||
docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
docker tag "${IMAGE_NEW}" "${IMAGE_LATEST}"
|
||||||
echo "New image pulled and tagged as latest"
|
echo "New image pulled and tagged as latest"
|
||||||
|
|
||||||
# 用 printf 拼 override.yml,彻底避免 bash heredoc 的三重坑:
|
|
||||||
# 1) 结束分隔符必须行首(否则 bash 不认,把后面整段代码都写进文件)
|
|
||||||
# 2) 内层 <<'XXX' 单引号包裹导致变量不展开(image: ${IMAGE_NEW} 会是字面量)
|
|
||||||
# 3) 外层 .yml parser 把 heredoc 里的 services: 当 YAML mapping key 解析
|
|
||||||
printf 'services:\n voicehub:\n image: %s\n' "${IMAGE_NEW}" > docker-compose.override.yml
|
printf 'services:\n voicehub:\n image: %s\n' "${IMAGE_NEW}" > docker-compose.override.yml
|
||||||
echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
echo "docker-compose.override.yml written, image=${IMAGE_NEW}"
|
||||||
echo "--- override.yml content ---"
|
echo "--- override.yml content ---"
|
||||||
@@ -870,5 +851,4 @@ jobs:
|
|||||||
echo "================================================================="
|
echo "================================================================="
|
||||||
REMOTE_SCRIPT_MAIN
|
REMOTE_SCRIPT_MAIN
|
||||||
|
|
||||||
# 清理私钥临时文件
|
|
||||||
rm -f "${SSH_KEY_FILE}"
|
rm -f "${SSH_KEY_FILE}"
|
||||||
|
|||||||
Reference in New Issue
Block a user