release: OpenLess 2.0.0-Beta.4+build.20260930 (#1126)

Integrate ASR/Omni fixes, API Route, Android IME and confirmation-based vocabulary learning. Complete settings transitions and macOS single-item credential storage. Synchronize all application versions to 2.0.0-Beta.4+build.20260930.

Integrated PRs: #1116, #1120, #1121, #1123, #1124, #1067 and #1128. Linux #1117 remains independent and is excluded from automatic release.

Validated: full cross-platform CI, 18 UI motion cases, 112 frontend test files, 1245 Core tests (1 ignored) and 572 macOS native tests.

Co-authored-by: HKLHaoBin <we3q@qq.com>
This commit is contained in:
TRIPandHKLHaoBin authored and GitHub committed 2026-09-30 20:32:53 +08:00
1 parent 5b39599486
commit 6f9af89583
409 files changed
+57100 -15507

No files matched your search

+28 -3
View File
@@ -4,6 +4,8 @@ name: Android APK (debug)
# - push v*-tauri tag → signed release APK + minisign + updater manifest → GitHub Release
# - workflow_dispatch → signed release APK when ANDROID_KEYSTORE_* secrets exist
# (overlay install + user data preserved); otherwise unsigned debug APK (annotated, non-blocking)
# - workflow_dispatch + signed_debug=true — debug APK signed with the release
# keystore (when secrets exist); artifact remains openless-android-debug-*.
#
# #1103 build-time changes:
# - Do not wipe Cargo/Gradle/target before cache post-save (removed Free disk step).
@@ -26,8 +28,12 @@ on:
description: 'Dispatch-only fast release profile (no LTO, codegen-units=16). Ignored on tags.'
type: boolean
default: false
signed_debug:
description: 'Build debug mode but sign it with the release keystore (requires ANDROID_KEYSTORE_* secrets).'
type: boolean
default: false
# 同一 tag 重复推送只跑最新一次;workflow_dispatch 用 run_id 隔离避免互相取消。
# Repeated pushes of the same tag run only the newest; workflow_dispatch isolates by run_id to avoid mutual cancellation.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' }}
@@ -62,6 +68,7 @@ jobs:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
INPUT_SIGNED_DEBUG: ${{ github.event.inputs.signed_debug }}
run: |
set -euo pipefail
is_tag=false
@@ -89,6 +96,11 @@ jobs:
echo "label=${{ github.ref_name }}" >> "$GITHUB_OUTPUT"
echo "signing=tag-release" >> "$GITHUB_OUTPUT"
echo "signing_label=Tag release with release keystore" >> "$GITHUB_OUTPUT"
elif [[ "${INPUT_SIGNED_DEBUG:-false}" == "true" ]] && $has_keystore; then
echo "mode=debug" >> "$GITHUB_OUTPUT"
echo "label=signed-debug-run-${{ github.run_number }}" >> "$GITHUB_OUTPUT"
echo "signing=signed-debug" >> "$GITHUB_OUTPUT"
echo "signing_label=Manual dispatch debug APK signed with release keystore" >> "$GITHUB_OUTPUT"
elif $has_keystore; then
echo "mode=release" >> "$GITHUB_OUTPUT"
echo "label=dispatch-run-${{ github.run_number }}" >> "$GITHUB_OUTPUT"
@@ -100,6 +112,9 @@ jobs:
echo "signing=debug-fallback" >> "$GITHUB_OUTPUT"
echo "signing_label=Unsigned debug fallback (no keystore secrets)" >> "$GITHUB_OUTPUT"
echo "::notice title=Unsigned debug APK::ANDROID_KEYSTORE_* secrets not configured. Building debug APK without release signing. Overlay install and user data preservation require the release keystore; uninstall before installing if replacing a signed build."
if [[ "${INPUT_SIGNED_DEBUG:-false}" == "true" ]]; then
echo "::warning::signed_debug was requested but Android keystore secrets are incomplete; falling back to an unsigned debug APK."
fi
fi
- name: Resolve ABI matrix
@@ -146,7 +161,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
# Android 不使用本地 Qwen3/Whisper C 子模块。
# Android does not use the local Qwen3/Whisper C submodules.
submodules: false
- name: Disable macOS-only Qwen3 MLX dependency
@@ -286,13 +301,15 @@ jobs:
run: node scripts/merge-android-updater-manifest.mjs
- name: Configure Android release signing
if: needs.plan.outputs.mode == 'release'
if: needs.plan.outputs.mode == 'release' || needs.plan.outputs.signing == 'signed-debug'
working-directory: openless-all/app
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
# signed_debug: also wire debug buildType to the release keystore
OPENLESS_SIGN_DEBUG: ${{ needs.plan.outputs.signing == 'signed-debug' && 'true' || 'false' }}
run: node scripts/configure-android-release-signing.mjs
- name: Prime Gradle wrapper
@@ -313,6 +330,10 @@ jobs:
- name: Build Android debug APK
if: needs.plan.outputs.mode == 'debug'
working-directory: openless-all/app
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: npm run tauri:android:build:debug
- name: Build Android release APK
@@ -378,6 +399,10 @@ jobs:
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
> **Signed release APK** (manual dispatch). Same keystore as tag releases — supports overlay install and preserves user data when upgrading from an existing signed install.
EOF
elif [ "${{ needs.plan.outputs.signing }}" = "signed-debug" ]; then
cat >> "$GITHUB_STEP_SUMMARY" << 'EOF'
> **Signed debug APK** (manual dispatch). Debug mode is retained for diagnostics; the release keystore is used so `adb install -r` can upgrade the matching signed app without changing the debug artifact name.
EOF
fi
publish-android-release:
+3 -3
View File
@@ -1,13 +1,13 @@
name: Linux egui build and backend checks
# Only the CI and tag-release workflows call this deb/rpm build and verification path.
# CI and independent manual Linux builds share this deb/rpm verification path.
# No standalone push trigger: a Linux PR must show one combined Linux check.
on:
workflow_call:
inputs:
# 'none' skips a change set that is only prose (see
# scripts/ci-changed-areas.sh): the callers that must always build the
# packages, including the tag release, leave the default in place.
# packages, including accepted tag builds, leave the default in place.
scope:
required: false
type: string
@@ -27,7 +27,7 @@ jobs:
# A cold dependency tree costs ~18 minutes; anything past this is a hang.
timeout-minutes: 90
env:
RELEASE_TAG: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri') && github.ref_name || '' }}
RELEASE_TAG: ${{ github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri') && github.ref_name || '' }}
# Debug info dominates both compile time and cache size for this host, and
# the gate below never reads it. The Linux job keeps its own cache so a
# red run still starts warm.
+57 -24
View File
@@ -20,7 +20,7 @@ on:
options: [all, macos]
default: all
# 同一 PR 快速重复推送时取消旧运行;workflow_dispatch 用 run_id 隔离。
# 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' }}
@@ -56,6 +56,34 @@ jobs:
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'
@@ -68,14 +96,15 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
# Android 不使用任何桌面本地 ASR 子模块;macOS MLX 依赖不进入此任务。
# Android uses no desktop local-ASR submodules; the macOS MLX dependency stays out of
# this job.
submodules: false
# Android 不编译任何本地 ASR C 代码:build.rs 按 TARGET 解析排除
# Android triple(不调 build_qwen_asr)。qwen3-asr-rs 是 path 依赖,
# Cargo 解析器跨所有 target 都要读它的 manifest——所以由下方的
# ci-disable-macos-qwen3.mjs 删掉该依赖行,否则 cargo check 硬失败。
# 去掉递归拉取省一次网络 fetch + 失败点。
# 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:
@@ -229,15 +258,16 @@ jobs:
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:
# 新增 shared Core 后,macOS 首次编译同时构建 MLX C++ 依赖和完整
# Tauri test binary;限制并发避免 arm64 runner 在峰值内存处被系统终止。
# 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
@@ -247,7 +277,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
# 只在 macOS 初始化 MLX 子模块;Windows 不解析该依赖。
# 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'
@@ -259,7 +289,7 @@ jobs:
cache: npm
cache-dependency-path: openless-all/app/package-lock.json
# macOS MSRV 在独立 job 并行检查;Windows 保留原有检查顺序。
# 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'
@@ -343,22 +373,25 @@ jobs:
if: runner.os == 'Windows'
run: cargo check --locked --manifest-path src-tauri/Cargo.toml
# test 编译并运行 lib/bin,覆盖原 cargo check 的生产 binary 路径。
# 避免先 metadata-only check、再为同一依赖图做一次 codegen。
# 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)
# Windows runner 能链接 lib test binary,但干净镜像缺少可选 native runtime
# DLL entrypoint 时,进程会在 test harness 启动前退出。这里保留 cfg/link
# 覆盖;共享 Core 的公开 compatibility contract 由下一步实际执行。
# 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)
# 独立 test crate 不链接完整 Tauri app lib,只验证公开 Core contract。
# Windows 专属 Tauri 测试在此 runner 只做上一步的 cfg/link 编译覆盖。
# 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
@@ -371,10 +404,10 @@ jobs:
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] 包。
# 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")
@@ -382,7 +415,7 @@ jobs:
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: 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"
+3 -37
View File
@@ -1,12 +1,9 @@
name: Release Linux egui
# An admin pushes v*-tauri only after CI and Linux device acceptance. This
# workflow calls the exact same deb/rpm build used by CI, then attaches its
# verified assets to the shared Tauri/Android Release (Beta remains a draft).
# Independent Linux builds retain the same tests and deb/rpm packaging as CI.
# After product acceptance, an administrator builds the accepted release tag and
# explicitly attaches the verified Actions artifacts to that existing Release.
on:
push:
tags:
- 'v*-tauri'
workflow_dispatch:
permissions:
@@ -21,34 +18,3 @@ jobs:
permissions:
contents: read
uses: ./.github/workflows/check-linux-egui.yml
publish-linux-egui:
# Manual builds only upload Actions artifacts; they never change a Release.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
needs: build-linux-egui
runs-on: ubuntu-24.04
timeout-minutes: 30
permissions:
contents: write
steps:
- uses: actions/download-artifact@v8
with:
name: openless-linux-egui-x86_64
path: linux-egui-packages
- name: Verify downloaded release assets
run: |
set -euo pipefail
test "$(find linux-egui-packages -maxdepth 1 -name '*.deb' | wc -l)" -eq 1
test "$(find linux-egui-packages -maxdepth 1 -name '*.rpm' | wc -l)" -eq 1
test "$(find linux-egui-packages -maxdepth 1 -name '*.AppImage' | wc -l)" -eq 0
( cd linux-egui-packages && sha256sum --check --strict SHA256SUMS )
- name: Attach Linux assets to the shared release draft
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.ref_name }}
draft: ${{ endsWith(github.ref_name, '-beta-tauri') || contains(github.ref_name, '-Beta.') }}
prerelease: ${{ endsWith(github.ref_name, '-beta-tauri') || contains(github.ref_name, '-Beta.') }}
files: linux-egui-packages/*
fail_on_unmatched_files: true
+107 -94
View File
@@ -1,20 +1,21 @@
name: Release Tauri (cross-platform)
# meta: ensure Actions indexes this workflow on forks (no behavior change).
# 触发条件:
# - 推 v*.*.*-tauri 形式的 tag(与老 Swift 版的 vX.Y.Z 区分开,不冲突)
# - 手动 dispatch(用于测试构建,不发版)
# Triggers:
# - push a v*.*.*-tauri tag (kept distinct from the legacy Swift vX.Y.Z tags, no conflict)
# - manual dispatch (for test builds, no release)
#
# 输出:
# macOS arm64/x64 .dmg + Windows x64 .msi/.exe,自动作为 GitHub Release 资产上传。
# Linux egui 由 release-linux-egui.yml 独立构建;本工作流不编译 Linux/Tauri。
# Outputs:
# macOS arm64/x64 .dmg + Windows x64 .msi/.exe, uploaded automatically as GitHub Release assets.
# Linux egui is built separately by release-linux-egui.yml; this workflow does not build Linux/Tauri.
#
# macOS 分发:
# - 配好 APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / APPLE_ID /
# APPLE_PASSWORD / APPLE_TEAM_ID 后,Tauri 会做 Developer ID 签名和公证。
# 用户从浏览器下载后不需要手工 xattr。
# - 未配置 Apple secrets 时自动回退 ad-hoc 签名,GitHub Actions 会打印 warning。
# - Windows 没签名(无证书),Win 11 SmartScreen 会警告 "未识别的发布者",用户点"仍要运行"。
# - 任意一个 platform 失败不影响另一个继续构建(fail-fast: false)。
# macOS distribution:
# - With APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / APPLE_ID /
# APPLE_PASSWORD / APPLE_TEAM_ID configured, Tauri performs Developer ID signing and
# notarization; users downloading from a browser do not need manual xattr.
# - Without Apple secrets, falls back to ad-hoc signing; GitHub Actions prints a warning.
# - Windows is unsigned (no certificate); Win 11 SmartScreen warns "Unknown publisher" and the
# user clicks "Run anyway".
# - One platform failing does not stop the other from building (fail-fast: false).
on:
push:
@@ -28,7 +29,8 @@ on:
options: [all, macos]
default: all
# 同一 tag 重复推送只跑最新一次;workflow_dispatch 用 run_id 隔离避免互相取消。
# Repeated pushes of the same tag run only the latest run; workflow_dispatch isolates by run_id
# to avoid mutual cancellation.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'push' }}
@@ -48,18 +50,19 @@ jobs:
env:
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
# 渠道由 tag 后缀决定:
# Channel is decided by the tag suffix:
# v<v>-beta-tauri / v<v>-Beta.N-tauri
# → beta 渠道(GitHub Release 标 prerelease,manifest 文件名带 -beta 后缀,
# 正式版用户的 endpoint 拿不到)
# v<v>-tauri → stable 渠道(正式版,文件名沿用旧约定,向后兼容)
# workflow_dispatch / 非 tag 触发时 github.ref_name 不是 tag 字符串,
# endsWith 返回 false,回退为 stable,不改变现有 dispatch 行为。
# -> beta channel (GitHub Release marked prerelease, manifest filename
# carries a -beta suffix so stable users' endpoint never sees it)
# v<v>-tauri -> stable channel (regular release; filenames follow the old
# convention for backward compatibility)
# On workflow_dispatch / non-tag triggers github.ref_name is not a tag string, endsWith
# returns false, and it falls back to stable — dispatch behavior unchanged.
OPENLESS_RELEASE_CHANNEL: ${{ (endsWith(github.ref_name, '-beta-tauri') || contains(github.ref_name, '-Beta.')) && 'beta' || 'stable' }}
steps:
- uses: actions/checkout@v4
with:
# MLX 子模块只在 macOS 发布构建需要。
# The MLX submodule is only needed for macOS release builds.
submodules: ${{ startsWith(matrix.platform, 'macos') && 'recursive' || 'false' }}
- name: Disable macOS-only Qwen3 MLX dependency
@@ -76,7 +79,8 @@ jobs:
with:
targets: ${{ matrix.rust-target }}
# 在恢复缓存前设置实际编译环境,让 cache key 包含 macOS profile。
# Set up the actual build environment before restoring caches so the cache key includes
# the macOS profile.
- name: Configure macOS build environment
if: startsWith(matrix.platform, 'macos')
working-directory: openless-all/app
@@ -199,12 +203,12 @@ jobs:
fi
done
# ── macOS:用我们自己的 build-mac.sh,统一处理签名、公证和 artifact 清理 ──
# ── macOS: use our own build-mac.sh, handling signing, notarization, and artifact cleanup ──
- name: Build (macOS)
if: startsWith(matrix.platform, 'macos')
working-directory: 'openless-all/app'
env:
INSTALL: '0' # CI 不要装到 /Applications,也不要 reset TCC
INSTALL: '0' # CI must not install to /Applications or reset TCC
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
run: bash scripts/build-mac.sh
@@ -217,9 +221,9 @@ jobs:
path: openless-all/app/src-tauri/target/cargo-timings/*.html
if-no-files-found: ignore
# ── Windows:先 build OpenLessIme.dll(x64+x86),再跑 tauri bundle。
# openless-ime.wxs 用 $(env.OPENLESS_IME_DLL_X64) / _X86 拿绝对路径,
# 跨 candle/light cwd 都能 resolve(Tauri wix bundler cwd 不固定)。
# ── Windows: build OpenLessIme.dll (x64+x86) first, then run tauri bundle.
# openless-ime.wxs reads absolute paths from $(env.OPENLESS_IME_DLL_X64) / _X86,
# resolvable across candle/light cwd changes (Tauri's wix bundler cwd is not fixed).
- name: Build Windows IME native DLLs
if: matrix.platform == 'windows-latest'
shell: pwsh
@@ -243,20 +247,22 @@ jobs:
"$($t.EnvName)=$dll" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "[ok] built $dll (exported $($t.EnvName))"
# bundle.resources 引用的是 src-tauri/openless-ime-payload/{x64,x86}/OpenLessIme.dll
# (仓库里 commit 的是 0 字节占位让 mac 本地 build 也能 resolve)。
# 这里用真 dll 覆盖占位,让 NSIS / MSI 都装上真文件;NSIS hook 的 regsvr32
# 同时会把 64 / 32 位 COM 注册到 HKLM\Software\Classes\CLSID 的
# KEY_WOW64_64KEY / KEY_WOW64_32KEY 两侧(windows_ime_profile.rs 都会查)。
# bundle.resources references src-tauri/openless-ime-payload/{x64,x86}/OpenLessIme.dll
# (the repo commits 0-byte placeholders so mac local builds can also resolve).
# Overwrite the placeholders with the real dlls so NSIS / MSI install real files;
# the NSIS hook's regsvr32 also registers the 64/32-bit COM classes under
# HKLM\Software\Classes\CLSID on both the KEY_WOW64_64KEY / KEY_WOW64_32KEY
# views (both are checked by windows_ime_profile.rs).
$payloadDir = Join-Path $appRoot "src-tauri\openless-ime-payload\$($t.Folder)"
New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null
Copy-Item -Force -Path $dll -Destination (Join-Path $payloadDir 'OpenLessIme.dll')
Write-Host "[ok] copied real $($t.Folder) dll into bundle.resources payload path"
}
# ── Windows tauri build:保持 bash shell,因为 PowerShell 调外部命令
# 会把 '{"bundle":...}' 的内部双引号吃掉、让 tauri 收到无效 JSON。
# 用 set +e + GITHUB_ENV 把 exit code 传到下一步给 Repair 判断。
# ── Windows tauri build: keep the bash shell, because PowerShell invoking external
# commands strips the inner double quotes of '{"bundle":...}' and tauri receives
# invalid JSON.
# Use set +e + GITHUB_ENV to pass the exit code to the next step for Repair.
- name: Build (Windows)
if: matrix.platform == 'windows-latest'
shell: bash
@@ -276,13 +282,13 @@ jobs:
msi_supported=0
echo "[info] Skipping MSI bundle for non-numeric prerelease version $app_version."
fi
# 拆两轮跑:Tauri 的签名 / updater artifact 阶段是 post-bundle 钩子,
# 任意 bundler 失败会让 *所有* bundle 的 .sig 跳过。MSI 必踩 ICE80,
# 所以单一 `tauri build` 永远拿不到 NSIS 的 .exe.sig。
# Pass 1:NSIS 独立跑——必须成功,产出 *_x64-setup.exe(.sig) 给 updater。
# Pass 2:MSI 独立跑——允许失败,Repair 步骤兜底 light.exe 重链。
# Beta 版跳过 MSI:Windows Installer / WiX 无法表示 `1.2.3-Beta.1`
# 这类非纯数字 prerelease 标识,稳定版仍然照常产出 MSI。
# Run in two passes: Tauri's signing / updater artifact stage is a post-bundle hook,
# and any bundler failure makes *all* bundles skip their .sig. MSI always hits ICE80,
# so a single `tauri build` can never produce the NSIS .exe.sig.
# Pass 1: NSIS alone — must succeed, producing *_x64-setup.exe(.sig) for the updater.
# Pass 2: MSI alone — allowed to fail; the Repair step re-links with light.exe.
# Beta skips MSI: Windows Installer / WiX cannot represent non-numeric prerelease
# identifiers like `1.2.3-Beta.1`; stable still produces MSI as usual.
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
npm run tauri -- build --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":true}}'
nsis_exit=$?
@@ -309,17 +315,18 @@ jobs:
fi
fi
echo "TAURI_BUILD_EXIT=$msi_exit" >> "$GITHUB_ENV"
# NSIS 是 updater 的硬依赖,挂了就直接 fail step。
# NSIS is a hard dependency of the updater; fail the step outright if it fails.
if [ "$nsis_exit" -ne 0 ]; then
echo "::error::NSIS bundle failed (exit $nsis_exit) — updater artifact unavailable."
exit 1
fi
# MSI 失败不挡——下一步 Repair 用 light.exe 重链(带 -sice:ICE80)。
# MSI failure does not block — the next Repair step re-links with light.exe (with -sice:ICE80).
exit 0
# ── 如果 tauri 在 wix link 阶段失败(candle 出了 wixobj,但 light 因 cwd
# 解析 wxs Source 找不到 IME DLL),从 appRoot 手动跑 light 兜底重链。
# 这是本地 windows-package-msvc.ps1::Repair-TauriMsiBundle 的同款手术。
# ── If tauri fails at the wix link stage (candle produced wixobj, but light cannot find
# the IME DLL because of cwd-relative wxs Source resolution), manually run light from
# appRoot to re-link. Same surgery as the local windows-package-msvc.ps1
# ::Repair-TauriMsiBundle.
- name: Repair Windows MSI if Tauri failed at WiX link
if: matrix.platform == 'windows-latest'
shell: pwsh
@@ -359,10 +366,10 @@ jobs:
$msiPath = Join-Path $bundleDir "OpenLess_${version}_x64_en-US.msi"
Push-Location $appRoot
try {
# -sice:ICE80:x86 IME DLL 与 x64 一同打进 INSTALLDIR\windows-ime\,
# 这是 32 位组件落在 64 位 Directory 下的合法场景(DLL 路径绝对指向,
# 不依赖 SysWOW64 重定向)。Tauri 自身没有暴露 light 透传参数,所以
# 必须在这里抑制,否则 LGHT0204 必失败。
# -sice:ICE80: the x86 IME DLL is bundled alongside x64 under INSTALLDIR\windows-ime\;
# a 32-bit component under a 64-bit Directory is legitimate here (the DLL path is
# absolute and does not rely on SysWOW64 redirection). Tauri exposes no pass-through
# for light arguments, so ICE80 must be suppressed here or LGHT0204 always fails.
& $light -nologo -sice:ICE80 -ext WixUIExtension -ext WixUtilExtension -loc $locale -out $msiPath $mainObj $imeObj
if ($LASTEXITCODE -ne 0) { throw "light.exe relink failed with exit $LASTEXITCODE" }
} finally {
@@ -414,26 +421,28 @@ jobs:
OPENLESS_UPDATE_ARCH: ${{ matrix.updater-arch }}
OPENLESS_UPDATE_REPO: Open-Less/openless
OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com
# beta 渠道时输出 latest-{tgt}-{arch}-beta.json,stable 沿用旧文件名。
# On the beta channel output latest-{tgt}-{arch}-beta.json; stable keeps the old filename.
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
# Beta 渠道里脚本要把 manifest.url 写成 releases/download/<tag>/...
# 而不是 releases/latest(后者永远 = Stable,Beta 用户按 url 下载会拉到错文件)。
# workflow_dispatch 时 github.ref_name 不是 tag——但那种情况下 channel=stable,
# 脚本不会读这个字段,安全。
# On the beta channel the script must write manifest.url as releases/download/<tag>/...
# rather than releases/latest (the latter is always = Stable, so beta users downloading
# via url would fetch the wrong file).
# On workflow_dispatch github.ref_name is not a tag — but then channel=stable and the
# script does not read this field, so it is safe.
OPENLESS_RELEASE_TAG: ${{ github.ref_name }}
run: node scripts/write-updater-manifest.mjs
# ── 收集产物 ──
# ── Collect artifacts ──
- name: List artifacts (debug)
shell: bash
working-directory: 'openless-all/app/src-tauri/target/release/bundle'
run: ls -la macos/ dmg/ nsis/ msi/ 2>/dev/null || true
# 防御性步骤:剥掉 macOS 产物上任何残留扩展属性 / quarantine。
# 理论上 GitHub Actions 输出的 .app/.dmg 不会带 com.apple.quarantine
# (xattr 也不会通过 actions/upload-artifact 跨机器持久化),但保留这一步
# 让"云端 artifact 一定干净"成为可验证的承诺。用户下载后再被本地浏览器
# 加 quarantine 时,按 release notes 的 `xattr -cr` 一行即可消除。
# Defensive step: strip any residual extended attributes / quarantine from macOS bundles.
# In theory GitHub Actions output .app/.dmg never carries com.apple.quarantine (and xattr
# does not persist across machines via actions/upload-artifact), but keeping this step
# makes "cloud artifacts are always clean" a verifiable promise. If the user's local
# browser adds quarantine after download, the `xattr -cr` line in the release notes
# removes it.
- name: Strip xattr / quarantine on macOS bundles
if: startsWith(matrix.platform, 'macos')
shell: bash
@@ -490,11 +499,11 @@ jobs:
openless-all/app/src-tauri/target/release/bundle/latest-windows-x86_64*.json
if-no-files-found: error
# ── tag 推送时,同步上传到 GitHub Release ──
# 只有 leader job (darwin/aarch64) 把 release body 写到文件,其余 matrix job
# 的 body_path 留空,softprops/action-gh-release@v2 在 body 为空时会保留
# existing release body 不动,避免每个 matrix job 都 append 一遍同样的 prelude
# 导致 release notes 重复 N 次。
# ── On tag push, also upload to the GitHub Release ──
# Only the leader job (darwin/aarch64) writes the release body to a file; the other matrix
# jobs leave body_path empty. softprops/action-gh-release@v2 keeps the existing release
# body when body is empty, so each matrix job does not append the same prelude again and
# duplicate the release notes N times.
- name: Prepare release body prelude
if: matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
shell: bash
@@ -512,13 +521,12 @@ jobs:
### 渠道说明
- 以 `-tauri` 结尾的 tag 是**正式版**(自动推送给所有 in-app 检查更新的用户)。
- 以 `-beta-tauri` 结尾或包含 `-Beta.N-tauri` 的 tag 是 **Beta 版**(GitHub 标 pre-release,**不**通过 in-app updater 推送给正式版用户;只对在「设置 → 关于 → 加入 Beta 渠道」开关切到 Beta 的用户可见,且需要手动从此页面下载安装)。
- `vX.Y.Z-tauri` 是**正式版**,使用 Stable 更新源。
- `vX.Y.Z-Beta.N-tauri` 是 **Beta 版**(GitHub pre-release)。在「设置 → 关于与更新 → 加入 Beta 频道」选择 Beta 更新源,也可从本页下载安装包。历史 `-beta-tauri` 标签继续兼容,Beta 不推送给 Stable 用户。
### 行为变更提示
- 流式输入默认开启;不兼容场景会自动回落到一次性插入。可在「设置 → 高级」关闭。
- 流式输入成功后默认把最终文本同步到剪贴板,方便再次粘贴;可在「设置 → 高级」关闭。
- 输入方式、流式输入与剪贴板选项位于「设置 → 录音与输入」,按平台能力显示。
EOF
echo "OPENLESS_RELEASE_BODY_PATH=$RUNNER_TEMP/release-body.md" >> "$GITHUB_ENV"
@@ -531,17 +539,17 @@ jobs:
# Keep Beta assets private until all platforms have built and the
# administrator has verified the downloadable packages.
draft: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
# beta 渠道的 release 必须标 prerelease=true:GitHub UI 会折叠它,
# 普通用户看不到;同时只上传 latest-*-beta.json,正式版用户的
# endpoint(latest-*.json)永远不会被覆盖,保证 Beta 不溢出正式版。
# Beta releases must be marked prerelease=true: GitHub UI collapses them and regular
# users do not see them; only latest-*-beta.json is uploaded, so stable users'
# endpoint (latest-*.json) is never overwritten and beta never leaks into stable.
prerelease: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
# 非 leader job 的 OPENLESS_RELEASE_BODY_PATH 是空字符串,softprops 在
# body 为空时走 `body || existing.body` 分支保留既有内容,不会覆盖。
# leader job 用默认 append_body=false,每次完整覆盖 release body 为
# "prelude + generated notes",re-run 同一 tag 时也保持 idempotent
# (append_body=true 会让 re-run 把上轮 body 拼到前面、再次复制)。
# Non-leader jobs have OPENLESS_RELEASE_BODY_PATH as an empty string; softprops keeps
# the existing content via the `body || existing.body` branch when body is empty.
# The leader job uses the default append_body=false and fully overwrites the release
# body with "prelude + generated notes" each time, staying idempotent on re-runs of
# the same tag (append_body=true would prepend the previous body again and duplicate).
body_path: ${{ env.OPENLESS_RELEASE_BODY_PATH }}
# generate_release_notes 也只在 leader 跑,避免 matrix jobs 重复生成。
# generate_release_notes also runs only on the leader, avoiding duplicates from matrix jobs.
generate_release_notes: ${{ matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' }}
files: |
openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg
@@ -553,14 +561,16 @@ jobs:
openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig
openless-all/app/src-tauri/target/release/bundle/latest-*.json
# ── 正式版发布后,自动更新 Homebrew cask ──
# 为什么放进这条流水线,而不是单独的 `release: published` 工作流:softprops 用默认
# GITHUB_TOKEN 创建的 Release 不会触发 `release` 事件的其它工作流(GitHub 防递归),
# 所以独立的 update-cask 工作流永远不会被自动触发(历史上只在一次手动场景跑过且失败)。
# 用 needs: build 串在构建之后、同一条流水线里,才能保证每次正式发版都自动更新 cask。
# ── After a stable release, update the Homebrew cask automatically ──
# Why in this pipeline instead of a separate `release: published` workflow: a Release created
# by softprops with the default GITHUB_TOKEN does not trigger other workflows on the `release`
# event (GitHub's anti-recursion rule), so a standalone update-cask workflow would never fire
# automatically. Chaining it with needs: build in the same pipeline is the only way to update
# the cask on every stable release.
#
# 仅正式版:v*-tauri 且非 -beta-tauri。beta 不碰 Homebrew,避免把预发布版推给
# `brew install --cask openless` 的用户。cask 文件在默认分支(beta)上,提交回该分支。
# Stable only: v*-tauri and not -beta-tauri. Beta does not touch Homebrew, avoiding pushing a
# prerelease to `brew install --cask openless` users. The cask lives on the default branch
# (beta); commits go back to that branch.
update-homebrew-cask:
name: Update Homebrew cask (stable only)
needs: build
@@ -590,7 +600,8 @@ jobs:
version="${TAG#v}"
version="${version%-tauri}"
# 直接下载 DMG 自己算 sha256,不依赖 GitHub 资产 digest 字段的时序/可用性。
# Download the DMGs directly and compute sha256 ourselves; do not depend on the
# timing/availability of GitHub's asset digest fields.
mkdir -p "$RUNNER_TEMP/dmg"
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
--dir "$RUNNER_TEMP/dmg" \
@@ -614,14 +625,16 @@ jobs:
run: |
set -euo pipefail
cask="Casks/openless.rb"
# ⚠️ cask 第 2 行 `arch arm: "aarch64", intel: "x64"` 也含 `intel: "..."`,
# 裸 `s/intel: "..."/` 会把 arch 指令一起改成哈希 → Intel 用户 brew 装包 404。
# 所以 intel 的 sed 用 `^[[:space:]]*intel:` 锚定 sha256 块那一行;arm 的 sed
# 靠 `sha256 arm: ` 前缀天然只命中 sha256 行。捕获组 \1 保留原缩进。
# Warning: line 2 of the cask, `arch arm: "aarch64", intel: "x64"`, also contains
# `intel: "..."`. A bare `s/intel: "..."/` would rewrite the arch instruction with a
# hash too -> brew installs 404 for Intel users. So the intel sed anchors the sha256
# line with `^[[:space:]]*intel:`, while the arm sed naturally matches only the sha256
# line via the `sha256 arm: ` prefix. Capture group \1 preserves the original indent.
sed -i "s/version \"[^\"]*\"/version \"$VERSION\"/" "$cask"
sed -i "s/sha256 arm: \"[^\"]*\"/sha256 arm: \"$ARM_SHA\"/" "$cask"
sed -i "s/^\([[:space:]]*\)intel: \"[^\"]*\"/\1intel: \"$INTEL_SHA\"/" "$cask"
# 防呆:arch 指令必须原样保留,且本次 sha256 必须确实写进去了,否则让 job 失败。
# Guard: the arch instruction must remain untouched and both new sha256 values must
# actually be written, otherwise fail the job.
grep -q 'arch arm: "aarch64", intel: "x64"' "$cask" \
|| { echo "::error::arch 指令被 sed 误伤"; exit 1; }
grep -q "\"$ARM_SHA\"" "$cask" && grep -q "\"$INTEL_SHA\"" "$cask" \
@@ -641,6 +654,6 @@ jobs:
echo "cask 已是 $VERSION,无需提交"
exit 0
fi
# [skip ci]:cask 文本改动不需要再跑一遍跨平台 ci.yml。
# [skip ci]: cask text changes do not need another cross-platform ci.yml run.
git commit -m "[cask] openless $VERSION (auto from release) [skip ci]"
git push