Files
TRIPandHKLHaoBin 6f9af89583 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>
2026-09-30 20:32:53 +08:00

660 lines
33 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: 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