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-beta-tauri / v-Beta.N-tauri # -> beta channel (GitHub Release marked prerelease, manifest filename # carries a -beta suffix so stable users' endpoint never sees it) # 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//... # 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