name: CI # 多平台跨语言质量门禁。release-tauri.yml 是发版流水线(仅打包构建),这里负责 # 在合并前快速验证两件事: # 1. 全部前端/契约测试与 vite bundle 通过(捕获行为、合同及跨 locale 类型 drift) # 2. Tauri 后端在 macOS / Windows、移动端在 Android 编译;Linux Core、 # remote_tls、egui 和 deb/rpm 统一使用发版构建链检查(PR 不上传 Release)。 # macOS/Windows/Android 不跑完整 Tauri bundle;Linux 则运行完整 deb/rpm 发版构建校验。 on: push: branches: [main, beta] pull_request: branches: [main, beta] workflow_dispatch: inputs: platform: description: Platforms to check (macos runs only macOS gates) type: choice options: [all, macos] default: all # Cancel old runs when the same PR is pushed repeatedly; workflow_dispatch is isolated by 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: # 判定这次改动能不能影响到各平台门禁。只有“改动物理上够不到”的变更才会 # 跳过对应 job:判定脚本对任何未知情况(没有基线、git 失败、空 diff)都返回 # true。push / tag / 手动运行不带基线,因此始终全量验证。 changes: name: Changed areas runs-on: ubuntu-latest timeout-minutes: 10 outputs: tauri: ${{ steps.detect.outputs.tauri }} msrv: ${{ steps.detect.outputs.msrv }} linux: ${{ steps.detect.outputs.linux }} steps: - uses: actions/checkout@v4 with: # 需要基线提交才能算出差异,客户端不再重复 fetch。 fetch-depth: 0 - name: Detect changed areas id: detect working-directory: openless-all/app env: BASE_SHA: ${{ github.event.pull_request.base.sha }} run: | set -euo pipefail for area in tauri msrv linux; do verdict=$(bash scripts/ci-changed-areas.sh "$area" "$BASE_SHA") # 失败即运行:脚本读不到差异时返回 true,这里也只接受明确的 false。 echo "$area=$( [ "$verdict" = 'false' ] && echo false || echo true )" >> "$GITHUB_OUTPUT" done ui-motion: name: UI motion (Chromium + WebKit) needs: [changes] if: (github.event_name != 'workflow_dispatch' || inputs.platform != 'macos') && needs.changes.outputs.tauri != 'false' runs-on: ubuntu-24.04 timeout-minutes: 20 defaults: run: working-directory: openless-all/app steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' cache: npm cache-dependency-path: openless-all/app/package-lock.json - run: npm ci - run: npm run build - run: npx playwright install --with-deps chromium webkit - run: npm run test:ui-motion - uses: actions/upload-artifact@v4 if: failure() with: name: ui-motion-failure path: | openless-all/app/test-results/ openless-all/app/playwright-report/ android-check: needs: [changes] if: (github.event_name != 'workflow_dispatch' || inputs.platform != 'macos') && needs.changes.outputs.tauri != 'false' name: Android cargo check runs-on: ubuntu-latest timeout-minutes: 60 defaults: run: working-directory: openless-all/app steps: - uses: actions/checkout@v4 with: # Android uses no desktop local-ASR submodules; the macOS MLX dependency stays out of # this job. submodules: false # Android compiles no local-ASR C code: build.rs excludes the Android triple by TARGET # (does not call build_qwen_asr). qwen3-asr-rs is a path dependency, and the Cargo # resolver must read its manifest for all targets — so the ci-disable-macos-qwen3.mjs # step below removes that dependency line, otherwise cargo check hard-fails. # Dropping recursive fetch saves one network fetch and one failure point. - 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' # 仓库缓存上限 10 GB,超出后按 LRU 淘汰。过去每个 PR 都会各存一份 # ~1.6 GB 的依赖树,把其他 job 的缓存挤掉,导致每次运行都从零编译。 # 现在 PR 只从基线分支恢复,只有分支推送和 tag 才写入缓存。 save-if: ${{ github.event_name != 'pull_request' }} - 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: Disable macOS-only Qwen3 MLX dependency run: node scripts/ci-disable-macos-qwen3.mjs - name: Check Tauri backend (Android target) run: cargo check --locked --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 --locked --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 # The Linux core/egui/deb+rpm checks live in a reusable workflow, which runs # the same `cargo test -p openless-core` as upstream's `linux-core-contract` # plus the Linux host, fcitx5 and package contracts. linux-egui-package: needs: [changes] if: github.event_name != 'workflow_dispatch' || inputs.platform != 'macos' name: Linux Core, egui and deb/rpm checks permissions: contents: read uses: ./.github/workflows/check-linux-egui.yml with: # 散文改动跳过整条 Linux 构建与打包链;输出缺失/未知时退回 full。 scope: ${{ needs.changes.outputs.linux == 'false' && 'none' || 'full' }} cross-platform: needs: [changes] # 失败即运行:只有判定脚本明确说“够不到”才跳过。 if: needs.changes.outputs.tauri != 'false' name: ${{ matrix.label }} checks strategy: # One platform failing must not block other platforms from getting verification results. fail-fast: false matrix: include: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.platform == 'macos' && '[{"os":"macos-latest","label":"macOS","preflight":false}]' || '[{"os":"macos-latest","label":"macOS","preflight":false},{"os":"windows-latest","label":"Windows","preflight":true}]') }} runs-on: ${{ matrix.os }} timeout-minutes: 60 env: # With the shared Core in place, macOS's first build compiles the MLX C++ dependencies and # the full Tauri test binary together; limit concurrency so the arm64 runner is not OOM-killed # at peak memory. CARGO_BUILD_JOBS: 2 CARGO_PROFILE_TEST_DEBUG: 0 CMAKE_BUILD_PARALLEL_LEVEL: 2 defaults: run: working-directory: openless-all/app steps: - uses: actions/checkout@v4 with: # Initialize the MLX submodule on macOS only; Windows does not resolve that dependency. submodules: ${{ matrix.os == 'macos-latest' && 'recursive' || 'false' }} - name: Disable macOS-only Qwen3 MLX dependency if: runner.os != 'macOS' run: node scripts/ci-disable-macos-qwen3.mjs - uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: openless-all/app/package-lock.json # macOS MSRV is checked in a separate parallel job; Windows keeps the original check order. - uses: dtolnay/rust-toolchain@1.88.0 if: runner.os == 'Windows' - uses: dtolnay/rust-toolchain@stable - name: Configure macOS check profile if: runner.os == 'macOS' run: echo "CARGO_PROFILE_DEV_DEBUG=0" >> "$GITHUB_ENV" - uses: swatinem/rust-cache@v2 if: runner.os == 'Windows' with: workspaces: 'openless-all/app/src-tauri -> target' save-if: ${{ github.event_name != 'pull_request' }} - name: Cache macOS stable dependencies if: runner.os == 'macOS' uses: swatinem/rust-cache@v2 with: key: macos-stable-v1 save-if: ${{ github.event_name != 'pull_request' }} workspaces: | openless-all/app -> target openless-all/app/src-tauri -> target - name: Cache macOS MLX native build if: runner.os == 'macOS' && runner.arch == 'ARM64' uses: ./.github/actions/cache-macos-mlx with: profile: debug - name: Prepare Windows Sherpa static libraries if: runner.os == 'Windows' shell: pwsh run: ./scripts/prepare-windows-sherpa.ps1 - 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", "./scripts/prepare-windows-sherpa.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: Install pinned Codex CLI for sandbox contract if: runner.os == 'macOS' run: npm install --global @openai/codex@0.146.0 - name: Configure isolated Codex home if: runner.os == 'macOS' shell: bash run: | set -euo pipefail codex_home="$RUNNER_TEMP/openless-codex-home" mkdir -p "$codex_home" echo "CODEX_HOME=$codex_home" >> "$GITHUB_ENV" - name: Run Codex writable-roots sandbox contract if: runner.os == 'macOS' env: OPENLESS_CODEX_TEST_EXE: codex run: cargo test --locked -p openless-core hardening_actually_narrows_the_writable_roots -- --ignored --nocapture --test-threads=1 - name: Check Tauri backend (cargo check) if: runner.os == 'Windows' run: cargo check --locked --manifest-path src-tauri/Cargo.toml # test compiles and runs lib/bin, covering the production binary paths that plain cargo # check misses. Avoids a metadata-only check followed by another codegen pass over the same # dependency graph. - name: Run Rust backend unit tests if: runner.os != 'Windows' run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --bins --timings - name: Compile Rust backend unit tests (Windows) # A Windows runner can link the lib test binary, but on a clean image lacking an optional # native runtime DLL entrypoint, the process exits before the test harness starts. Keep # the cfg/link coverage here; the shared Core's public compatibility contract is actually # executed in the next step. if: runner.os == 'Windows' run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --no-run - name: Run Rust-only backend unit tests (Windows) # The standalone test crate does not link the full Tauri app lib; it verifies the public # Core contract only. Windows-specific Tauri tests on this runner only get the cfg/link # compile coverage from the step above. if: runner.os == 'Windows' run: cargo test --locked --manifest-path src-tauri/backend-tests/Cargo.toml - name: Check Tauri backend with Rust 1.88 MSRV if: runner.os == 'Windows' run: cargo +1.88.0 check --locked --manifest-path src-tauri/Cargo.toml - name: Compile backend tests with Rust 1.88 MSRV if: runner.os == 'Windows' 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 # Runs on both platforms: the Windows runner ships git-bash, giving consistent behavior # across shells. Fail immediately on version drift instead of discovering it at release. # Checks 5 places: package.json / package-lock.json (root + nested) / # tauri.conf.json / Cargo.toml / the [openless] package in Cargo.lock. 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: find the version line right after the [openless] package name 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" macos-msrv: needs: [changes] # 失败即运行:只有判定脚本明确说“够不到”才跳过。 if: needs.changes.outputs.msrv != 'false' name: macOS Rust 1.88 MSRV runs-on: macos-latest timeout-minutes: 60 env: CARGO_BUILD_JOBS: 2 CARGO_PROFILE_DEV_DEBUG: 0 CARGO_PROFILE_TEST_DEBUG: 0 CMAKE_BUILD_PARALLEL_LEVEL: 2 defaults: run: working-directory: openless-all/app steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/setup-node@v4 with: node-version: "22" cache: npm cache-dependency-path: openless-all/app/package-lock.json - uses: dtolnay/rust-toolchain@1.88.0 - uses: swatinem/rust-cache@v2 with: key: macos-msrv-v1 save-if: ${{ github.event_name != 'pull_request' }} workspaces: | openless-all/app/src-tauri -> target openless-all/app/src-tauri/backend-tests -> target - name: Cache macOS MLX native build if: runner.arch == 'ARM64' uses: ./.github/actions/cache-macos-mlx with: profile: debug - run: npm ci - run: npm run build - name: Check Tauri backend with Rust 1.88 MSRV run: cargo +1.88.0 check --locked --manifest-path src-tauri/Cargo.toml --timings - 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 --timings