优化CI 构建 docker image流程

This commit is contained in:
kira1928 committed 2026-02-22 02:12:26 +09:00
1 parent e130e963c7
commit adbcb3063f
6 files changed
+356 -36

No files matched your search

+128 -10
View File
@@ -81,30 +81,148 @@ jobs:
dist/**/*.7z
prerelease: ${{ contains(env.GIT_TAG, 'rc') }}
# Docker 镜像构建:按平台分矩阵,arm64 使用原生 ARM runner
# 优化策略:
# 1. amd64/arm64 使用原生 runner,避免 QEMU 模拟
# 2. 内置工具在 CI 原生环境中预下载,COPY 进镜像(避免在 QEMU 下解压 xz)
# 3. bgo 二进制直接从 artifact 获取,不再从 GitHub Releases curl 下载
release-docker-images:
runs-on: ubuntu-latest
needs:
- publish-release
needs: [publish-release]
strategy:
fail-fast: false
matrix:
include:
- platform: linux/amd64
runner: ubuntu-latest
arch: amd64
- platform: linux/arm64/v8
runner: ubuntu-24.04-arm
arch: arm64
- platform: linux/arm/v7
runner: ubuntu-24.04-arm # arm/v7 仍需 QEMU,但 ARM runner 上模拟 armv7 比 x86 模拟快
arch: arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
- run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
- run: if ! echo $GIT_TAG | grep "rc" >/dev/null; then DOCKER_TAGS=chigusa/bililive-go:$GIT_TAG,chigusa/bililive-go:latest; else DOCKER_TAGS=chigusa/bililive-go:$GIT_TAG; fi; echo "DOCKER_TAGS=$DOCKER_TAGS" >> $GITHUB_ENV
# 下载编译好的二进制 artifact(从 release-bins-sharded 产物中提取)
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: dist
# 准备 Docker 构建上下文:提取对应架构的二进制
- name: Prepare bgo binary for Docker
run: |
mkdir -p docker-context/bin
# 从 shard artifacts 中查找对应架构的 tar.gz
TARBALL=$(find dist -name "bililive-linux-${{ matrix.arch }}.tar.gz" -print -quit)
if [ -z "$TARBALL" ]; then
echo "::error::找不到 bililive-linux-${{ matrix.arch }}.tar.gz"
exit 1
fi
echo "找到二进制包: $TARBALL"
tar xzf "$TARBALL" -C docker-context/bin "bililive-linux-${{ matrix.arch }}"
mv "docker-context/bin/bililive-linux-${{ matrix.arch }}" docker-context/bin/bililive-go
chmod +x docker-context/bin/bililive-go
ls -la docker-context/bin/
# 在原生环境中预下载内置工具(避免在 QEMU 下解压 xz 的巨大开销)
- name: Pre-download built-in tools
run: |
chmod +x scripts/docker-prepare-tools.sh
scripts/docker-prepare-tools.sh "${{ matrix.arch }}" docker-context/tools
# 仅 arm/v7 需要 QEMU(用于 apt-get 安装 ffmpeg 等)
- name: Set up QEMU
if: matrix.arch == 'arm'
uses: docker/setup-qemu-action@v3
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build the Docker image
# 构建并推送单平台镜像(by digest,不关联 tag)
- name: Build and push single-platform image
id: build
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile
build-args: tag=${{ env.GIT_TAG }}
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7
platforms: ${{ matrix.platform }}
push: true
tags: ${{ env.DOCKER_TAGS }}
cache-from: type=gha,scope=bililive-go
cache-to: type=gha,mode=max,scope=bililive-go
tags: ""
outputs: type=image,name=chigusa/bililive-go,push-by-digest=true,name-canonical=true
cache-from: type=gha,scope=docker-${{ matrix.arch }}
cache-to: type=gha,mode=max,scope=docker-${{ matrix.arch }}
# 导出 digest 供后续合并
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: docker-digest-${{ matrix.arch }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
# 合并各平台镜像为 multi-arch manifest
merge-docker-manifest:
runs-on: ubuntu-latest
needs: [release-docker-images]
steps:
- uses: actions/checkout@v4
- run: echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
- run: |
if ! echo $GIT_TAG | grep "rc" >/dev/null; then
DOCKER_TAGS="chigusa/bililive-go:$GIT_TAG,chigusa/bililive-go:latest"
else
DOCKER_TAGS="chigusa/bililive-go:$GIT_TAG"
fi
echo "DOCKER_TAGS=$DOCKER_TAGS" >> $GITHUB_ENV
- name: Download all digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: docker-digest-*
merge-multiple: true
- name: Docker Setup Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Create and push multi-arch manifest
working-directory: /tmp/digests
run: |
# 构建 digest 参数列表
DIGEST_ARGS=""
for dgst in *; do
DIGEST_ARGS="$DIGEST_ARGS chigusa/bililive-go@sha256:$dgst"
done
# 为每个 tag 创建 manifest
IFS=',' read -ra TAGS <<< "$DOCKER_TAGS"
for tag in "${TAGS[@]}"; do
echo "创建 manifest: $tag"
docker buildx imagetools create -t "$tag" $DIGEST_ARGS
done
- name: Inspect final image
run: docker buildx imagetools inspect "chigusa/bililive-go:$GIT_TAG"
+6
View File
@@ -192,3 +192,9 @@ test/launcher-config-local.json
# npm 锁文件(项目统一使用 yarn)
package-lock.json
# CI Docker 构建临时目录(保留 .gitkeep 确保目录结构存在)
docker-context/bin/*
!docker-context/bin/.gitkeep
docker-context/tools/*
!docker-context/tools/.gitkeep
+59 -26
View File
@@ -1,4 +1,12 @@
# syntax=docker/dockerfile:1.7
#
# 支持两种构建模式:
# CI 模式: docker-context/ 已由 CI 脚本预下载好二进制和工具,直接 COPY(快)
# 本地模式: 传入 --build-arg tag=vX.Y.Z,容器内自动下载二进制并运行 sync-built-in-tools(兼容 Windows/Mac)
#
# 本地构建示例:
# docker build --build-arg tag=v0.8.0-rc.3 -t bililive-go .
# docker build --build-arg tag=v0.8.0-rc.3 --platform linux/arm64 -t bililive-go:arm64 .
FROM ubuntu:22.04
ARG tag
@@ -17,6 +25,7 @@ ENV PUID=0 PGID=0 UMASK=022
RUN mkdir -p $OUTPUT_DIR && \
mkdir -p $CONF_DIR && \
mkdir -p /opt/bililive/tools && \
apt-get update && \
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
curl \
@@ -38,35 +47,59 @@ RUN mkdir -p $OUTPUT_DIR && \
cp -r -f /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
apt-get clean && rm -rf /var/lib/apt/lists/*
RUN set -x; \
case $(arch) in \
aarch64) go_arch=arm64 ;; \
arm*) go_arch=arm ;; \
i386|i686) go_arch=386 ;; \
x86_64) go_arch=amd64 ;; \
*) echo "Unsupported arch: $(arch)"; exit 1 ;; \
esac; \
echo "Downloading release ${tag} for arch ${go_arch}"; \
cd /tmp && curl -sSLO "https://github.com/bililive-go/bililive-go/releases/download/${tag}/bililive-linux-${go_arch}.tar.gz" && \
tar zxvf "bililive-linux-${go_arch}.tar.gz" "bililive-linux-${go_arch}" && \
chmod +x "bililive-linux-${go_arch}" && \
mv "./bililive-linux-${go_arch}" /usr/bin/bililive-go && \
rm "./bililive-linux-${go_arch}.tar.gz"; \
if [ "${tag}" != "$(/usr/bin/bililive-go --version 2>&1 | tr -d '\n')" ]; then exit 1; fi
# For local testing: copy pre-built binary from build context instead of downloading
# COPY bin/bililive-linux-amd64 /usr/bin/bililive-go
# RUN chmod +x /usr/bin/bililive-go
# ===========================================================================
# 二进制安装(双模式)
# CI 模式: docker-context/bin/ 已包含预编译的 bililive-go → 直接 COPY
# 本地模式: 传入 --build-arg tag=vX.Y.Z → 从 GitHub Releases 下载
# ===========================================================================
COPY docker-context/bin/ /tmp/prebuilt-bin/
RUN set -eux; \
if [ -f /tmp/prebuilt-bin/bililive-go ]; then \
echo "=== CI 模式: 使用预编译二进制 ==="; \
mv /tmp/prebuilt-bin/bililive-go /usr/bin/bililive-go; \
elif [ -n "${tag}" ]; then \
echo "=== 本地模式: 从 GitHub Releases 下载 (tag=${tag}) ==="; \
case $(arch) in \
aarch64) go_arch=arm64 ;; \
arm*) go_arch=arm ;; \
i386|i686) go_arch=386 ;; \
x86_64) go_arch=amd64 ;; \
*) echo "Unsupported arch: $(arch)"; exit 1 ;; \
esac; \
cd /tmp && \
curl -sSLO "https://github.com/bililive-go/bililive-go/releases/download/${tag}/bililive-linux-${go_arch}.tar.gz" && \
tar zxvf "bililive-linux-${go_arch}.tar.gz" "bililive-linux-${go_arch}" && \
chmod +x "bililive-linux-${go_arch}" && \
mv "./bililive-linux-${go_arch}" /usr/bin/bililive-go && \
rm "./bililive-linux-${go_arch}.tar.gz"; \
if [ "${tag}" != "$(/usr/bin/bililive-go --version 2>&1 | tr -d '\n')" ]; then \
echo "版本验证失败: 期望 ${tag},实际 $(/usr/bin/bililive-go --version 2>&1 | tr -d '\n')"; \
exit 1; \
fi; \
else \
echo "错误: 必须提供预编译二进制 (docker-context/bin/) 或 --build-arg tag=vX.Y.Z"; \
exit 1; \
fi; \
chmod +x /usr/bin/bililive-go; \
rm -rf /tmp/prebuilt-bin
COPY config.docker.yml $CONF_DIR/config.yml
RUN --mount=type=cache,id=bililive-tools-$TARGETOS-$TARGETARCH$TARGETVARIANT,sharing=locked,target=/cache/bililive/tools \
set -eux; \
echo "Preparing bililive tools cache for bililive-tools-$TARGETOS-$TARGETARCH$TARGETVARIANT..."; \
mkdir -p /opt/bililive/tools /cache/bililive/tools; \
ls -lR /cache/bililive/tools || true; \
/usr/bin/bililive-go --sync-built-in-tools-to-path /cache/bililive/tools || true; \
cp -a /cache/bililive/tools/. /opt/bililive/tools/
# ===========================================================================
# 内置工具安装(双模式)
# CI 模式: docker-context/tools/ 已包含预下载的工具 → 直接 COPY
# 本地模式: 使用 bililive-go --sync-built-in-tools-to-path 下载
# ===========================================================================
COPY docker-context/tools/ /opt/bililive/tools/
RUN set -eux; \
TOOL_COUNT=$(find /opt/bililive/tools -mindepth 1 -not -name '.gitkeep' -print -quit | wc -l); \
if [ "$TOOL_COUNT" -gt 0 ]; then \
echo "=== CI 模式: 使用预下载的内置工具 ==="; \
find /opt/bililive/tools -name '.gitkeep' -delete 2>/dev/null || true; \
else \
echo "=== 本地模式: 通过 sync-built-in-tools 下载 ==="; \
/usr/bin/bililive-go --sync-built-in-tools-to-path /opt/bililive/tools || true; \
fi
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
View File
Whitespace-only changes.
+150
View File
@@ -0,0 +1,150 @@
#!/usr/bin/env bash
# docker-prepare-tools.sh
# 在原生环境中为 Docker 构建预下载内置工具
# 用法: ./scripts/docker-prepare-tools.sh <arch> <output_dir>
# 示例: ./scripts/docker-prepare-tools.sh arm64 docker-context/tools
#
# 支持的架构: amd64, arm64, arm (armv7)
# 此脚本在 CI 的原生 x86/ARM runner 上运行,避免在 QEMU 中解压大文件
#
# 重要: 解压逻辑必须与 remotetools 的 extractDownloadedFile() 行为一致:
# 1. 先解压到临时目录
# 2. 如果解压后顶层只有一个目录,则自动提升该子目录(strip 顶层)
# 3. 最终移动到目标目录
set -euo pipefail
ARCH="${1:?用法: $0 <arch> <output_dir>}"
OUTPUT_DIR="${2:?用法: $0 <arch> <output_dir>}"
mkdir -p "$OUTPUT_DIR"
# ===========================================================================
# 辅助函数
# 模拟 remotetools 的 extractDownloadedFile() 行为:
# - 解压到临时目录
# - 如果顶层只有一个子目录,自动 strip 该目录层
# ===========================================================================
download_and_extract() {
local url="$1"
local dest="$2" # 最终目标目录(例如 tools/ffmpeg/n8.0-latest)
local filename
filename=$(basename "$url")
# 确保目标目录的父目录存在
mkdir -p "$(dirname "$dest")"
# 使用临时目录解压(与 remotetools 行为一致)
local tmp_dir="${dest}.tmp_extract"
rm -rf "$tmp_dir"
mkdir -p "$tmp_dir"
echo ">>> 下载: $url"
curl -sSL -o "/tmp/$filename" "$url"
echo " 解压到临时目录..."
case "$filename" in
*.tar.xz)
tar xf "/tmp/$filename" -C "$tmp_dir"
;;
*.tar.gz)
tar xzf "/tmp/$filename" -C "$tmp_dir"
;;
*.zip)
unzip -qo "/tmp/$filename" -d "$tmp_dir"
;;
*)
echo " 错误: 不支持的格式: $filename"
rm -rf "$tmp_dir"
exit 1
;;
esac
rm -f "/tmp/$filename"
# 模拟 remotetools 的 "单目录提升" 逻辑:
# 如果解压后顶层只有一个子目录,将其内容提升为目标目录
local entries
entries=$(ls -1 "$tmp_dir" | wc -l)
local first_entry
first_entry=$(ls -1 "$tmp_dir" | head -1)
if [ "$entries" -eq 1 ] && [ -d "$tmp_dir/$first_entry" ]; then
echo " 检测到单一顶层目录 '$first_entry',自动提升"
rm -rf "$dest"
mv "$tmp_dir/$first_entry" "$dest"
rm -rf "$tmp_dir"
else
echo " 直接使用解压内容"
rm -rf "$dest"
mv "$tmp_dir" "$dest"
fi
echo " 完成: $dest"
}
echo "=== 为 linux/$ARCH 预下载 Docker 内置工具 ==="
# --- ffmpeg ---
# 注意: arm/armv7 使用 apt 安装的 ffmpeg,不需要下载
if [ "$ARCH" = "amd64" ]; then
download_and_extract \
"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n8.0-latest-linux64-gpl-8.0.tar.xz" \
"$OUTPUT_DIR/ffmpeg/n8.0-latest"
elif [ "$ARCH" = "arm64" ]; then
download_and_extract \
"https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n8.0-latest-linuxarm64-gpl-8.0.tar.xz" \
"$OUTPUT_DIR/ffmpeg/n8.0-latest"
fi
# --- dotnet ---
if [ "$ARCH" = "amd64" ]; then
download_and_extract \
"https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.20/aspnetcore-runtime-8.0.20-linux-x64.tar.gz" \
"$OUTPUT_DIR/dotnet/8.0.20"
elif [ "$ARCH" = "arm64" ]; then
download_and_extract \
"https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.20/aspnetcore-runtime-8.0.20-linux-arm64.tar.gz" \
"$OUTPUT_DIR/dotnet/8.0.20"
elif [ "$ARCH" = "arm" ]; then
download_and_extract \
"https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/8.0.20/aspnetcore-runtime-8.0.20-linux-arm.tar.gz" \
"$OUTPUT_DIR/dotnet/8.0.20"
fi
# --- bililive-recorder ---
# 跨平台 .NET 程序,所有架构相同
download_and_extract \
"https://github.com/BililiveRecorder/BililiveRecorder/releases/download/v2.17.3/BililiveRecorder-CLI-any.zip" \
"$OUTPUT_DIR/bililive-recorder/v2.17.3"
# --- node ---
if [ "$ARCH" = "amd64" ]; then
download_and_extract \
"https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-x64.tar.xz" \
"$OUTPUT_DIR/node/v20.10.0"
elif [ "$ARCH" = "arm64" ]; then
download_and_extract \
"https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-arm64.tar.xz" \
"$OUTPUT_DIR/node/v20.10.0"
elif [ "$ARCH" = "arm" ]; then
download_and_extract \
"https://nodejs.org/dist/v20.10.0/node-v20.10.0-linux-armv7l.tar.xz" \
"$OUTPUT_DIR/node/v20.10.0"
fi
# --- biliLive-tools ---
if [ "$ARCH" = "amd64" ]; then
download_and_extract \
"https://github.com/kira1928/biliLive-tools/releases/download/3.1.2-bgo.2/bililive-cli-lib-ubuntu-x64.zip" \
"$OUTPUT_DIR/biliLive-tools/3.1.2-bgo.2"
elif [ "$ARCH" = "arm64" ]; then
download_and_extract \
"https://github.com/kira1928/biliLive-tools/releases/download/3.1.2-bgo.2/bililive-cli-lib-ubuntu-arm64.zip" \
"$OUTPUT_DIR/biliLive-tools/3.1.2-bgo.2"
elif [ "$ARCH" = "arm" ]; then
download_and_extract \
"https://github.com/kira1928/biliLive-tools/releases/download/3.1.2-bgo.2/bililive-cli-lib-ubuntu-armv7.zip" \
"$OUTPUT_DIR/biliLive-tools/3.1.2-bgo.2"
fi
echo "=== 所有工具下载完成 ==="
du -sh "$OUTPUT_DIR"/* 2>/dev/null || true
+13
View File
@@ -70,6 +70,19 @@ func getConfigBesidesExecutable() (*configs.Config, error) {
if err != nil {
return nil, err
}
// 如果是由 Launcher 启动的子进程(新版本 exe 位于 .appdata/versions/{version}/),
// 优先从 Launcher(入口程序)的 exe 旁边查找用户的真实 config.yml。
// 因为 release 压缩包解压后,新版本 exe 旁边总会有一个默认的 config.yml,
// 必须优先使用用户原始目录的配置文件。
if launcherExe := os.Getenv("BILILIVE_LAUNCHER_EXE"); launcherExe != "" {
launcherConfigPath := filepath.Join(filepath.Dir(launcherExe), "config.yml")
if config, err := configs.NewConfigWithFile(launcherConfigPath); err == nil {
return config, nil
}
}
// 回退:在当前 exe 旁边查找(用户直接双击运行的场景)
configPath := filepath.Join(filepath.Dir(exePath), "config.yml")
config, err := configs.NewConfigWithFile(configPath)
if err != nil {