release: OpenLess 2.0.0-Beta.4+build.20260930 (#1126) #141
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). | |
| # 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 |