Files
ESP32S3_vt/.github/workflows/build-client.yml
T
sloray 03f7279b53
构建客户端 / 检查(类型 + 单元测试) (push) Successful in 1m22s
构建客户端 / Linux x64(deb + AppImage) (push) Successful in 7m2s
构建客户端 / macOS 通用(.app + .dmg) (push) Skipped
构建客户端 / Windows x64(NSIS 安装包) (push) Skipped
ci: 修两处 Gitea 上跑不了的东西(upload-artifact v4 → v3、去掉缓存)
第一次真实运行的日志(job 14)钉出来的两件事:

1. 上传产物那一步一直是失败的:
     GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
     and download-artifact@v4+ are not currently supported on GHES
   v4 里的 @actions/artifact 会拒绝非 github.com 的服务器。Gitea 实现的是 v3 那套
   接口(ACTIONS_RUNTIME_URL),所以三个平台都换成 upload-artifact@v3,并加一个
   .github/actionlint.yaml 把"v3 太旧"这条 GitHub 规则的误报关掉(原因写在里面)。

   产物路径本身没问题:日志里 tauri build 已经产出 deb/AppImage,v4 在报错之前也
   认出了那批文件("The least common ancestor is .../release/bundle")。

2. 缓存服务在 job 容器里连不上:
     Failed to restore: getCacheEntry failed: connect ETIMEDOUT 172.18.0.2:37315
   setup-node 的 cache: pnpm 每次要干等 4 分 39 秒才放弃,cargo 缓存那步也白等 26 秒。
   先全部去掉(两个 job 每轮省约 10 分钟),文件头第 3 条写明了等缓存修好怎么加回来。

其余在真机上都是绿的:rustup 兜底、pnpm 12.11.2、Node 24.22.0、tsc、vite、
protocol 15 / cam-core 80 / src-tauri 30 个测试、tauri build(deb + AppImage)全过。
2026-10-15 20:49:25 +08:00

