Repository navigation
854 lines (788 loc) · 39.2 KB
/
Copy pathci.yml
File metadata and controls
854 lines (788 loc) · 39.2 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
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
name: CI
# 多平台跨语言质量门禁。release-tauri.yml 是发版流水线(仅打包构建),这里负责
# 在合并前快速验证两件事:
# 1. 全部前端/契约测试与 vite bundle 通过(捕获行为、合同及跨 locale 类型 drift)
# 2. Tauri 后端在 macOS / Windows、移动端在 Android 编译;Linux Core、
# remote_tls、egui 和 deb/rpm 统一使用发版构建链检查(PR 不上传 Release)。
# macOS/Windows/Android 不跑完整 Tauri bundle;Linux 则运行完整 deb/rpm 发版构建校验。
on:
push:
branches: ['**']
pull_request:
branches: [main, beta]
workflow_dispatch:
inputs:
platform:
description: Platforms to check (macos runs only macOS gates)
type: choice
options: [all, macos]
default: all
macos_runner:
description: macOS runner (Intel fallback when hosted ARM capacity is unavailable)
type: choice
options: [macos-latest, macos-15-intel]
default: macos-latest
# Cancel old runs when the same PR is pushed repeatedly; workflow_dispatch is isolated by run_id.
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
# 判定这次改动能不能影响到各平台门禁。只有“改动物理上够不到”的变更才会
# 跳过对应 job:判定脚本对任何未知情况(没有基线、git 失败、空 diff)都返回
# true。push / tag / 手动运行不带基线,因此始终全量验证。
changes:
name: Changed areas
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
tauri: ${{ steps.detect.outputs.tauri }}
msrv: ${{ steps.detect.outputs.msrv }}
linux: ${{ steps.detect.outputs.linux }}
harmony: ${{ steps.detect.outputs.harmony }}
# Validate the pinned minimum compiler whenever native sources can change.
msrv_needed: ${{ steps.detect.outputs.msrv }}
steps:
- uses: actions/checkout@v4
with:
# 需要基线提交才能算出差异,客户端不再重复 fetch。
fetch-depth: 0
- name: Detect changed areas
id: detect
working-directory: openless-all/app
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
run: |
set -euo pipefail
for area in tauri msrv linux harmony; do
verdict=$(bash scripts/ci-changed-areas.sh "$area" "$BASE_SHA")
# 失败即运行:脚本读不到差异时返回 true,这里也只接受明确的 false。
echo "$area=$( [ "$verdict" = 'false' ] && echo false || echo true )" >> "$GITHUB_OUTPUT"
done
ui-motion:
name: UI motion (Chromium + WebKit)
needs: [changes]
if: (github.event_name != 'workflow_dispatch' || inputs.platform != 'macos') && needs.changes.outputs.tauri != 'false'
runs-on: ubuntu-24.04
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Cache npm from beta
uses: ./.github/actions/cache-npm
- run: npm ci
- run: npm run build
- name: Resolve Playwright browser version
id: playwright_version
run: |
version=$(node -p "require('@playwright/test/package.json').version")
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Restore Playwright browsers
id: playwright_cache
uses: actions/cache/restore@v4
with:
path: ~/.cache/ms-playwright
# Browser revisions follow Playwright, not unrelated npm lock changes.
key: playwright-v1-${{ runner.os }}-${{ runner.arch }}-ubuntu24-${{ steps.playwright_version.outputs.version }}
- name: Install Playwright system dependencies
run: npx playwright install-deps chromium webkit
- name: Install Playwright browsers on cache miss
if: steps.playwright_cache.outputs.cache-hit != 'true'
run: npx playwright install chromium webkit
- name: Save Playwright browsers on beta builds
if: steps.playwright_cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
uses: actions/cache/save@v4
with:
path: ~/.cache/ms-playwright
key: ${{ steps.playwright_cache.outputs.cache-primary-key }}
- run: npm run test:ui-motion
- uses: actions/upload-artifact@v4
if: failure()
with:
name: ui-motion-failure
path: |
openless-all/app/test-results/
openless-all/app/playwright-report/
android-check:
needs: [changes]
if: (github.event_name != 'workflow_dispatch' || inputs.platform != 'macos') && needs.changes.outputs.tauri != 'false'
name: Android cargo check
runs-on: ubuntu-latest
timeout-minutes: 60
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
# Optional local runtimes are not source submodules of the main application.
submodules: false
- name: Setup Android SDK + NDK
uses: android-actions/setup-android@v3
with:
packages: platform-tools
- name: Install NDK
shell: bash
run: |
set -euo pipefail
sdkmanager "ndk;26.1.10909125" "platforms;android-36" "build-tools;36.0.0"
ndk_dir="$ANDROID_HOME/ndk/26.1.10909125"
echo "ANDROID_NDK_HOME=$ndk_dir" >> "$GITHUB_ENV"
echo "NDK_HOME=$ndk_dir" >> "$GITHUB_ENV"
set +o pipefail
yes | sdkmanager --licenses
set -o pipefail
- name: Configure NDK toolchain for cargo
shell: bash
run: |
set -euo pipefail
prebuilt="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64"
echo "CC_aarch64_linux_android=$prebuilt/bin/aarch64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CXX_aarch64_linux_android=$prebuilt/bin/aarch64-linux-android24-clang++" >> "$GITHUB_ENV"
echo "AR_aarch64_linux_android=$prebuilt/bin/llvm-ar" >> "$GITHUB_ENV"
echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=$prebuilt/bin/aarch64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CC_x86_64_linux_android=$prebuilt/bin/x86_64-linux-android24-clang" >> "$GITHUB_ENV"
echo "CXX_x86_64_linux_android=$prebuilt/bin/x86_64-linux-android24-clang++" >> "$GITHUB_ENV"
echo "AR_x86_64_linux_android=$prebuilt/bin/llvm-ar" >> "$GITHUB_ENV"
echo "CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=$prebuilt/bin/x86_64-linux-android24-clang" >> "$GITHUB_ENV"
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Cache npm from beta
uses: ./.github/actions/cache-npm
- uses: actions/setup-java@v4
with:
distribution: zulu
java-version: "17"
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,x86_64-linux-android
- uses: swatinem/rust-cache@v2
with:
shared-key: tauri-android
workspaces: 'openless-all/app/src-tauri -> target'
# Only beta maintains the shared dependency tree; PRs restore it.
save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
- uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ github.ref != 'refs/heads/beta' || (github.event_name != 'push' && github.event_name != 'workflow_dispatch') }}
- name: Install Linux check dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential curl file libpulse0 libssl-dev wget
- name: Install frontend dependencies
run: npm ci
- name: Build frontend (tsc + vite)
# generate_context! requires frontendDist (../dist) to exist on Android too.
run: npm run build
- name: Check Android updater pubkey matches tauri.conf.json
run: npm run check:android-updater-pubkey
# arm64 Android links sherpa-onnx's shared libraries (local ASR). Its
# build script finds them through SHERPA_ONNX_LIB_DIR and has no Android
# prebuilt download of its own.
- name: Prepare sherpa-onnx Android libraries
run: echo "SHERPA_ONNX_LIB_DIR=$(node scripts/sherpa-onnx-android.mjs)" >> "$GITHUB_ENV"
- name: Check Tauri backend (Android target)
run: cargo check --locked --manifest-path src-tauri/Cargo.toml --target aarch64-linux-android
- name: Initialize generated Android project
run: npm run tauri -- android init --ci
- name: Generate Tauri Gradle dependency scripts
env:
TAURI_ANDROID_PROJECT_PATH: ${{ github.workspace }}/openless-all/app/src-tauri/gen/android
TAURI_ANDROID_PACKAGE_UNESCAPED: com.openless.app
WRY_ANDROID_LIBRARY: openless_lib
WRY_ANDROID_KOTLIN_FILES_OUT_DIR: ${{ github.workspace }}/openless-all/app/src-tauri/gen/android/app/src/main/java/com/openless/app/generated
WRY_ANDROID_PACKAGE: com.openless.app
run: |
mkdir -p "$WRY_ANDROID_KOTLIN_FILES_OUT_DIR"
cargo check --locked --manifest-path src-tauri/Cargo.toml --target x86_64-linux-android
- name: Copy Android production and test scaffolding
run: node scripts/copy-android-scaffolding.mjs
- name: Restore Android Rime instrumentation dependencies
id: rime_cache
uses: actions/cache/restore@v4
with:
path: ${{ runner.temp }}/openless-cache/mobile-rime
key: mobile-rime-ndk26-x86_64-${{ hashFiles('openless-all/app/scripts/build-mobile-rime.py', 'openless-all/app/mobile-ime/**') }}
- name: Build and stage Rime for Android instrumentation tests
env:
OPENLESS_CACHE_ROOT: ${{ runner.temp }}/openless-cache
run: python3 scripts/build-mobile-rime.py android --abi x86_64 --stage
- name: Save Android Rime dependencies on beta builds
# 400 MB per entry: pull request scopes are never restored again, so only
# the beta branch may write this cache.
if: steps.rime_cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
uses: actions/cache/save@v4
with:
path: ${{ runner.temp }}/openless-cache/mobile-rime
key: ${{ steps.rime_cache.outputs.cache-primary-key }}
- name: Merge Shizuku manifest
run: node scripts/merge-android-shizuku-manifest.mjs
- name: Patch Shizuku Gradle dependencies
run: node scripts/patch-android-shizuku-deps.mjs
- name: Run JVM credential tests and compile instrumentation tests
# These tests are Kotlin-only. The Rust target is checked above; direct Gradle
# rustBuild requires the live Tauri CLI RPC server used by `tauri android build`.
run: >-
src-tauri/gen/android/gradlew
--project-dir src-tauri/gen/android
:app:testX86_64DebugUnitTest
:app:assembleX86_64DebugAndroidTest
-x :app:rustBuildX86_64Debug
--no-daemon
- name: Enable KVM for Android instrumentation tests
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
| sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Restore the pinned Android emulator and system image
id: emulator_cache
uses: actions/cache/restore@v4
with:
path: |
${{ env.ANDROID_HOME }}/emulator
${{ env.ANDROID_HOME }}/system-images/android-35/google_apis/x86_64
key: android-emulator-v2-16555280-api35-google_apis-x86_64-${{ runner.os }}-${{ runner.arch }}
- name: Prepare the pinned Android emulator and system image
env:
EMULATOR_CACHE_HIT: ${{ steps.emulator_cache.outputs.cache-hit }}
shell: bash
run: |
set -euo pipefail
image="$ANDROID_HOME/system-images/android-35/google_apis/x86_64"
validate_emulator() {
local version
# Drain the launcher output before grep: an early reader exit causes SIGPIPE.
if version=$("$1" -version 2>&1) && grep -Fq 'build_id 16555280' <<< "$version"; then
return 0
fi
printf '%s\n' "$version" >&2
return 1
}
validate_image() { [ -s "$image/system.img" ] && [ -s "$image/package.xml" ]; }
# Register the pinned package before sdkmanager resolves image dependencies.
register_emulator() {
if [ ! -s "$ANDROID_HOME/emulator/package.xml" ]; then
python3 - "$ANDROID_HOME/emulator" <<'PY'
from pathlib import Path
import sys
import xml.etree.ElementTree as ET
emulator = Path(sys.argv[1])
props = dict(line.split('=', 1) for line in (emulator / 'source.properties').read_text().splitlines() if '=' in line)
ET.register_namespace('common', 'http://schemas.android.com/repository/android/common/02')
ET.register_namespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance')
root = ET.Element('{http://schemas.android.com/repository/android/common/02}repository', {'xmlns:generic': 'http://schemas.android.com/repository/android/generic/02'})
package = ET.SubElement(root, 'localPackage', {'path': 'emulator', 'obsolete': 'false'})
ET.SubElement(package, 'type-details', {'{http://www.w3.org/2001/XMLSchema-instance}type': 'generic:genericDetailsType'})
revision = ET.SubElement(package, 'revision')
for tag, value in zip(('major', 'minor', 'micro'), props['Pkg.Revision'].strip().split('.')[:3]):
ET.SubElement(revision, tag).text = str(int(value))
ET.SubElement(package, 'display-name').text = 'Android Emulator'
ET.ElementTree(root).write(emulator / 'package.xml', encoding='utf-8', xml_declaration=True)
PY
fi
}
if [ "$EMULATOR_CACHE_HIT" != 'true' ]; then
work=$(mktemp -d "$RUNNER_TEMP/openless-emulator.XXXXXX")
trap 'rm -rf "$work"' EXIT
for attempt in 1 2 3; do
echo "Install emulator build 16555280 and API 35 image (attempt $attempt)"
if curl -fL --connect-timeout 30 --max-time 180 \
-o "$work/emulator.zip" https://dl.google.com/android/repository/emulator-linux_x64-16555280.zip \
&& unzip -tq "$work/emulator.zip" \
&& unzip -oq "$work/emulator.zip" -d "$work" \
&& validate_emulator "$work/emulator/emulator"; then
rm -rf "$ANDROID_HOME/emulator"
mv "$work/emulator" "$ANDROID_HOME/emulator"
register_emulator
if sdkmanager 'system-images;android-35;google_apis;x86_64' && validate_image; then
break
fi
rm -rf "$ANDROID_HOME/emulator"
fi
rm -rf "$work/emulator" "$image"
if [ "$attempt" -eq 3 ]; then
echo '::error::could not install the pinned emulator and system image after 3 attempts'
exit 1
fi
sleep 15
done
fi
validate_emulator "$ANDROID_HOME/emulator/emulator" \
|| { echo '::error::cached emulator does not match build 16555280'; exit 1; }
validate_image || { echo '::error::Android API 35 system image is incomplete'; exit 1; }
register_emulator
- name: Save the Android emulator on beta builds
if: steps.emulator_cache.outputs.cache-hit != 'true' && github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
uses: actions/cache/save@v4
with:
path: |
${{ env.ANDROID_HOME }}/emulator
${{ env.ANDROID_HOME }}/system-images/android-35/google_apis/x86_64
key: ${{ steps.emulator_cache.outputs.cache-primary-key }}
- name: Run Android Keystore instrumentation tests
shell: bash
run: |
set -euo pipefail
# SDK tools and the pinned emulator must share the same AVD registry.
export ANDROID_AVD_HOME="$RUNNER_TEMP/openless-ci-avds"
mkdir -p "$ANDROID_AVD_HOME"
avd="$RUNNER_TEMP/openless-ci.avd"
printf 'no\n' | avdmanager create avd --force --name openless-ci \
--package 'system-images;android-35;google_apis;x86_64' --path "$avd"
sed '/^hw[.]keyboard[ =]/d' "$avd/config.ini" > "$avd/config.ini.tmp"
mv "$avd/config.ini.tmp" "$avd/config.ini"
echo 'hw.keyboard=no' >> "$avd/config.ini"
log="$RUNNER_TEMP/openless-android-emulator.log"
emulator_pid=''
cleanup() {
status=$?
trap - EXIT
if [ -n "$emulator_pid" ]; then
if kill -0 "$emulator_pid" 2>/dev/null; then
timeout 10s adb -s emulator-5554 emu kill >/dev/null 2>&1 || true
kill "$emulator_pid" 2>/dev/null || true
fi
wait "$emulator_pid" 2>/dev/null || true
fi
if [ "$status" -ne 0 ]; then cat "$log" >&2 || true; fi
exit "$status"
}
trap cleanup EXIT
"$ANDROID_HOME/emulator/emulator" -avd openless-ci -port 5554 -no-window \
-gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim > "$log" 2>&1 &
emulator_pid=$!
export ANDROID_SERIAL=emulator-5554
timeout 600s bash -c '
until [ "$(adb -s emulator-5554 shell getprop sys.boot_completed 2>/dev/null | tr -d "\r")" = 1 ]; do
kill -0 "$1" 2>/dev/null || exit 1
sleep 2
done
' bash "$emulator_pid"
adb -s emulator-5554 shell input keyevent 82
for setting in window_animation_scale transition_animation_scale animator_duration_scale; do
adb -s emulator-5554 shell settings put global "$setting" 0
done
# Rime is staged for this emulator ABI; do not launch the Tauri Rust build.
src-tauri/gen/android/gradlew --project-dir src-tauri/gen/android \
:app:connectedX86_64DebugAndroidTest -x :app:rustBuildX86_64Debug --no-daemon
- name: Upload Android emulator failure log
if: failure()
uses: actions/upload-artifact@v4
with:
name: android-instrumentation-failure
path: ${{ runner.temp }}/openless-android-emulator.log
if-no-files-found: ignore
# HarmonyOS covers three layers, in three places:
# * the Rust host is compiled and tested by the Linux job
# (`cargo test --locked -p openless-harmony-host`);
# * ArkTS cannot be compiled without the Huawei SDK, so this job checks what
# is reproducible on a hosted runner: the committed IPC audit still matches
# the code, the ArkTS contract tests pass, and the ArkWeb bundle builds;
# * when `HARMONY_SDK_URL` is configured, the last step additionally builds a
# real HAP, which is what actually type-checks the ArkTS sources.
harmony-check:
needs: [changes]
if: (github.event_name != 'workflow_dispatch' || inputs.platform != 'macos') && needs.changes.outputs.harmony != 'false'
name: Harmony (ArkTS contracts, Rust host, HAP)
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
submodules: false
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Cache npm from beta
uses: ./.github/actions/cache-npm
- run: npm ci
- name: Shared IPC audit matches the code
# The report is committed so the ArkTS coverage is reviewable; a stale
# report hides commands that the Harmony host does not implement yet.
run: |
node scripts/audit-harmony-ipc.mjs
git diff --exit-code harmony/ipc-audit.json
- name: Harmony ArkTS contracts
run: |
node scripts/harmony-entry-startup.test.mjs
node scripts/harmony-app-dictation.test.mjs
node scripts/harmony-voice-configuration.test.mjs
node scripts/harmony-panel-layout.test.mjs
node scripts/harmony-editor-compat.test.mjs
node scripts/harmony-clipboard-history.test.mjs
node scripts/harmony-symbol-gesture.test.mjs
node scripts/harmony-rime-startup.test.mjs
# The ArkWeb bundle is not built here: `package-harmony-hap.sh` ->
# `build-harmony-frontend.sh` runs the same
# `npm run build -- --base=./ --outDir=…`, so building it twice only
# doubled the vite pass.
- uses: dtolnay/rust-toolchain@stable
with:
# Tier-2 OHOS target; `rustup target list` above lists it for stable.
targets: aarch64-unknown-linux-ohos
- uses: swatinem/rust-cache@v2
with:
workspaces: 'openless-all/app -> target'
# The HAP embeds a release cdylib; keep its own key so the debug trees
# of the other jobs do not evict it.
key: harmony-host
save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
# No toolchain cache. The download is 2.1 GB at ~100 MB/s plus ~70 s to
# unpack, while the extracted tree is 1.77 GB of the repository's 10 GB
# cache budget - and `actions/cache` wrote one copy per pull request
# scope. Downloading is both cheaper and shorter than restoring it.
- name: Build the HAP (compiles ArkTS and the Rust host)
env:
OPENLESS_HARMONY_CHANNEL: none
run: |
set -euo pipefail
bash scripts/prepare-harmony-sdk.sh
bash scripts/package-harmony-hap.sh
# AppGallery needs a numeric versionName, so the packaging script keeps
# only the numeric prefix ("2.0.2", not "2.0.2-Beta.1").
version_name=$(node -p "require('./package.json').version" | sed 's/[-+].*//')
version_code=$(cat linux-egui/package-revision)
bash scripts/verify-harmony-hap.sh \
target/harmony-packages/OpenLess_harmony_arm64.hap \
"${OPENLESS_HARMONY_TOOLCHAIN:-$HOME/.local/share/harmony/current}" \
"$version_name" "$version_code" --allow-unsigned
# The Linux core/egui/deb+rpm checks live in a reusable workflow, which runs
# the same `cargo test -p openless-core` as upstream's `linux-core-contract`
# plus the Linux host, fcitx5 and package contracts.
linux-egui-package:
needs: [changes]
if: github.event_name != 'workflow_dispatch' || inputs.platform != 'macos'
name: Linux Core, egui and deb/rpm checks
permissions:
contents: read
uses: ./.github/workflows/check-linux-egui.yml
with:
# 散文改动跳过整条 Linux 构建与打包链;输出缺失/未知时退回 full。
scope: ${{ needs.changes.outputs.linux == 'false' && 'none' || 'full' }}
windows-frontend:
# Split out of `cross-platform`: the job used to serialise 6 minutes of
# frontend work behind 15 minutes of Rust compiling on the same runner, and
# the Windows runner is the slowest of the three. Running them in parallel
# costs one extra setup but removes ~6 minutes from the critical path.
needs: [changes]
if: (github.event_name != 'workflow_dispatch' || inputs.platform != 'macos') && needs.changes.outputs.tauri != 'false'
name: Windows frontend and contracts
runs-on: windows-latest
timeout-minutes: 40
env:
CARGO_PROFILE_TEST_DEBUG: 0
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
submodules: false
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Cache npm from beta
uses: ./.github/actions/cache-npm
- name: Install frontend dependencies
run: npm ci
- name: Check Windows prerequisites
shell: pwsh
run: ./scripts/windows-preflight.ps1 -Toolchain msvc
- name: Check PowerShell scripts
shell: pwsh
run: |
foreach ($script in @("./scripts/windows-preflight.ps1", "./scripts/windows-build-gnu.ps1", "./scripts/windows-runtime-smoke.ps1", "./scripts/prepare-windows-sherpa.ps1")) {
$errors = $null
[System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw $script), [ref]$errors) | Out-Null
if ($errors) {
$errors | Format-List
exit 1
}
}
- name: Build frontend
run: npm run build
- name: Run the platform-sensitive contracts
# `process.platform` branches and the `windows-*` contracts only mean
# anything here; everything else runs on macOS (full suite) and Linux.
run: node scripts/frontend-test-runner.mjs --platform-only windows
native-capsule:
# The capsule crates compile into the desktop shells, so a break there would
# otherwise only surface inside the long Tauri matrix. They are checked on
# both desktop platforms inside this workflow rather than in a second
# workflow: one run per pull request, one `Changed areas` gate, one cache
# policy. Being a `push` job on the default branches is also what keeps the
# Rust cache warm - a pull-request-only workflow can never save a tree the
# next pull request is allowed to restore.
needs: [changes]
if: needs.changes.outputs.tauri != 'false'
name: Native capsule (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.platform == 'macos' && '["macos-latest"]' || '["macos-latest","windows-latest"]') }}
runs-on: ${{ matrix.os }}
timeout-minutes: 25
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
submodules: false
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
# Stable prefix per platform instead of the job name, so renaming or
# splitting the job keeps the tree.
shared-key: native-capsule
workspaces: openless-all/app -> target
# Pull requests restore the beta tree and never write their own copy:
# `refs/pull/<n>/merge` scopes are never restored again and only evict
# the branch caches (see docs/ci-trigger-and-cache-policy.md).
save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
- run: cargo check --locked -p openless-native-capsule
- run: cargo test --locked -p openless-capsule --features ui,host
- run: cargo clippy --locked -p openless-native-capsule -p openless-capsule --features openless-capsule/ui,openless-capsule/host -- -D warnings
cross-platform:
needs: [changes]
# 失败即运行:只有判定脚本明确说“够不到”才跳过。
if: needs.changes.outputs.tauri != 'false'
name: ${{ matrix.label }} checks
strategy:
# One platform failing must not block other platforms from getting verification results.
fail-fast: false
matrix:
include: ${{ fromJSON(github.event_name == 'workflow_dispatch' && inputs.platform == 'macos' && '[{"os":"macos-latest","label":"macOS","preflight":false}]' || '[{"os":"macos-latest","label":"macOS","preflight":false},{"os":"windows-latest","label":"Windows","preflight":true}]') }}
# Dispatch-only fallback; push/PR runs keep the default matrix and all test gates.
runs-on: ${{ matrix.label == 'macOS' && github.event_name == 'workflow_dispatch' && inputs.macos_runner || matrix.os }}
timeout-minutes: 60
env:
# The shared Core and full Tauri test binary compile together; limit concurrency
# so the arm64 runner is not OOM-killed
# at peak memory.
CARGO_BUILD_JOBS: 2
CARGO_PROFILE_TEST_DEBUG: 0
CMAKE_BUILD_PARALLEL_LEVEL: 2
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
# No legacy native-ASR submodules remain in this application.
submodules: false
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Cache npm from beta
uses: ./.github/actions/cache-npm
- uses: actions/setup-python@v5
if: runner.os == 'Windows'
with:
python-version: "3.x"
# macOS MSRV is checked in a separate parallel job; Windows keeps the original check order.
# Same rule as the macOS MSRV job: a second toolchain is only worth the
# install + full rebuild while the declared MSRV is behind stable.
- uses: dtolnay/rust-toolchain@1.99.0
if: runner.os == 'Windows' && needs.changes.outputs.msrv_needed == 'true'
- uses: dtolnay/rust-toolchain@stable
- name: Configure macOS check profile
if: runner.os == 'macOS'
run: echo "CARGO_PROFILE_DEV_DEBUG=0" >> "$GITHUB_ENV"
- uses: swatinem/rust-cache@v2
if: runner.os == 'Windows'
with:
# `shared-key` replaces the job name in the cache prefix, so the release
# workflow restoring the same key can reuse this tree. Keep the two in
# sync (scripts/ci-cache-sharing.test.mjs enforces it).
shared-key: tauri-windows
workspaces: 'openless-all/app/src-tauri -> target'
save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
- name: Cache macOS stable dependencies
if: runner.os == 'macOS'
uses: swatinem/rust-cache@v2
with:
# Same prefix as `release-tauri.yml`, which is what lets a tag build
# restore the baseline branch's dependency tree instead of a cold one.
shared-key: tauri-macos
save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
workspaces: |
openless-all/app -> target
openless-all/app/src-tauri -> target
- name: Prepare Windows Sherpa static libraries
if: runner.os == 'Windows'
shell: pwsh
run: ./scripts/prepare-windows-sherpa.ps1
- name: Install frontend dependencies
run: npm ci
- name: Check PowerShell scripts
if: matrix.preflight
shell: pwsh
run: |
foreach ($script in @("./scripts/windows-preflight.ps1", "./scripts/windows-build-gnu.ps1", "./scripts/windows-runtime-smoke.ps1", "./scripts/prepare-windows-sherpa.ps1")) {
$errors = $null
[System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw $script), [ref]$errors) | Out-Null
if ($errors) {
$errors | Format-List
exit 1
}
}
- name: Build the frontend for the Tauri context
# `tauri::generate_context!()` embeds `../dist` at compile time, so the
# Rust compile below needs the frontend built even on Windows, where the
# contract suite runs in its own job.
if: runner.os == 'Windows'
run: npm run build
- name: Build frontend and run frontend/contract tests
# Windows runs the platform-sensitive subset in its own job: spawning 144
# node processes there costs ~3x the macOS time, and the rest of the suite
# is already covered here. The frontend build it needs is the step above.
if: runner.os == 'macOS'
run: npm test
# Direct Cargo checks do not execute Tauri beforeBuildCommand.
- name: Build native capsule for Tauri checks
run: npm run build:native-capsule:dev
- name: Install pinned Codex CLI for sandbox contract
if: runner.os == 'macOS'
run: npm install --global @openai/codex@0.146.0
- name: Configure isolated Codex home
if: runner.os == 'macOS'
shell: bash
run: |
set -euo pipefail
codex_home="$RUNNER_TEMP/openless-codex-home"
mkdir -p "$codex_home"
echo "CODEX_HOME=$codex_home" >> "$GITHUB_ENV"
- name: Run Codex writable-roots sandbox contract
if: runner.os == 'macOS'
env:
OPENLESS_CODEX_TEST_EXE: codex
run: cargo test --locked -p openless-core hardening_actually_narrows_the_writable_roots -- --ignored --nocapture --test-threads=1
# test compiles and runs lib/bin, covering the production binary paths that plain cargo
# check misses. Avoids a metadata-only check followed by another codegen pass over the same
# dependency graph.
- name: Run Rust backend unit tests
if: runner.os != 'Windows'
run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --bins --timings
- name: Compile Rust backend unit tests (Windows)
# A Windows runner can link the lib test binary, but on a clean image lacking an optional
# native runtime DLL entrypoint, the process exits before the test harness starts. Keep
# the cfg/link coverage here; the shared Core's public compatibility contract is actually
# executed in the next step.
if: runner.os == 'Windows'
# `--bins` keeps the binary coverage that the removed `cargo check` used to
# provide: `cargo check` writes metadata, so the following codegen pass
# recompiled the whole dependency graph anyway (measured: 6.4 min of it).
run: cargo test --locked --manifest-path src-tauri/Cargo.toml --lib --bins --no-run
- name: Run Rust-only backend unit tests (Windows)
# The standalone test crate does not link the full Tauri app lib; it verifies the public
# Core contract only. Windows-specific Tauri tests on this runner only get the cfg/link
# compile coverage from the step above.
if: runner.os == 'Windows'
run: cargo test --locked --manifest-path src-tauri/backend-tests/Cargo.toml
- name: Run native recorder module tests (Windows)
# Exercise the production recorder module without hardware, Tauri, MLX, or ASR submodules.
if: runner.os == 'Windows'
run: python scripts/test-recorder-module.py
- name: Run Windows audio COM lifecycle tests
# These tests use independent processes for COM/enumerator lifetime checks and do not
# require a microphone. Device-dependent capture/playback tests remain explicitly ignored.
if: runner.os == 'Windows'
run: cargo test --locked --manifest-path src-tauri/vendor/cpal/Cargo.toml --lib --test windows_lifecycle
- name: Check Tauri backend with Rust 1.99 MSRV
if: runner.os == 'Windows' && needs.changes.outputs.msrv_needed == 'true'
run: cargo +1.99.0 check --locked --manifest-path src-tauri/Cargo.toml
- name: Compile backend tests with Rust 1.99 MSRV
if: runner.os == 'Windows' && needs.changes.outputs.msrv_needed == 'true'
run: cargo +1.99.0 test --locked --manifest-path src-tauri/backend-tests/Cargo.toml --no-run
- name: Build the Windows TSF input method
# The TSF DLL has its own vcxproj and is not part of the Tauri build, so
# nothing else in CI ever compiles it. A compile error here would
# otherwise only surface on a Windows machine with Visual Studio.
if: runner.os == 'Windows'
uses: microsoft/setup-msbuild@v2
with:
msbuild-version: '17.x'
- name: Compile the TSF IME (x64, Release)
if: runner.os == 'Windows'
working-directory: openless-all/app/windows-ime
run: msbuild OpenLessIme.sln /p:Configuration=Release /p:Platform=x64 /m /v:minimal
- name: Verify version sync across all 5 files
# Runs on both platforms: the Windows runner ships git-bash, giving consistent behavior
# across shells. Fail immediately on version drift instead of discovering it at release.
# Checks 5 places: package.json / package-lock.json (root + nested) /
# tauri.conf.json / Cargo.toml / the [openless] package in Cargo.lock.
shell: bash
run: |
PKG=$(node -p "require('./package.json').version")
LOCK_ROOT=$(node -p "require('./package-lock.json').version")
LOCK_NESTED=$(node -p "require('./package-lock.json').packages[''].version")
TAU=$(node -p "require('./src-tauri/tauri.conf.json').version")
CRG=$(grep -E '^version = ' src-tauri/Cargo.toml | head -1 | sed -E 's/^version = "(.+)"$/\1/')
# Cargo.lock: find the version line right after the [openless] package name
CARGO_LOCK_VER=$(awk 'BEGIN{found=0} /^name = "openless"$/{found=1; next} found && /^version = /{gsub(/"/,""); print $3; exit}' src-tauri/Cargo.lock)
echo "package.json = $PKG"
echo "package-lock root = $LOCK_ROOT"
echo "package-lock nested = $LOCK_NESTED"
echo "tauri.conf.json = $TAU"
echo "Cargo.toml = $CRG"
echo "Cargo.lock openless = $CARGO_LOCK_VER"
mismatch=0
for v in "$LOCK_ROOT" "$LOCK_NESTED" "$TAU" "$CRG" "$CARGO_LOCK_VER"; do
if [ "$v" != "$PKG" ]; then mismatch=1; fi
done
if [ "$mismatch" -ne 0 ]; then
echo "::error::版本号未对齐 — 请用 scripts/bump-version.sh 同步更新"
exit 1
fi
echo "[ok] 全部 5 处版本号一致:$PKG"
macos-msrv:
needs: [changes]
# Run the declared minimum toolchain even when stable has the same version.
if: needs.changes.outputs.msrv != 'false'
name: macOS Rust 1.99 MSRV
runs-on: ${{ github.event_name == 'workflow_dispatch' && inputs.macos_runner || 'macos-latest' }}
timeout-minutes: 60
env:
CARGO_BUILD_JOBS: 2
CARGO_PROFILE_DEV_DEBUG: 0
CARGO_PROFILE_TEST_DEBUG: 0
CMAKE_BUILD_PARALLEL_LEVEL: 2
defaults:
run:
working-directory: openless-all/app
steps:
- uses: actions/checkout@v4
with:
submodules: false
- uses: actions/setup-node@v4
with:
node-version: "22"
- name: Cache npm from beta
uses: ./.github/actions/cache-npm
- uses: dtolnay/rust-toolchain@1.99.0
- uses: swatinem/rust-cache@v2
with:
key: macos-msrv-v1
save-if: ${{ github.ref == 'refs/heads/beta' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
workspaces: |
openless-all/app/src-tauri -> target
openless-all/app/src-tauri/backend-tests -> target
- run: npm ci
- run: npm run build
- name: Build native capsule for Tauri checks
run: npm run build:native-capsule:dev
- name: Check Tauri backend with Rust 1.99 MSRV
run: cargo +1.99.0 check --locked --manifest-path src-tauri/Cargo.toml --timings
- name: Compile backend tests with Rust 1.99 MSRV
run: cargo +1.99.0 test --locked --manifest-path src-tauri/backend-tests/Cargo.toml --no-run --timings