Merge remote-tracking branch 'origin/beta' into fix/audit-1.3.14-foll… #112
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release Tauri (cross-platform) | |
| # meta: ensure Actions indexes this workflow on forks (no behavior change). | |
| # 触发条件: | |
| # - 推 v*.*.*-tauri 形式的 tag(与老 Swift 版的 vX.Y.Z 区分开,不冲突) | |
| # - 手动 dispatch(用于测试构建,不发版) | |
| # | |
| # 输出: | |
| # macOS arm64/x64 .dmg + Windows x64 .msi/.exe + Linux x64 .deb/.rpm/.AppImage,自动作为 GitHub Release 资产上传。 | |
| # | |
| # 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)。 | |
| on: | |
| push: | |
| tags: | |
| - 'v*-tauri' | |
| workflow_dispatch: | |
| jobs: | |
| build: | |
| permissions: | |
| contents: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: macos-latest | |
| rust-target: aarch64-apple-darwin | |
| updater-target: darwin | |
| updater-arch: aarch64 | |
| - platform: macos-15-intel | |
| rust-target: x86_64-apple-darwin | |
| updater-target: darwin | |
| updater-arch: x86_64 | |
| - platform: windows-latest | |
| rust-target: x86_64-pc-windows-msvc | |
| updater-target: windows | |
| updater-arch: x86_64 | |
| - platform: ubuntu-22.04 | |
| rust-target: x86_64-unknown-linux-gnu | |
| updater-target: linux | |
| updater-arch: x86_64 | |
| runs-on: ${{ matrix.platform }} | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| # 渠道由 tag 后缀决定: | |
| # 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 行为。 | |
| OPENLESS_RELEASE_CHANNEL: ${{ (endsWith(github.ref_name, '-beta-tauri') || contains(github.ref_name, '-Beta.')) && 'beta' || 'stable' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| # vendor/qwen-asr 是 macOS 上 build.rs 必须的 git submodule(cc-rs 编译 | |
| # Open-Less/qwen-asr fork 的 C 源),不拉就会在 mac 端 cargo build 阶段挂掉。 | |
| submodules: recursive | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: 'openless-all/app/package-lock.json' | |
| - uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.rust-target }} | |
| - name: Cache Cargo | |
| uses: swatinem/rust-cache@v2 | |
| with: | |
| workspaces: 'openless-all/app/src-tauri -> target' | |
| - name: Install Linux bundle deps | |
| if: matrix.platform == 'ubuntu-22.04' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| build-essential \ | |
| curl \ | |
| file \ | |
| libasound2-dev \ | |
| libayatana-appindicator3-dev \ | |
| libfuse2 \ | |
| librsvg2-dev \ | |
| libssl-dev \ | |
| libwebkit2gtk-4.1-dev \ | |
| libxdo-dev \ | |
| patchelf \ | |
| rpm \ | |
| wget | |
| - name: Install npm deps | |
| working-directory: 'openless-all/app' | |
| run: npm ci | |
| - name: Check updater signing availability | |
| if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri') | |
| shell: bash | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| run: | | |
| if [ -z "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then | |
| echo "::error::TAURI_SIGNING_PRIVATE_KEY is required for signed auto-update artifacts." | |
| exit 1 | |
| fi | |
| - name: Check Apple signing availability | |
| if: startsWith(matrix.platform, 'macos') && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri') | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| missing=() | |
| for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID; do | |
| if [ -z "${!name:-}" ]; then | |
| missing+=("$name") | |
| fi | |
| done | |
| if [ "${#missing[@]}" -gt 0 ]; then | |
| echo "::warning::macOS release will use ad-hoc signing because Apple signing/notarization secrets are missing: ${missing[*]}" | |
| fi | |
| - name: Import Apple Developer ID certificate | |
| if: startsWith(matrix.platform, 'macos') | |
| shell: bash | |
| env: | |
| APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} | |
| APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} | |
| run: | | |
| if [ -z "${APPLE_CERTIFICATE:-}" ] || [ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]; then | |
| echo "No Apple certificate secrets configured; macOS build will use ad-hoc signing." | |
| exit 0 | |
| fi | |
| KEYCHAIN_PASSWORD="${KEYCHAIN_PASSWORD:-$(openssl rand -base64 32)}" | |
| CERT_PATH="$RUNNER_TEMP/openless-certificate.p12" | |
| KEYCHAIN_PATH="$RUNNER_TEMP/openless-build.keychain-db" | |
| echo "$APPLE_CERTIFICATE" | base64 --decode > "$CERT_PATH" | |
| security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security default-keychain -s "$KEYCHAIN_PATH" | |
| security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| security set-keychain-settings -t 3600 -u "$KEYCHAIN_PATH" | |
| security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" | |
| DEVELOPER_ID_INFO="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep 'Developer ID Application' | head -n 1)" | |
| if [ -n "$DEVELOPER_ID_INFO" ]; then | |
| CERT_INFO="$DEVELOPER_ID_INFO" | |
| else | |
| CERT_INFO="$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" | grep -E 'Apple Distribution|Apple Development' | head -n 1)" | |
| fi | |
| if [ -z "$CERT_INFO" ]; then | |
| echo "Apple certificate imported, but no usable code-signing identity was found." | |
| security find-identity -v -p codesigning "$KEYCHAIN_PATH" | |
| exit 1 | |
| fi | |
| CERT_ID="$(echo "$CERT_INFO" | awk -F'"' '{print $2}')" | |
| echo "APPLE_SIGNING_IDENTITY=$CERT_ID" >> "$GITHUB_ENV" | |
| echo "Imported Apple signing identity: $CERT_ID" | |
| - name: Configure Apple notarization | |
| if: startsWith(matrix.platform, 'macos') | |
| shell: bash | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| APPLE_PROVIDER_SHORT_NAME: ${{ secrets.APPLE_PROVIDER_SHORT_NAME }} | |
| run: | | |
| for name in APPLE_ID APPLE_PASSWORD APPLE_TEAM_ID APPLE_PROVIDER_SHORT_NAME; do | |
| value="${!name:-}" | |
| if [ -n "$value" ]; then | |
| echo "$name=$value" >> "$GITHUB_ENV" | |
| fi | |
| done | |
| # ── macOS:用我们自己的 build-mac.sh,统一处理签名、公证和 artifact 清理 ── | |
| - name: Build (macOS) | |
| if: startsWith(matrix.platform, 'macos') | |
| working-directory: 'openless-all/app' | |
| env: | |
| INSTALL: '0' # CI 不要装到 /Applications,也不要 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 | |
| # ── 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 不固定)。 | |
| - name: Build Windows IME native DLLs | |
| if: matrix.platform == 'windows-latest' | |
| shell: pwsh | |
| working-directory: 'openless-all/app' | |
| run: | | |
| $appRoot = (Resolve-Path .).Path | |
| foreach ($t in @( | |
| @{ Platform = 'x64'; Folder = 'x64'; EnvName = 'OPENLESS_IME_DLL_X64' }, | |
| @{ Platform = 'Win32'; Folder = 'x86'; EnvName = 'OPENLESS_IME_DLL_X86' } | |
| )) { | |
| $out = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\$($t.Folder)\Release" | |
| $obj = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\obj\$($t.Folder)\Release" | |
| ./scripts/windows-ime-build.ps1 -Configuration Release -Platform $t.Platform -OutputDirectory $out -IntermediateDirectory $obj | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "OpenLessIme $($t.Platform) build failed with exit $LASTEXITCODE" | |
| } | |
| $dll = (Resolve-Path (Join-Path $out 'OpenLessIme.dll')).Path | |
| if (-not (Test-Path $dll)) { | |
| throw "OpenLessIme.dll not produced at $dll" | |
| } | |
| "$($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 都会查)。 | |
| $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 判断。 | |
| - name: Build (Windows) | |
| if: matrix.platform == 'windows-latest' | |
| shell: bash | |
| working-directory: 'openless-all/app' | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| set +e | |
| # 拆两轮跑: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。 | |
| if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then | |
| npm run tauri -- build --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":true}}' | |
| nsis_exit=$? | |
| if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ]; then | |
| echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV" | |
| echo "[info] Skipping MSI bundle for beta release channel." | |
| msi_exit=0 | |
| else | |
| echo "OPENLESS_SKIP_WINDOWS_MSI=0" >> "$GITHUB_ENV" | |
| npm run tauri -- build --bundles msi --config '{"bundle":{"createUpdaterArtifacts":true}}' | |
| msi_exit=$? | |
| fi | |
| else | |
| npm run tauri -- build --bundles nsis | |
| nsis_exit=$? | |
| if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ]; then | |
| echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV" | |
| echo "[info] Skipping MSI bundle for beta release channel." | |
| msi_exit=0 | |
| else | |
| echo "OPENLESS_SKIP_WINDOWS_MSI=0" >> "$GITHUB_ENV" | |
| npm run tauri -- build --bundles msi | |
| msi_exit=$? | |
| fi | |
| fi | |
| echo "TAURI_BUILD_EXIT=$msi_exit" >> "$GITHUB_ENV" | |
| # NSIS 是 updater 的硬依赖,挂了就直接 fail step。 | |
| 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)。 | |
| exit 0 | |
| # ── 如果 tauri 在 wix link 阶段失败(candle 出了 wixobj,但 light 因 cwd | |
| # 解析 wxs Source 找不到 IME DLL),从 appRoot 手动跑 light 兜底重链。 | |
| # 这是本地 windows-package-msvc.ps1::Repair-TauriMsiBundle 的同款手术。 | |
| - name: Repair Windows MSI if Tauri failed at WiX link | |
| if: matrix.platform == 'windows-latest' | |
| shell: pwsh | |
| working-directory: 'openless-all/app' | |
| run: | | |
| $exitCode = $env:TAURI_BUILD_EXIT | |
| if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') { | |
| Write-Host "[ok] Skipping MSI repair for beta release channel." | |
| return | |
| } | |
| $appRoot = (Resolve-Path .).Path | |
| $msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue | Select-Object -First 1 | |
| if ($msi) { | |
| Write-Host "[ok] MSI already produced by tauri: $($msi.FullName); no repair needed." | |
| return | |
| } | |
| if (-not $exitCode -or $exitCode -eq '0') { | |
| throw "Tauri exited 0 but no MSI found at src-tauri\target\release\bundle\msi\" | |
| } | |
| Write-Warning "Tauri MSI failed (exit $exitCode). Attempting manual light.exe relink from app root." | |
| $wixRoot = Join-Path $appRoot 'src-tauri\target\release\wix\x64' | |
| $mainObj = Join-Path $wixRoot 'main.wixobj' | |
| $imeObj = Join-Path $wixRoot 'openless-ime.wixobj' | |
| $locale = Join-Path $wixRoot 'locale.wxl' | |
| foreach ($p in @($mainObj, $imeObj, $locale)) { | |
| if (-not (Test-Path $p)) { | |
| throw "Required WiX object missing: $p — tauri build aborted before candle ran. Check the Build (Windows) step log." | |
| } | |
| } | |
| $light = Get-ChildItem "$env:LOCALAPPDATA\tauri\WixTools*\light.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object FullName | | |
| Select-Object -Last 1 -ExpandProperty FullName | |
| if (-not $light) { throw "WiX light.exe not found under $env:LOCALAPPDATA\tauri\WixTools*" } | |
| $version = (Get-Content src-tauri\tauri.conf.json -Raw | ConvertFrom-Json).version | |
| $bundleDir = Join-Path $appRoot 'src-tauri\target\release\bundle\msi' | |
| New-Item -ItemType Directory -Force -Path $bundleDir | Out-Null | |
| $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 必失败。 | |
| & $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 { | |
| Pop-Location | |
| } | |
| Write-Host "[ok] MSI rebuilt at $msiPath" | |
| - name: Verify Windows installers register TSF IME | |
| if: matrix.platform == 'windows-latest' | |
| shell: pwsh | |
| working-directory: 'openless-all/app' | |
| run: | | |
| $nsis = Get-ChildItem "src-tauri\target\release\bundle\nsis\*.exe" -ErrorAction Stop | Select-Object -First 1 | |
| if (-not $nsis) { throw "NSIS installer not found." } | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $nsis.FullName -InstallerKind nsis | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "NSIS installer smoke failed with exit $LASTEXITCODE" | |
| } | |
| if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') { | |
| Write-Host "[ok] Skipping MSI smoke for beta release channel." | |
| return | |
| } | |
| $msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction Stop | Select-Object -First 1 | |
| if (-not $msi) { throw "MSI installer not found." } | |
| powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $msi.FullName -InstallerKind msi | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "MSI installer smoke failed with exit $LASTEXITCODE" | |
| } | |
| # ── Linux:先编译 fcitx5 插件,再产 deb / rpm / AppImage ── | |
| - name: Build fcitx5 plugin | |
| if: matrix.platform == 'ubuntu-22.04' | |
| shell: bash | |
| working-directory: 'openless-all/scripts/linux-fcitx5-plugin' | |
| run: | | |
| # v1.3.4-3 之后 Ubuntu 22.04 universe 把 fcitx5-dev 这个 meta 包移除了 | |
| # (v1.3.4-4/-5/-6 在此步连挂三次,错误 "Unable to locate package fcitx5-dev")。 | |
| # v1.3.4-6 加的 apt-cache search 诊断显示,22.04 jammy 实际只剩拆分后的 | |
| # libfcitx5{core,utils,config}-dev / fcitx5-modules-dev 等子包, | |
| # 但 fcitx5-dev 这个 umbrella 包确认已不在源里。 | |
| # | |
| # CMakeLists.txt 实际只需要: | |
| # find_package(Fcitx5Core) → libfcitx5core-dev | |
| # find_package(Fcitx5Utils) → libfcitx5utils-dev | |
| # find_package(Fcitx5Module) → fcitx5-modules-dev | |
| # 另外 fcitx5 自己的 CMake config 文件依赖 KDE ECM。 | |
| # 保留 add-apt-repository universe + 本步骤内 apt-get update 作为通用防御。 | |
| sudo add-apt-repository -y universe || true | |
| sudo apt-get update | |
| if ! sudo apt-get install -y \ | |
| cmake \ | |
| extra-cmake-modules \ | |
| libfcitx5core-dev \ | |
| libfcitx5utils-dev \ | |
| libfcitx5config-dev \ | |
| fcitx5-modules-dev; then | |
| echo "::error::fcitx5 dev packages install failed. Diagnostics ↓" | |
| echo "--- apt-cache search fcitx5 ---" | |
| apt-cache search fcitx5 || true | |
| echo "--- /etc/os-release ---" | |
| cat /etc/os-release || true | |
| echo "--- /etc/apt/sources.list.d ---" | |
| ls -la /etc/apt/sources.list.d/ || true | |
| exit 1 | |
| fi | |
| mkdir -p build && cd build | |
| # 显式 CMAKE_INSTALL_PREFIX=/usr:fcitx5 在 Ubuntu 上的运行时插件搜索 | |
| # 路径是 /usr/lib/<arch>/fcitx5/,默认 prefix=/usr/local 会让 .deb 把 | |
| # 插件安到 /usr/local/lib/fcitx5/,fcitx5 永远找不到。 | |
| # 历史上 v1.3.4-3 也是这条 bug 路径,只是 cmake -LA 那行先挂掉,没人意识到。 | |
| cmake .. -DCMAKE_INSTALL_PREFIX=/usr 2>&1 | tee cmake-configure.log | |
| make | |
| # v1.3.4-7 暴露:fcitx5 5.0.14 把 FCITX_INSTALL_* 设为普通变量而非 CACHE, | |
| # `cmake -LA` 抓不到 → grep 退码 1 → set -e -o pipefail 整步退出。 | |
| # 改从 configure 阶段 message(STATUS ...) 的输出抓(CMakeLists.txt 已经打印)。 | |
| FCITX_ADDON_DIR=$(grep "FCITX_INSTALL_ADDONDIR:" cmake-configure.log \ | |
| | tail -1 | sed -E 's/^[^:]*: ([^ ]+).*/\1/') | |
| FCITX_PKGDATA_DIR=$(grep "FCITX_INSTALL_PKGDATADIR:" cmake-configure.log \ | |
| | tail -1 | sed -E 's/^[^:]*: ([^ ]+).*/\1/') | |
| if [ -z "$FCITX_ADDON_DIR" ] || [ -z "$FCITX_PKGDATA_DIR" ]; then | |
| echo "::error::Failed to extract FCITX_INSTALL_* paths from cmake configure log" | |
| echo "--- cmake-configure.log (last 50 lines) ---" | |
| tail -50 cmake-configure.log | |
| exit 1 | |
| fi | |
| echo "Detected: addon=$FCITX_ADDON_DIR pkgdata=$FCITX_PKGDATA_DIR" | |
| echo "FCITX_ADDON_DIR=$FCITX_ADDON_DIR" >> "$GITHUB_ENV" | |
| echo "FCITX_ADDON_CONF_DIR=${FCITX_PKGDATA_DIR}/addon" >> "$GITHUB_ENV" | |
| # 对 RPM 目标映射路径:Debian multiarch(如 /usr/lib/x86_64-linux-gnu) | |
| # -> /usr/lib64(RPM 标准)。conf 路径跨发行版一致。 | |
| RPM_ADDON_DIR=$(echo "$FCITX_ADDON_DIR" \ | |
| | sed 's|/usr/lib/[^/]*/fcitx5|/usr/lib64/fcitx5|;s|/usr/lib/x86_64-linux-gnu/fcitx5|/usr/lib64/fcitx5|') | |
| echo "FCITX_RPM_ADDON_DIR=$RPM_ADDON_DIR" >> "$GITHUB_ENV" | |
| # 把插件 .so + .conf 复制到 src-tauri/linux-fcitx5-plugin/ 下面, | |
| # 供 tauri deb/rpm bundler 的 files 配置使用。 | |
| mkdir -p "$GITHUB_WORKSPACE/openless-all/app/src-tauri/linux-fcitx5-plugin" | |
| cp libopenless.so "$GITHUB_WORKSPACE/openless-all/app/src-tauri/linux-fcitx5-plugin/" | |
| cp openless.conf "$GITHUB_WORKSPACE/openless-all/app/src-tauri/linux-fcitx5-plugin/" | |
| - name: Build (Linux) | |
| if: matrix.platform == 'ubuntu-22.04' | |
| shell: bash | |
| working-directory: 'openless-all/app' | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} | |
| run: | | |
| # deb/rpm:通过 files 映射把插件安装到系统 fcitx5 路径。 | |
| # AppImage:通过 bundle.resources 把 .so 打进包内,运行时由 | |
| # ensure_plugin_installed() 自动安装到 ~/.local/ 下。 | |
| # 插件 .so + .conf 由上一步 Build fcitx5 plugin 生成并复制到 | |
| # src-tauri/linux-fcitx5-plugin/ 下。 | |
| cat > /tmp/tauri-linux-config.json << CONFIG_EOF | |
| { | |
| "bundle": { | |
| "resources": ["linux-fcitx5-plugin/libopenless.so"], | |
| "linux": { | |
| "deb": { | |
| "depends": ["fcitx5", "fcitx5-module-dbus", "libdbus-1-3"], | |
| "files": { | |
| "${FCITX_ADDON_DIR}/libopenless.so": "linux-fcitx5-plugin/libopenless.so", | |
| "${FCITX_ADDON_CONF_DIR}/openless.conf": "linux-fcitx5-plugin/openless.conf" | |
| } | |
| }, | |
| "rpm": { | |
| "depends": ["fcitx5", "fcitx5-module-dbus"], | |
| "files": { | |
| "${FCITX_RPM_ADDON_DIR}/libopenless.so": "linux-fcitx5-plugin/libopenless.so", | |
| "${FCITX_ADDON_CONF_DIR}/openless.conf": "linux-fcitx5-plugin/openless.conf" | |
| } | |
| } | |
| } | |
| } | |
| } | |
| CONFIG_EOF | |
| if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then | |
| jq '.bundle.createUpdaterArtifacts = true' /tmp/tauri-linux-config.json > /tmp/tauri-linux-config-signed.json | |
| CONFIG_FILE=/tmp/tauri-linux-config-signed.json | |
| else | |
| CONFIG_FILE=/tmp/tauri-linux-config.json | |
| fi | |
| npm run tauri -- build --bundles deb,rpm,appimage --config "$CONFIG_FILE" | |
| - name: Disambiguate macOS updater bundle filename | |
| if: startsWith(matrix.platform, 'macos') && env.TAURI_SIGNING_PRIVATE_KEY != '' | |
| shell: bash | |
| working-directory: 'openless-all/app/src-tauri/target/release/bundle/macos' | |
| run: | | |
| if [ -f OpenLess.app.tar.gz ]; then | |
| mv OpenLess.app.tar.gz "OpenLess_${{ matrix.updater-arch }}.app.tar.gz" | |
| fi | |
| if [ -f OpenLess.app.tar.gz.sig ]; then | |
| mv OpenLess.app.tar.gz.sig "OpenLess_${{ matrix.updater-arch }}.app.tar.gz.sig" | |
| fi | |
| - name: Write updater manifest | |
| if: env.TAURI_SIGNING_PRIVATE_KEY != '' | |
| shell: bash | |
| working-directory: 'openless-all/app' | |
| env: | |
| TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} | |
| OPENLESS_UPDATE_TARGET: ${{ matrix.updater-target }} | |
| OPENLESS_UPDATE_ARCH: ${{ matrix.updater-arch }} | |
| OPENLESS_UPDATE_REPO: appergb/openless | |
| OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com | |
| # beta 渠道时输出 latest-{tgt}-{arch}-beta.json,stable 沿用旧文件名。 | |
| 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, | |
| # 脚本不会读这个字段,安全。 | |
| OPENLESS_RELEASE_TAG: ${{ github.ref_name }} | |
| run: node scripts/write-updater-manifest.mjs | |
| # ── 收集产物 ── | |
| - name: List artifacts (debug) | |
| shell: bash | |
| working-directory: 'openless-all/app/src-tauri/target/release/bundle' | |
| run: ls -la macos/ dmg/ nsis/ msi/ deb/ rpm/ appimage/ 2>/dev/null || true | |
| # 防御性步骤:剥掉 macOS 产物上任何残留扩展属性 / quarantine。 | |
| # 理论上 GitHub Actions 输出的 .app/.dmg 不会带 com.apple.quarantine | |
| # (xattr 也不会通过 actions/upload-artifact 跨机器持久化),但保留这一步 | |
| # 让"云端 artifact 一定干净"成为可验证的承诺。用户下载后再被本地浏览器 | |
| # 加 quarantine 时,按 release notes 的 `xattr -cr` 一行即可消除。 | |
| - name: Strip xattr / quarantine on macOS bundles | |
| if: startsWith(matrix.platform, 'macos') | |
| shell: bash | |
| working-directory: 'openless-all/app/src-tauri/target/release/bundle' | |
| run: | | |
| for path in macos/*.app dmg/*.dmg; do | |
| if [ -e "$path" ]; then | |
| echo "▶ stripping xattr: $path" | |
| xattr -cr "$path" || true | |
| xattr -lr "$path" || true | |
| fi | |
| done | |
| - name: Upload macOS artifacts | |
| if: startsWith(matrix.platform, 'macos') | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openless-macos-${{ matrix.updater-arch }} | |
| path: | | |
| openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg | |
| if-no-files-found: error | |
| - name: Upload macOS updater artifacts | |
| if: startsWith(matrix.platform, 'macos') && env.TAURI_SIGNING_PRIVATE_KEY != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openless-macos-${{ matrix.updater-arch }}-updater | |
| path: | | |
| openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz | |
| openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig | |
| openless-all/app/src-tauri/target/release/bundle/latest-darwin-${{ matrix.updater-arch }}*.json | |
| if-no-files-found: error | |
| - name: Upload Windows artifacts | |
| if: matrix.platform == 'windows-latest' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openless-windows-x64 | |
| path: | | |
| openless-all/app/src-tauri/target/release/bundle/nsis/*.exe | |
| openless-all/app/src-tauri/target/release/bundle/msi/*.msi | |
| if-no-files-found: error | |
| - name: Upload Windows updater artifacts | |
| if: matrix.platform == 'windows-latest' && env.TAURI_SIGNING_PRIVATE_KEY != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openless-windows-x64-updater | |
| path: | | |
| openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig | |
| openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig | |
| openless-all/app/src-tauri/target/release/bundle/latest-windows-x86_64*.json | |
| if-no-files-found: error | |
| - name: Upload fcitx5 plugin artifact (standalone) | |
| if: matrix.platform == 'ubuntu-22.04' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openless-fcitx5-plugin-linux-x64 | |
| path: | | |
| ${{ github.workspace }}/openless-all/scripts/linux-fcitx5-plugin/build/libopenless.so | |
| ${{ github.workspace }}/openless-all/scripts/linux-fcitx5-plugin/build/openless.conf | |
| if-no-files-found: error | |
| - name: Upload Linux artifacts | |
| if: matrix.platform == 'ubuntu-22.04' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openless-linux-x64 | |
| path: | | |
| openless-all/app/src-tauri/target/release/bundle/deb/*.deb | |
| openless-all/app/src-tauri/target/release/bundle/rpm/*.rpm | |
| openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage | |
| if-no-files-found: error | |
| - name: Upload Linux updater artifacts | |
| if: matrix.platform == 'ubuntu-22.04' && env.TAURI_SIGNING_PRIVATE_KEY != '' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: openless-linux-x64-updater | |
| path: | | |
| openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage.sig | |
| openless-all/app/src-tauri/target/release/bundle/latest-linux-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 次 (v1.3.4-tauri 出现 4 次的 root cause: | |
| # 4 个 matrix job × append_body=true × 共享同一份 body)。 | |
| - 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 | |
| run: | | |
| cat > "$RUNNER_TEMP/release-body.md" << 'EOF' | |
| ### macOS 用户首次安装提示 | |
| 下载 DMG 拖入 `/Applications` 后,**必须**在终端运行: | |
| ```bash | |
| xattr -cr /Applications/OpenLess.app | |
| ``` | |
| 否则 Gatekeeper 会提示「OpenLess 已损坏」——这是因为当前 build 用 ad-hoc 签名、没做 Apple 公证。 | |
| ### 渠道说明 | |
| - 以 `-tauri` 结尾的 tag 是**正式版**(自动推送给所有 in-app 检查更新的用户)。 | |
| - 以 `-beta-tauri` 结尾或包含 `-Beta.N-tauri` 的 tag 是 **Beta 版**(GitHub 标 pre-release,**不**通过 in-app updater 推送给正式版用户;只对在「设置 → 关于 → 加入 Beta 渠道」开关切到 Beta 的用户可见,且需要手动从此页面下载安装)。 | |
| ### 行为变更提示 | |
| - 流式输入默认开启;不兼容场景会自动回落到一次性插入。可在「设置 → 高级」关闭。 | |
| - 流式输入成功后默认把最终文本同步到剪贴板,方便再次粘贴;可在「设置 → 高级」关闭。 | |
| EOF | |
| echo "OPENLESS_RELEASE_BODY_PATH=$RUNNER_TEMP/release-body.md" >> "$GITHUB_ENV" | |
| - name: Create / update release | |
| if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri') | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: 'OpenLess ${{ github.ref_name }}' | |
| draft: false | |
| # beta 渠道的 release 必须标 prerelease=true:GitHub UI 会折叠它, | |
| # 普通用户看不到;同时只上传 latest-*-beta.json,正式版用户的 | |
| # endpoint(latest-*.json)永远不会被覆盖,保证 Beta 不溢出正式版。 | |
| 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 拼到前面、再次复制)。 | |
| body_path: ${{ env.OPENLESS_RELEASE_BODY_PATH }} | |
| # generate_release_notes 也只在 leader 跑:避免 4 个 matrix jobs | |
| # 产生 4 份相同的 What's Changed 段。 | |
| generate_release_notes: ${{ matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' }} | |
| files: | | |
| openless-all/app/src-tauri/target/release/bundle/dmg/*.dmg | |
| openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz | |
| openless-all/app/src-tauri/target/release/bundle/macos/*.app.tar.gz.sig | |
| openless-all/app/src-tauri/target/release/bundle/nsis/*.exe | |
| openless-all/app/src-tauri/target/release/bundle/nsis/*.exe.sig | |
| openless-all/app/src-tauri/target/release/bundle/msi/*.msi | |
| openless-all/app/src-tauri/target/release/bundle/msi/*.msi.sig | |
| openless-all/app/src-tauri/target/release/bundle/deb/*.deb | |
| openless-all/app/src-tauri/target/release/bundle/rpm/*.rpm | |
| openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage | |
| openless-all/app/src-tauri/target/release/bundle/appimage/*.AppImage.sig | |
| openless-all/app/src-tauri/target/release/bundle/latest-*.json | |
| # fcitx5 插件独立下载:供 AppImage 用户或手动安装 | |
| openless-all/scripts/linux-fcitx5-plugin/build/libopenless.so | |
| openless-all/scripts/linux-fcitx5-plugin/build/openless.conf | |
| # ── 正式版发布后,自动更新 Homebrew cask ── | |
| # 为什么放进这条流水线,而不是单独的 `release: published` 工作流:softprops 用默认 | |
| # GITHUB_TOKEN 创建的 Release 不会触发 `release` 事件的其它工作流(GitHub 防递归), | |
| # 所以独立的 update-cask 工作流永远不会被自动触发(历史上只在一次手动场景跑过且失败)。 | |
| # 用 needs: build 串在构建之后、同一条流水线里,才能保证每次正式发版都自动更新 cask。 | |
| # | |
| # 仅正式版:v*-tauri 且非 -beta-tauri。beta 不碰 Homebrew,避免把预发布版推给 | |
| # `brew install --cask openless` 的用户。cask 文件在默认分支(beta)上,提交回该分支。 | |
| update-homebrew-cask: | |
| name: Update Homebrew cask (stable only) | |
| needs: build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| if: >- | |
| startsWith(github.ref, 'refs/tags/v') | |
| && endsWith(github.ref, '-tauri') | |
| && !endsWith(github.ref, '-beta-tauri') | |
| && !contains(github.ref_name, '-Beta.') | |
| steps: | |
| - name: Checkout default branch (cask 住在这里) | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Resolve version + DMG sha256 from the release | |
| id: meta | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| TAG: ${{ github.ref_name }} | |
| run: | | |
| set -euo pipefail | |
| version="${TAG#v}" | |
| version="${version%-tauri}" | |
| # 直接下载 DMG 自己算 sha256,不依赖 GitHub 资产 digest 字段的时序/可用性。 | |
| mkdir -p "$RUNNER_TEMP/dmg" | |
| gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \ | |
| --dir "$RUNNER_TEMP/dmg" \ | |
| --pattern '*aarch64.dmg' --pattern '*_x64.dmg' | |
| arm_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*aarch64.dmg | cut -d' ' -f1)" | |
| intel_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*_x64.dmg | cut -d' ' -f1)" | |
| { | |
| echo "version=$version" | |
| echo "arm_sha=$arm_sha" | |
| echo "intel_sha=$intel_sha" | |
| } >> "$GITHUB_OUTPUT" | |
| echo "cask → version=$version arm=$arm_sha intel=$intel_sha" | |
| - name: Update Casks/openless.rb | |
| env: | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| ARM_SHA: ${{ steps.meta.outputs.arm_sha }} | |
| INTEL_SHA: ${{ steps.meta.outputs.intel_sha }} | |
| 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 保留原缩进。 | |
| 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 失败。 | |
| grep -q 'arch arm: "aarch64", intel: "x64"' "$cask" \ | |
| || { echo "::error::arch 指令被 sed 误伤"; exit 1; } | |
| grep -q "\"$ARM_SHA\"" "$cask" && grep -q "\"$INTEL_SHA\"" "$cask" \ | |
| || { echo "::error::sha256 未正确写入 cask"; exit 1; } | |
| echo "----- updated $cask -----" | |
| cat "$cask" | |
| - name: Commit & push cask bump | |
| env: | |
| VERSION: ${{ steps.meta.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add Casks/openless.rb | |
| if git diff --cached --quiet; then | |
| echo "cask 已是 $VERSION,无需提交" | |
| exit 0 | |
| fi | |
| # [skip ci]:cask 文本改动不需要再跑一遍跨平台 ci.yml。 | |
| git commit -m "[cask] openless $VERSION (auto from release) [skip ci]" | |
| git push |