428 lines
21 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.
# 构建桌面客户端(desktop/,Tauri v2 + React)。
#
# 产物都挂在这次运行下面(Actions 页面直接下载),保留 14 天:
# Linux x64 deb + AppImage cam-desktop_<版本>_amd64.deb / .AppImage ← 默认每次编
# Windows x64 NSIS 安装包 cam-desktop_<版本>_x64-setup.exe ← 有 runner 才编
# macOS 通用 .app + .dmg arm64 与 x86_64 合成一个包 ← 有 runner 才编
#
# ── 触发 ─────────────────────────────────────────────────────────────────────
# * push 到 main/master —— 只认 desktop/、protocol/ 与本文件的改动,改固件不会
# 白占 runner
# * 打 v* tag、开 PR
# * 手动 Run workflow:逐个平台勾选,不进 paths 过滤("就想编一次"走它)
# Windows 与 macOS 两个 job 目前默认不跑,原因与打开方式见下面第 1 条。
#
# ── 跑在 Gitea 上要先对上的两件事 ────────────────────────────────────────────
# 1. runner 标签是注册 runner 时自己起的名字,猜不到,所以三个 job 都写成
# "仓库变量优先、常见标签兜底":
# runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
# 标签对不上时 job 是**排队**而不是报错 —— 排队会一直占着这次运行不结束,
# 所以第一次跑之前先看一眼 runner 页面上那几行标签。
#
# 本仓库目前只有 Linux 的两个 runner 标签(ubuntu-latest / ubuntu-24.04 /
# ubuntu-22.04),所以**默认只跑 Linux**:Windows 与 macOS 两个 job 要等
# 仓库变量 ENABLE_WINDOWS / ENABLE_MACOS 设成 true 才会跑(手动 Run workflow
# 时另看勾选框),机器的标签则用 RUNNER_WINDOWS / RUNNER_MACOS 指过去。
# 等那两台机器上的 runner 注册好再打开,否则每次 push 都会多出两个永远排队的
# job。
#
# 为什么不能拿 Linux runner 交叉编出来:
# * Windows:Rust 侧用 mingw 交叉编还能出个裸 exe,但 tauri 的 NSIS 打包器
# 本身只能在 Windows 上跑,出不了安装包;
# * macOS:要 Apple SDK 与 codesign/lipo,只能在 macOS 上编。
# 所以这两个平台的 job 留着是为了"机器到位那天直接开",不是摆设。
# 那两台机器上要预装好:Windows 的 MSVC Build Tools(link.exe)、macOS 的
# Xcode Command Line Tools,以及 rustup(dtolnay/rust-toolchain 靠它装工具链)。
# Linux 的 job 通常跑在容器里,所以没装 rustup 也没关系 —— 见下面那步兜底。
# 2. 这个文件放在 .github/workflows/ 下就够:Gitea 的 [actions] WORKFLOW_DIRS
# 默认值就是 [".gitea/workflows", ".github/workflows"](源码里的默认值,且
# .yml 与 .yaml 都认),所以两个目录都是原生支持的,不用挪。
# 3. 两处"GitHub 能跑、Gitea 上不行"的地方,都是 2026-10-15 第一次真实运行的
# 日志里钉出来的:
# * 传产物用 upload-artifact@v3,**不能用 v4**。v4 里的 @actions/artifact 会
# 拒绝非 github.com 的服务器,日志原文:
# GHESNotSupportedError: @actions/artifact v2.0.0+, upload-artifact@v4+
# and download-artifact@v4+ are not currently supported on GHES
# Gitea 实现的是 v3 那套接口(ACTIONS_RUNTIME_URL),所以 v3 能传上去。
# * 一个缓存都没开。这台实例的缓存服务在 job 容器里连不上,日志原文:
# Failed to restore: getCacheEntry failed: connect ETIMEDOUT 172.18.0.2:37315
# setup-node 的 cache 每次要干等 4 分 39 秒才放弃(两个 job 各一次)。
# 等缓存能从容器访问到了再加回来,写法就是:
# setup-node 的 with 里加:cache: pnpm / cache-dependency-path: desktop/pnpm-lock.yaml
# 各 job 里加:
# - uses: actions/cache@v4
# with:
# path: |
# ~/.cargo/registry/index
# ~/.cargo/registry/cache
# ~/.cargo/git/db
# key: ${{ runner.os }}-cargo-registry-${{ hashFiles('protocol/Cargo.lock', 'desktop/core/Cargo.lock', 'desktop/src-tauri/Cargo.lock') }}
# restore-keys: ${{ runner.os }}-cargo-registry-
#
# ── 三个平台都显式写 --target,这不是啰嗦 ────────────────────────────────────
# desktop/.cargo/config.toml 里钉着 `target = "aarch64-apple-darwin"`(本机开发用;
# 它存在的理由是 desktop/ 嵌在固件目录树里,不然会继承根目录那份
# xtensa-esp32s3-espidf 的交叉编译配置)。CI 上不覆盖它,Linux 与 Windows 的 runner
# 会去编 macOS 的目标,然后在依赖深处以 libc 找不到 MAP_FAILED 这类形式失败。
# 所以每个 job 都把真正的目标交两遍:CARGO_BUILD_TARGET 给裸 cargo(测试)用,
# --target 给 tauri build 用(打包路径也跟着它走)。
#
# ── 还有一条:仓库根目录的 rust-toolchain.toml 钉的是 `esp` ─────────────────
# 那是给 ESP32 固件的定制工具链,CI 上没有、也不打算装(espup 要下几百 MB)。
# rustup 是按**当前目录**往上找 rust-toolchain.toml 的:
# * desktop/** 有自己的 rust-toolchain.toml(stable),所以那里的 cargo 直接可用;
# * protocol/ 没有,会一路找到根目录那个 esp —— 所以那条测试写成 `cargo +stable`,
# 显式越过它。
# 任何在仓库根目录直接跑 cargo 的步骤都会去要 esp 工具链,别加。
#
# ── 没做的事 ────────────────────────────────────────────────────────────────
# 不签名、不公证:Windows 的 exe 与 macOS 的 app 在别人机器上会被 SmartScreen /
# Gatekeeper 拦一次(右键打开即可)。要做的话给 macOS 那几个 job 加
# APPLE_CERTIFICATE / APPLE_SIGNING_IDENTITY / APPLE_ID / APPLE_PASSWORD /
# APPLE_TEAM_ID(tauri 认这些环境变量,构建步骤不用改)。也没有自动发 Release:
# 要发的话在最后加一个下载全部 artifact 再创建 release 的 job。
name: 构建客户端
on:
push:
branches: [main, master]
tags: ["v*"]
# GitHub 语义下 tag 推送不受 paths 过滤影响;如果你们的 Gitea 版本把 tag 也
# 一起过滤掉了(打 tag 不触发),把这三行 paths 删掉即可。
paths:
- "desktop/**"
- "protocol/**"
- ".github/workflows/build-client.yml"
pull_request:
paths:
- "desktop/**"
- "protocol/**"
- ".github/workflows/build-client.yml"
workflow_dispatch:
inputs:
# Linux 的 runner 有,默认勾上;另两个平台目前没有 runner,默认不勾 ——
# 勾了而没有对应 runner,那个 job 会一直排队。
linux:
description: "构建 Linux(deb + AppImage)"
type: boolean
default: true
windows:
description: "构建 Windows(NSIS 安装包)——需要 Windows runner"
type: boolean
default: false
macos:
description: "构建 macOS(通用 dmg)——需要 macOS runner"
type: boolean
default: false
# 同一分支连推两次时把上一次没跑完的取消掉:自建 runner 数量有限,让过期的构建
# 占着机器没有意义。老版本 runner 忽略这个键,不影响构建。
concurrency:
group: build-client-${{ github.ref }}
cancel-in-progress: true
env:
# 与本机开发对齐:node 24 / pnpm 12(pnpm-lock.yaml 就是 pnpm 12.8.1 写的,
# lockfileVersion 9.0)。改这两个数之前先确认 lock 文件还认。
NODE_VERSION: "24"
PNPM_VERSION: "12"
CARGO_TERM_COLOR: always
RUST_BACKTRACE: "1"
jobs:
# ───────────────────────────────────────────────────────────────────────────
# 守门:类型检查、前端构建、两个不依赖窗口的 crate 的单元测试。
# 下面每个构建 job 都 needs 它 —— 编到一半才发现 tsc 报错,是最浪费 runner 的
# 一种失败。src-tauri 的 30 个测试放在 Linux 那个 job 里跑(它本来就要装
# webkit 依赖,而且那几条录制测试要真的起 ffmpeg)。
# ───────────────────────────────────────────────────────────────────────────
checks:
name: 检查(类型 + 单元测试)
runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
# runner 镜像里多半没有 rustup,而 dtolnay/rust-toolchain 靠它装工具链。
# 这里只装一个最小的 rustup 本身,工具链交给下面那个 action —— 这样自建
# 镜像不用为了 CI 专门烤一个带 Rust 的镜像。
# 国内网络慢的话,在 runner 的环境变量里给 RUSTUP_DIST_SERVER 指一个镜像
# (如 https://mirrors.ustc.edu.cn/rust-static)会快很多。
- name: 确保有 rustup
run: |
if command -v rustup >/dev/null 2>&1; then
rustup --version
exit 0
fi
if command -v curl >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
else
echo "装 rustup 需要 curl 或 wget,这个镜像里两个都没有" >&2
exit 1
fi
if [ -n "$GITHUB_PATH" ]; then
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
# pnpm 装在 setup-node 之前:这一步只靠 runner 镜像里自带的 node(实测
# /opt/hostedtoolcache 里有 24.x),而下面的 pnpm install 要用它。
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
- name: 类型检查(tsc --noEmit)
working-directory: desktop
run: pnpm run check
- name: 前端构建(vite)
working-directory: desktop
run: pnpm build
- name: 装 Rust(stable)
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
# protocol 没有自己的 rust-toolchain.toml,会被根目录那个 `esp` 接住,
# 所以这里必须 +stable 显式指定。
- name: protocol 单元测试(15)
working-directory: protocol
run: cargo +stable test --locked --target x86_64-unknown-linux-gnu
# core 在 desktop/ 下面,desktop/rust-toolchain.toml 已经是 stable,不用 +stable。
- name: cam-core 单元测试(80)
working-directory: desktop/core
run: cargo test --locked --target x86_64-unknown-linux-gnu
# ───────────────────────────────────────────────────────────────────────────
# Linux x64:deb + AppImage
# ───────────────────────────────────────────────────────────────────────────
linux:
name: Linux x64(deb + AppImage)
needs: checks
if: ${{ github.event.inputs.linux != 'false' }}
runs-on: ${{ vars.RUNNER_LINUX || 'ubuntu-latest' }}
timeout-minutes: 120
env:
TARGET: x86_64-unknown-linux-gnu
CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu
steps:
- uses: actions/checkout@v4
- name: 确保有 rustup
run: |
if command -v rustup >/dev/null 2>&1; then
rustup --version
exit 0
fi
if command -v curl >/dev/null 2>&1; then
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
elif command -v wget >/dev/null 2>&1; then
wget -qO- https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain none
else
echo "装 rustup 需要 curl 或 wget,这个镜像里两个都没有" >&2
exit 1
fi
if [ -n "$GITHUB_PATH" ]; then
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
fi
- name: 装系统依赖
run: |
# 容器里跑的 runner 通常是 root,没有 sudo;托管 runner 上有。
SUDO=""
[ "$(id -u)" -eq 0 ] || SUDO=sudo
$SUDO apt-get update
$SUDO apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
patchelf \
libxdo-dev \
libssl-dev \
xdg-utils \
build-essential curl wget file \
ffmpeg
# AppImage 打包要运行 linuxdeploy(它本身是个 AppImage);24.04 起包名带 t64。
$SUDO apt-get install -y libfuse2 || $SUDO apt-get install -y libfuse2t64 || true
- name: 装 Rust(stable)
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-unknown-linux-gnu
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
# 上面 apt 装了 ffmpeg,所以 recording.rs 里那几条"真的起 ffmpeg 并读回帧数"
# 的测试不会走 has_ffmpeg() 的跳过分支,是真的在验。
# 如果这台 runner 上它们长期不稳定,删掉这一步即可 —— 它们不在守门 job 里,
# 不影响另外两个平台的产物。
- name: Tauri 后端单元测试(30)
working-directory: desktop/src-tauri
run: cargo test --locked --target ${{ env.TARGET }}
# beforeBuildCommand(pnpm build)由 tauri 自己跑,这里不用再 build 一次。
- name: 打包(tauri build)
working-directory: desktop
run: pnpm tauri build --target ${{ env.TARGET }} --bundles deb,appimage
- name: 上传产物
uses: actions/upload-artifact@v3
with:
name: cam-desktop-linux-x64
# 一个都没找到就报错:产物路径写错却"构建成功"是最难发现的一种失败。
if-no-files-found: error
retention-days: 14
path: |
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/deb/*.deb
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/appimage/*.AppImage
# ───────────────────────────────────────────────────────────────────────────
# Windows x64:NSIS 安装包
# ───────────────────────────────────────────────────────────────────────────
windows:
name: Windows x64(NSIS 安装包)
needs: checks
# 默认不跑:本仓库目前没有 Windows runner,跑了只会一直排队。有了机器之后
# 在仓库变量里加 ENABLE_WINDOWS=true 就会跟着每次 push 自动跑;手动触发时
# 勾 "构建 Windows" 也能单独跑一次。标签用 RUNNER_WINDOWS 指。
if: >-
${{
(github.event_name == 'workflow_dispatch' && github.event.inputs.windows == 'true')
|| (github.event_name != 'workflow_dispatch' && vars.ENABLE_WINDOWS == 'true')
}}
# 自建 Windows runner 上要有 MSVC Build Tools(tauri 的 Rust 侧要 link.exe)、
# WebView2 运行时(装了包的应用要它才能起窗口;打包本身不需要)与 rustup。
runs-on: ${{ vars.RUNNER_WINDOWS || 'windows-latest' }}
timeout-minutes: 120
env:
TARGET: x86_64-pc-windows-msvc
CARGO_BUILD_TARGET: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- name: 装 Rust(stable)
uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
# nsis 是纯安装包,不需要 WiX;tauri 会自己把 NSIS 下到用户缓存里,所以
# 这台 runner 要能访问 GitHub(离线环境改成 --bundles msi 也一样要下 WiX)。
- name: 打包(tauri build)
working-directory: desktop
run: pnpm tauri build --target ${{ env.TARGET }} --bundles nsis
- name: 上传产物
uses: actions/upload-artifact@v3
with:
name: cam-desktop-windows-x64
if-no-files-found: error
retention-days: 14
path: |
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/nsis/*.exe
# ───────────────────────────────────────────────────────────────────────────
# macOS 通用包:.app + .dmg
# ───────────────────────────────────────────────────────────────────────────
macos:
name: macOS 通用(.app + .dmg)
needs: checks
# 同 Windows:默认不跑,有机器之后设仓库变量 ENABLE_MACOS=true,标签用
# RUNNER_MACOS 指。
if: >-
${{
(github.event_name == 'workflow_dispatch' && github.event.inputs.macos == 'true')
|| (github.event_name != 'workflow_dispatch' && vars.ENABLE_MACOS == 'true')
}}
# 自建 macOS runner 上要有 Xcode Command Line Tools(链接要它)与 rustup。
runs-on: ${{ vars.RUNNER_MACOS || 'macos-latest' }}
timeout-minutes: 150
env:
# 注意:这里**不能**设 CARGO_BUILD_TARGET —— universal-apple-darwin 是 tauri
# 自己的伪目标(编两遍再 lipo 合成),裸 cargo 不认它。这个 job 里也没有裸
# cargo 步骤,所有目标都由 tauri build 的 --target 交代。
TARGET: universal-apple-darwin
steps:
- uses: actions/checkout@v4
# 通用包要两个架构的 std 都在。
- name: 装 Rust(stable + 两个 mac 目标)
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin,x86_64-apple-darwin
- name: 装 pnpm
uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- name: 装 Node
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
- name: 前端依赖(严格按 lock 装)
working-directory: desktop
run: pnpm install --frozen-lockfile
# 只想要当前架构、想省一半编译时间的话,把 --target 换成 aarch64-apple-darwin
# (Apple Silicon 的 runner)并去掉上面 targets 里的 x86_64 即可。
- name: 打包(tauri build)
working-directory: desktop
run: pnpm tauri build --target ${{ env.TARGET }} --bundles app,dmg
# 第二行是兜底:不同 tauri-cli 版本给通用包放的目录略有差异,宁可多写一行,
# 也不要上传时静默少东西。(注意 path 是块标量,里面写 # 是路径不是注释。)
- name: 上传产物
uses: actions/upload-artifact@v3
with:
name: cam-desktop-macos-universal
if-no-files-found: error
retention-days: 14
path: |
desktop/src-tauri/target/${{ env.TARGET }}/release/bundle/dmg/*.dmg
desktop/src-tauri/target/*/release/bundle/dmg/*.dmg