-
Notifications
You must be signed in to change notification settings - Fork 0
701 lines (677 loc) · 37.1 KB
/
Copy pathci.yml
File metadata and controls
701 lines (677 loc) · 37.1 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
name: CI
on:
push:
branches: [master, main]
pull_request:
# One run per branch. Pushing three times in ten minutes used to start three full matrices, and the
# first two spend Windows and macOS minutes answering a question that is already out of date — while
# the third waits behind them in the same queue.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
# No `RUSTFLAGS` here on purpose.
#
# Setting it as an environment variable does not *add* to `.cargo/config.toml` — it replaces the
# `rustflags` in it, which is where this project's linker configuration lives: `-rpath,$ORIGIN`,
# so a binary finds ONNX Runtime and sherpa-onnx beside itself. A CI build with `RUSTFLAGS` set is
# therefore linked differently from every developer's build and from the release bundle, which is
# the kind of difference that is only discovered by a user whose download will not start.
#
# `-D warnings` is passed to clippy on its own command line below, where it belongs: it is a lint
# policy, not a link setting, and applying it through the environment also applies it to every
# dependency's build.
# Every job carries a `timeout-minutes`.
#
# GitHub's default is six hours. A Playwright suite that waits for a locator that will never appear
# does not fail — it holds a runner until somebody notices, and on a private repository that is six
# hours of billed minutes for a test that was already broken in the first thirty seconds. The
# numbers below are roughly three times the observed run, which is loose enough not to flake on a
# slow morning and tight enough to be a report rather than a bill.
jobs:
check:
name: fmt · clippy · test
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
# Two packages, two reasons.
#
# ALSA: `summo-audio` links cpal, which needs the headers to *build* even in a job that will
# never open a microphone — and the failure names `alsa-sys`, not a crate anybody wrote.
- name: System dependencies
run: sudo apt-get update && sudo apt-get install -y libasound2-dev
- uses: Swatinem/rust-cache@v2
- run: cargo fmt --all -- --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace
# The interface has 245 tests, a type checker, a linter and a formatter, and until now CI ran
# none of them — every one of those checks existed only on the machine of whoever remembered.
web:
name: types · lint · format · test
runs-on: ubuntu-latest
timeout-minutes: 15
defaults:
run:
working-directory: apps/web
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
working-directory: .
- run: pnpm exec tsc --noEmit
- run: pnpm lint
- run: pnpm exec prettier --check .
- run: pnpm test
- run: pnpm build
# What the browser downloads before it can paint. A number that only gets looked at when
# somebody complains the app is slow to open is a number that has already grown for a year.
- run: pnpm budget
# Which platforms this run links on.
#
# `cargo clippy --workspace --all-targets` in the `check` job already type-checks every crate on
# every push, so what the matrix below adds is *linking per platform* — a packaging question, and
# packaging is what `master` and a tag are for. On a pull request that was five extra platforms,
# holding up review of a one-line change behind a question that will be asked again on merge
# anyway.
#
# `x86_64-apple-darwin` is not in this list: ONNX Runtime publishes no build for Intel macOS any
# more, so it cannot link. The reasoning is in `release.yml`, next to the matrix it was cut from.
plan:
name: which platforms
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
targets: ${{ steps.pick.outputs.targets }}
steps:
- id: pick
run: |
full='[{"os":"ubuntu-latest","target":"x86_64-unknown-linux-gnu"},
{"os":"ubuntu-24.04-arm","target":"aarch64-unknown-linux-gnu"},
{"os":"macos-latest","target":"aarch64-apple-darwin"},
{"os":"windows-latest","target":"x86_64-pc-windows-msvc"}]'
one='[{"os":"ubuntu-latest","target":"x86_64-unknown-linux-gnu"}]'
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "targets=$(echo "$one" | tr -d '\n ')" >> "$GITHUB_OUTPUT"
else
echo "targets=$(echo "$full" | tr -d '\n ')" >> "$GITHUB_OUTPUT"
fi
desktop:
name: build ${{ matrix.target }}
needs: plan
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
include: ${{ fromJSON(needs.plan.outputs.targets) }}
# No `aarch64-pc-windows-msvc`.
#
# `audiopus_sys` 0.1.8 has no ARM64 Windows support: its build script picks an MSVC
# library directory from an `ARCHITECTURE` constant that is only defined for x86 and x64,
# so the build fails to *compile the build script itself* — `cannot find value
# ARCHITECTURE in this scope`. Nothing in this repository can fix that from here.
#
# Listing the target and letting it fail every run is worse than not claiming it: a red
# CI that is expected to be red is a CI nobody reads. It comes back when the crate gains
# ARM64 support or when Opus is linked some other way.
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# `rustup target add`, not the action's `targets:` input.
#
# `rust-toolchain.toml` pins 1.95.0, so cargo uses 1.95.0 no matter what the action installed.
# The action added the target to *stable*, and every cross build then failed with "can't find
# crate for `core`… the target may not be installed" — while pointing at a target that had
# been installed, for a different toolchain.
- run: rustup target add ${{ matrix.target }}
- name: System dependencies
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libasound2-dev
# `audiopus_sys` cannot find a system Opus on a runner, so it builds the bundled copy — and
# that starts with `autogen.sh`, which needs autotools. Without them the build script panics
# with `autoreconf: command not found`, several hundred crates in.
- name: Autotools, for building Opus from source
if: runner.os == 'macOS'
run: brew install autoconf automake libtool
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- run: cargo build --workspace --release --target ${{ matrix.target }}
# Mobile is milestone 11, but the core crates must stay portable from day one. Cross-compiling
# here catches a desktop-only dependency the week it is added, not a year later.
mobile-portability:
name: cross-compile ${{ matrix.target }}
runs-on: ${{ matrix.os }}
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
- { os: macos-latest, target: aarch64-apple-ios }
- { os: ubuntu-latest, target: aarch64-linux-android }
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# `rustup target add`, not the action's `targets:` input.
#
# `rust-toolchain.toml` pins 1.95.0, so cargo uses 1.95.0 no matter what the action installed.
# The action added the target to *stable*, and every cross build then failed with "can't find
# crate for `core`… the target may not be installed" — while pointing at a target that had
# been installed, for a different toolchain.
- run: rustup target add ${{ matrix.target }}
- name: System dependencies
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libasound2-dev
# `audiopus_sys` cannot find a system Opus on a runner, so it builds the bundled copy — and
# that starts with `autogen.sh`, which needs autotools. Without them the build script panics
# with `autoreconf: command not found`, several hundred crates in.
- name: Autotools, for building Opus from source
if: runner.os == 'macOS'
run: brew install autoconf automake libtool
- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}
- if: matrix.target == 'aarch64-linux-android'
uses: nttld/setup-ndk@v1
with:
ndk-version: r27c
add-to-path: true
# `add-to-path` puts the NDK root on PATH, not the compilers: those live three directories
# down, and `cc-rs` looked for `aarch64-linux-android24-clang` and found nothing. The error
# names the tool and not the fact that it is present a few directories away.
- if: matrix.target == 'aarch64-linux-android'
run: echo "$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" >> "$GITHUB_PATH"
# The whole workspace, not a hand-picked four.
#
# The list used to be `summo-core summo-models summo-vad summo-bench`, chosen when nobody knew
# how much of the rest would survive the trip. All of it does — the engine with its bundled
# interface, the vault, the agent, sherpa-onnx and ONNX Runtime included — so checking four
# crates was understating what works and leaving the other twenty free to break quietly.
#
# `check` rather than `build` for Android: linking needs the NDK's sysroot for every C++
# dependency, which is a lot of minutes to prove something the type checker already proved.
# The desktop matrix above does the real linking.
- name: The whole workspace stays portable
env:
CC_aarch64_linux_android: aarch64-linux-android24-clang
CXX_aarch64_linux_android: aarch64-linux-android24-clang++
AR_aarch64_linux_android: llvm-ar
run: cargo check --target ${{ matrix.target }} --workspace
# The Android app, assembled.
#
# `mobile-portability` above type-checks the workspace for `aarch64-linux-android`, which is a
# different claim: it says the code is well-formed for the platform, not that it links, and
# certainly not that anything can be installed. Everything this job found was invisible to a type
# check — a missing trait import, a missing icon, a codec that cross-compiles only if asked, and a
# CLI the build could not resolve. `apps/mobile/README.md` said "no `.apk` has ever been
# produced"; this is the job that stops that being true again.
#
# Only on `master` and on a change under `apps/mobile`. It is a twenty-minute build for a claim
# that does not change when somebody edits a screen.
android:
name: the Android app assembles, and runs
runs-on: ubuntu-latest
# Two Rust builds and an emulator. The arm64 APK is what ships; the x86_64 one is what can be
# started on a runner, and starting it is the whole point of the job's second half.
timeout-minutes: 100
if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'mobile')
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# `rustup target add` rather than the action's input, for the reason in every other job here:
# `rust-toolchain.toml` pins the version, and the action adds the target to *stable*.
- run: rustup target add aarch64-linux-android
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- uses: android-actions/setup-android@v3
- name: The NDK
run: |
sdkmanager --install "ndk;27.2.12479018" > /dev/null
echo "NDK_HOME=$ANDROID_HOME/ndk/27.2.12479018" >> "$GITHUB_ENV"
# bindgen runs its own clang, and that clang does not inherit the cross-compiler's idea of
# where the headers are. `sherpa-rs-sys` generates bindings, so without this it reads the
# *host's* `/usr/include` and dies on `bits/libc-header-start.h`, several hundred crates
# into an otherwise correct Android build.
sysroot="$ANDROID_HOME/ndk/27.2.12479018/toolchains/llvm/prebuilt/linux-x86_64/sysroot"
echo "BINDGEN_EXTRA_CLANG_ARGS_aarch64_linux_android=--sysroot=$sysroot --target=aarch64-linux-android26" >> "$GITHUB_ENV"
- uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target\napps/mobile/src-tauri -> target"
key: android
# Opus, cross-compiled. `audiopus_sys` runs `configure` with no `--host`, so its bundled build
# either refuses to run its own test program or produces an x86-64 library the linker rejects.
# `OPUS_LIB_DIR` is the crate's own way out. See the script.
- name: Opus for arm64
run: |
echo "OPUS_LIB_DIR=$(./scripts/opus-android.sh arm64-v8a | tail -1)" >> "$GITHUB_ENV"
echo "OPUS_STATIC=1" >> "$GITHUB_ENV"
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm -C apps/mobile exec tauri android init --skip-targets-install
# `init` regenerates the Gradle project from a template, over a project that is committed.
# The permissions below are hand-written and are the difference between an app that records
# and one that installs, opens and cannot — so this asserts they survived, rather than
# discovering months later that a template overwrote them and every build since was deaf.
- name: The manifest still asks for a microphone
run: |
manifest=apps/mobile/src-tauri/gen/android/app/src/main/AndroidManifest.xml
for permission in RECORD_AUDIO MODIFY_AUDIO_SETTINGS FOREGROUND_SERVICE_MICROPHONE; do
grep -q "android.permission.$permission" "$manifest" \
|| { echo "::error file=$manifest::$permission was lost — did 'android init' overwrite the manifest?"; exit 1; }
done
# And that a release build is still allowed to talk to its own engine. Without this line
# the app installs, opens, starts its daemon and cannot reach it — see the config itself.
grep -q 'android:networkSecurityConfig' "$manifest" \
|| { echo "::error file=$manifest::the network security config was lost — a release build cannot reach 127.0.0.1 without it"; exit 1; }
# Signed when this repository has a key, unsigned when it does not — which is every fork and
# every pull request from one. The keystore never becomes a file that could be committed or
# swept into an artefact: it is written to the runner's temp directory and the job ends.
#
# A missing secret is an empty string, so this step simply does nothing on a fork, and
# `build.gradle.kts` says out loud which of the two it did.
- name: The signing key, if this repository has one
env:
KEYSTORE: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
if: env.KEYSTORE != ''
run: |
echo "$KEYSTORE" | base64 -d > "$RUNNER_TEMP/summo.keystore"
echo "ANDROID_KEYSTORE=$RUNNER_TEMP/summo.keystore" >> "$GITHUB_ENV"
echo "ANDROID_KEYSTORE_PASSWORD=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" >> "$GITHUB_ENV"
echo "ANDROID_KEY_ALIAS=${{ secrets.ANDROID_KEY_ALIAS }}" >> "$GITHUB_ENV"
echo "ANDROID_KEY_PASSWORD=${{ secrets.ANDROID_KEY_PASSWORD }}" >> "$GITHUB_ENV"
- run: pnpm -C apps/mobile exec tauri android build --apk --target aarch64
- uses: actions/upload-artifact@v4
with:
name: summo-android-arm64
path: apps/mobile/src-tauri/gen/android/app/build/outputs/apk/**/*.apk
retention-days: 14
# And then it is run, which is a claim nothing here could make until now.
#
# An arm64 APK cannot be started on an x86-64 runner, so this builds the same app for the
# emulator's architecture and drives that. It is a second Rust build and it is worth it: the
# first time this app was ever launched, it opened, drew its interface, started its daemon and
# said "Failed to fetch" — a release build sets `usesCleartextTraffic="false"`, so Android
# refused every request the webview made to the engine inside its own process. The debug
# build sets it to "true", which is why the only configuration that worked was the one nobody
# ships. `scripts/android-smoke.sh` is what would have caught it.
- name: Opus for x86_64, for the emulator build
run: echo "OPUS_LIB_DIR_X86=$(./scripts/opus-android.sh x86_64 | tail -1)" >> "$GITHUB_ENV"
- name: Build for the emulator
env:
OPUS_LIB_DIR: ${{ env.OPUS_LIB_DIR_X86 }}
run: pnpm -C apps/mobile exec tauri android build --apk --target x86_64
# KVM is present on the runner and owned by a group the job is not in, so without this the
# emulator falls back to software and takes long enough to hit the timeout.
- name: Let the emulator use KVM
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
# Android will not install an APK with no signature at all — `INSTALL_PARSE_FAILED_NO
# _CERTIFICATES` — and this repository's key is a secret that forks do not have and that a
# pull request from one cannot read. So when the build produced an unsigned APK, it is signed
# here with a key generated for this job and thrown away with the runner.
#
# Nothing about that key is secret and nothing depends on it: it exists so the package manager
# has a signature to parse. The release APK, signed with the real key, is the artefact above —
# this is the copy that gets installed on an emulator and deleted.
- name: A signature, so the emulator will install it
run: |
apk="$(ls -t apps/mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/*.apk | head -1)"
echo "built: $apk"
if [ "${apk#*unsigned}" != "$apk" ]; then
tools="$(ls -d "$ANDROID_HOME"/build-tools/* | tail -1)"
keytool -genkeypair -dname "CN=summo smoke test" -alias smoke -validity 30 \
-keyalg RSA -keysize 2048 -storetype PKCS12 \
-keystore "$RUNNER_TEMP/smoke.p12" -storepass throwaway -keypass throwaway
"$tools/apksigner" sign --ks "$RUNNER_TEMP/smoke.p12" --ks-pass pass:throwaway \
--out "$RUNNER_TEMP/smoke.apk" "$apk"
echo "signed with a throwaway key — this repository has no Android key configured"
else
cp "$apk" "$RUNNER_TEMP/smoke.apk"
echo "already signed by this repository's key"
fi
- name: The app runs, and reaches its own engine
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
target: google_apis
arch: x86_64
# No window, no audio, no boot animation: nothing here looks at pixels. The assertion is
# read out of the accessibility tree, which is where a WebView publishes its text.
emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect -no-snapshot
script: ./scripts/android-smoke.sh "$RUNNER_TEMP/smoke.apk"
# The shell users actually install. It is not a workspace member — a Tauri app has its own
# lockfile and its own build script — so `cargo build --workspace` never touched it, and it had
# stopped compiling against Tauri v2 without anybody finding out.
desktop-shell:
name: the desktop shell compiles
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target\napps/desktop/src-tauri -> target"
# See the note in the browser job: the native libraries live outside `target/`.
# Every platform's answer to "where does a library cache go". Missing paths are skipped,
# so all six are listed rather than branching on the runner.
cache-directories: |
~/.cache/sherpa-rs
~/.cache/ort.pyke.io
~/Library/Caches/sherpa-rs
~/Library/Caches/ort.pyke.io
~/AppData/Local/sherpa-rs
~/AppData/Local/ort.pyke.io
- name: System libraries Tauri links against
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev libasound2-dev
# The sidecar is the daemon with the interface compiled into it, so the interface has to be
# built before it. On a developer's machine `apps/web/dist` is already there from the last
# `pnpm build`, which is why this was never noticed locally; a fresh checkout has no `dist`
# and `include_dir!` fails with "is not a directory".
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm -C apps/web build
# `generate_context!` reads the icon and the sidecar at compile time, so both have to be
# there before `cargo check` will even parse the file.
- run: ./scripts/sidecar.sh
- run: cargo check
working-directory: apps/desktop/src-tauri
# The shell is outside the workspace, so the `fmt · clippy · test` job above never saw it —
# and it had drifted out of format and had no tests at all while it grew the code that starts
# the daemon. Cheap here: everything is already compiled by the check above.
- run: cargo fmt --check
working-directory: apps/desktop/src-tauri
- run: cargo test
working-directory: apps/desktop/src-tauri
# Compiling the shell is not the same claim as producing something a person can install, and the
# difference is where the bugs were: the daemon and the libraries it loads have to be *inside* the
# bundle, in the places the shell looks for them. That is decided by the bundler, and until this
# job existed nothing had ever run it.
#
# Gated like the Android build, and for the same reason: it is a release-profile build of a
# recognition stack, and it does not change when somebody edits a screen.
desktop-bundle:
name: the desktop app packages
runs-on: ubuntu-latest
timeout-minutes: 90
if: github.event_name == 'push' || contains(github.event.pull_request.labels.*.name, 'desktop')
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
workspaces: ". -> target\napps/desktop/src-tauri -> target"
key: desktop-bundle
cache-directories: |
~/.cache/sherpa-rs
~/.cache/ort.pyke.io
- name: System libraries Tauri links against
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev libasound2-dev patchelf
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
# Both Linux formats, because they fail differently and the release found that out the hard
# way: the `.deb` bundled cleanly and the AppImage died with "failed to run linuxdeploy" —
# its tooling is an AppImage, mounting one needs FUSE, and the runner images no longer ship
# it. Checking only the format that works is how a release breaks on the day it is cut.
#
# The cost is honest: AppImage's tools are downloaded from a third party at bundle time, so
# this check has a network dependency the rest of CI does not.
- name: Build the installers
env:
APPIMAGE_EXTRACT_AND_RUN: 1
# `--verbose` because the bundler's own failure message is `failed to run linuxdeploy` and
# nothing else. The reason — a dependency it could not resolve — is in the child's output,
# which is printed only at this level.
run: pnpm -C apps/desktop exec tauri build --bundles deb --bundles appimage --verbose
# What the bundle has to contain, checked rather than assumed. Each of these was missing at
# some point this week, and each produced an app that opened and did nothing.
- name: The daemon and its libraries are inside it
run: |
package="$(find apps/desktop/src-tauri/target/release/bundle/deb -name '*.deb' | head -1)"
test -n "$package" || { echo "::error::no .deb was produced"; exit 1; }
dpkg-deb -c "$package" > /tmp/contents.txt
grep -q "summo-engine" /tmp/contents.txt \
|| { echo "::error::the bundle has no daemon in it"; cat /tmp/contents.txt; exit 1; }
# Every library the staged daemon declares, and everything those declare in turn, has to
# be in the bundle. `sidecar.sh` fails when it cannot find one; this checks the rest of
# the way — that what it found survived into the package.
for lib in $(ls apps/desktop/src-tauri/binaries/lib | grep -v README); do
grep -q "$lib" /tmp/contents.txt \
|| { echo "::error::$lib was staged and is not in the .deb"; exit 1; }
done
echo "--- what is in it ---"
awk '{print $NF}' /tmp/contents.txt | grep -E "summo|lib/" | head -40
# And then it is run.
#
# Everything above this line asks what is *in* the package. Every packaging bug this project
# has had looked the same from outside — an app that installs, opens a window and shows
# nothing, because the daemon it spawns died one line into its life on a library it could not
# find. A contents check cannot see that: the file is present, correct, and dead on arrival.
#
# So the AppImage is started under a virtual display and asked to prove it is alive. It is
# the format worth running of the two: the `.deb` bundles cleanly on machines where the
# AppImage does not, and the AppImage is the single file most Linux users will actually
# download.
- name: The app starts, and its daemon answers
run: |
sudo apt-get install -y xvfb
./scripts/smoke-desktop.sh \
"$(find apps/desktop/src-tauri/target/release/bundle/appimage -name '*.AppImage' | head -1)"
- uses: actions/upload-artifact@v4
with:
name: summo-desktop-linux-x86_64
path: |
apps/desktop/src-tauri/target/release/bundle/deb/*.deb
apps/desktop/src-tauri/target/release/bundle/appimage/*.AppImage
retention-days: 14
registry:
name: validate the published registry
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
# The catalogue is a separate repository. Checking it out here catches the case that matters:
# a tightening of the manifest rules in this repo that would reject a manifest already in the
# wild, which would break `summo pull` for everybody rather than only in review.
#
# `continue-on-error`, because the repository does not exist yet. Without it every push to
# this one fails red on something nobody in this repository can fix, and a CI that is always
# red is a CI nobody reads. The step below notices the absence and says so instead.
- id: catalogue
continue-on-error: true
uses: actions/checkout@v4
with:
repository: Techainer/summo-registry
path: registry
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- run: cargo test -p summo-models
- name: Validate every published manifest against this build
if: steps.catalogue.outcome == 'success'
run: cargo run --quiet -p summo-cli -- registry check registry
- name: Say so when there is no published registry to check
if: steps.catalogue.outcome != 'success'
run: |
echo "::warning::Techainer/summo-registry is not reachable, so no published manifest was checked."
# The two in-process translation runtimes. `cargo test --workspace` covers neither: the crate is
# empty without a feature, so a change that breaks the ONNX path compiles and tests green.
#
# Only the ONNX one. The GGUF one builds llama.cpp, which is several minutes and a clang install
# for a path the same tests already exercise the shape of.
translation:
name: the in-process translator compiles and passes
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
- run: cargo clippy -p summo-mt --features onnx --all-targets -- -D warnings
- run: cargo test -p summo-mt --features onnx
# `summo-mt` alone needs nothing from the system; `summo-engine` below pulls cpal, which wants
# the ALSA headers to build even where no microphone is ever opened.
- name: System dependencies
run: sudo apt-get update && sudo apt-get install -y libasound2-dev
# The engine has one `LocalModel` arm per runtime, each behind its own feature, so the
# combination the release ships — ONNX in, llama.cpp out — is a build nothing else covers.
# It broke twice while being written: an unused `source` parameter, and a `no_runtime` helper
# that is unreachable when both runtimes are present. `mt-gguf` is deliberately not checked
# here for the reason above; the tag build is where llama.cpp would be compiled, and it is
# not in the shipped feature set.
- run: cargo clippy -p summo-engine --features mt-onnx --all-targets -- -D warnings
# The browser suites. They need a daemon with the interface compiled in, which is why they are
# their own job: the check above builds the web app, this one bakes it into the binary and drives
# it. Ten suites, and they are the only tests that see what a user sees.
browser:
name: browser suites
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
# Inside the workspace, not beside it. `actions/checkout` refuses any path outside
# `GITHUB_WORKSPACE`, so `path: ../summo-registry` never worked — the job failed before it
# reached a single suite. The suites read `SUMMO_REGISTRY_DIR` instead of assuming a sibling
# directory.
- uses: actions/checkout@v4
continue-on-error: true
with:
repository: Techainer/summo-registry
path: .registry
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
# ONNX Runtime and sherpa-onnx are downloaded by their build scripts into `~/.cache`, and
# the *paths* to them are recorded in `target/`. Caching `target/` without caching those
# directories restores a build that believes the libraries are present, on a runner where
# they are not — and the link fails with `unable to find library -lonnxruntime`, several
# hundred crates after the point where anything could have noticed.
# Every platform's answer to "where does a library cache go". Missing paths are skipped,
# so all six are listed rather than branching on the runner.
cache-directories: |
~/.cache/sherpa-rs
~/.cache/ort.pyke.io
~/Library/Caches/sherpa-rs
~/Library/Caches/ort.pyke.io
~/AppData/Local/sherpa-rs
~/AppData/Local/ort.pyke.io
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
# ALSA for cpal, and Opus for the recorder — the `bundled,models` build below links both.
# The release bundle sets `OPUS_STATIC=1` and builds Opus from source instead, which is why
# this job never needed it until the whole-product suite arrived: without the package the
# daemon starts and dies with `libopus.so.0: cannot open shared object file`, and the harness
# reports it as "the daemon did not come up".
- name: System dependencies
run: sudo apt-get update && sudo apt-get install -y libasound2-dev libopus-dev
- run: pnpm install --frozen-lockfile
# Chromium is ~130 MB, downloaded on every run of this job. Keyed on the Playwright version,
# because a cached browser from an older Playwright is a browser it refuses to launch.
- id: playwright
run: echo "version=$(pnpm -C apps/web exec playwright --version | tr -d ' ')" >> "$GITHUB_OUTPUT"
- uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}
- run: pnpm -C apps/web exec playwright install --with-deps chromium
- run: pnpm -C apps/web build
# `--features bundled` is what puts the interface inside the binary. Without it every suite
# fails on a missing header, which is a long way from the cause — `e2e/daemon.mjs` detects
# that case and says so, and this is the line that prevents it.
# Linted here rather than in the `check` job, which has no `apps/web/dist` — `include_dir!`
# fails to *parse* the file without one. `bundled` is the shape that ships, so leaving it out
# of the lint means the arm every user runs is the one arm nobody checks.
- run: cargo clippy -p summo-engine --features bundled --all-targets -- -D warnings
- run: cargo build --bin summo-engine --features bundled
- run: pnpm -C apps/web e2e
env:
SUMMO_REGISTRY_DIR: ${{ github.workspace }}/.registry
# The clean-install story, against the binary a user downloads rather than the daemon the
# suites above start for themselves.
#
# It lived only in the release workflow, which is to say it ran on tags and nowhere else —
# and by the time a tag was cut it had rotted: it clicked a button named "New" that the empty
# state had since learned to offer as well, and Playwright refused the ambiguity. A test that
# runs twice a year is a test that fails when you need it.
- run: cargo build -p summo-cli --features serve,bundled
- run: node apps/web/e2e/first-run.mjs target/debug/summo 8793
# The whole product, once, with recognition actually compiled in.
#
# Every suite above runs against `--features bundled`, which has no speech model support at
# all — so for months the one test that drives audio → text → file was the one test nobody
# ran. It took a WAV path and a token on the command line; now it boots its own daemon,
# installs a model from the local mirror and records a committed fixture, which is what makes
# it runnable here.
#
# `bundled,models` is a heavier build — it links sherpa-onnx — so it is a second step rather
# than a change to the one above: the ten fast suites should not wait for it to fail.
- run: cargo build --bin summo-engine --features bundled,models
# sherpa-onnx ships as a shared library, and it is the crate's *build script* that puts it
# beside the binary — so a cache hit, which skips the build script, produces a binary linked
# with an `$ORIGIN` rpath and nothing at `$ORIGIN` to find. The daemon then dies with
# "libsherpa-onnx-c-api.so: cannot open shared object file" and the harness reports it as
# "the daemon did not come up", which is a long way from the cause.
#
# Rebuilding just that crate is the cheapest way to make the copy happen again, and it only
# costs anything on the runs where it was missing.
- name: The speech libraries, beside the binary
run: |
ls -l target/debug/libsherpa* 2>/dev/null || echo "not beside the binary"
if [ ! -e target/debug/libsherpa-onnx-c-api.so ]; then
cargo clean -p sherpa-rs-sys
cargo build --bin summo-engine --features bundled,models
ls -l target/debug/libsherpa*
fi
- run: pnpm -C apps/web e2e:full-flow
env:
SUMMO_REGISTRY_DIR: ${{ github.workspace }}/.registry
# The pictures, when something went wrong.
#
# These suites fail with a sentence — "the drop target was never highlighted", "contrast 3.1
# on Giọng nói" — and the sentence is the *conclusion*. What a person needs to act on it is
# the frame it was drawn from, and until now that only existed inside a runner that had
# already been deleted. `if: failure()` because a green run's screenshots are 44 files nobody
# opens.
- if: failure()
uses: actions/upload-artifact@v4
with:
name: screens
path: /tmp/shots
retention-days: 7
if-no-files-found: ignore