forked from tetherto/qvac
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathintegration-mobile-test-qvac-lib-infer-llamacpp-llm.yml
More file actions
1636 lines (1426 loc) · 79.5 KB
/
Copy pathintegration-mobile-test-qvac-lib-infer-llamacpp-llm.yml
File metadata and controls
1636 lines (1426 loc) · 79.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
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Mobile Integration Tests (LLM)
on:
workflow_call:
inputs:
ref:
description: "Git ref to checkout"
type: string
required: false
repository:
description: "Repository to checkout"
type: string
required: false
qvac_perf_runs:
description: "Override QVAC_PERF_RUNS (number of counted iterations per perf test). Empty = test default."
type: string
required: false
default: ""
qvac_perf_warmup_runs:
description: "Override QVAC_PERF_WARMUP_RUNS (number of warmup iterations per perf test). Empty = test default."
type: string
required: false
default: ""
workflow_dispatch:
inputs:
ref:
description: "Git ref (branch/tag/SHA) to test"
type: string
required: false
default: main
package:
description: "Full NPM package spec to test (default: @qvac/llm-llamacpp@latest)"
type: string
required: true
default: "@qvac/llm-llamacpp@latest"
qvac_perf_runs:
description: "Override QVAC_PERF_RUNS (number of counted iterations per perf test). Empty = test default."
type: string
required: false
default: ""
qvac_perf_warmup_runs:
description: "Override QVAC_PERF_WARMUP_RUNS (number of warmup iterations per perf test). Empty = test default."
type: string
required: false
default: ""
env:
NODE_VERSION: "lts/*"
ADDON_NAME: "@qvac/llm-llamacpp"
PREBUILD_ARTIFACT_PREFIX: "llama-cpp-" # Prefix for prebuild artifacts (empty string for generic patterns)
TEST_FRAMEWORK_REF: "main" # Branch/tag of qvac-test-addon-mobile framework
APP_BUNDLE_ID: "io.tether.test.qvac" # Bundle ID for the test app (same for all addons)
ADDON_WORKDIR: "addon/packages/qvac-lib-infer-llamacpp-llm"
jobs:
build-and-test:
name: Build ${{ matrix.platform }} and Run E2E Tests
runs-on: ${{ matrix.runner }}
environment: release
timeout-minutes: 120
continue-on-error: true # Don't block PR merges if tests fail
permissions:
contents: read
packages: read
pull-requests: write # Allow commenting on PRs
id-token: write
strategy:
fail-fast: false
matrix:
include:
- platform: Android
os: ubuntu-24.04
runner: ai-run-linux
- platform: iOS
os: macos-14
runner: macos-14
steps:
# Validate input data to prevent "Artifact Poisoning"
- name: Validate Dispatch Inputs
if: github.event_name == 'workflow_dispatch' && github.event.inputs.package
run: |
if [[ ! "${{ github.event.inputs.package }}" =~ ^@qvac/ ]]; then
echo "::error::Invalid package scope. Only @qvac/* is allowed."
exit 1
fi
# Free up disk space on Ubuntu runner to prevent "No space left on device" errors
- name: Free up disk space
if: matrix.platform == 'Android'
run: |
echo "Disk space before cleanup:"
df -h
# Remove unnecessary software to free up disk space (|| true to handle self-hosted runners)
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /opt/ghc || true
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
sudo rm -rf /opt/hostedtoolcache/go || true
sudo rm -rf /opt/hostedtoolcache/Python || true
sudo rm -rf /opt/hostedtoolcache/Ruby || true
sudo rm -rf /usr/local/lib/android/sdk/ndk || true
sudo rm -rf /usr/local/share/boost || true
sudo rm -rf /usr/share/swift || true
sudo docker image prune --all --force || true
# Clean APT cache
sudo apt-get clean || true
echo "Disk space after cleanup:"
df -h
- name: Checkout addon repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
with:
repository: ${{ inputs.repository || github.repository }}
ref: ${{ inputs.ref || github.ref }}
token: ${{ secrets.PAT_TOKEN }}
path: addon
fetch-depth: 0
- name: Checkout mobile test framework
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 6.0.2
with:
repository: tetherto/qvac-test-addon-mobile
ref: ${{ env.TEST_FRAMEWORK_REF }}
token: ${{ secrets.PAT_TOKEN }}
path: test-framework
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # 6.3.0
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install global dependencies
run: |
echo "Installing global dependencies..."
npm install -g @expo/cli@latest
- name: Download Android prebuilds (from artifacts)
if: matrix.platform == 'Android' && !inputs.package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
with:
path: ${{ env.ADDON_WORKDIR }}/prebuilds
pattern: ${{ env.PREBUILD_ARTIFACT_PREFIX }}android-*
merge-multiple: true
continue-on-error: true
- name: Download iOS prebuilds (from artifacts)
if: matrix.platform == 'iOS' && !inputs.package
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # 8.0.1
with:
path: ${{ env.ADDON_WORKDIR }}/prebuilds
pattern: ${{ env.PREBUILD_ARTIFACT_PREFIX }}ios-*
merge-multiple: true
continue-on-error: true
- name: Download prebuilds from package
if: inputs.package
working-directory: ${{ env.ADDON_WORKDIR }}
run: |
PACKAGE_SPEC="${{ inputs.package }}"
echo "📦 Downloading $PACKAGE_SPEC from npm for manual trigger..."
PACKAGE_NAME="${PACKAGE_SPEC%@*}"
if ! npm pack "$PACKAGE_SPEC" --ignore-scripts; then
echo "ERROR: Failed to download $PACKAGE_SPEC from npm"
echo "Please check that the package exists at https://www.npmjs.com/package/$PACKAGE_NAME"
exit 1
fi
# Extract the tarball (pattern matches any addon name)
tar -xzf *.tgz
# Validate prebuilds directory exists
if [ ! -d "package/prebuilds" ]; then
echo "ERROR: No prebuilds directory found in package"
echo "The downloaded package may not contain prebuilt binaries"
exit 1
fi
# Move prebuilds to expected location
mv package/prebuilds ./prebuilds
# Cleanup
rm -rf package *.tgz
echo "✅ Prebuilds downloaded from npm:"
ls -la prebuilds/
- name: Verify and prepare prebuilds
working-directory: ${{ env.ADDON_WORKDIR }}
run: |
echo "Checking for prebuilds..."
echo "Current directory: $(pwd)"
if [ -d "prebuilds" ] && [ "$(ls -A prebuilds)" ]; then
echo "✅ Prebuilds found from artifacts:"
ls -la prebuilds/
else
echo "❌ ERROR: No prebuilds found!"
echo " This workflow requires prebuilds to be available."
echo " Either:"
echo " 1. Run this workflow after prebuild job completes"
echo " 2. Or commit prebuilds to the repository"
exit 1
fi
# Copy mobile prebuilds if needed
if npm run mobile:copy-prebuilds 2>/dev/null; then
echo "✅ Mobile prebuilds prepared"
else
echo "⚠️ mobile:copy-prebuilds script not available or failed"
fi
- name: Remove desktop prebuilds to save disk space
working-directory: ${{ env.ADDON_WORKDIR }}
run: |
echo "Removing desktop prebuilds to save disk space (keeping Android + iOS)..."
echo "Before cleanup:"
du -sh prebuilds/* 2>/dev/null || true
# Remove desktop prebuilds only (not needed for mobile tests)
rm -rf prebuilds/darwin-* prebuilds/win32-* prebuilds/linux-* 2>/dev/null || true
echo "After cleanup (Android + iOS only):"
du -sh prebuilds/* 2>/dev/null || true
df -h
- name: Verify test files exist
working-directory: ${{ env.ADDON_WORKDIR }}
run: |
echo "Verifying addon has mobile tests..."
if [ ! -d "test/mobile" ]; then
echo "❌ ERROR: test/mobile directory not found!"
echo ""
echo "This workflow requires the addon to have mobile tests at:"
echo " test/mobile/"
echo ""
echo "Please create this directory with your test files."
echo "See qvac-test-addon-mobile README for test file format."
exit 1
fi
# Check for .cjs test files
CJS_COUNT=$(find test/mobile -name "*.cjs" -type f | wc -l)
if [ "$CJS_COUNT" -eq 0 ]; then
echo "❌ ERROR: No .cjs test files found in test/mobile!"
exit 1
fi
echo "✅ Mobile test files found:"
ls -la test/mobile/*.cjs
# Check if testAssets exists
if [ -d "test/mobile/testAssets" ]; then
echo ""
echo "✅ Test assets found:"
ls -lah test/mobile/testAssets/
else
echo ""
echo "ℹ️ No testAssets directory (this is optional)"
fi
- name: Install Ninja build tool
if: matrix.platform == 'iOS'
run: |
echo "📦 Installing Ninja build system..."
brew install ninja
ninja --version
echo "✅ Ninja installed successfully"
- name: Install addon dependencies
working-directory: ${{ env.ADDON_WORKDIR }}
run: |
echo "Installing addon dependencies..."
npm install --ignore-scripts
- name: Validate mobile tests are up-to-date
working-directory: ${{ env.ADDON_WORKDIR }}
run: npm run test:mobile:validate
- name: Pack addon
working-directory: ${{ env.ADDON_WORKDIR }}
run: |
echo "Packing addon..."
if npm run build:pack 2>/dev/null; then
echo "✅ Addon packed using build:pack script"
else
echo "📦 Using npm pack directly..."
mkdir -p dist
npm pack --pack-destination dist --ignore-scripts
fi
# Verify pack file exists
PACK_FILE=$(ls dist/*.tgz | head -1)
if [ -f "$PACK_FILE" ]; then
SIZE=$(du -h "$PACK_FILE" | cut -f1)
echo "✅ Pack file created: $PACK_FILE (Size: $SIZE)"
else
echo "❌ Pack file not found in dist/"
exit 1
fi
- name: Setup test framework dependencies
working-directory: test-framework
run: |
echo "Setting up mobile test framework..."
npm install --ignore-scripts
echo "✅ Test framework dependencies installed"
- name: Build test app with addon
working-directory: test-framework
run: |
echo "Building test app with addon..."
echo "This will:"
echo " 1. Install the addon package"
echo " 2. Extract test code from addon's test/mobile/ directory"
echo " 3. Auto-detect and order test files by dependencies"
echo " 4. Generate backend.cjs with test functions"
echo " 5. Generate e2e tests for each test function"
echo " 6. Copy testAssets if available"
echo " 7. Bundle the app"
echo ""
ADDON_PATH="${{ github.workspace }}/${{ env.ADDON_WORKDIR }}"
npm run build "$ADDON_PATH" "$ADDON_PATH/test/mobile"
echo ""
echo "✅ Test app built successfully"
# Verify critical files were generated
if [ ! -f "backend/backend.cjs" ]; then
echo "❌ ERROR: backend/backend.cjs was not generated!"
exit 1
fi
if [ ! -f "e2e/tests/app.test.js" ]; then
echo "❌ ERROR: e2e/tests/app.test.js was not generated!"
exit 1
fi
if [ ! -f "backend/app.bundle" ]; then
echo "❌ ERROR: backend/app.bundle was not created!"
exit 1
fi
echo "✅ All required files generated successfully"
# Show what tests were extracted
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "EXTRACTED TEST FUNCTIONS:"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
if [ -f "app/testConfig.js" ]; then
cat app/testConfig.js
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- name: Display build summary
if: always()
working-directory: test-framework
run: |
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "📊 BUILD SUMMARY"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "Platform: ${{ matrix.platform }}"
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "Package: ${{ inputs.package }}"
else
echo "Addon: ${{ env.ADDON_NAME }} (from PR artifacts)"
fi
echo ""
echo "Generated Files:"
echo " backend/backend.cjs: $([ -f backend/backend.cjs ] && echo '✅' || echo '❌')"
echo " backend/app.bundle: $([ -f backend/app.bundle ] && echo '✅' || echo '❌')"
echo " app/testConfig.js: $([ -f app/testConfig.js ] && echo '✅' || echo '❌')"
echo " app/assetManifest.js: $([ -f app/assetManifest.js ] && echo '✅' || echo '❌')"
echo " e2e/tests/app.test.js: $([ -f e2e/tests/app.test.js ] && echo '✅' || echo '❌')"
echo ""
echo "Test Assets:"
if [ -d "testAssets" ]; then
ASSET_COUNT=$(find testAssets -type f | wc -l)
echo " ✅ $ASSET_COUNT file(s) in testAssets/"
else
echo " ℹ️ No testAssets (optional)"
fi
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
# Android-specific steps
- name: Set up JDK 17
if: matrix.platform == 'Android'
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 5.2.0
with:
java-version: 17
distribution: temurin
- name: Setup Android SDK
if: matrix.platform == 'Android'
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # 3.2.2
- name: Generate Android project
if: matrix.platform == 'Android'
working-directory: test-framework
run: |
echo "Generating Android project with Expo..."
npx expo prebuild --platform android --clean
- name: Build Android APK
if: matrix.platform == 'Android'
id: build_apk
working-directory: test-framework
run: |
echo "Building Android APK for Device Farm..."
export JAVA_HOME=$JAVA_HOME_17_X64
# Bundle JavaScript
echo "Bundling JavaScript code..."
npm run bundle
if [ $? -ne 0 ]; then
echo "❌ Bundle failed"
exit 1
fi
echo "✅ Bundle completed successfully"
# Build RELEASE APK (not debug) to ensure JS bundle is included
# Debug builds skip bundling by default and try to connect to Metro
# Release builds embed the JS bundle in the APK
cd android
echo "Building APK with Gradle (RELEASE with embedded JS bundle)..."
./gradlew assembleRelease \
-PreactNativeArchitectures=arm64-v8a \
--no-daemon \
--no-build-cache \
--stacktrace
cd ..
# Find the APK (look for release)
APK_PATH=$(find android/app/build/outputs/apk -name "*.apk" | grep "release" | grep -v "unaligned" | head -1)
if [ -f "$APK_PATH" ]; then
# Convert to absolute path
APK_ABSOLUTE_PATH="${{ github.workspace }}/test-framework/$APK_PATH"
SIZE=$(du -h "$APK_PATH" | cut -f1)
echo "✅ APK built successfully: $APK_PATH (Size: $SIZE)"
echo "apk_path=$APK_ABSOLUTE_PATH" >> $GITHUB_OUTPUT
echo "app_type=ANDROID_APP" >> $GITHUB_OUTPUT
echo "app_name=test-app-${{ matrix.platform }}.apk" >> $GITHUB_OUTPUT
# Clean up build intermediates to free disk space
echo "Cleaning up build intermediates..."
rm -rf android/app/build/intermediates
rm -rf android/.gradle
df -h
else
echo "❌ APK file not found"
echo "Searching in android/app/build/outputs/apk:"
find android/app/build/outputs/apk -type f 2>/dev/null || echo "Directory not found"
exit 1
fi
# iOS-specific steps
- name: Set up Xcode version
if: matrix.platform == 'iOS'
run: |
echo "Available Xcode versions:"
ls /Applications | grep Xcode || echo "No Xcode apps found"
echo ""
echo "Current Xcode (before switch):"
xcodebuild -version
# React Native requires Xcode >= 16.1
# Use Xcode 16.1 (has iOS 18.1 SDK which is stable and pre-installed)
if [ -d "/Applications/Xcode_16.1.app" ]; then
echo ""
echo "✅ Switching to Xcode 16.1..."
sudo xcode-select -s /Applications/Xcode_16.1.app
elif [ -d "/Applications/Xcode_16.1.0.app" ]; then
echo ""
echo "✅ Switching to Xcode 16.1.0..."
sudo xcode-select -s /Applications/Xcode_16.1.0.app
elif [ -d "/Applications/Xcode_16.2.app" ]; then
echo ""
echo "⚠️ Using Xcode 16.2 (16.1 not found)..."
sudo xcode-select -s /Applications/Xcode_16.2.app
else
echo ""
echo "❌ ERROR: No suitable Xcode version found (need >= 16.1)"
exit 1
fi
echo ""
echo "Current Xcode (after switch):"
xcodebuild -version
echo ""
echo "Available iOS SDKs:"
xcodebuild -showsdks | grep -i ios
- name: Install CocoaPods
if: matrix.platform == 'iOS'
run: |
sudo gem install cocoapods
pod --version
- name: Create Keychain and Import Certificate
if: matrix.platform == 'iOS'
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.TEST_APP_APPLE_DISTRIBUTION_CERTIFICATE }}
P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.TEST_APP_APPLE_PROVISIONING_PROFILE }}
KEYCHAIN_PASSWORD: ${{ secrets.APPLE_KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Extract UUID first, then copy with UUID as filename
PP_UUID=$(/usr/libexec/PlistBuddy -c 'Print :UUID' /dev/stdin <<< $(security cms -D -i $PP_PATH))
echo "PP_UUID=$PP_UUID" >> $GITHUB_ENV
echo "Provisioning Profile UUID: $PP_UUID"
# Copy provisioning profile with UUID as filename
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$PP_UUID.mobileprovision
security find-identity -p codesigning -v
- name: Verify provisioning profile
if: matrix.platform == 'iOS'
run: |
echo "🔍 Verifying provisioning profile..."
echo "PP_UUID: $PP_UUID"
PP_FILE=~/Library/MobileDevice/Provisioning\ Profiles/$PP_UUID.mobileprovision
if [ ! -f "$PP_FILE" ]; then
echo "❌ Provisioning profile file not found at: $PP_FILE"
ls -la ~/Library/MobileDevice/Provisioning\ Profiles/
exit 1
fi
echo "📋 Provisioning Profile Details:"
security cms -D -i "$PP_FILE" > /tmp/profile.plist
PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print :Name" /tmp/profile.plist 2>/dev/null || echo "Unknown")
PROFILE_BUNDLE_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:application-identifier" /tmp/profile.plist 2>/dev/null || echo "Unknown")
PROFILE_TEAM_ID=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:com.apple.developer.team-identifier" /tmp/profile.plist 2>/dev/null || echo "Unknown")
# Detect profile type (Development, Ad Hoc, App Store, Enterprise)
HAS_DEVICES=$(/usr/libexec/PlistBuddy -c "Print :ProvisionedDevices" /tmp/profile.plist 2>/dev/null && echo "yes" || echo "no")
PROVISIONS_ALL=$(/usr/libexec/PlistBuddy -c "Print :ProvisionsAllDevices" /tmp/profile.plist 2>/dev/null || echo "false")
HAS_GET_TASK_ALLOW=$(/usr/libexec/PlistBuddy -c "Print :Entitlements:get-task-allow" /tmp/profile.plist 2>/dev/null || echo "false")
if [[ "$PROVISIONS_ALL" == "true" ]]; then
PROFILE_TYPE="Enterprise"
EXPORT_METHOD="enterprise"
elif [[ "$HAS_DEVICES" == "yes" && "$HAS_GET_TASK_ALLOW" == "true" ]]; then
PROFILE_TYPE="Development"
EXPORT_METHOD="development"
elif [[ "$HAS_DEVICES" == "yes" && "$HAS_GET_TASK_ALLOW" == "false" ]]; then
PROFILE_TYPE="Ad Hoc"
EXPORT_METHOD="ad-hoc"
else
PROFILE_TYPE="App Store"
EXPORT_METHOD="app-store"
fi
echo " Name: $PROFILE_NAME"
echo " Type: $PROFILE_TYPE"
echo " Export Method: $EXPORT_METHOD"
echo " Application ID: $PROFILE_BUNDLE_ID"
echo " Team ID: $PROFILE_TEAM_ID"
echo " Expected Bundle ID: ${{ env.APP_BUNDLE_ID }}"
# Save export method for next step
echo "EXPORT_METHOD=$EXPORT_METHOD" >> $GITHUB_ENV
# Extract just the bundle ID part (remove team prefix)
BUNDLE_ID_ONLY=$(echo "$PROFILE_BUNDLE_ID" | sed 's/^[^.]*\.//')
if [[ "$BUNDLE_ID_ONLY" != "${{ env.APP_BUNDLE_ID }}" ]]; then
echo ""
echo "❌ ERROR: Provisioning profile bundle ID mismatch!"
echo " Profile has: $BUNDLE_ID_ONLY"
echo " Expected: ${{ env.APP_BUNDLE_ID }}"
echo ""
echo "The provisioning profile was created for a different bundle identifier."
echo "Please create a new provisioning profile for: ${{ env.APP_BUNDLE_ID }}"
exit 1
fi
echo "✅ Provisioning profile matches expected bundle ID"
- name: Generate iOS project
if: matrix.platform == 'iOS'
working-directory: test-framework
run: |
echo "Generating iOS project with Expo..."
npx expo prebuild --platform ios --clean
- name: Install iOS dependencies
if: matrix.platform == 'iOS'
working-directory: test-framework/ios
run: |
echo "Installing CocoaPods dependencies..."
pod install --repo-update
- name: Build and Archive iOS App
if: matrix.platform == 'iOS'
id: build_ios
working-directory: test-framework
run: |
echo "Building iOS app for Device Farm..."
# Bundle JavaScript first
echo "Bundling JavaScript code..."
npm run bundle
if [ $? -ne 0 ]; then
echo "❌ Bundle failed"
exit 1
fi
echo "✅ Bundle completed successfully"
# Get scheme name
cd ios
SCHEME_NAME=$(xcodebuild -list | grep -A 1 "Schemes:" | grep -v "Schemes:" | head -1 | xargs)
echo "Detected scheme: $SCHEME_NAME"
# Debug: Check bundle identifier in project
echo "🔍 Checking project configuration..."
BUNDLE_ID=$(xcodebuild -showBuildSettings -workspace $SCHEME_NAME.xcworkspace -scheme "$SCHEME_NAME" -configuration Release -destination "generic/platform=iOS" 2>/dev/null | grep PRODUCT_BUNDLE_IDENTIFIER | head -1 | awk '{print $3}')
echo "Bundle Identifier in project: $BUNDLE_ID"
if [[ "$BUNDLE_ID" != "${{ env.APP_BUNDLE_ID }}" ]]; then
echo "⚠️ Warning: Bundle ID mismatch in Xcode project!"
echo " Expected: ${{ env.APP_BUNDLE_ID }}"
echo " Found: $BUNDLE_ID"
fi
# Debug: Check provisioning profile
echo "🔍 Provisioning profile UUID: $PP_UUID"
security cms -D -i ~/Library/MobileDevice/Provisioning\ Profiles/$PP_UUID.mobileprovision | grep -A 5 "application-identifier\|Name\|TeamIdentifier" | head -20 || echo "Could not read profile details"
# Archive for iOS device
xcodebuild -workspace $SCHEME_NAME.xcworkspace \
-scheme "$SCHEME_NAME" \
-sdk iphoneos \
-configuration Release \
-destination "generic/platform=iOS" \
-archivePath $RUNNER_TEMP/$SCHEME_NAME.xcarchive \
CODE_SIGN_STYLE=Manual \
PROVISIONING_PROFILE_SPECIFIER="$PP_UUID" \
CODE_SIGN_IDENTITY="Apple Distribution" \
DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" \
clean archive
- name: Export IPA
if: matrix.platform == 'iOS'
id: export_ipa
working-directory: test-framework/ios
run: |
SCHEME_NAME=$(xcodebuild -list | grep -A 1 "Schemes:" | grep -v "Schemes:" | head -1 | xargs)
# Create export options using auto-detected export method
# The EXPORT_METHOD was determined in the "Verify provisioning profile" step
echo "📦 Using export method: $EXPORT_METHOD"
EXPORT_OPTS_PATH=$RUNNER_TEMP/ExportOptions.plist
cat > $EXPORT_OPTS_PATH << 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>method</key>
<string>$EXPORT_METHOD</string>
<key>teamID</key>
<string>${{ secrets.APPLE_TEAM_ID }}</string>
<key>signingStyle</key>
<string>manual</string>
<key>provisioningProfiles</key>
<dict>
<key>${{ env.APP_BUNDLE_ID }}</key>
<string>$PP_UUID</string>
</dict>
</dict>
</plist>
EOF
echo "📋 Export options:"
cat $EXPORT_OPTS_PATH
xcodebuild -exportArchive \
-archivePath $RUNNER_TEMP/$SCHEME_NAME.xcarchive \
-exportOptionsPlist $EXPORT_OPTS_PATH \
-exportPath $RUNNER_TEMP/build
IPA_FILE=$(find $RUNNER_TEMP/build -name "*.ipa" | head -1)
if [ -f "$IPA_FILE" ]; then
echo "✅ IPA exported: $IPA_FILE"
echo "apk_path=$IPA_FILE" >> $GITHUB_OUTPUT
echo "app_type=IOS_APP" >> $GITHUB_OUTPUT
echo "app_name=test-app-${{ matrix.platform }}.ipa" >> $GITHUB_OUTPUT
else
echo "❌ IPA file not found"
exit 1
fi
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 # 6.0.0
with:
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
aws-region: us-west-2
role-session-name: device-farm-upload
role-duration-seconds: 7200 # 2hrs for device farm tests
- name: Upload App to Device Farm
id: upload_app
run: |
if [ "${{ matrix.platform }}" == "Android" ]; then
APP_PATH="${{ steps.build_apk.outputs.apk_path }}"
APP_TYPE="${{ steps.build_apk.outputs.app_type }}"
APP_NAME="${{ steps.build_apk.outputs.app_name }}"
else
APP_PATH="${{ steps.export_ipa.outputs.apk_path }}"
APP_TYPE="${{ steps.export_ipa.outputs.app_type }}"
APP_NAME="${{ steps.export_ipa.outputs.app_name }}"
fi
echo "📤 Uploading app to AWS Device Farm..."
UPLOAD_RESPONSE=$(aws devicefarm create-upload \
--project-arn "${{ secrets.LLM_AWS_DEVICE_FARM_PROJECT_ARN }}" \
--name "$APP_NAME" \
--type "$APP_TYPE" \
--output json)
if [ $? -ne 0 ]; then
echo "❌ Error creating upload in Device Farm"
echo "Response: $UPLOAD_RESPONSE"
exit 1
fi
APP_UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.upload.url')
APP_UPLOAD_ARN=$(echo $UPLOAD_RESPONSE | jq -r '.upload.arn')
echo "app_upload_arn=$APP_UPLOAD_ARN" >> $GITHUB_OUTPUT
echo "App upload ARN: $APP_UPLOAD_ARN"
echo "Uploading app file: $APP_PATH"
curl -T "$APP_PATH" "$APP_UPLOAD_URL"
if [ $? -ne 0 ]; then
echo "❌ Error uploading app file using curl"
exit 1
fi
# Wait for processing
echo "⏳ Waiting for upload to be processed..."
MAX_ATTEMPTS=30
ATTEMPT=1
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
STATUS=$(aws devicefarm get-upload --arn "$APP_UPLOAD_ARN" --query "upload.status" --output text)
echo "Status (attempt $ATTEMPT/$MAX_ATTEMPTS): $STATUS"
if [ "$STATUS" = "SUCCEEDED" ]; then
echo "✅ App upload successful"
break
fi
if [ "$STATUS" = "FAILED" ]; then
echo "❌ Upload failed"
aws devicefarm get-upload --arn "$APP_UPLOAD_ARN"
exit 1
fi
sleep 10
ATTEMPT=$((ATTEMPT + 1))
done
- name: Verify test package generation
working-directory: test-framework/e2e
run: |
echo "Verifying e2e test package..."
if [ ! -f "package.json" ]; then
echo "❌ ERROR: e2e/package.json not found!"
exit 1
fi
if [ ! -f "tests/app.test.js" ]; then
echo "❌ ERROR: e2e/tests/app.test.js not found!"
exit 1
fi
echo "✅ E2E test files verified"
echo ""
echo "Test package contents:"
ls -la
echo ""
echo "Test files:"
ls -la tests/
- name: Package and Upload Test Package
id: upload_test_package
working-directory: test-framework
run: |
echo "📦 Packaging e2e tests..."
cd e2e
# Install dependencies before packing
npm install --ignore-scripts
# Create tarball
npm pack --ignore-scripts
# Create zip with test files only (no node_modules - will be installed on Device Farm)
ZIP_NAME="e2e-tests-${{ matrix.platform }}.zip"
zip -r "$ZIP_NAME" \
package.json \
tests/ \
*.tgz
echo "📦 Package contents (excluding node_modules):"
unzip -l "$ZIP_NAME" | head -20
# Verify zip was created
if [ ! -f "$ZIP_NAME" ]; then
echo "❌ ERROR: Failed to create test package zip"
exit 1
fi
SIZE=$(du -h "$ZIP_NAME" | cut -f1)
echo "✅ Test package created: $ZIP_NAME (Size: $SIZE)"
mv "$ZIP_NAME" "$GITHUB_WORKSPACE/"
# Upload test package to AWS Device Farm
echo "📤 Uploading test package to AWS Device Farm..."
UPLOAD_RESPONSE=$(aws devicefarm create-upload \
--project-arn "${{ secrets.LLM_AWS_DEVICE_FARM_PROJECT_ARN }}" \
--name "$ZIP_NAME" \
--type "APPIUM_NODE_TEST_PACKAGE" \
--output json)
if [ $? -ne 0 ]; then
echo "❌ Error creating test package upload in Device Farm"
echo "Response: $UPLOAD_RESPONSE"
exit 1
fi
TEST_UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.upload.url')
TEST_UPLOAD_ARN=$(echo $UPLOAD_RESPONSE | jq -r '.upload.arn')
echo "test_package_upload_arn=$TEST_UPLOAD_ARN" >> $GITHUB_OUTPUT
echo "Test package upload ARN: $TEST_UPLOAD_ARN"
echo "Uploading to: $TEST_UPLOAD_URL"
curl -T "$GITHUB_WORKSPACE/$ZIP_NAME" "$TEST_UPLOAD_URL"
if [ $? -ne 0 ]; then
echo "❌ Error uploading test package using curl"
exit 1
fi
# Wait for processing
echo "⏳ Waiting for test package to be processed..."
MAX_ATTEMPTS=30
ATTEMPT=1
while [ $ATTEMPT -le $MAX_ATTEMPTS ]; do
STATUS=$(aws devicefarm get-upload --arn "$TEST_UPLOAD_ARN" --query "upload.status" --output text)
echo "Test package status (attempt $ATTEMPT/$MAX_ATTEMPTS): $STATUS"
if [ "$STATUS" = "SUCCEEDED" ]; then
echo "✅ Test package upload successful"
break
fi
if [ "$STATUS" = "FAILED" ]; then
echo "❌ Test package upload failed"
aws devicefarm get-upload --arn "$TEST_UPLOAD_ARN"
exit 1
fi
sleep 10
ATTEMPT=$((ATTEMPT + 1))
done
if [ $ATTEMPT -gt $MAX_ATTEMPTS ]; then
echo "❌ Timeout waiting for test package processing"
exit 1
fi
# NOTE: Everything below remains unchanged from your source workflow.
# The only monorepo-related change in this entire file is that "addon" operations
# now target addon/packages/qvac-lib-infer-llamacpp-llm via env.ADDON_WORKDIR.
- name: Create and Upload Test Spec
id: upload_test_spec
run: |
echo "📝 Creating test spec for custom environment mode..."
echo "Platform: ${{ matrix.platform }}"
if [ "${{ matrix.platform }}" == "Android" ]; then
PLATFORM="Android"
AUTOMATION="UiAutomator2"
HOST_LINE="android_test_host: amazon_linux_2"
BUNDLE_ID="${{ env.APP_BUNDLE_ID }}"
WDIO_CONFIG='exports.config={runner:"local",hostname:"127.0.0.1",port:4723,path:"/wd/hub",specs:["*.spec.js","*.test.js"],maxInstances:1,bail:0,capabilities:[{platformName:"Android","appium:automationName":"UiAutomator2","appium:appPackage":"'${{ env.APP_BUNDLE_ID }}'","appium:appActivity":"'${{ env.APP_BUNDLE_ID }}'.MainActivity","appium:newCommandTimeout":300,"appium:autoGrantPermissions":true,"appium:autoAcceptAlerts":true,"appium:noReset":true,"appium:dontStopAppOnReset":true,"appium:forceAppLaunch":false}],logLevel:"debug",waitforTimeout:120000,connectionRetryTimeout:30000,connectionRetryCount:3,services:[],framework:"mocha",reporters:["spec"],mochaOpts:{ui:"bdd",timeout:1800000,grep:"__MOCHA_GREP__"},before:async function(capabilities,specs,browser){const BUNDLE_ID="'${{ env.APP_BUNDLE_ID }}'";const TEST_FILTER="__TEST_FILTER__";const QVAC_PERF_RUNS_VALUE="__QVAC_PERF_RUNS__";const QVAC_PERF_WARMUP_RUNS_VALUE="__QVAC_PERF_WARMUP_RUNS__";global.appCrashed=false;global.checkAppCrash=async(stage)=>{try{const state=await browser.queryAppState(BUNDLE_ID);console.log("["+stage+"] App state: "+state+" (4=foreground,3=background,1=not running)");if(state<3){console.error("\\n🛑 APP CRASHED at "+stage+"! State="+state);console.error("Check device logs for BareKit/native errors.\\n");global.appCrashed=true;setTimeout(function(){process.exit(1);},5000);}return state;}catch(e){console.log("["+stage+"] queryAppState error: "+e.message);return-1;}};console.log("Checking initial app state...");await global.checkAppCrash("startup");console.log("Waiting for app to initialize...");await browser.pause(5000);await global.checkAppCrash("after-pause");const initText=await browser.$("android=new UiSelector().textContains(\"INITIALIZED\")");await initText.waitForDisplayed({timeout:60000});await global.checkAppCrash("after-init");if(TEST_FILTER!=="__TEST_FILTER__"){try{const b64=Buffer.from(TEST_FILTER).toString("base64");await browser.pushFile("/data/local/tmp/testFilter.txt",b64);console.log("Pushed test filter: "+TEST_FILTER);}catch(e){console.log("pushFile failed: "+e.message);}}if(QVAC_PERF_RUNS_VALUE.length>0||QVAC_PERF_WARMUP_RUNS_VALUE.length>0){try{var perfCfg="QVAC_PERF_RUNS="+QVAC_PERF_RUNS_VALUE+"\\nQVAC_PERF_WARMUP_RUNS="+QVAC_PERF_WARMUP_RUNS_VALUE+"\\n";var pcb64=Buffer.from(perfCfg).toString("base64");await browser.pushFile("/data/local/tmp/qvacPerfConfig.txt",pcb64);console.log("Pushed perf config: "+perfCfg.replace(/\\n/g," "));}catch(e){console.log("perfConfig pushFile failed: "+e.message);}}console.log("App initialized, clicking Run Automated Tests...");const button=await browser.$("android=new UiSelector().textContains(\"Run Automated Tests\")");await button.waitForDisplayed({timeout:15000});await button.click();console.log("Button clicked!");await browser.pause(5000);await global.checkAppCrash("after-click");global.crashMonitor=setInterval(async()=>{if(global.appCrashed)return;try{const s=await browser.queryAppState(BUNDLE_ID);if(s<3){console.error("\\n🛑 BACKGROUND CRASH DETECTED! App state="+s);global.appCrashed=true;setTimeout(function(){process.exit(1);},5000);}}catch(e){}},15000);},after:async function(){if(global.crashMonitor)clearInterval(global.crashMonitor);},afterTest:async function(test,context,{error}){if(global.appCrashed)return;await global.checkAppCrash("after-test:"+test.title);}};'
else
PLATFORM="iOS"
AUTOMATION="XCUITest"
HOST_LINE="ios_test_host: macos_sequoia"
BUNDLE_ID="${{ env.APP_BUNDLE_ID }}"
WDIO_CONFIG='exports.config={runner:"local",hostname:"127.0.0.1",port:4723,path:"/wd/hub",specs:["*.spec.js","*.test.js"],maxInstances:1,bail:0,capabilities:[{platformName:"iOS","appium:automationName":"XCUITest","appium:bundleId":"'${{ env.APP_BUNDLE_ID }}'","appium:newCommandTimeout":300,"appium:noReset":true,"appium:forceAppLaunch":false,"appium:usePrebuiltWDA":true,"appium:wdaLocalPort":8100,"appium:showIOSLog":true,"appium:realDeviceLogger":"/usr/local/lib/node_modules/appium/node_modules/deviceconsole/deviceconsole"}],logLevel:"debug",waitforTimeout:120000,connectionRetryTimeout:30000,connectionRetryCount:3,services:[],framework:"mocha",reporters:["spec"],mochaOpts:{ui:"bdd",timeout:1800000,grep:"__MOCHA_GREP__"},before:async function(capabilities,specs,browser){const BUNDLE_ID="'${{ env.APP_BUNDLE_ID }}'";const TEST_FILTER="__TEST_FILTER__";const QVAC_PERF_RUNS_VALUE="__QVAC_PERF_RUNS__";const QVAC_PERF_WARMUP_RUNS_VALUE="__QVAC_PERF_WARMUP_RUNS__";global.appCrashed=false;global.flushBareLog=async function(reason){try{var _h=require("http");var lb64=await new Promise(function(ok,fail){var bd=JSON.stringify({path:"@"+BUNDLE_ID+":documents/bare_console.log"});var rq=_h.request({hostname:"127.0.0.1",port:4723,path:"/wd/hub/session/"+browser.sessionId+"/appium/device/pull_file",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(bd)}},function(rs){var d="";rs.on("data",function(c){d+=c;});rs.on("end",function(){try{ok(JSON.parse(d).value);}catch(e){fail(e);}});});rq.on("error",fail);rq.write(bd);rq.end();});var logTxt=Buffer.from(lb64,"base64").toString();var logDir=process.env.DEVICEFARM_LOG_DIR||".";require("fs").writeFileSync(logDir+"/bare_console.log",logTxt);console.log("[bare-log] "+reason+" flush ok ("+logTxt.length+" bytes)");}catch(e){console.log("[bare-log] "+reason+" flush failed: "+e.message);}};global.checkAppCrash=async(stage)=>{try{const state=await browser.queryAppState(BUNDLE_ID);console.log("["+stage+"] App state: "+state+" (4=foreground,3=background,1=not running)");if(state<3){console.error("\\n🛑 APP CRASHED at "+stage+"! State="+state);console.error("Check device logs for BareKit/native errors.\\n");global.appCrashed=true;setTimeout(function(){process.exit(1);},5000);try{await browser.pause(1500);await Promise.race([global.flushBareLog("crash-"+stage),new Promise(function(_,rj){setTimeout(function(){rj(new Error("bare-log flush timed out"));},3000);})]);}catch(_){}}return state;}catch(e){console.log("["+stage+"] queryAppState error: "+e.message);return-1;}};console.log("Checking initial app state...");await global.checkAppCrash("startup");console.log("Waiting for app to initialize...");await browser.pause(5000);await global.checkAppCrash("after-pause");const initText=await browser.$("-ios predicate string:label CONTAINS \"INITIALIZED\"");await initText.waitForDisplayed({timeout:60000});await global.checkAppCrash("after-init");if(TEST_FILTER!=="__TEST_FILTER__"){try{const b64=Buffer.from(TEST_FILTER).toString("base64");await browser.pushFile("@"+BUNDLE_ID+":documents/testFilter.txt",b64);console.log("Pushed test filter: "+TEST_FILTER);}catch(e){console.log("pushFile failed: "+e.message);}}if(QVAC_PERF_RUNS_VALUE.length>0||QVAC_PERF_WARMUP_RUNS_VALUE.length>0){try{var perfCfg="QVAC_PERF_RUNS="+QVAC_PERF_RUNS_VALUE+"\\nQVAC_PERF_WARMUP_RUNS="+QVAC_PERF_WARMUP_RUNS_VALUE+"\\n";var pcb64=Buffer.from(perfCfg).toString("base64");await browser.pushFile("@"+BUNDLE_ID+":documents/qvacPerfConfig.txt",pcb64);console.log("Pushed perf config: "+perfCfg.replace(/\\n/g," "));}catch(e){console.log("perfConfig pushFile failed: "+e.message);}}console.log("App initialized, clicking Run Automated Tests...");const button=await browser.$("-ios predicate string:label CONTAINS \"Run Automated Tests\"");await button.waitForDisplayed({timeout:15000});await button.click();console.log("Button clicked!");await browser.pause(5000);await global.checkAppCrash("after-click");global.crashMonitor=setInterval(async()=>{if(global.appCrashed)return;try{const s=await browser.queryAppState(BUNDLE_ID);if(s<3){console.error("\\n🛑 BACKGROUND CRASH DETECTED! App state="+s);global.appCrashed=true;setTimeout(function(){process.exit(1);},5000);try{await browser.pause(1500);await Promise.race([global.flushBareLog("crash-bg"),new Promise(function(_,rj){setTimeout(function(){rj(new Error("bare-log flush timed out"));},3000);})]);}catch(_){}}}catch(e){}},15000);},after:async function(){if(global.crashMonitor)clearInterval(global.crashMonitor);console.log("[bare-log] Waiting for log flush...");await browser.pause(3000);if(global.flushBareLog)await global.flushBareLog("after");},afterTest:async function(test,context,{error}){if(global.appCrashed)return;await global.checkAppCrash("after-test:"+test.title);}};'
fi
WDIO_B64=$(echo "$WDIO_CONFIG" | base64 | tr -d '\n')
generate_spec() {
local output_file="$1"
local wdio_b64="$2"
{
printf 'version: 0.1\n'
if [ -n "$HOST_LINE" ]; then
printf '%s\n' "$HOST_LINE"
fi
printf '\nphases:\n install:\n commands:\n'
printf ' - echo "Setting up Node.js environment..."\n'
printf ' - export NVM_DIR=$HOME/.nvm\n'
printf ' - . $NVM_DIR/nvm.sh 2>/dev/null || true\n'
printf ' - nvm install 18 2>/dev/null || true\n'
printf ' - nvm use 18 2>/dev/null || true\n'
printf ' - node --version || echo "Using system node"\n'
printf '\n pre_test:\n commands:\n'
printf ' - echo "Setting up test environment..."\n'
printf ' - cd $DEVICEFARM_TEST_PACKAGE_PATH\n'
printf ' - ls -la\n'
printf ' - echo "Installing dependencies (clean install)..."\n'
printf ' - rm -rf node_modules package-lock.json 2>/dev/null || true\n'
printf ' - npm install --legacy-peer-deps 2>&1\n'
printf ' - echo "Verifying wdio installation..."\n'
printf ' - ls -la node_modules/.bin/ | grep wdio || echo "wdio not found in .bin"\n'
printf ' - node node_modules/@wdio/cli/bin/wdio.js --version || echo "wdio version check failed"\n'
printf ' - echo "Creating wdio config for Device Farm..."\n'
printf ' - echo "%s" | base64 -d > tests/wdio.config.devicefarm.js\n' "$wdio_b64"
printf ' - cat tests/wdio.config.devicefarm.js\n'
if [ "${{ matrix.platform }}" == "iOS" ]; then
printf ' - echo "Configuring WebDriverAgent for iOS..."\n'
printf ' - export DEVICEFARM_APPIUM_WDA_DERIVED_DATA_PATH=$DEVICEFARM_APPIUM_WDA_DERIVED_DATA_PATH_V9\n'
printf ' - echo "WDA Path: $DEVICEFARM_APPIUM_WDA_DERIVED_DATA_PATH"\n'
fi
printf ' - echo "Starting Appium server..."\n'
printf ' - export APPIUM_BASE_PATH=/wd/hub\n'
printf ' - |\n'
printf ' appium --base-path=$APPIUM_BASE_PATH --log-timestamp \\\n'
printf ' --log-no-colors --relaxed-security --default-capabilities \\\n'
printf ' "{\\"appium:deviceName\\": \\"$DEVICEFARM_DEVICE_NAME\\", \\\n'
printf ' \\"platformName\\": \\"$DEVICEFARM_DEVICE_PLATFORM_NAME\\", \\\n'
printf ' \\"appium:app\\": \\"$DEVICEFARM_APP_PATH\\", \\\n'
printf ' \\"appium:udid\\":\\"$DEVICEFARM_DEVICE_UDID\\", \\\n'
printf ' \\"appium:platformVersion\\": \\"$DEVICEFARM_DEVICE_OS_VERSION\\", \\\n'
printf ' \\"appium:chromedriverExecutableDir\\": \\"$DEVICEFARM_CHROMEDRIVER_EXECUTABLE_DIR\\", \\\n'
printf ' \\"appium:wdaLocalPort\\": 8100, \\\n'
printf ' \\"appium:derivedDataPath\\": \\"$DEVICEFARM_APPIUM_WDA_DERIVED_DATA_PATH\\", \\\n'
printf ' \\"appium:usePrebuiltWDA\\": true, \\\n'
printf ' \\"appium:automationName\\": \\"%s\\"}" \\\n' "$AUTOMATION"
printf ' >> $DEVICEFARM_LOG_DIR/appium.log 2>&1 &\n'
printf ' - echo "Waiting for Appium to be ready (max 30 seconds)..."\n'
printf ' - |\n'
printf ' appium_initialization_time=0\n'
printf ' until curl --silent --fail "http://0.0.0.0:4723${APPIUM_BASE_PATH}/status"; do\n'
printf ' if [[ $appium_initialization_time -gt 30 ]]; then\n'
printf ' echo "Appium did not start within 30 seconds. Exiting..."\n'
printf ' cat $DEVICEFARM_LOG_DIR/appium.log\n'
printf ' exit 1\n'
printf ' fi\n'
printf ' appium_initialization_time=$((appium_initialization_time + 1))\n'
printf ' echo "Waiting for Appium to start on port 4723 (${appium_initialization_time}s/30s)..."\n'
printf ' sleep 1\n'
printf ' done\n'
printf ' - echo "Appium server is ready!"\n'
printf ' - curl -s http://0.0.0.0:4723${APPIUM_BASE_PATH}/status || echo "Status check failed"\n'
printf '\n test:\n commands:\n'
printf ' - echo "Running WebDriverIO tests..."\n'
printf ' - cd $DEVICEFARM_TEST_PACKAGE_PATH\n'