Files
openless/.github/workflows/ci.yml
T
KingandChris233 c84bfb98f3 chore(ci): optimize CI caching, concurrency, and Tauri security (#937)
* ci: optimize CI caching, concurrency, and Tauri security

Address multiple CI/CD efficiency and security gaps identified during
project optimization analysis. The changes reduce PR build times,
prevent wasteful concurrent runs, tighten the Tauri security boundary,
and eliminate unnecessary submodule fetches on non-macOS jobs.

CI workflow improvements:
- Add swatinem/rust-cache@v2 to both ci.yml jobs (android-check and
  cross-platform), matching the caching already present in
  release-tauri.yml. This converts PR Rust compilation from full
  rebuilds to incremental builds, saving an estimated 60-80% of
  compile time per run.
- Add concurrency groups to all three workflows. PR pushes cancel
  superseded runs to free concurrency slots; tag pushes and manual
  dispatches are isolated by run_id to avoid mutually canceling
  release builds. Previously, rapid PR pushes triggered redundant
  parallel CI runs with no cancellation.
- Unify Node version to 22 across release-tauri.yml and
  android-apk.yml, which were previously pinned to 20. This ensures
  consistent build environments across all workflows.

Submodule and security hardening:
- Conditionally fetch the vendor/qwen-asr submodule only on macOS
  jobs across all three workflows. The submodule is compiled
  exclusively by build.rs on macOS (build_qwen_asr_macos); Linux,
  Windows, and Android builds never reference it. Removing the
  unconditional recursive checkout eliminates a network fetch and
  a submodule-availability failure point on every non-macOS job.
- Set withGlobalTauri to false in tauri.conf.json. The frontend
  exclusively uses @tauri-apps/api imports and never references the
  window.__TAURI__ global, so disabling it removes an unnecessary
  XSS attack surface without functional impact.

Dependency hygiene:
- Move @types/dompurify from dependencies to devDependencies in
  package.json. Type declaration packages are build-time only and
  should not ship in the production bundle.

* fix(ci): address review findings

* fix(ci): isolate manual CI workflow runs

---------

Co-authored-by: Chris233 <h-chris233@outlook.com>
2026-08-10 23:06:12 +08:00

296 lines
12 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: CI
# 多平台跨语言质量门禁。release-tauri.yml 是发版流水线(仅打包构建),这里负责
# 在合并前快速验证两件事:
# 1. 全部前端/契约测试与 vite bundle 通过(捕获行为、合同及跨 locale 类型 drift)
# 2. Rust 后端在 macOS / Windows / Linux 都能 cargo check 过(捕获 cfg 漏分支 / 平台 API 误用)
# 跑 build-mac.sh / windows-package-msvc.ps1 / Tauri bundle 太重;只跑轻量 cargo check + vite build。
on:
push:
branches: [main, beta]
pull_request:
branches: [main, beta]
workflow_dispatch:
# 同一 PR 快速重复推送时取消旧运行;workflow_dispatch 用 run_id 隔离。
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
android-check:
name: Android cargo check
runs-on: ubuntu-latest
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
# vendor/qwen-asr 仅 macOS 上 build.rs 会编译(build.rs:49 build_qwen_asr_macos);
# Android/Linux 的 cargo check 不需要,去掉递归拉取省一次网络 fetch + 失败点。
- name: Setup Android SDK + NDK
uses: android-actions/setup-android@v3
with:
packages: platform-tools
- name: Install NDK
shell: bash
run: |
set -euo pipefail
sdkmanager "ndk;26.1.10909125" "platforms;android-36" "build-tools;36.0.0"
ndk_dir="$ANDROID_HOME/ndk/26.1.10909125"
echo "ANDROID_NDK_HOME=$ndk_dir" >> "$GITHUB_ENV"
echo "NDK_HOME=$ndk_dir" >> "$GITHUB_ENV"
set +o pipefail
yes | sdkmanager --licenses
set -o pipefail
- name: Configure NDK toolchain for cargo
shell: bash
run: |
set -euo pipefail
prebuilt="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64"
echo "CC_aarch64_linux_android=$prebuilt/bin/aarch64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CXX_aarch64_linux_android=$prebuilt/bin/aarch64-linux-android24-clang++" >> "$GITHUB_ENV"
echo "AR_aarch64_linux_android=$prebuilt/bin/llvm-ar" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$prebuilt/bin/aarch64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CC_x86_64_linux_android=$prebuilt/bin/x86_64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CXX_x86_64_linux_android=$prebuilt/bin/x86_64-linux-android24-clang++" >> "$GITHUB_ENV"
echo "AR_x86_64_linux_android=$prebuilt/bin/llvm-ar" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$prebuilt/bin/x86_64-linux-android24-clang" >> "$GITHUB_ENV"
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: openless-all/app/package-lock.json
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: "17"
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,x86_64-linux-android
- uses: swatinem/rust-cache@v2
with:
workspaces: 'openless-all/app/src-tauri -> target'
- uses: gradle/actions/setup-gradle@v4
- name: Install Linux check dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential curl file libssl-dev wget
- name: Install frontend dependencies
run: npm ci
- name: Build frontend (tsc + vite)
# generate_context! requires frontendDist (../dist) to exist on Android too.
run: npm run build
- name: Check Android updater pubkey matches tauri.conf.json
run: npm run check:android-updater-pubkey
- name: Check Tauri backend (Android target)
run: cargo check --manifest-path src-tauri/Cargo.toml --target aarch64-linux-android
- name: Initialize generated Android project
run: npm run tauri -- android init --ci
- name: Generate Tauri Gradle dependency scripts
env:
TAURI_ANDROID_PROJECT_PATH: ${{ github.workspace }}/openless-all/app/src-tauri/gen/android
TAURI_ANDROID_PACKAGE_UNESCAPED: com.openless.app
WRY_ANDROID_LIBRARY: openless_lib
WRY_ANDROID_KOTLIN_FILES_OUT_DIR: ${{ github.workspace }}/openless-all/app/src-tauri/gen/android/app/src/main/java/com/openless/app/generated
WRY_ANDROID_PACKAGE: com.openless.app
run: |
mkdir -p "$WRY_ANDROID_KOTLIN_FILES_OUT_DIR"
cargo check --manifest-path src-tauri/Cargo.toml --target x86_64-linux-android
- name: Copy Android production and test scaffolding
run: node scripts/copy-android-scaffolding.mjs
- name: Merge Shizuku manifest
run: node scripts/merge-android-shizuku-manifest.mjs
- name: Patch Shizuku Gradle dependencies
run: node scripts/patch-android-shizuku-deps.mjs
- name: Run JVM credential tests and compile instrumentation tests
# These tests are Kotlin-only. The Rust target is checked above; direct Gradle
# rustBuild requires the live Tauri CLI RPC server used by `tauri android build`.
run: >-
src-tauri/gen/android/gradlew
--project-dir src-tauri/gen/android
:app:testX86_64DebugUnitTest
:app:assembleX86_64DebugAndroidTest
-x :app:rustBuildX86_64Debug
--no-daemon
- name: Enable KVM for Android instrumentation tests
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Run Android Keystore instrumentation tests
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 35
target: google_apis
arch: x86_64
# The instrumentation suite exercises AndroidKeyStore without launching Tauri.
script: >-
cd openless-all/app &&
src-tauri/gen/android/gradlew
--project-dir src-tauri/gen/android
:app:connectedX86_64DebugAndroidTest
-x :app:rustBuildX86_64Debug
--no-daemon
cross-platform:
name: ${{ matrix.label }} checks
strategy:
# 一个平台挂掉不阻塞其他平台拿到验证结果。
fail-fast: false
matrix:
include:
- os: macos-latest
label: macOS
preflight: false
- os: windows-latest
label: Windows
preflight: true
- os: ubuntu-latest
label: Linux
preflight: false
runs-on: ${{ matrix.os }}
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
# vendor/qwen-asr 仅 macOS 上 build.rs 会编译(build.rs:49 build_qwen_asr_macos)。
# Windows/Linux 的 cargo check 不需要子模块;用矩阵条件避免非 macOS job
# 多拉一次 git submodule(省网络 fetch + 去掉一个失败点)。
submodules: ${{ matrix.os == 'macos-latest' && 'recursive' || 'false' }}
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: npm
cache-dependency-path: openless-all/app/package-lock.json
# 现有测试继续用 stable;额外安装声明的 MSRV,供下方兼容性门禁显式调用。
- uses: dtolnay/rust-toolchain@1.88.0
- uses: dtolnay/rust-toolchain@stable
- uses: swatinem/rust-cache@v2
with:
workspaces: 'openless-all/app/src-tauri -> target'
- name: Install Linux check dependencies
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y \
build-essential \
curl \
file \
libasound2-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libssl-dev \
libwebkit2gtk-4.1-dev \
libxdo-dev \
wget
- name: Install frontend dependencies
run: npm ci
- name: Check Windows prerequisites
if: matrix.preflight
shell: pwsh
run: ./scripts/windows-preflight.ps1 -Toolchain msvc
- name: Check PowerShell scripts
if: matrix.preflight
shell: pwsh
run: |
foreach ($script in @("./scripts/windows-preflight.ps1", "./scripts/windows-build-gnu.ps1", "./scripts/windows-runtime-smoke.ps1")) {
$errors = $null
[System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw $script), [ref]$errors) | Out-Null
if ($errors) {
$errors | Format-List
exit 1
}
}
- name: Build frontend and run frontend/contract tests
run: npm test
- name: Check Tauri backend (cargo check)
run: cargo check --manifest-path src-tauri/Cargo.toml
- name: Run Rust backend unit tests
if: runner.os != 'Windows'
run: cargo test --manifest-path src-tauri/Cargo.toml --lib
- name: Compile Rust backend unit tests (Windows)
# Windows runner 能链接 lib test binary,但干净镜像缺少可选 native runtime
# DLL entrypoint 时,进程会在 test harness 启动前退出。这里保留 cfg/link
# 覆盖;Rust-only 后端单测由下一步实际执行。
if: runner.os == 'Windows'
run: cargo test --manifest-path src-tauri/Cargo.toml --lib --no-run
- name: Run Rust-only backend unit tests (Windows)
# 只把纯 Rust 后端模块编进独立 test crate,不链接完整 Tauri app lib。
# 这样 Windows CI 能实际执行 coordinator/hotkey/recorder/insertion 逻辑单测。
if: runner.os == 'Windows'
run: cargo test --manifest-path src-tauri/backend-tests/Cargo.toml
- name: Check Tauri backend with Rust 1.88 MSRV
run: cargo +1.88.0 check --locked --manifest-path src-tauri/Cargo.toml
- name: Compile backend tests with Rust 1.88 MSRV
run: cargo +1.88.0 test --locked --manifest-path src-tauri/backend-tests/Cargo.toml --no-run
- name: Verify version sync across all 5 files
# 两个平台都跑这个校验:Windows runner 自带 git-bash,跨 shell 表现一致。
# 一旦版本号 drift 立刻 fail,避免发版时再发现漏改。
# 校验 5 处:package.json / package-lock.json (root + nested) /
# tauri.conf.json / Cargo.toml / Cargo.lock 的 [openless] 包。
shell: bash
run: |
PKG=$(node -p "require('./package.json').version")
LOCK_ROOT=$(node -p "require('./package-lock.json').version")
LOCK_NESTED=$(node -p "require('./package-lock.json').packages[''].version")
TAU=$(node -p "require('./src-tauri/tauri.conf.json').version")
CRG=$(grep -E '^version = ' src-tauri/Cargo.toml | head -1 | sed -E 's/^version = "(.+)"$/\1/')
# Cargo.lock:找 [openless] 包紧跟的 version 行
CARGO_LOCK_VER=$(awk 'BEGIN{found=0} /^name = "openless"$/{found=1; next} found && /^version = /{gsub(/"/,""); print $3; exit}' src-tauri/Cargo.lock)
echo "package.json = $PKG"
echo "package-lock root = $LOCK_ROOT"
echo "package-lock nested = $LOCK_NESTED"
echo "tauri.conf.json = $TAU"
echo "Cargo.toml = $CRG"
echo "Cargo.lock openless = $CARGO_LOCK_VER"
mismatch=0
for v in "$LOCK_ROOT" "$LOCK_NESTED" "$TAU" "$CRG" "$CARGO_LOCK_VER"; do
if [ "$v" != "$PKG" ]; then mismatch=1; fi
done
if [ "$mismatch" -ne 0 ]; then
echo "::error::版本号未对齐 — 请用 scripts/bump-version.sh 同步更新"
exit 1
fi
echo "[ok] 全部 5 处版本号一致:$PKG"