Skip to content

.github/workflows/integration-mobile-test-qvac-lib-infer-llamacpp-embed.yml #4

.github/workflows/integration-mobile-test-qvac-lib-infer-llamacpp-embed.yml

.github/workflows/integration-mobile-test-qvac-lib-infer-llamacpp-embed.yml #4

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