-
Notifications
You must be signed in to change notification settings - Fork 346
659 lines (608 loc) · 33.4 KB
/
Copy pathrelease-tauri.yml
File metadata and controls
659 lines (608 loc) · 33.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
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