Repository navigation
661 lines (580 loc) · 27.2 KB
/
Copy pathcommunity.yml
File metadata and controls
661 lines (580 loc) · 27.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
name: Community
# Spec 0090 §1: beweist, dass das öffentliche Repo (dieser Workflow läuft
# aus einem frischen Klon, ohne Secrets) eigenständig baut — schlägt ein
# Schritt hier fehl, weil etwas außerhalb dieses Repositorys fehlt, ist das
# ein Fehler. Der Test-Job (Spec 0090 §2) umfasst die Rust-Schritte (A9),
# die Grenzprüfung (A10), die Drittlizenzen (A11) und das Frontend (A5).
# `push` nur auf `main`: Commits eines PR-Branches laufen über
# `pull_request`. Vorher lief jeder PR-Commit zweimal unter denselben
# Check-Namen, und ein roter Lauf von beiden blockierte den Merge (#26).
# Dieser Workflow führt nur Prüfungen aus; er baut und veröffentlicht keine
# Release-Artefakte.
# Push-Läufe auf den langlebigen Branches des Versionszyklus: `main`
# (Live-Version), `develop` (nächste Version) sowie `release/*` und
# `hotfix/*`. Der Merge-Schritt wertet den letzten Push-Lauf des
# Basis-Branches aus, um einen roten Basis-Branch zu erkennen.
on:
push:
branches: [main, develop, "release/**", "hotfix/**"]
pull_request:
# Ein neuer Push auf denselben PR bricht den noch laufenden Lauf ab.
# Jeder Push-Lauf (`main`, `develop`, `release/*`, `hotfix/*`) bekommt eine eigene Gruppe (`run_id`): Eine
# gemeinsame Gruppe würde einen wartenden `main`-Lauf verwerfen, sobald
# ein weiterer ansteht, auch ohne `cancel-in-progress`.
concurrency:
group: community-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Read pinned Rust toolchain
id: rust-toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ steps.rust-toolchain.outputs.channel }}
components: rustfmt, clippy
- name: Print Rust version
run: rustc --version
- name: Cache cargo registry & build artifacts
uses: Swatinem/rust-cache@v2
# Nur auf Linux nötig: Tauri (genauer `wry`) linkt gegen WebKitGTK,
# ohne diese System-Pakete schlägt bereits `cargo build`/`clippy` für
# `apps/smart-ssh-community` fehl. Die Paketliste, der .deb-Cache und
# der Wiederholungsversuch stehen in `.github/actions/linux-deps`.
- name: Install Linux dependencies
if: matrix.os == 'ubuntu-latest'
timeout-minutes: 22
uses: ./.github/actions/linux-deps
# The gate itself lives in `scripts/gate.sh` (one definition for local
# runs and CI; every Cargo call uses `--locked`). Linux and macOS call
# it per stage. Windows keeps the Cargo steps inline in the default
# shell with the same commands and flags: under Git Bash (`shell: bash`)
# perl cannot build the vendored OpenSSL (missing
# Locale::Maketext::Simple). Keep the Windows commands identical to
# the `rust` stage of `scripts/gate.sh`.
- name: Rust gate (fmt, clippy, test, build)
if: runner.os != 'Windows'
shell: bash
run: scripts/gate.sh --ci rust
- name: cargo fmt --check
if: runner.os == 'Windows'
run: cargo fmt --all --check
- name: cargo clippy
if: runner.os == 'Windows'
run: cargo clippy --workspace --all-targets --locked -- -D warnings
- name: cargo test
if: runner.os == 'Windows'
env:
SMART_SSH_REQUIRE_CLIPBOARD_TESTS: "1"
run: cargo test --workspace --locked --no-fail-fast
- name: cargo build
if: runner.os == 'Windows'
run: cargo build --workspace --locked
# Spec 0084, A3/T4 (details in `scripts/gate.sh`, stage `boundary`).
- name: Grenzprüfung app-logic ist Tauri-frei (Spec 0084, A3)
shell: bash
run: scripts/gate.sh --ci boundary
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: apps/smart-ssh-community/frontend/package-lock.json
- name: Install frontend dependencies
working-directory: apps/smart-ssh-community/frontend
run: npm ci
# Spec 0099, A3.1 (Spec 0090, A11): das Drittlizenz-Skript läuft bei
# jedem CI-Lauf auf **allen drei** Matrix-Plattformen, nicht nur auf
# einer — ein plattformspezifischer Fehler (etwa unter PowerShell)
# fällt so schon hier auf, nicht erst im Release-Lauf.
# Dieselbe feste Version wie in
# `scripts/generate-third-party-notices.mjs`
# (`EXPECTED_CARGO_ABOUT_VERSION`, A1.6) — ein Auseinanderlaufen fällt
# dort als Abbruch auf, nicht erst als stiller Unterschied.
- name: Install cargo-about
uses: taiki-e/install-action@v2
with:
tool: cargo-about@0.9.2
# Erzeugt `public/third-party-notices.txt`, damit die anschließenden
# Frontend-Tests (Spec 0099, A3.1) gegen eine echte Ausgabe statt
# nur gegen ihren eigenen Skip-Zweig laufen. Kein `continue-on-error`:
# ein Lizenz-/Werkzeug-Fehler hier färbt den Job rot (A1.3, A3.1).
- name: Generate third-party license notices
working-directory: apps/smart-ssh-community/frontend
run: npm run generate-notices
# Spec 0090, A5 / Spec 0100, A6.1: oxlint errors (incl. every
# `jsx-a11y` rule) and failing vitest cases fail the job. On Linux and
# macOS `scripts/gate.sh frontend` runs tsc, lint, vitest and build.
- name: Frontend (tsc, lint, tests, build)
if: runner.os != 'Windows'
shell: bash
run: scripts/gate.sh --ci frontend
- name: Frontend lint
if: runner.os == 'Windows'
working-directory: apps/smart-ssh-community/frontend
run: npm run lint
# Issue #841: allowlist entries of the file-size ratchet may only go
# down. Needs the base branch, which the shallow checkout lacks.
- name: File-size allowlist vs. base branch
if: github.event_name == 'pull_request' && runner.os == 'Linux'
run: |
git fetch --no-tags --depth=1 origin "$GITHUB_BASE_REF"
node scripts/check-file-size.mjs --base FETCH_HEAD
- name: Frontend tests
if: runner.os == 'Windows'
working-directory: apps/smart-ssh-community/frontend
run: npm test
- name: Frontend build
if: runner.os == 'Windows'
working-directory: apps/smart-ssh-community/frontend
run: npm run build
# Spec 0090 §3: Dependency-Audit (cargo-audit, RustSec-Advisories) und
# Lizenz-/Herkunfts-Compliance (cargo-deny, A12/A13). Eigenständiger,
# nicht-Matrix-Job auf `ubuntu-latest` (das Ergebnis hängt nicht vom
# Betriebssystem ab, ein einzelner Lauf reicht).
#
# Spec 0090, A2: beide Schritte unten (`cargo deny check licenses sources
# bans` und `cargo audit`) sind Pflichtschritte ohne `continue-on-error` —
# ein Lizenz-, Herkunfts- oder Bann-Verstoß sowie jede nicht ausgenommene
# Sicherheitslücke färben diesen Job rot. Hinweise (unmaintained/unsound)
# und yanked-Crates bleiben sichtbare, nicht blockierende Warnungen (s.
# Kommentare bei den einzelnen Schritten und in `.cargo/audit.toml`).
dependency-audit:
name: Dependency Audit (cargo-deny / cargo-audit)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Read pinned Rust toolchain
id: rust-toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ steps.rust-toolchain.outputs.channel }}
- name: Print Rust version
run: rustc --version
- name: Cache cargo registry & build artifacts
uses: Swatinem/rust-cache@v2
- name: Install cargo-deny and cargo-audit
uses: taiki-e/install-action@v2
with:
tool: cargo-deny,cargo-audit
# Advisories bleiben cargo-audits Aufgabe (Spec 0090, A3: Ausnahmen
# nur an einer Stelle) — dieser Aufruf prüft bewusst nur Lizenzen,
# Herkunft und Banne (A12, A13). Konfiguration: `deny.toml` im
# Repo-Root; bewusst freigegebene Fälle sind dort mit Begründung
# eingetragen (s. dortige Kommentare).
#
# Spec 0090, A2: dieser Schritt ist blockierend, kein
# `continue-on-error` — ein Lizenz-, Herkunfts- oder Bann-Verstoß
# färbt den Job rot.
- name: cargo deny check (licenses, sources, bans)
run: cargo deny check licenses sources bans
# Konfiguration: `.cargo/audit.toml` (der von cargo-audit selbst
# automatisch erkannte Pfad, s. dortiger Kommentar zum Schema/zur
# Pfad-Konvention).
- name: cargo audit
run: cargo audit
# Spec 0090, A4: eine Ausnahme in `.cargo/audit.toml`, die auf keinen
# Befund im aktuellen Lockfile mehr trifft (weil ein Advisory
# zurückgezogen wurde oder das betroffene Crate nicht mehr im Baum
# steht), soll auffallen statt unbemerkt stehen zu bleiben. Dazu läuft
# `cargo audit` hier ein zweites Mal, in einem frischen Verzeichnis
# ohne `.cargo/audit.toml` (also ohne die Ausnahmeliste), gegen
# dasselbe Lockfile — die JSON-Ausgabe zeigt dann jeden Befund, Lücken
# UND Hinweise. Jede in `.cargo/audit.toml` eingetragene ID, die darin
# nicht mehr vorkommt, ist eine überholte Ausnahme. Das Verbot aus A2
# (kein `continue-on-error`) gilt nur für die beiden Schritte oben,
# nicht für diesen Zwischenaufruf: `cargo audit` endet hier
# erwartungsgemäß mit Exit != 0 (RUSTSEC-2023-0071 hat keinen Fix) —
# das ist kein Fehler dieses Schritts, ausgewertet wird ausschließlich
# seine JSON-Ausgabe. Fehlt sie oder lässt sie sich nicht lesen, ist
# dieser Schritt rot.
- name: Audit-Ausnahmen gegen aktuelle Funde abgleichen (Spec 0090, A4)
shell: bash
run: |
# spec-reviewer-Fund (Runde 1): `shell: bash` läuft in GitHub
# Actions mit `-eo pipefail` (errexit). Ein einfaches
# `var=$(cmd)` reicht den Exit-Code von `cmd` an die Zuweisung
# weiter - und `cargo audit` OHNE Ausnahmeliste endet hier bei
# JEDEM Lauf mit Exit != 0 (RUSTSEC-2023-0071 hat keinen Fix,
# s. Kommentar oben). Ohne den `if !`-Kontext unten würde
# errexit die Shell an dieser Stelle sofort beenden, bevor die
# eigentliche Stale-Prüfung je läuft - der Schritt wäre dann
# immer rot, unabhängig vom Zustand der Ausnahmeliste. Ein
# `if`/`if !`-Kontext ist von errexit ausgenommen (POSIX/bash),
# deshalb dort geprüft, nie per separatem `$?` danach.
scratch="$(mktemp -d)"
cp Cargo.lock "$scratch/Cargo.lock"
json_output=""
if ! json_output="$(cd "$scratch" && cargo audit --json -f Cargo.lock)"; then
: # erwarteter Exit != 0, s. Kommentar oben - kein Fehler dieses Schritts
fi
if [ -z "$json_output" ]; then
echo "::error::cargo audit ohne Ausnahmeliste lieferte keine Ausgabe - Abgleich (Spec 0090 A4) konnte nicht laufen"
exit 1
fi
found_ids=""
if ! found_ids="$(printf '%s' "$json_output" | jq -r '
[ (.vulnerabilities.list[]?.advisory.id),
(.warnings[]?[]?.advisory.id // empty) ]
| unique[]
')"; then
echo "::error::JSON-Ausgabe von cargo audit liess sich nicht lesen - Abgleich (Spec 0090 A4) konnte nicht laufen"
exit 1
fi
# spec-reviewer-Fund (Runde 1, zurückgestellt->jetzt mitbehoben):
# nur der `ignore = [ ... ]`-Block wird durchsucht, nicht die
# gesamte Datei - ein künftiger Kommentar, der eine ID in
# Anführungszeichen zitiert (z. B. beim Dokumentieren einer
# entfernten Ausnahme), würde sonst fälschlich als aktive
# Ausnahme gezählt.
stale=0
while IFS= read -r id; do
[ -z "$id" ] && continue
if ! grep -qx "$id" <<< "$found_ids"; then
echo "::error::Ausnahme $id in .cargo/audit.toml trifft auf keinen aktuellen Befund mehr (Spec 0090, A4) - entfernen"
stale=1
fi
done < <(sed -n '/^ignore = \[/,/^\]/p' .cargo/audit.toml | grep -oE '"RUSTSEC-[0-9]{4}-[0-9]+"' | tr -d '"' | sort -u)
exit "$stale"
# Browser tests (issue #166, ADR 0120): Playwright runs the real frontend
# in Chromium and WebKit, in two window sizes, against a fake backend that
# replaces the Tauri IPC layer. A separate job, so the names of the
# existing checks stay unchanged. Linux only: the layout checks use
# geometry, not screenshots, and `--with-deps` installs the system
# packages through apt.
browser-tests:
name: Browser Tests (Playwright)
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: apps/smart-ssh-community/frontend
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
cache-dependency-path: apps/smart-ssh-community/frontend/package-lock.json
- name: Install frontend dependencies
run: npm ci
# The browser builds depend only on the installed Playwright version,
# so that version is the cache key.
- name: Read Playwright version
id: playwright
run: echo "version=$(node -p "require('@playwright/test/package.json').version")" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ steps.playwright.outputs.version }}-chromium-webkit
- name: Install Playwright browsers (with system dependencies)
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install --with-deps chromium webkit
# The cache holds the browsers only, not the system packages.
- name: Install Playwright system dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: npx playwright install-deps chromium webkit
# With `CI` set, the config forbids `test.only`, starts its own dev
# server and writes the HTML report. Traces are kept for failed tests.
- name: Browser tests (Playwright)
run: npm run test:e2e
# Lists tests that passed only on a retry (summary, annotations, file).
# Never changes the job result; the test step above decides that.
- name: Report flaky browser tests
if: always()
run: node e2e/report-flaky.mjs
- name: Upload flaky browser test list
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: flaky-browser-tests
path: apps/smart-ssh-community/frontend/e2e/flaky-tests.txt
if-no-files-found: ignore
retention-days: 14
- name: Upload Playwright traces and report
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: playwright-traces
path: |
apps/smart-ssh-community/frontend/e2e/test-results/
apps/smart-ssh-community/frontend/e2e/playwright-report/
retention-days: 14
# Spec 0090 §2a (A14–A17): starts the real app through tauri-driver on a
# seeded data directory and checks the keychain, upgrade and password
# startup paths. Linux only; on pull requests only when a startup-relevant
# path changed.
real-app-changes:
name: Real app changes
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run: ${{ steps.filter.outputs.run }}
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Decide whether the real-app job runs
id: filter
env:
EVENT: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$EVENT" != "pull_request" ]; then
echo "run=true" >> "$GITHUB_OUTPUT"
exit 0
fi
git fetch --no-tags --depth=1 origin "$BASE_SHA" "$HEAD_SHA"
files="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")"
if echo "$files" | grep -E '^(crates/app-shell/|crates/app-logic/src/database_startup|crates/app-logic/src/master_password|crates/real-app-tests/|apps/smart-ssh-community/frontend/src/main\.tsx$|apps/smart-ssh-community/frontend/src/.*StartupGate\.tsx$|\.github/workflows/community\.yml$|\.github/actions/linux-deps/)'; then
echo "run=true" >> "$GITHUB_OUTPUT"
else
echo "run=false" >> "$GITHUB_OUTPUT"
fi
real-app-tests:
name: Real app startup (tauri-driver)
needs: real-app-changes
if: needs.real-app-changes.outputs.run == 'true'
runs-on: ubuntu-latest
# A slow apt mirror once used up the whole job budget during the
# package install; the install has its own step timeout, so this only
# has to leave room for the build and the tests.
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Read pinned Rust toolchain
id: rust-toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
with:
toolchain: ${{ steps.rust-toolchain.outputs.channel }}
- name: Print Rust version
run: rustc --version
- name: Cache cargo registry & build artifacts
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: 'npm'
cache-dependency-path: apps/smart-ssh-community/frontend/package-lock.json
- name: Install Linux dependencies
timeout-minutes: 22
uses: ./.github/actions/linux-deps
with:
extra-packages: webkit2gtk-driver gnome-keyring dbus-x11
- name: Install tauri-driver
run: cargo install tauri-driver --locked
# The frontend has to be embedded: a plain debug build loads the dev URL.
- name: Build frontend
working-directory: apps/smart-ssh-community/frontend
run: |
npm ci
npm run build
- name: Build app (frontend embedded)
run: cargo build --locked -p smart-ssh-community --features tauri/custom-protocol
- name: Real app startup tests
timeout-minutes: 15
env:
SMART_SSH_REAL_APP_LOG_DIR: ${{ runner.temp }}/real-app-logs
run: |
xvfb-run -a dbus-run-session -- bash -c '
echo -n "ci-keyring-pw" | gnome-keyring-daemon --unlock --components=secrets
cargo test --locked -p real-app-tests -- --ignored --test-threads=1'
- name: Upload app logs
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: real-app-logs
path: ${{ runner.temp }}/real-app-logs/
if-no-files-found: ignore
retention-days: 14
# Issue #681: builds the Android debug APK on every pull request so that
# changes breaking the Android build are caught before merge (Spec 0114,
# docs/android-build.md). Required for merge like `test`.
android-build:
name: Android debug APK
runs-on: ubuntu-latest
timeout-minutes: 45
env:
NDK_VERSION: 27.3.13750724
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Setup JDK 17
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: 17
- name: Setup Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: 'npm'
cache-dependency-path: apps/smart-ssh-community/frontend/package-lock.json
- name: Read pinned Rust toolchain
id: rust-toolchain
shell: bash
run: echo "channel=$(sed -n 's/^channel *= *"\(.*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
with:
toolchain: ${{ steps.rust-toolchain.outputs.channel }}
targets: aarch64-linux-android,x86_64-linux-android
- name: Print Rust version
run: rustc --version
- name: Cache cargo registry & build artifacts
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
key: android
- name: Cache Gradle
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('apps/smart-ssh-community/gen/android/**/*.gradle*', 'apps/smart-ssh-community/gen/android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-
- name: Install Android SDK platform and pinned NDK
run: |
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --licenses > /dev/null || true
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" \
"platforms;android-36" "build-tools;35.0.0" "ndk;${NDK_VERSION}"
echo "NDK_HOME=$ANDROID_HOME/ndk/${NDK_VERSION}" >> "$GITHUB_ENV"
echo "ANDROID_NDK_ROOT=$ANDROID_HOME/ndk/${NDK_VERSION}" >> "$GITHUB_ENV"
# Vendored OpenSSL (SQLCipher), whisper.cpp and bindgen need the
# cross toolchain spelled out (docs/android-build.md).
- name: Configure cross-compilation environment
run: |
TOOLCHAIN="$ANDROID_HOME/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64"
BIN="$TOOLCHAIN/bin"
SYSROOT="$TOOLCHAIN/sysroot"
for t in aarch64 x86_64; do
u="${t}_linux_android"
{
echo "CC_${u}=$BIN/${t}-linux-android26-clang"
echo "AR_${u}=$BIN/llvm-ar"
echo "RANLIB_${u}=$BIN/llvm-ranlib"
echo "BINDGEN_EXTRA_CLANG_ARGS_${u}=--sysroot=$SYSROOT"
} >> "$GITHUB_ENV"
done
- name: Install tauri-cli
run: cargo install tauri-cli --version '^2' --locked
- name: Install frontend dependencies
working-directory: apps/smart-ssh-community/frontend
run: npm ci
- name: Build Android debug APK
working-directory: apps/smart-ssh-community
# No `--locked`: `cargo tauri android build` does not forward it to Cargo.
run: cargo tauri android build --debug --apk --target aarch64 --target x86_64 --ci
# Android 15+ with 16 KB pages needs every .so aligned to 0x4000+.
- name: Check 16 KB page alignment of native libraries
run: |
READELF="$ANDROID_HOME/ndk/${NDK_VERSION}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf"
rm -rf "$RUNNER_TEMP/apk-check" && mkdir -p "$RUNNER_TEMP/apk-check"
fail=0; count=0
for apk in $(find apps/smart-ssh-community/gen/android/app/build/outputs/apk -name '*.apk'); do
d="$RUNNER_TEMP/apk-check/$(basename "$apk" .apk)"
mkdir -p "$d" && unzip -q -o "$apk" 'lib/*.so' -d "$d"
done
while IFS= read -r so; do
count=$((count+1))
while read -r align; do
if [ $((align)) -lt 16384 ]; then
echo "::error::$so has LOAD alignment $align (< 0x4000)"; fail=1
fi
done < <("$READELF" -lW "$so" | awk '$1=="LOAD"{print $NF}')
done < <(find "$RUNNER_TEMP/apk-check" -name '*.so')
echo "checked $count libraries"
[ "$count" -gt 0 ] || { echo "::error::no .so found in APK"; exit 1; }
exit $fail
- name: Upload APK
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: android-debug-apk
path: apps/smart-ssh-community/gen/android/app/build/outputs/apk/**/*.apk
if-no-files-found: error
retention-days: 7
# Issue #797: starts the debug APK from `android-build` in an x86_64
# emulator and fails when the app crashes at launch (docs/android-build.md).
android-launch-smoke:
name: Android launch smoke test
needs: android-build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Download APK
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: android-debug-apk
path: apk
- name: Enable 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
- name: Cache AVD
id: avd-cache
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: |
~/.android/avd/*
~/.android/adb*
key: avd-api34-x86_64-default-v1
- name: Create AVD snapshot for the cache
if: steps.avd-cache.outputs.cache-hit != 'true'
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
with:
api-level: 34
arch: x86_64
target: default
force-avd-creation: false
emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
disable-animations: false
script: echo "Generated AVD snapshot for caching."
- name: Launch app and check it stays up
uses: reactivecircus/android-emulator-runner@62dbb605bba737720e10b196cb4220d374026a6d # v2.33.0
with:
api-level: 34
arch: x86_64
target: default
force-avd-creation: false
emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
disable-animations: true
script: ./scripts/android-launch-smoke.sh apk "${{ github.workspace }}/app-logcat.txt"
- name: Upload logcat
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: android-launch-logcat
path: app-logcat.txt
if-no-files-found: warn
retention-days: 7