.github/workflows/integration-mobile-test-qvac-lib-infer-llamacpp-embed.yml #8
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Run Mobile Integration Tests (Qvac-lib-infer-llamacpp-embed) | |
| on: | |
| workflow_call: | |
| inputs: | |
| ref: | |
| description: "Git ref to checkout" | |
| type: string | |
| required: false | |
| repository: | |
| description: "Repository to checkout" | |
| type: string | |
| required: false | |
| workdir: | |
| description: "Package working directory in monorepo (e.g., packages/qvac-lib-infer-llamacpp-embed)" | |
| type: string | |
| required: false | |
| default: "packages/qvac-lib-infer-llamacpp-embed" | |
| 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/llamacpp-embed@latest)" | |
| type: string | |
| required: false | |
| default: "@qvac/llamacpp-embed@latest" | |
| workdir: | |
| description: "Package working directory in monorepo (e.g., packages/qvac-lib-infer-llamacpp-embed)" | |
| type: string | |
| required: false | |
| default: "packages/qvac-lib-infer-llamacpp-embed" | |
| env: | |
| NODE_VERSION: "lts/*" | |
| ADDON_NAME: "@qvac/llamacpp-embed" | |
| PREBUILD_ARTIFACT_PREFIX: "llamacpp-embed-" | |
| TEST_FRAMEWORK_REF: "main" | |
| APP_BUNDLE_ID: "io.tether.test.qvac" | |
| PKG_DIR: ${{ inputs.workdir || github.event.inputs.workdir || 'packages/qvac-lib-infer-llamacpp-embed' }} | |
| jobs: | |
| build-and-test: | |
| name: Build ${{ matrix.platform }} and Run E2E Tests | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 120 | |
| continue-on-error: true | |
| permissions: | |
| contents: read | |
| packages: read | |
| pull-requests: 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: | |
| - name: Free up disk space | |
| if: matrix.platform == 'Android' | |
| run: | | |
| echo "Disk space before cleanup:" | |
| df -h | |
| 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 | |
| sudo apt-get clean || true | |
| echo "Disk space after cleanup:" | |
| df -h | |
| - name: Checkout addon repository | |
| uses: actions/checkout@v4 | |
| with: | |
| path: addon | |
| ref: ${{ inputs.ref || github.ref }} | |
| repository: ${{ inputs.repository || github.repository }} | |
| token: ${{ secrets.PAT_TOKEN }} | |
| fetch-depth: 0 | |
| - name: Checkout mobile test framework | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: tetherto/qvac-test-addon-mobile | |
| token: ${{ secrets.PAT_TOKEN }} | |
| path: test-framework | |
| ref: ${{ env.TEST_FRAMEWORK_REF }} | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Configure scoped registry for @qvac and @tetherto packages | |
| env: | |
| GPR_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| GIT_PAT: ${{ secrets.PAT_TOKEN }} | |
| run: | | |
| echo "Configuring scoped registry for @tetherto and @qvac packages..." | |
| cd addon | |
| cat > .npmrc <<NPMRC | |
| always-auth=true | |
| registry=https://registry.npmjs.org/ | |
| @qvac:registry=https://registry.npmjs.org/ | |
| @tetherto:registry=https://npm.pkg.github.com/ | |
| //registry.npmjs.org/:_authToken=${NPM_TOKEN} | |
| //npm.pkg.github.com/:_authToken=${GPR_TOKEN} | |
| NPMRC | |
| cd ../test-framework | |
| cat > .npmrc <<NPMRC | |
| always-auth=true | |
| registry=https://registry.npmjs.org/ | |
| @qvac:registry=https://registry.npmjs.org/ | |
| @tetherto:registry=https://npm.pkg.github.com/ | |
| //registry.npmjs.org/:_authToken=${NPM_TOKEN} | |
| //npm.pkg.github.com/:_authToken=${GPR_TOKEN} | |
| NPMRC | |
| git config --global url."https://${GIT_PAT}:@github.com/".insteadOf "https://github.com/" | |
| - 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' && github.event_name != 'workflow_dispatch' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: ${{ env.PREBUILD_ARTIFACT_PREFIX }}android-* | |
| path: addon/${{ env.PKG_DIR }}/prebuilds | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Download iOS prebuilds (from artifacts) | |
| if: matrix.platform == 'iOS' && github.event_name != 'workflow_dispatch' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: ${{ env.PREBUILD_ARTIFACT_PREFIX }}ios-* | |
| path: addon/${{ env.PKG_DIR }}/prebuilds | |
| merge-multiple: true | |
| continue-on-error: true | |
| - name: Download prebuilds (from npm - workflow_dispatch) | |
| if: github.event_name == 'workflow_dispatch' | |
| working-directory: ./addon/${{ env.PKG_DIR }} | |
| run: | | |
| PACKAGE_SPEC="${{ inputs.package }}" | |
| echo "📦 Downloading $PACKAGE_SPEC from npm for manual trigger..." | |
| PACKAGE_NAME="${PACKAGE_SPEC%@*}" | |
| if ! npm pack "$PACKAGE_SPEC"; 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 | |
| tar -xzf *.tgz | |
| 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 | |
| mv package/prebuilds ./prebuilds | |
| rm -rf package *.tgz | |
| echo "✅ Prebuilds downloaded from npm:" | |
| ls -la prebuilds/ | |
| - name: Verify and prepare prebuilds | |
| working-directory: ./addon/${{ env.PKG_DIR }} | |
| run: | | |
| echo "Checking for prebuilds..." | |
| if [ -d "prebuilds" ] && [ "$(ls -A prebuilds)" ]; then | |
| echo "✅ Prebuilds found from artifacts:" | |
| ls -la prebuilds/ | |
| else | |
| echo "⚠️ No prebuilds from artifacts, checking source..." | |
| if [ -d "prebuilds" ] && [ "$(ls -A prebuilds)" ]; then | |
| echo "✅ Prebuilds found in source:" | |
| 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 | |
| fi | |
| 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: ./addon/${{ env.PKG_DIR }} | |
| run: | | |
| echo "Removing desktop prebuilds to save disk space (keeping Android + iOS)..." | |
| echo "Before cleanup:" | |
| du -sh prebuilds/* 2>/dev/null || true | |
| 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: ./addon/${{ env.PKG_DIR }} | |
| 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 | |
| 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 | |
| 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: ./addon/${{ env.PKG_DIR }} | |
| run: | | |
| echo "Installing addon dependencies..." | |
| npm install | |
| - name: Validate mobile tests are up-to-date | |
| working-directory: ./addon/${{ env.PKG_DIR }} | |
| run: npm run test:mobile:validate | |
| - name: Pack addon | |
| working-directory: ./addon/${{ env.PKG_DIR }} | |
| 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 | |
| fi | |
| 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 | |
| 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}/addon/${{ env.PKG_DIR }}" | |
| npm run build "$ADDON_PATH" "$ADDON_PATH/test/mobile" | |
| echo "" | |
| echo "✅ Test app built successfully" | |
| 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" | |
| 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@v4 | |
| with: | |
| java-version: "17" | |
| distribution: "temurin" | |
| - name: Setup Android SDK | |
| if: matrix.platform == 'Android' | |
| uses: android-actions/setup-android@v3 | |
| - 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 | |
| echo "Bundling JavaScript code..." | |
| npm run bundle | |
| if [ $? -ne 0 ]; then | |
| echo "❌ Bundle failed" | |
| exit 1 | |
| fi | |
| echo "✅ Bundle completed successfully" | |
| cd android | |
| echo "Building APK with Gradle (RELEASE with embedded JS bundle)..." | |
| ./gradlew assembleRelease \ | |
| -PreactNativeArchitectures=arm64-v8a \ | |
| --no-daemon \ | |
| --no-build-cache \ | |
| --stacktrace | |
| cd .. | |
| APK_PATH=$(find android/app/build/outputs/apk -name "*.apk" | grep "release" | grep -v "unaligned" | head -1) | |
| if [ -f "$APK_PATH" ]; then | |
| 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 | |
| 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 | |
| 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.APPLE_DISTRIBUTION_CERTIFICATE }} | |
| P12_PASSWORD: ${{ secrets.APPLE_P12_PASSWORD }} | |
| BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.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 | |
| 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" | |
| 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") | |
| 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 }}" | |
| echo "EXPORT_METHOD=$EXPORT_METHOD" >> $GITHUB_ENV | |
| 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..." | |
| echo "Bundling JavaScript code..." | |
| npm run bundle | |
| if [ $? -ne 0 ]; then | |
| echo "❌ Bundle failed" | |
| exit 1 | |
| fi | |
| echo "✅ Bundle completed successfully" | |
| cd ios | |
| SCHEME_NAME=$(xcodebuild -list | grep -A 1 "Schemes:" | grep -v "Schemes:" | head -1 | xargs) | |
| echo "Detected scheme: $SCHEME_NAME" | |
| 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 | |
| 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" | |
| 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) | |
| 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 | |
| uses: aws-actions/configure-aws-credentials@v3 | |
| with: | |
| aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | |
| aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| aws-region: us-west-2 | |
| - 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.AWS_DEVICE_FARM_PROJECT_ARN_LLAMACPP_EMBED }}" \ | |
| --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 | |
| 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 |