Repository navigation
670 lines (600 loc) · 27.4 KB
/
Copy pathanalysis.yml
File metadata and controls
670 lines (600 loc) · 27.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: flutter analysis
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
# The full native builds (iOS Linux engine, macOS, Windows) run only from
# here. Each is a platform runner for 10-45 minutes, and `check` already
# compiles and tests every Dart change on a pull request.
workflow_dispatch:
# A new push to a pull request makes the run for the previous one moot.
# Only pull requests: every push to main is a state a release can be cut from,
# and a manual run was asked for.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
# For the `changes` job's file list. Read-only, and only for this repo.
pull-requests: read
jobs:
# Which of the jobs this diff can actually affect.
#
# `Rust tests (windows-latest)` is the one that costs real time (~7-9 min,
# most of it a cold compile); a docs-only or Dart-only PR paid it for
# nothing. The native app builds are not filtered here: they run only on
# `workflow_dispatch`.
#
# Asks GitHub for the file list rather than diffing refs: no checkout, no
# fetch depth to get wrong, and no merge-base to compute against a base
# branch that may have moved since. A pull request has one of its own.
#
# Anything else — a push to main, a manual run — answers "everything": main is
# the ref releases are cut from, and narrowing what runs there trades away the
# wrong thing.
changes:
name: What changed
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
rust: ${{ steps.filter.outputs.rust }}
dart: ${{ steps.filter.outputs.dart }}
frontend: ${{ steps.filter.outputs.frontend }}
docs: ${{ steps.filter.outputs.docs }}
website: ${{ steps.filter.outputs.website }}
android: ${{ steps.filter.outputs.android }}
env:
GH_TOKEN: ${{ github.token }}
steps:
- id: filter
shell: bash
run: |
set -euo pipefail
# Which filters are answered from the diff: all of them on a pull
# request, none otherwise.
strict=' rust dart frontend docs website android '
if [[ "${{ github.event_name }}" == 'pull_request' ]]; then
files="$(gh api --paginate \
"repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files" \
--jq '.[].filename')"
else
strict=''
files=''
echo "not a pull request: running everything"
fi
echo "changed files:"
echo "$files"
# emit <filter> <pattern> [exclude]: files matching `exclude` are
# left out before `pattern` is asked.
emit() {
if [[ "$strict" != *" $1 "* ]]; then
echo "$1=true" >> "$GITHUB_OUTPUT"
return
fi
local covered="$files"
if [[ -n "${3:-}" ]]; then
covered="$(grep -vE "$3" <<< "$files" || true)"
fi
if grep -qE "$2" <<< "$covered"; then
echo "$1=true" >> "$GITHUB_OUTPUT"
else
echo "$1=false" >> "$GITHUB_OUTPUT"
echo "::notice::skipping the $1 jobs: nothing they cover changed"
fi
}
# Patterns are grep -E against whole paths. A change to this workflow
# turns everything on — the filters are the thing under test then —
# and `packages/` are the submodules the app is built from, so a
# gitlink move there is a source change to all of it.
#
# `.github/actions/` and `build.yml` are here for a reason of the same
# kind and were missed: `setup-flutter` is the composite action every
# job below starts with, so a broken SDK pin in it would have been
# merged after only the always-on reproducibility job, and `build.yml`
# does not run on a pull request at all — so its own changes were
# reaching main with nothing having compiled anything.
shared='^\.github/(workflows/(analysis|build)\.yml$|actions/)|^pubspec\.(yaml|lock)$|^packages/'
# Not the panel and its SDK under `monitor/`: the agent reads only the
# panel's build output, at run time, and the tests build none of it.
emit rust "$shared|^crates/|^monitor/|^Cargo\.(toml|lock)$" '^monitor/(frontend|sdk)/'
# `crates/` because `flutter test` loads the FFI dylib and the
# generated bindings come from there.
#
# `macos/` because `test/platform/macos_entitlements_test.dart` is a plain
# test and runs here — the `macOS build` job below says exactly that
# and then relies on it.
#
# `docs/schemas/` because `test/unit/theme_schema_test.dart` reads the
# manifest schema and holds it against the parser; a schema-only
# change is exactly what that test is for.
#
# `store/` because `test/unit/theme_store_tree_test.dart` reads the
# official theme repository the way the store does.
#
# The rest are files a plain test reads straight from the checkout,
# so a change to any of them alone is what that test guards:
# `assets/` (rootfs manifest, geo, distro icons, yabs),
# `docs/examples/` (`theme_app_test.dart`), the doc pages and the
# bundled `serverbox-help` skill whose links `builtin_skill_test.dart`
# holds against each other, the iOS plists and privacy manifests
# (`ios_multitasking_test.dart`, `privacy_manifest_test.dart`), the
# Android backup policy (`android_backup_policy_test.dart`) and the
# DMG release script (`macos_entitlements_test.dart`).
emit dart "$shared|^lib/|^test/|^integration_test/|^crates/|^macos/|^Cargo\.(toml|lock)$|^analysis_options\.yaml$|^docs/(schemas|examples|src/content/docs)/|^store/|^assets/|^\.claude/skills/serverbox-help/|^ios/[^/]+/(Info-[A-Za-z]+\.plist|PrivacyInfo\.xcprivacy)$|^ios/Runner\.xcodeproj/project\.pbxproj$|^android/app/src/main/(AndroidManifest\.xml|res/xml/)|^scripts/release/release-macos-dmg\.sh$"
emit frontend "$shared|^monitor/frontend/|^packages/webui/"
# `store/` (and fl_lib, under `$shared`) for the theme manifests the
# docs job validates against that same schema; `assets/catalog/` for
# the catalog the docs describe.
emit docs "$shared|^docs/|^assets/catalog/|^store/"
# `store/` because the website build packs it into the store's
# tarball (scripts/store-tarball.sh).
emit website "$shared|^website/|^packages/webui/|^store/|^scripts/store-tarball\.sh$"
emit android "$shared|^android/"
reproducibilityCheck:
# These are deterministic-input guards, not a reproducibility proof. The
# two independent Android builds are compared in android-reproducible.yml.
name: Build input checks
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
- uses: ./.github/actions/setup-flutter
- name: Check Android split-build reproducibility
shell: bash
run: |
grep_status=0
git grep -n -F 'BuildConfig.VERSION_CODE' -- android/app/src/main || grep_status=$?
if [[ "$grep_status" -eq 0 ]]; then
echo "::error::BuildConfig.VERSION_CODE is ABI-specific for split APKs and must not be compiled into shared Android bytecode."
exit 1
elif [[ "$grep_status" -ne 1 ]]; then
echo "::error::git grep failed with status ${grep_status}."
exit "$grep_status"
fi
- name: Check locked dependencies
run: |
flutter pub get --enforce-lockfile
git diff --exit-code -- pubspec.yaml pubspec.lock
- name: Check JNI build-id patch
run: scripts/release/patch-jni-build-id.sh
rustTests:
name: Rust tests (${{ matrix.os }})
needs: changes
if: needs.changes.outputs.rust == 'true'
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 30
env:
# File and line in a backtrace, without the full debuginfo: that is most
# of what the linker writes, and linking the test binaries — over forty
# for the monitor alone — was most of this job on Windows.
CARGO_PROFILE_DEV_DEBUG: line-tables-only
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
# The toolchain the app's build hook is pinned to, rather than the
# runner's `stable`: that one moves on its own, and a lint added in a new
# release failed pull requests that changed nothing it looks at.
# Hosted Windows runners check out with `core.autocrlf=true`, so the CR
# is stripped before the `$`-anchored match; an empty result fails here
# instead of as a confusing `rustup` error.
- name: Install the pinned Rust toolchain
shell: bash
run: |
v="$(tr -d '\r' < crates/sbm_ffi/rust-toolchain.toml | sed -n 's/^channel = "\(.*\)"$/\1/p')"
test -n "$v" || { echo "no channel in crates/sbm_ffi/rust-toolchain.toml" >&2; exit 1; }
rustup toolchain install "$v" --profile minimal -c clippy --no-self-update
echo "RUSTUP_TOOLCHAIN=$v" >> "$GITHUB_ENV"
# This job's own `cargo` builds into `target/`, so rust-cache applies
# here, unlike to the build hook (see `build-hook-cache`). After the
# toolchain step: the key includes the rustc it finds. Saved from main
# only, so pull requests restore main's and add nothing to the 10 GB.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Test Rust workspace
if: matrix.os == 'ubuntu-latest'
run: cargo test --workspace --no-fail-fast
# Only the crates with code of their own for Windows: the native sampler,
# the agent (ConPTY, paths, services) and the script's PowerShell side.
# The rest is platform-neutral and tested above; that `sbm_ffi` builds
# for Windows is the manual `Windows build` job's to show.
- name: Test Windows-specific crates
if: matrix.os == 'windows-latest'
run: cargo test -p sbm_parser -p sbm_native -p server_box_monitor --no-fail-fast
- name: Clippy
if: matrix.os == 'ubuntu-latest'
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
monitorFrontend:
name: Monitor frontend
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
persist-credentials: false
submodules: recursive
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
cache: npm
cache-dependency-path: monitor/frontend/package-lock.json
# packages/webui is a `file:` dependency, so the install above only
# symlinks it and its own dependencies are never fetched. `npm run check`
# is what needs them here — svelte-check reads that package's source, and
# without them reports every import in it as a missing module.
- name: Install dependencies
working-directory: monitor/frontend
run: |
npm ci
npm ci --prefix ../../packages/webui
- name: Test, lint, check and build
working-directory: monitor/frontend
# `--ignore-scripts` skips the `prebuild` hook, which installs
# packages/webui — the step above has already done exactly that, and
# this is the one place where both run. Nowhere else may drop the hook:
# Cloudflare Pages and a local `npm run build` have no step above them,
# and the hook is the only thing that installs it there.
run: |
npm exec vitest -- run
npm run lint
npm run check
npm run build --ignore-scripts
# Installed with bun and built with node, which is how both of these are
# written: the lockfile committed beside them is `bun.lock`, and the build
# script runs `astro`/`vite` under node.
#
# `npm install` was reading neither. It ignores a bun lockfile entirely and
# resolves the semver ranges afresh on every run, so what CI built was
# whatever npm had published that hour — and on 2026-09-03 that was astro
# 7.3.0, whose own `dist/assets/vite-plugin-assets.js` imports
# `astro/_internal/logger`, a subpath its `package.json` does not export.
# Every docs build failed, on branches that had not touched docs in weeks.
# A committed lockfile CI does not read is not a lockfile.
docs:
name: Documentation site
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
# npm, and a `package-lock.json`, because Dependabot maintains one and
# does not maintain a `bun.lock`: it bumped `@astrojs/starlight` in
# docs/package.json alone, and `bun install --frozen-lockfile` refused
# the tree it left behind. Every Dependabot PR against docs/ or website/
# failed that way and no version of it could pass — the update the PR
# exists for is the one thing the bot cannot write. The other three npm
# directories here have always had both files updated together.
- name: Install and build
working-directory: docs
run: |
npm ci
npm run build
# Held against the schema that is meant to describe them. Nothing loads
# that file at runtime — it exists for editors — so a field renamed in the
# parser and not in the schema would otherwise be found by an author, in
# an editor, and not by anything here. The one rule the schema cannot
# state (a color whose key has no image) is a Dart test's, on the other
# side of this same file.
# The themes built into the app are fl_lib's, and only that submodule is
# needed for them.
- name: Check out fl_lib
run: git submodule update --init --depth 1 packages/fl_lib
- name: Check the theme manifests against the schema
run: |
# `store/` may hold no theme; an unmatched glob is then dropped
# rather than passed on as a file name.
shopt -s nullglob
pipx run --spec check-jsonschema==0.38.2 check-jsonschema \
--schemafile docs/schemas/fsbt-manifest.schema.json \
docs/examples/aurora/manifest.toml \
packages/fl_lib/assets/themes/*/manifest.toml \
store/themes/*/manifest.toml
website:
name: Project website
needs: changes
if: needs.changes.outputs.website == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
persist-credentials: false
submodules: recursive
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24
# Same as the docs above, npm for the same reason. This one had not broken
# yet, which was luck rather than a difference: its next Dependabot PR
# would have.
- name: Install and build
working-directory: website
run: |
npm ci
npm run build
# The engine built from source, which nothing else in CI does: `Build ios` in
# build.yml links the libraries `scripts/ensure-ish-libs.sh` fetches from the
# release built for this revision, so a source change that breaks the build
# would reach a phone before it reached anyone's attention.
#
# Both settings, because each one has already failed in a way the other could
# not show: with the switch on, the linker once dead-stripped the whole engine
# and produced a 58 KB binary; with it off, the documented check for a clean
# strip turned out to pass on a binary that still had the engine in it.
ishEngine:
name: iOS Linux engine
if: github.event_name == 'workflow_dispatch'
runs-on: macos-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
- uses: ./.github/actions/setup-flutter
# meson and ninja build the engine; llvm because the guest VDSO targets
# Linux, which Apple's clang cannot; lld because Homebrew ships it apart
# from llvm and that is the linker the VDSO asks for by name; libarchive
# because fakefsify links it.
- name: Install the engine's build tools
run: brew install meson ninja llvm lld libarchive
- name: Install dependencies
run: flutter pub get
- name: Build the engine
run: scripts/build-ish-ios.sh device
# On by default (ios/Flutter/Ish.xcconfig); the libraries built above are
# stamped with this revision, so the build phase keeps them rather than
# fetching the release.
- name: Build the app with the engine linked
run: flutter build ios --release --no-codesign
- name: The engine is in the binary
run: scripts/check-ish-linkage.sh build/ios/iphoneos/Runner.app/Runner on
# In the untracked local override, which is where a developer turns it
# off too.
- name: And the switch takes it out again
shell: bash
run: |
echo 'SBM_ISH = 0' > ios/Flutter/IshLocal.xcconfig
flutter build ios --release --no-codesign
scripts/check-ish-linkage.sh build/ios/iphoneos/Runner.app/Runner off
# That the unsandboxed macOS product still builds, entitlements and all.
#
# It was written to run `integration_test` on a macOS device — the questions
# only a running app can answer: where this build's data goes, whether the
# keychain hands over the box key, whether `defaults` reads the other build's
# preferences. It never once passed. A hosted runner has no session for the
# app's output to be read from, so every file fails at load with "The log
# reader stopped unexpectedly, or never started", and the launch itself hits
# flutter/flutter#176850 ("Failed to foreground app; open returned 1"), which
# is open and reproduces on ordinary desktops too.
#
# So this builds instead of pretending to test. The entitlements the products
# ship with are guarded by `test/platform/macos_entitlements_test.dart`, which is a
# plain test and runs in `check` below; what is left for a macOS runner is
# whether the project and the Rust static library still link. The
# running-app half needs a machine, and has nowhere in CI to run.
macosBuild:
name: macOS build
if: github.event_name == 'workflow_dispatch'
runs-on: macos-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
- uses: ./.github/actions/setup-flutter
- uses: ./.github/actions/build-hook-cache
with:
step: restore
- name: Install dependencies
run: flutter pub get
# No signing identity here, and none needed: the app is signed ad-hoc so
# it can run at all. The entitlements go with it — trimmed to the ones
# that do not need a provisioning profile, keeping the only one these
# tests are about.
- name: Sign locally
run: |
set -euo pipefail
project=macos/Runner.xcodeproj/project.pbxproj
sed -i '' \
-e 's/CODE_SIGN_IDENTITY = "Apple Development";/CODE_SIGN_IDENTITY = "-";/' \
-e 's/CODE_SIGN_STYLE = Automatic;/CODE_SIGN_STYLE = Manual;/' \
-e 's/DEVELOPMENT_TEAM = BA88US33G6;/DEVELOPMENT_TEAM = "";/' \
"$project"
cat > macos/Runner/DebugProfile.entitlements <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<false/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>
EOF
# `test/platform/macos_entitlements_test.dart` is what guards the entitlements the
# products actually ship with; it runs in `check`, on a checkout the step
# above has not rewritten.
- name: Build the unsandboxed app
run: flutter build macos --debug
- uses: ./.github/actions/build-hook-cache
with:
step: save
windowsBuild:
name: Windows build
if: github.event_name == 'workflow_dispatch'
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
- name: Install Flutter
uses: ./.github/actions/setup-flutter
- uses: ./.github/actions/build-hook-cache
with:
step: restore
- name: Install dependencies
run: flutter pub get --enforce-lockfile
- name: Build the Windows app
run: flutter build windows --debug --no-pub
- uses: ./.github/actions/build-hook-cache
with:
step: save
# The Kotlin side's JVM unit tests (`android/app/src/test`). No device and
# no Rust: they need the Kotlin sources compiled and nothing else.
androidUnitTests:
name: Android unit tests
needs: changes
if: needs.changes.outputs.android == 'true'
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
- uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1
with:
distribution: "zulu"
java-version: "21"
- uses: ./.github/actions/setup-flutter
# `gradlew` is not checked in (`android/.gitignore`): Flutter writes it,
# and `local.properties` with the SDK's location, on its first build.
# `--config-only` does that and nothing else.
- name: Generate the Gradle project
run: flutter build apk --config-only
# Without `compileFlutterBuildDebug`, which the Flutter Gradle plugin
# hangs in front of every variant: it is a whole `flutter assemble` —
# Dart and the Rust library for each ABI — and was twelve of the job's
# thirteen minutes, for tests that load neither.
- name: Run unit tests
working-directory: android
run: ./gradlew :app:testDebugUnitTest -x :app:compileFlutterBuildDebug
check:
needs: changes
if: needs.changes.outputs.dart == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
- uses: ./.github/actions/setup-flutter
- name: Install dependencies
run: flutter pub get
# Consider passing '--fatal-infos' for slightly stricter analysis.
# `integration_test` too: nothing else in CI reads that directory — the
# suites in it need a device — so without this a change there is unseen
# until somebody runs it by hand on one.
- name: Analyze project source
run: flutter analyze lib test integration_test
# Sharded because `flutter test` compiles test files one at a time — its
# `TestCompiler` is a queue, and concurrency only applies to running them. On
# one runner that was ~6.5 of the 10 minutes, for ~300 files whose tests took
# under 5 minutes of CPU between them.
#
# By file, and not with `--total-shards`: that slices the tests *inside*
# every suite (test_core's `_shardSuite`), so each shard still loads and
# compiles all of them. Tried first; every shard took as long as the whole
# run had.
tests:
name: tests (${{ matrix.shard }}/3)
needs: changes
if: needs.changes.outputs.dart == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
lfs: true
fetch-depth: 1
submodules: recursive
persist-credentials: false
- uses: ./.github/actions/setup-flutter
- name: Install dependencies
run: flutter pub get
# The `cargo build` below, not the build hook. One shard saves; all
# three build the same thing.
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
shared-key: flutter-tests-ffi
save-if: ${{ github.ref == 'refs/heads/main' && matrix.shard == 0 }}
# FFI-dependent suites (frb_parser/windows/server_status_update_req...)
# load the dylib from target/debug via test/helpers/rust_lib_helper.dart
- name: Build native FFI library
run: cargo build -p sbm_ffi
# `flutter test` builds sbm_ffi again through the build hook, for a
# library no unit test loads (they take the one built above).
- uses: ./.github/actions/build-hook-cache
with:
step: restore
# Every third file, from a sorted list, so the split is stable and a new
# test file lands in exactly one shard. `git ls-files` rather than a
# directory walk: only what is checked in, the same set `flutter test`
# finds on its own, and a pathspec `*` crosses directories.
- name: Run tests
shell: bash
run: |
set -euo pipefail
mapfile -t files < <(git ls-files -- 'test/*_test.dart' | sort \
| awk -v n=3 -v i=${{ matrix.shard }} '(NR - 1) % n == i')
echo "${#files[@]} test files in this shard"
flutter test "${files[@]}"
# From one shard: all three build the same thing.
- if: matrix.shard == 0
uses: ./.github/actions/build-hook-cache
with:
step: save