Repository navigation
500 lines (483 loc) · 22.5 KB
/
Copy pathci.yml
File metadata and controls
500 lines (483 loc) · 22.5 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
name: CI
on:
workflow_dispatch:
push:
branches: [main]
pull_request:
# The merge queue runs CI on the PR merged onto the current main BEFORE it
# lands, so a PR that is green alone but red together with what just
# merged is stopped in the queue instead of on main (that shape reddened
# main twice in Sep 2026). Turning the queue on is a branch-ruleset
# setting; this trigger is what lets CI report on those runs.
merge_group:
# least privilege: CI only reads the checkout, it never writes back
permissions:
contents: read
concurrency:
# New main/PR pushes supersede older checks on the same ref, including
# queued summary jobs. Keep merge-queue runs protected on their own refs.
group: ci-${{ github.event_name == 'merge_group' && github.event.merge_group.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' || github.event_name == 'push' }}
jobs:
# ── The test leg, in pieces ──────────────────────────────────────────────
# One `typecheck + test (<os>)` job used to run everything in sequence:
# typecheck, lint, the whole vitest suite, then three smokes that only ran
# when everything before them passed. The suite runs its files serially
# (vite.config.ts, fileParallelism:false) and takes 17–19 minutes on one
# runner, so slow runners hit the job cap and read as red PRs, and a broken
# smoke stayed hidden behind any vitest failure for a day. Each piece is now
# its own job: platform-independent checks run once, the suite runs in three
# shards per platform, and every smoke reports on its own. The `gate` jobs
# at the end keep the three check names the branch ruleset requires.
static:
name: typecheck + lint
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Validate locale catalogs
run: pnpm i18n:check
- run: pnpm typecheck
- run: pnpm lint
- run: pnpm check:electron
- name: production UI build
run: pnpm exec vite build
vitest:
# Admit the expensive runner fan-out only after the quick checks pass.
# A failing preflight still fails the required gates; nothing is waived.
needs: static
# vitest splits the file list deterministically across shards, so a
# failure names the same shard on every run and one shard is reproducible
# locally with the same --shard flag.
name: vitest (${{ matrix.os }}, shard ${{ matrix.shard }}/4)
strategy:
fail-fast: false
matrix:
# The harness and pure desktop capabilities stay portable across all
# supported host platforms. POSIX-only process tests self-skip.
os: [macos-latest, ubuntu-latest, windows-latest]
shard: [1, 2, 3, 4]
runs-on: ${{ matrix.os }}
# vitest splits by file count, not by duration, so the shard holding the
# heavy e2e files ran 13 minutes on macOS/Ubuntu and 19 on Windows with
# three shards; four keeps the heaviest near 10 and 15. The cap catches a
# hang, not normal growth: raise it with the suite, or add a shard.
timeout-minutes: ${{ matrix.os == 'windows-latest' && 25 || 20 }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Prepare Electron test runtime
# Electron lazily downloads its runtime on first import. Resolve it
# before Vitest starts so a cold download cannot consume the fixture
# timeout.
run: node --input-type=commonjs -e "require('electron')"
- name: Run tests
# The github-actions reporter turns each failure into an annotation
# on the PR, so the failing test and its assertion are readable
# without opening the job log.
run: pnpm exec vitest run --shard=${{ matrix.shard }}/4 --reporter=default --reporter=github-actions
- name: Broker and Electron unit tests
if: matrix.shard == 1
run: pnpm broker:test && pnpm test:electron
behavior-evals:
needs: static
# The offline behavior-eval tiers (#1503): the evals' own vitest gate
# (redaction, trace, golden replay), the tier-1 scenario runner, and the
# tier-2 golden replay through the same entrypoint developers use. Tier
# 3's live models stay opt-in: CI never sets OMB_EVAL_LIVE or any live
# env, and the last step proves the offline gate holds by asserting the
# skip line is what CI gets.
name: behavior evals (offline tiers 1-3)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Eval suite typecheck (standalone tsconfig)
# The root typecheck builds src and server only; evals carries its
# own tsconfig, so its type gate runs here with the rest of the
# eval surfaces.
run: pnpm exec tsc -p evals/tsconfig.json
- name: Eval vitest gate (redaction, trace, golden replay)
# Same reporter pair as the vitest shards, so a failure annotates
# the PR with the failing test and assertion.
run: pnpm exec vitest run --config evals/vitest.config.ts --reporter=default --reporter=github-actions
- name: Tier 1 scenarios (scripted harness, no models)
run: pnpm eval
- name: Tier 2 golden replay against committed baselines
run: pnpm eval --golden
- name: Tier 3 stays offline in CI (the skip is the pass condition)
run: |
output="$(pnpm eval --live 2>&1)"
printf '%s\n' "$output"
printf '%s\n' "$output" | grep -q "live tier skipped"
- name: Upload eval run reports on failure
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: behavior-evals-reports-${{ github.sha }}-${{ github.run_attempt }}
path: evals/reports/runs/
if-no-files-found: warn
retention-days: 7
packaged-server:
needs: static
# `pnpm build:server`, then the bundle is copied out of the repo and run
# with no node_modules in reach: every bare import must be inlined.
name: packaged server smoke (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:packaged-server
windows-cua:
needs: static
name: Windows CUA host smoke
runs-on: windows-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Smoke the staged Windows CUA host (no computer actions)
timeout-minutes: 5
run: pnpm build:cua:win && pnpm smoke:cua-win
electron-smokes:
needs: static
# Real Electron utility processes against disposable homes and fake CLIs.
# Never the live app. Each script prints its own evidence lines.
name: Electron smokes (macOS)
runs-on: macos-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Prepare Electron test runtime
run: node --input-type=commonjs -e "require('electron')"
- name: Verify native approval mode transitions
run: pnpm exec electron scripts/smoke-approval-modes.cjs
- name: Verify app permissions and guarded display capture
run: node scripts/smoke-app-permissions.mjs
- name: Verify paired phone authorization against packaged harness
# Forks dist-server/index.js: the bundle the packaged-server job
# builds in ITS checkout. This job builds its own copy.
run: pnpm build:server && pnpm build:companion && pnpm exec electron scripts/smoke-phone-relay-auth.cjs
gate:
# The branch ruleset requires exactly these three check names — they were
# the per-OS test job. Each now reports the combined result of every job
# above, so the requirement holds without a settings change. Once the
# ruleset requires this gate directly, collapse the matrix to one job.
name: typecheck + test (${{ matrix.os }})
needs: [static, vitest, behavior-evals, packaged-server, windows-cua, electron-smokes]
# Still report failed/skipped dependencies, but don't keep a superseded
# workflow alive just to schedule its summary jobs.
if: ${{ !cancelled() }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest, windows-latest]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Every required job passed
env:
NEEDS: ${{ toJSON(needs) }}
run: |
node --input-type=module -e '
const needs = JSON.parse(process.env.NEEDS);
const rows = Object.entries(needs).map(([job, { result }]) => `${result.padEnd(9)} ${job}`);
console.log(rows.join("\n"));
const bad = Object.entries(needs).filter(([, { result }]) => result !== "success");
if (bad.length) { console.error(`\n${bad.length} required job(s) did not pass: ${bad.map(([job]) => job).join(", ")}`); process.exit(1); }
'
cloudflared-linux-arm64:
needs: static
name: cloudflared installer (Linux ARM64)
runs-on: ubuntu-24.04-arm
timeout-minutes: 5
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
- name: Install and reuse the pinned connector in an isolated fixture
env:
OMB_ARM64_FIXTURE: ${{ runner.temp }}/omb-cloudflared-arm64
run: |
node --input-type=module -e 'import assert from "node:assert/strict"; assert.equal(process.platform, "linux"); assert.equal(process.arch, "arm64");'
node scripts/prepare-cloudflared.mjs --current --root "$OMB_ARM64_FIXTURE"
node scripts/prepare-cloudflared.mjs --current --root "$OMB_ARM64_FIXTURE"
"$OMB_ARM64_FIXTURE/dist-native/cloudflared/linux-arm64/cloudflared" version
foss:
needs: static
# LICENSING.md promises that deleting enterprise/ leaves a working
# open-source edition. Prove it on every change: typecheck, the edition
# tests, and a boot that reports "oss" even with a key set.
name: open-source edition builds without enterprise/
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Delete the enterprise folder
run: rm -rf enterprise
- run: pnpm typecheck
- run: pnpm exec vitest run server/enterprise.test.ts server/brand.test.ts
- name: The server boots and reports the open-source edition
run: |
mkdir -p /tmp/omb-foss/home
HOME=/tmp/omb-foss/home OMB_DATA_DIR=/tmp/omb-foss/data OMB_PORT=18797 OMB_WEBHOOK_PORT=18796 \
OMB_BROWSER_CONNECTION=/tmp/omb-foss/browser-connection.json OMB_LICENSE_KEY=omb1.not.real \
node --experimental-strip-types server/index.ts > /tmp/omb-foss/server.log 2>&1 &
for i in $(seq 1 60); do curl -sf http://127.0.0.1:18797/api/health >/dev/null && break; sleep 1; done
curl -sf http://127.0.0.1:18797/api/edition | tee /tmp/omb-foss/edition.json; echo
grep -q '"edition":"oss"' /tmp/omb-foss/edition.json
grep -q 'no enterprise layer exists' /tmp/omb-foss/edition.json
kill %1; wait %1 || true
control-plane:
needs: static
name: control-plane check + workerd tests + dry run
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm control-plane:check
- run: pnpm control-plane:test
- run: pnpm control-plane:dry-run
ui-smoke:
needs: static
# Advisory, outside the required `test` matrix: the renderer recipe in
# docs/verification/chat-ui.md run by a machine. It downloads the pinned
# agent-browser release (digest verified) and its Chrome, drives the real
# <App/> against the isolated fake-engine fixture, and keeps the screenshot.
name: renderer smoke (control-omb ui, Ubuntu 24.04)
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Let Chrome build its sandbox on this runner
# Ubuntu 24.04's AppArmor profile blocks unprivileged user namespaces,
# so the downloaded Chrome for Testing dies with "No usable sandbox".
# This is Chrome's own documented fix for 23.10+; a developer machine
# needs nothing, and the sandbox itself stays on.
run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
- name: Drive the real renderer through control-omb ui
env:
OMB_UI_E2E: "1"
OMB_UI_EVIDENCE_DIR: .omb-scratch/verify-evidence
run: pnpm exec vitest run scripts/testing/control-omb-ui.e2e.test.ts scripts/testing/bot-tools-ui.e2e.test.ts scripts/testing/thinking-timer-ui.e2e.test.ts scripts/testing/team-share-ui.e2e.test.ts scripts/testing/org-library-ui.e2e.test.ts scripts/testing/presets-ui.e2e.test.ts
- name: Upload the renderer screenshot
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: control-omb-ui-chat-screenshot
path: |
.omb-scratch/verify-evidence/chat-ui.png
.omb-scratch/verify-evidence/thinking-timer-anchored.png
.omb-scratch/verify-evidence/share-team-dialog.png
if-no-files-found: warn
retention-days: 7
package-linux:
needs: static
name: package + smoke (Ubuntu 24.04 x64)
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: pnpm/action-setup@ff378ebe6b225b0680b81c1ad4498ae0d1d3a5e3 # v6.0.10
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
cache: pnpm
- name: Install package validation and native smoke tools
run: >-
sudo apt-get update && sudo apt-get install -y
at-spi2-core dbus-x11 desktop-file-utils libxi6 libxkbcommon0 squashfs-tools x11-utils xdotool xvfb
- run: pnpm install --frozen-lockfile
- name: Stage the pinned CUA runtime
run: pnpm build:cua:linux
- name: Prove overlay-free X11 input routing
run: dbus-run-session -- xvfb-run -a pnpm smoke:cua-x11-input
- name: Package from the verified offline CUA stage
run: pnpm package:linux:offline
- run: node scripts/verify-linux-package.mjs
- name: Match a normal Ubuntu package parent on the ephemeral runner
run: |
test ! -L /opt
test "$(stat -c '%F %U:%G' /opt)" = "directory root:root"
case "$(stat -c '%a' /opt)" in
755) ;;
775|777) sudo chmod 0755 /opt ;;
*) echo "Unexpected /opt mode: $(stat -c '%a' /opt)" >&2; exit 1 ;;
esac
test "$(stat -c '%U:%G %a' /opt)" = "root:root 755"
- name: Reproduce and verify an in-place DEB upgrade
run: |
deb=(release/*.deb)
test "${#deb[@]}" -eq 1
sudo --preserve-env=CI,RUNNER_TEMP node scripts/smoke-deb-upgrade.mjs "${deb[0]}"
- name: Configure Chromium sandbox for the unpacked app
run: |
sudo chown root:root release/linux-unpacked/chrome-sandbox
sudo chmod 4755 release/linux-unpacked/chrome-sandbox
test "$(stat -c '%U:%G %a' release/linux-unpacked/chrome-sandbox)" = "root:root 4755"
- name: Verify the installed browser with Ubuntu sandbox restrictions
run: node scripts/smoke-browser-bundle.mjs --resources /opt/OpenMausBot/resources
- name: Launch packaged app and verify lifecycle
env:
OMB_KEEP_SMOKE_DIR: "1"
OMB_SMOKE_INSTALLED_DEB: "1"
run: pnpm smoke:linux-package
- name: Remove the installed upgrade fixture
if: always()
run: sudo dpkg --purge openmausbot || true
- name: Upload smoke diagnostics on failure
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: openmausbot-ubuntu-smoke-diagnostics
path: |
/tmp/omb-linux-smoke-*
/tmp/omb-linux-smoke-runtime-*
if-no-files-found: warn
include-hidden-files: true
retention-days: 7
ios:
needs: static
name: Swift tests + iOS build + thread UI
runs-on: macos-latest
timeout-minutes: 35
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Swift package tests
run: swift test --package-path ios
- name: Generate the Xcode project
run: |
brew install xcodegen
cd ios
xcodegen generate
- name: Build the simulator app
run: |
cd ios
xcodebuild \
-project OpenMausCompanion.xcodeproj \
-scheme OpenMausCompanion \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
CODE_SIGNING_ALLOWED=NO \
build
- name: Verify thread navigation on disposable iPhone and iPad simulators
run: bash scripts/verify-ios-thread-navigation-ci.sh
- name: Keep native thread test results and screenshots
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-thread-ui-${{ github.sha }}-${{ github.run_attempt }}
path: ${{ runner.temp }}/omb-ios-threads-*.xcresult
if-no-files-found: warn
include-hidden-files: true
retention-days: 7
# Deliberately not scoped to android/**: :core's test fixtures are read from
# ios/Tests/CompanionCoreTests/Fixtures, so a path filter would skip the
# Kotlin suite on exactly the change that breaks it.
android:
needs: static
name: Kotlin tests + Android build
runs-on: ubuntu-24.04
timeout-minutes: 25
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-java@dd06d9cba3e5552c54d9f8ea23572deb30010f7c # v6.0.0
with:
distribution: temurin
java-version: 17
cache: gradle
# Robolectric fetches android-all-instrumented from Maven Central at
# *test* time into ~/.m2 — 278 MB per clean machine that no Gradle
# cache reaches. This replaces the action's default path, so
# ~/.gradle/caches has to be named alongside it.
cache-path: |
~/.gradle/caches
~/.m2/repository/org/robolectric
- name: Kotlin tests and debug / preview APKs
run: |
cd android
./gradlew --no-daemon :core:test :app:testDebugUnitTest :app:assembleDebug :app:assemblePreview
- name: Upload the installable Android preview
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: android-threads-preview-${{ github.sha }}-${{ github.run_attempt }}
path: android/app/build/outputs/apk/preview/app-preview.apk
if-no-files-found: error
retention-days: 14
- name: Upload the Android test reports on failure
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: android-test-reports
path: |
android/core/build/reports/tests
android/core/build/test-results
android/app/build/reports/tests
android/app/build/test-results
if-no-files-found: warn
retention-days: 7