643 lines
33 KiB
YAML
643 lines
33 KiB
YAML
name: Release Tauri (cross-platform)
|
||
# meta: ensure Actions indexes this workflow on forks (no behavior change).
|
||
# 触发条件:
|
||
# - 推 v*.*.*-tauri 形式的 tag(与老 Swift 版的 vX.Y.Z 区分开,不冲突)
|
||
# - 手动 dispatch(用于测试构建,不发版)
|
||
#
|
||
# 输出:
|
||
# macOS arm64/x64 .dmg + Windows x64 .msi/.exe,自动作为 GitHub Release 资产上传。
|
||
# Linux egui 由 release-linux-egui.yml 独立构建;本工作流不编译 Linux/Tauri。
|
||
#
|
||
# macOS 分发:
|
||
# - 配好 APPLE_CERTIFICATE / APPLE_CERTIFICATE_PASSWORD / APPLE_ID /
|
||
# APPLE_PASSWORD / APPLE_TEAM_ID 后,Tauri 会做 Developer ID 签名和公证。
|
||
# 用户从浏览器下载后不需要手工 xattr。
|
||
# - 未配置 Apple secrets 时自动回退 ad-hoc 签名,GitHub Actions 会打印 warning。
|
||
# - Windows 没签名(无证书),Win 11 SmartScreen 会警告 "未识别的发布者",用户点"仍要运行"。
|
||
# - 任意一个 platform 失败不影响另一个继续构建(fail-fast: false)。
|
||
|
||
on:
|
||
push:
|
||
tags:
|
||
- 'v*-tauri'
|
||
workflow_dispatch:
|
||
inputs:
|
||
platform:
|
||
description: Desktop platforms to build (manual builds do not publish a release)
|
||
type: choice
|
||
options: [all, macos]
|
||
default: all
|
||
|
||
# 同一 tag 重复推送只跑最新一次;workflow_dispatch 用 run_id 隔离避免互相取消。
|
||
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 }}
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
# 渠道由 tag 后缀决定:
|
||
# v<v>-beta-tauri / v<v>-Beta.N-tauri
|
||
# → beta 渠道(GitHub Release 标 prerelease,manifest 文件名带 -beta 后缀,
|
||
# 正式版用户的 endpoint 拿不到)
|
||
# v<v>-tauri → stable 渠道(正式版,文件名沿用旧约定,向后兼容)
|
||
# workflow_dispatch / 非 tag 触发时 github.ref_name 不是 tag 字符串,
|
||
# endsWith 返回 false,回退为 stable,不改变现有 dispatch 行为。
|
||
OPENLESS_RELEASE_CHANNEL: ${{ (endsWith(github.ref_name, '-beta-tauri') || contains(github.ref_name, '-Beta.')) && 'beta' || 'stable' }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
with:
|
||
# MLX 子模块只在 macOS 发布构建需要。
|
||
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 }}
|
||
|
||
# 在恢复缓存前设置实际编译环境,让 cache key 包含 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:用我们自己的 build-mac.sh,统一处理签名、公证和 artifact 清理 ──
|
||
- name: Build (macOS)
|
||
if: startsWith(matrix.platform, 'macos')
|
||
working-directory: 'openless-all/app'
|
||
env:
|
||
INSTALL: '0' # CI 不要装到 /Applications,也不要 reset TCC
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
run: bash scripts/build-mac.sh
|
||
|
||
- 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),再跑 tauri bundle。
|
||
# openless-ime.wxs 用 $(env.OPENLESS_IME_DLL_X64) / _X86 拿绝对路径,
|
||
# 跨 candle/light cwd 都能 resolve(Tauri wix bundler cwd 不固定)。
|
||
- name: Build Windows IME native DLLs
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: pwsh
|
||
working-directory: 'openless-all/app'
|
||
run: |
|
||
$appRoot = (Resolve-Path .).Path
|
||
foreach ($t in @(
|
||
@{ Platform = 'x64'; Folder = 'x64'; EnvName = 'OPENLESS_IME_DLL_X64' },
|
||
@{ Platform = 'Win32'; Folder = 'x86'; EnvName = 'OPENLESS_IME_DLL_X86' }
|
||
)) {
|
||
$out = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\$($t.Folder)\Release"
|
||
$obj = Join-Path $appRoot "src-tauri\target\windows-ime-msvc\obj\$($t.Folder)\Release"
|
||
./scripts/windows-ime-build.ps1 -Configuration Release -Platform $t.Platform -OutputDirectory $out -IntermediateDirectory $obj
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "OpenLessIme $($t.Platform) build failed with exit $LASTEXITCODE"
|
||
}
|
||
$dll = (Resolve-Path (Join-Path $out 'OpenLessIme.dll')).Path
|
||
if (-not (Test-Path $dll)) {
|
||
throw "OpenLessIme.dll not produced at $dll"
|
||
}
|
||
"$($t.EnvName)=$dll" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||
Write-Host "[ok] built $dll (exported $($t.EnvName))"
|
||
|
||
# bundle.resources 引用的是 src-tauri/openless-ime-payload/{x64,x86}/OpenLessIme.dll
|
||
# (仓库里 commit 的是 0 字节占位让 mac 本地 build 也能 resolve)。
|
||
# 这里用真 dll 覆盖占位,让 NSIS / MSI 都装上真文件;NSIS hook 的 regsvr32
|
||
# 同时会把 64 / 32 位 COM 注册到 HKLM\Software\Classes\CLSID 的
|
||
# KEY_WOW64_64KEY / KEY_WOW64_32KEY 两侧(windows_ime_profile.rs 都会查)。
|
||
$payloadDir = Join-Path $appRoot "src-tauri\openless-ime-payload\$($t.Folder)"
|
||
New-Item -ItemType Directory -Force -Path $payloadDir | Out-Null
|
||
Copy-Item -Force -Path $dll -Destination (Join-Path $payloadDir 'OpenLessIme.dll')
|
||
Write-Host "[ok] copied real $($t.Folder) dll into bundle.resources payload path"
|
||
}
|
||
|
||
# ── Windows tauri build:保持 bash shell,因为 PowerShell 调外部命令
|
||
# 会把 '{"bundle":...}' 的内部双引号吃掉、让 tauri 收到无效 JSON。
|
||
# 用 set +e + GITHUB_ENV 把 exit code 传到下一步给 Repair 判断。
|
||
- name: Build (Windows)
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: bash
|
||
working-directory: 'openless-all/app'
|
||
env:
|
||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||
run: |
|
||
set +e
|
||
# 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
|
||
# 拆两轮跑:Tauri 的签名 / updater artifact 阶段是 post-bundle 钩子,
|
||
# 任意 bundler 失败会让 *所有* bundle 的 .sig 跳过。MSI 必踩 ICE80,
|
||
# 所以单一 `tauri build` 永远拿不到 NSIS 的 .exe.sig。
|
||
# Pass 1:NSIS 独立跑——必须成功,产出 *_x64-setup.exe(.sig) 给 updater。
|
||
# Pass 2:MSI 独立跑——允许失败,Repair 步骤兜底 light.exe 重链。
|
||
# Beta 版跳过 MSI:Windows Installer / WiX 无法表示 `1.2.3-Beta.1`
|
||
# 这类非纯数字 prerelease 标识,稳定版仍然照常产出 MSI。
|
||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
|
||
npm run tauri -- build --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":true}}'
|
||
nsis_exit=$?
|
||
if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ] || [ "$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 是 updater 的硬依赖,挂了就直接 fail step。
|
||
if [ "$nsis_exit" -ne 0 ]; then
|
||
echo "::error::NSIS bundle failed (exit $nsis_exit) — updater artifact unavailable."
|
||
exit 1
|
||
fi
|
||
# MSI 失败不挡——下一步 Repair 用 light.exe 重链(带 -sice:ICE80)。
|
||
exit 0
|
||
|
||
# ── 如果 tauri 在 wix link 阶段失败(candle 出了 wixobj,但 light 因 cwd
|
||
# 解析 wxs Source 找不到 IME DLL),从 appRoot 手动跑 light 兜底重链。
|
||
# 这是本地 windows-package-msvc.ps1::Repair-TauriMsiBundle 的同款手术。
|
||
- name: Repair Windows MSI if Tauri failed at WiX link
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: pwsh
|
||
working-directory: 'openless-all/app'
|
||
run: |
|
||
$exitCode = $env:TAURI_BUILD_EXIT
|
||
if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') {
|
||
Write-Host "[ok] Skipping MSI repair for beta release channel."
|
||
return
|
||
}
|
||
$appRoot = (Resolve-Path .).Path
|
||
$msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||
if ($msi) {
|
||
Write-Host "[ok] MSI already produced by tauri: $($msi.FullName); no repair needed."
|
||
return
|
||
}
|
||
if (-not $exitCode -or $exitCode -eq '0') {
|
||
throw "Tauri exited 0 but no MSI found at src-tauri\target\release\bundle\msi\"
|
||
}
|
||
Write-Warning "Tauri MSI failed (exit $exitCode). Attempting manual light.exe relink from app root."
|
||
$wixRoot = Join-Path $appRoot 'src-tauri\target\release\wix\x64'
|
||
$mainObj = Join-Path $wixRoot 'main.wixobj'
|
||
$imeObj = Join-Path $wixRoot 'openless-ime.wixobj'
|
||
$locale = Join-Path $wixRoot 'locale.wxl'
|
||
foreach ($p in @($mainObj, $imeObj, $locale)) {
|
||
if (-not (Test-Path $p)) {
|
||
throw "Required WiX object missing: $p — tauri build aborted before candle ran. Check the Build (Windows) step log."
|
||
}
|
||
}
|
||
$light = Get-ChildItem "$env:LOCALAPPDATA\tauri\WixTools*\light.exe" -ErrorAction SilentlyContinue |
|
||
Sort-Object FullName |
|
||
Select-Object -Last 1 -ExpandProperty FullName
|
||
if (-not $light) { throw "WiX light.exe not found under $env:LOCALAPPDATA\tauri\WixTools*" }
|
||
$version = (Get-Content src-tauri\tauri.conf.json -Raw | ConvertFrom-Json).version
|
||
$bundleDir = Join-Path $appRoot 'src-tauri\target\release\bundle\msi'
|
||
New-Item -ItemType Directory -Force -Path $bundleDir | Out-Null
|
||
$msiPath = Join-Path $bundleDir "OpenLess_${version}_x64_en-US.msi"
|
||
Push-Location $appRoot
|
||
try {
|
||
# -sice:ICE80:x86 IME DLL 与 x64 一同打进 INSTALLDIR\windows-ime\,
|
||
# 这是 32 位组件落在 64 位 Directory 下的合法场景(DLL 路径绝对指向,
|
||
# 不依赖 SysWOW64 重定向)。Tauri 自身没有暴露 light 透传参数,所以
|
||
# 必须在这里抑制,否则 LGHT0204 必失败。
|
||
& $light -nologo -sice:ICE80 -ext WixUIExtension -ext WixUtilExtension -loc $locale -out $msiPath $mainObj $imeObj
|
||
if ($LASTEXITCODE -ne 0) { throw "light.exe relink failed with exit $LASTEXITCODE" }
|
||
} finally {
|
||
Pop-Location
|
||
}
|
||
Write-Host "[ok] MSI rebuilt at $msiPath"
|
||
|
||
- name: Verify Windows installers register TSF IME
|
||
if: matrix.platform == 'windows-latest'
|
||
shell: pwsh
|
||
working-directory: 'openless-all/app'
|
||
run: |
|
||
$nsis = Get-ChildItem "src-tauri\target\release\bundle\nsis\*.exe" -ErrorAction Stop | Select-Object -First 1
|
||
if (-not $nsis) { throw "NSIS installer not found." }
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $nsis.FullName -InstallerKind nsis
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "NSIS installer smoke failed with exit $LASTEXITCODE"
|
||
}
|
||
if ($env:OPENLESS_SKIP_WINDOWS_MSI -eq '1') {
|
||
Write-Host "[ok] Skipping MSI smoke for beta release channel."
|
||
return
|
||
}
|
||
$msi = Get-ChildItem "src-tauri\target\release\bundle\msi\*.msi" -ErrorAction Stop | Select-Object -First 1
|
||
if (-not $msi) { throw "MSI installer not found." }
|
||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\windows-ime-install-smoke.ps1 -InstallerPath $msi.FullName -InstallerKind msi
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "MSI installer smoke failed with exit $LASTEXITCODE"
|
||
}
|
||
|
||
- 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
|
||
# beta 渠道时输出 latest-{tgt}-{arch}-beta.json,stable 沿用旧文件名。
|
||
OPENLESS_RELEASE_CHANNEL: ${{ env.OPENLESS_RELEASE_CHANNEL }}
|
||
# Beta 渠道里脚本要把 manifest.url 写成 releases/download/<tag>/...
|
||
# 而不是 releases/latest(后者永远 = Stable,Beta 用户按 url 下载会拉到错文件)。
|
||
# workflow_dispatch 时 github.ref_name 不是 tag——但那种情况下 channel=stable,
|
||
# 脚本不会读这个字段,安全。
|
||
OPENLESS_RELEASE_TAG: ${{ github.ref_name }}
|
||
run: node scripts/write-updater-manifest.mjs
|
||
|
||
# ── 收集产物 ──
|
||
- name: List artifacts (debug)
|
||
shell: bash
|
||
working-directory: 'openless-all/app/src-tauri/target/release/bundle'
|
||
run: ls -la macos/ dmg/ nsis/ msi/ 2>/dev/null || true
|
||
|
||
# 防御性步骤:剥掉 macOS 产物上任何残留扩展属性 / quarantine。
|
||
# 理论上 GitHub Actions 输出的 .app/.dmg 不会带 com.apple.quarantine
|
||
# (xattr 也不会通过 actions/upload-artifact 跨机器持久化),但保留这一步
|
||
# 让"云端 artifact 一定干净"成为可验证的承诺。用户下载后再被本地浏览器
|
||
# 加 quarantine 时,按 release notes 的 `xattr -cr` 一行即可消除。
|
||
- name: Strip xattr / quarantine on macOS bundles
|
||
if: startsWith(matrix.platform, 'macos')
|
||
shell: bash
|
||
working-directory: 'openless-all/app/src-tauri/target/release/bundle'
|
||
run: |
|
||
for path in macos/*.app dmg/*.dmg; do
|
||
if [ -e "$path" ]; then
|
||
echo "▶ stripping xattr: $path"
|
||
xattr -cr "$path" || true
|
||
xattr -lr "$path" || true
|
||
fi
|
||
done
|
||
|
||
- name: Upload macOS artifacts
|
||
if: startsWith(matrix.platform, 'macos')
|
||
uses: actions/upload-artifact@v4
|
||
with:
|
||
name: openless-macos-${{ matrix.updater-arch }}
|
||
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
|
||
|
||
# ── tag 推送时,同步上传到 GitHub Release ──
|
||
# 只有 leader job (darwin/aarch64) 把 release body 写到文件,其余 matrix job
|
||
# 的 body_path 留空,softprops/action-gh-release@v2 在 body 为空时会保留
|
||
# existing release body 不动,避免每个 matrix job 都 append 一遍同样的 prelude
|
||
# 导致 release notes 重复 N 次。
|
||
- name: Prepare release body prelude
|
||
if: matrix.updater-target == 'darwin' && matrix.updater-arch == 'aarch64' && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||
shell: bash
|
||
run: |
|
||
cat > "$RUNNER_TEMP/release-body.md" << 'EOF'
|
||
### macOS 用户首次安装提示
|
||
|
||
下载 DMG 拖入 `/Applications` 后,**必须**在终端运行:
|
||
|
||
```bash
|
||
xattr -cr /Applications/OpenLess.app
|
||
```
|
||
|
||
否则 Gatekeeper 会提示「OpenLess 已损坏」——这是因为当前 build 用 ad-hoc 签名、没做 Apple 公证。
|
||
|
||
### 渠道说明
|
||
|
||
- 以 `-tauri` 结尾的 tag 是**正式版**(自动推送给所有 in-app 检查更新的用户)。
|
||
- 以 `-beta-tauri` 结尾或包含 `-Beta.N-tauri` 的 tag 是 **Beta 版**(GitHub 标 pre-release,**不**通过 in-app updater 推送给正式版用户;只对在「设置 → 关于 → 加入 Beta 渠道」开关切到 Beta 的用户可见,且需要手动从此页面下载安装)。
|
||
|
||
### 行为变更提示
|
||
|
||
- 流式输入默认开启;不兼容场景会自动回落到一次性插入。可在「设置 → 高级」关闭。
|
||
- 流式输入成功后默认把最终文本同步到剪贴板,方便再次粘贴;可在「设置 → 高级」关闭。
|
||
EOF
|
||
echo "OPENLESS_RELEASE_BODY_PATH=$RUNNER_TEMP/release-body.md" >> "$GITHUB_ENV"
|
||
|
||
- name: Create / update release
|
||
if: startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-tauri')
|
||
uses: softprops/action-gh-release@v2
|
||
with:
|
||
tag_name: ${{ github.ref_name }}
|
||
name: 'OpenLess ${{ github.ref_name }}'
|
||
# Keep Beta assets private until all platforms have built and the
|
||
# administrator has verified the downloadable packages.
|
||
draft: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
|
||
# beta 渠道的 release 必须标 prerelease=true:GitHub UI 会折叠它,
|
||
# 普通用户看不到;同时只上传 latest-*-beta.json,正式版用户的
|
||
# endpoint(latest-*.json)永远不会被覆盖,保证 Beta 不溢出正式版。
|
||
prerelease: ${{ env.OPENLESS_RELEASE_CHANNEL == 'beta' }}
|
||
# 非 leader job 的 OPENLESS_RELEASE_BODY_PATH 是空字符串,softprops 在
|
||
# body 为空时走 `body || existing.body` 分支保留既有内容,不会覆盖。
|
||
# leader job 用默认 append_body=false,每次完整覆盖 release body 为
|
||
# "prelude + generated notes",re-run 同一 tag 时也保持 idempotent
|
||
# (append_body=true 会让 re-run 把上轮 body 拼到前面、再次复制)。
|
||
body_path: ${{ env.OPENLESS_RELEASE_BODY_PATH }}
|
||
# generate_release_notes 也只在 leader 跑,避免 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
|
||
|
||
# ── 正式版发布后,自动更新 Homebrew cask ──
|
||
# 为什么放进这条流水线,而不是单独的 `release: published` 工作流:softprops 用默认
|
||
# GITHUB_TOKEN 创建的 Release 不会触发 `release` 事件的其它工作流(GitHub 防递归),
|
||
# 所以独立的 update-cask 工作流永远不会被自动触发(历史上只在一次手动场景跑过且失败)。
|
||
# 用 needs: build 串在构建之后、同一条流水线里,才能保证每次正式发版都自动更新 cask。
|
||
#
|
||
# 仅正式版:v*-tauri 且非 -beta-tauri。beta 不碰 Homebrew,避免把预发布版推给
|
||
# `brew install --cask openless` 的用户。cask 文件在默认分支(beta)上,提交回该分支。
|
||
update-homebrew-cask:
|
||
name: Update Homebrew cask (stable only)
|
||
needs: build
|
||
runs-on: ubuntu-latest
|
||
permissions:
|
||
contents: write
|
||
if: >-
|
||
startsWith(github.ref, 'refs/tags/v')
|
||
&& endsWith(github.ref, '-tauri')
|
||
&& !endsWith(github.ref, '-beta-tauri')
|
||
&& !contains(github.ref_name, '-Beta.')
|
||
steps:
|
||
- name: Checkout default branch (cask 住在这里)
|
||
uses: actions/checkout@v4
|
||
with:
|
||
ref: ${{ github.event.repository.default_branch }}
|
||
token: ${{ secrets.GITHUB_TOKEN }}
|
||
|
||
- name: Resolve version + DMG sha256 from the release
|
||
id: meta
|
||
env:
|
||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||
TAG: ${{ github.ref_name }}
|
||
run: |
|
||
set -euo pipefail
|
||
version="${TAG#v}"
|
||
version="${version%-tauri}"
|
||
|
||
# 直接下载 DMG 自己算 sha256,不依赖 GitHub 资产 digest 字段的时序/可用性。
|
||
mkdir -p "$RUNNER_TEMP/dmg"
|
||
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" \
|
||
--dir "$RUNNER_TEMP/dmg" \
|
||
--pattern '*aarch64.dmg' --pattern '*_x64.dmg'
|
||
|
||
arm_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*aarch64.dmg | cut -d' ' -f1)"
|
||
intel_sha="$(sha256sum "$RUNNER_TEMP"/dmg/*_x64.dmg | cut -d' ' -f1)"
|
||
|
||
{
|
||
echo "version=$version"
|
||
echo "arm_sha=$arm_sha"
|
||
echo "intel_sha=$intel_sha"
|
||
} >> "$GITHUB_OUTPUT"
|
||
echo "cask → version=$version arm=$arm_sha intel=$intel_sha"
|
||
|
||
- name: Update Casks/openless.rb
|
||
env:
|
||
VERSION: ${{ steps.meta.outputs.version }}
|
||
ARM_SHA: ${{ steps.meta.outputs.arm_sha }}
|
||
INTEL_SHA: ${{ steps.meta.outputs.intel_sha }}
|
||
run: |
|
||
set -euo pipefail
|
||
cask="Casks/openless.rb"
|
||
# ⚠️ cask 第 2 行 `arch arm: "aarch64", intel: "x64"` 也含 `intel: "..."`,
|
||
# 裸 `s/intel: "..."/` 会把 arch 指令一起改成哈希 → Intel 用户 brew 装包 404。
|
||
# 所以 intel 的 sed 用 `^[[:space:]]*intel:` 锚定 sha256 块那一行;arm 的 sed
|
||
# 靠 `sha256 arm: ` 前缀天然只命中 sha256 行。捕获组 \1 保留原缩进。
|
||
sed -i "s/version \"[^\"]*\"/version \"$VERSION\"/" "$cask"
|
||
sed -i "s/sha256 arm: \"[^\"]*\"/sha256 arm: \"$ARM_SHA\"/" "$cask"
|
||
sed -i "s/^\([[:space:]]*\)intel: \"[^\"]*\"/\1intel: \"$INTEL_SHA\"/" "$cask"
|
||
# 防呆:arch 指令必须原样保留,且本次 sha256 必须确实写进去了,否则让 job 失败。
|
||
grep -q 'arch arm: "aarch64", intel: "x64"' "$cask" \
|
||
|| { echo "::error::arch 指令被 sed 误伤"; exit 1; }
|
||
grep -q "\"$ARM_SHA\"" "$cask" && grep -q "\"$INTEL_SHA\"" "$cask" \
|
||
|| { echo "::error::sha256 未正确写入 cask"; exit 1; }
|
||
echo "----- updated $cask -----"
|
||
cat "$cask"
|
||
|
||
- name: Commit & push cask bump
|
||
env:
|
||
VERSION: ${{ steps.meta.outputs.version }}
|
||
run: |
|
||
set -euo pipefail
|
||
git config user.name "github-actions[bot]"
|
||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||
git add Casks/openless.rb
|
||
if git diff --cached --quiet; then
|
||
echo "cask 已是 $VERSION,无需提交"
|
||
exit 0
|
||
fi
|
||
# [skip ci]:cask 文本改动不需要再跑一遍跨平台 ci.yml。
|
||
git commit -m "[cask] openless $VERSION (auto from release) [skip ci]"
|
||
git push
|