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>
660 lines
33 KiB
YAML
660 lines
33 KiB
YAML
name: Release Tauri (cross-platform)
|
||
# meta: ensure Actions indexes this workflow on forks (no behavior change).
|
||
# 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)
|
||
#
|
||
# 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 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:
|
||
tags:
|
||
- 'v*-tauri'
|
||
workflow_dispatch:
|
||
inputs:
|
||
platform:
|
||
description: Desktop platforms to build (manual builds do not publish a release)
|
||
type: choice
|
||
options: [all, macos]
|
||
default: all
|
||
|
||
# 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' }}
|
||
|
||
jobs:
|
||
build:
|
||
permissions:
|
||
contents: write
|
||
strategy:
|
||
fail-fast: false
|
||
matrix:
|
||
include: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.platform == 'macos' && '[{"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":"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"}]') }}
|
||
runs-on: ${{ matrix.platform }}
|
||
# A release build takes ~33 minutes from a cold cache; anything past this is
|
||
# a hang that would otherwise hold the runner and the release for hours.
|
||
timeout-minutes: 120
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
# Channel is decided by the tag suffix:
|
||
# v<v>-beta-tauri / v<v>-Beta.N-tauri
|
||
# -> 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:
|
||
# The MLX submodule is only needed for macOS release builds.
|
||
submodules: ${{ startsWith(matrix.platform, 'macos') && 'recursive' || 'false' }}
|
||
|
||
- name: Disable macOS-only Qwen3 MLX dependency
|
||
if: ${{ !startsWith(matrix.platform, 'macos') }}
|
||
run: node openless-all/app/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'
|
||
|
||
- uses: dtolnay/rust-toolchain@stable
|
||
with:
|
||
targets: ${{ matrix.rust-target }}
|
||
|
||
# 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
|
||
run: bash scripts/macos-build-env.sh
|
||
|
||
- name: Cache Cargo
|
||
if: matrix.platform == 'windows-latest'
|
||
uses: swatinem/rust-cache@v2
|
||
with:
|
||
workspaces: 'openless-all/app/src-tauri -> target'
|
||
|
||
- name: Cache macOS release dependencies
|
||
if: startsWith(matrix.platform, 'macos')
|
||
uses: swatinem/rust-cache@v2
|
||
with:
|
||
key: macos-release-v1
|
||
workspaces: 'openless-all/app/src-tauri -> target'
|
||
|
||
- name: Cache macOS MLX native build
|
||
if: matrix.updater-arch == 'aarch64'
|
||
uses: ./.github/actions/cache-macos-mlx
|
||
with:
|
||
profile: release
|
||
|
||
- name: Prepare Windows Sherpa static libraries
|
||
if: matrix.platform == 'windows-latest'
|
||
working-directory: 'openless-all/app'
|
||
shell: pwsh
|
||
run: ./scripts/prepare-windows-sherpa.ps1
|
||
|
||
- 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: 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 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
|
||
|
||
- name: Upload macOS Cargo timings
|
||
if: always() && startsWith(matrix.platform, 'macos')
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: macos-cargo-timings-${{ matrix.updater-arch }}
|
||
path: openless-all/app/src-tauri/target/cargo-timings/*.html
|
||
if-no-files-found: ignore
|
||
|
||
# ── 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
|
||
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 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: 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
|
||
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
|
||
# WiX/MSI only accepts a numeric prerelease identifier. Manual
|
||
# validation runs use the repository's Beta version even though the
|
||
# ref is a branch (and therefore OPENLESS_RELEASE_CHANNEL=stable),
|
||
# so detect the version explicitly instead of relying on the tag.
|
||
app_version=$(node -p "require('./src-tauri/tauri.conf.json').version")
|
||
msi_supported=1
|
||
if [[ "$app_version" =~ -[^0-9] ]]; then
|
||
msi_supported=0
|
||
echo "[info] Skipping MSI bundle for non-numeric prerelease version $app_version."
|
||
fi
|
||
# 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=$?
|
||
if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ] || [ "$msi_supported" -eq 0 ]; then
|
||
echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV"
|
||
echo "[info] Skipping MSI bundle for unsupported prerelease/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" ] || [ "$msi_supported" -eq 0 ]; then
|
||
echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV"
|
||
echo "[info] Skipping MSI bundle for unsupported prerelease/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 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 failure does not block — the next Repair step re-links with light.exe (with -sice:ICE80).
|
||
exit 0
|
||
|
||
# ── 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
|
||
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: 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 {
|
||
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"
|
||
}
|
||
|
||
- 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: Open-Less/openless
|
||
OPENLESS_UPDATE_MIRROR_BASE_URL: https://fastgit.cc/https://github.com
|
||
# On the beta channel output latest-{tgt}-{arch}-beta.json; stable keeps the old filename.
|
||
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
|
||
# 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
|
||
|
||
# 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
|
||
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 }}
|
||
compression-level: 0
|
||
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
|
||
compression-level: 0
|
||
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
|
||
|
||
# ── 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
|
||
run: |
|
||
cat > "$RUNNER_TEMP/release-body.md" << 'EOF'
|
||
### macOS 用户首次安装提示
|
||
|
||
下载 DMG 拖入 `/Applications` 后,**必须**在终端运行:
|
||
|
||
```bash
|
||
xattr -cr /Applications/OpenLess.app
|
||
```
|
||
|
||
否则 Gatekeeper 会提示「OpenLess 已损坏」——这是因为当前 build 用 ad-hoc 签名、没做 Apple 公证。
|
||
|
||
### 渠道说明
|
||
|
||
- `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"
|
||
|
||
- 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 }}'
|
||
# Keep Beta assets private until all platforms have built and the
|
||
# administrator has verified the downloadable packages.
|
||
draft: ${{ env.OPENLESS_RELEASE_CHANNEL == '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' }}
|
||
# 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 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
|
||
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/latest-*.json
|
||
|
||
# ── 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.
|
||
#
|
||
# 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
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 30
|
||
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}"
|
||
|
||
# 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" \
|
||
--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"
|
||
# 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"
|
||
# 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" \
|
||
|| { 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 text changes do not need another cross-platform ci.yml run.
|
||
git commit -m "[cask] openless $VERSION (auto from release) [skip ci]"
|
||
git push
|