Expo EAS
Build iOS Simulator and Android APK artifacts in Expo EAS, hand them off to Autonomy, and run mobile journeys on every change.
Expo EAS owns the build pipeline for Expo projects. Use this integration when you want EAS to produce the iOS Simulator .app or the Android .apk and Autonomy to run mobile journeys against it on push, pull request, or manual dispatch.
Prerequisites
Before enabling EAS runs
- An Expo project with EAS already configured for iOS.
- An Autonomy project linked to the repository.
- At least one mobile test that passes against a manual upload.
- An Autonomy test plan id for each platform the EAS workflow should trigger.
Required secrets
Store these as EAS environment variables or sensitive secrets. Do not commit them, and do not expose them through an EXPO_PUBLIC_* variable — Expo inlines those into the client bundle.
| Variable | Required | Description |
|---|---|---|
| AUTONOMY_API_KEY | Yes | Autonomy API key with permission to upload builds and trigger runs. |
| AUTONOMY_API_URL | Yes | Autonomy API origin, for example https://api.autonomyqa.com or your Convex site URL. |
| AUTONOMY_IOS_TEST_PLAN_ID | No | Test plan to execute against the uploaded iOS build. Required if the workflow runs iOS. |
| AUTONOMY_ANDROID_TEST_PLAN_ID | No | Test plan to execute against the uploaded Android build. Required if the workflow runs Android. |
| AUTONOMY_IOS_BUNDLE_ID | No | iOS bundle identifier for the Simulator app target. |
| AUTONOMY_ANDROID_PACKAGE_NAME | No | Android package name for the APK or AAB target. |
You can find these in the Autonomy dashboard:
- API key: Profile → API Keys
- API URL: Project settings or deployment settings
- Test plan IDs: Tests tab → plan menu → Copy ID. Use a separate plan per platform so failures point at the right surface.
Build profiles
Add or reuse build profiles in eas.json. For iOS, Autonomy needs a .app Simulator build, not a signed .ipa. The profile must set ios.simulator to true. For Android, use a development or internal-distribution profile that emits an APK.
1{2 "build": {3 "autonomy-ios-simulator": {4 "distribution": "internal",5 "ios": {6 "simulator": true7 }8 },9 "autonomy-android-apk": {10 "distribution": "internal",11 "android": {12 "buildType": "apk"13 }14 }15 }16}Workflow
Add .eas/workflows/autonomy-mobile-tests.yml. The workflow builds each platform you care about, downloads the artifact, uploads it through the artifact API, scans it, and triggers the matching Autonomy test plan.
The example below runs both platforms. Drop the job you do not need if you ship one platform only.
name: Autonomy Mobile Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
ios:
name: Build and test iOS with Autonomy
type: build
params:
platform: ios
profile: autonomy-ios-simulator
steps:
- eas/checkout
- eas/install_node_modules
- eas/prebuild
- eas/build
- eas/download_build:
id: download
path: ./build
- name: Upload to Autonomy and trigger plan
env:
AUTONOMY_API_KEY: ${{ secrets.AUTONOMY_API_KEY }}
AUTONOMY_API_URL: ${{ secrets.AUTONOMY_API_URL }}
AUTONOMY_TEST_PLAN_ID: ${{ secrets.AUTONOMY_IOS_TEST_PLAN_ID }}
AUTONOMY_IOS_BUNDLE_ID: ${{ secrets.AUTONOMY_IOS_BUNDLE_ID }}
run: |
set -euo pipefail
COMMIT_SHA="${EAS_BUILD_GIT_COMMIT_HASH:-}"
APP_PATH=$(find ./build -name "*.app" -type d | head -n 1)
test -n "$APP_PATH"
APP_DIR=$(dirname "$APP_PATH")
APP_NAME=$(basename "$APP_PATH")
(cd "$APP_DIR" && zip -qr app.zip "$APP_NAME")
UPLOAD_URL=$(curl -fsS -X POST "$AUTONOMY_API_URL/api/artifacts/upload-url" \
-H "Authorization: Bearer $AUTONOMY_API_KEY" \
-H "Content-Type: application/json" | jq -r '.uploadUrl')
STORAGE_ID=$(curl -fsS -X POST "$UPLOAD_URL" \
-H "Content-Type: application/zip" \
--data-binary "@$APP_DIR/app.zip" | jq -r '.storageId')
curl -fsS -X POST "$AUTONOMY_API_URL/api/artifacts/scan" \
-H "Authorization: Bearer $AUTONOMY_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg storageId "$STORAGE_ID" '{ storageId: $storageId, platform: "ios", fileName: "app.zip", contentType: "application/zip" }')"
curl -fsS -X POST "$AUTONOMY_API_URL/api/trigger" \
-H "Authorization: Bearer $AUTONOMY_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg testPlanId "$AUTONOMY_TEST_PLAN_ID" \
--arg storageId "$STORAGE_ID" \
--arg bundleId "$AUTONOMY_IOS_BUNDLE_ID" \
--arg commitSha "$COMMIT_SHA" \
'{ testPlanId: $testPlanId, platforms: ["ios"], targets: { ios: { storageId: $storageId, sourceMode: "upload", bundleId: $bundleId, fileName: "app.zip" } }, deployment: { provider: "expo-eas", commitSha: $commitSha } }')"
android:
name: Build and test Android with Autonomy
type: build
params:
platform: android
profile: autonomy-android-apk
steps:
- eas/checkout
- eas/install_node_modules
- eas/prebuild
- eas/build
- eas/download_build:
id: download
path: ./build
- name: Upload to Autonomy and trigger plan
env:
AUTONOMY_API_KEY: ${{ secrets.AUTONOMY_API_KEY }}
AUTONOMY_API_URL: ${{ secrets.AUTONOMY_API_URL }}
AUTONOMY_TEST_PLAN_ID: ${{ secrets.AUTONOMY_ANDROID_TEST_PLAN_ID }}
AUTONOMY_ANDROID_PACKAGE_NAME: ${{ secrets.AUTONOMY_ANDROID_PACKAGE_NAME }}
run: |
set -euo pipefail
COMMIT_SHA="${EAS_BUILD_GIT_COMMIT_HASH:-}"
APK_PATH=$(find ./build ( -name "*.apk" -o -name "*.aab" ) | head -n 1)
test -n "$APK_PATH"
FILE_NAME=$(basename "$APK_PATH")
UPLOAD_URL=$(curl -fsS -X POST "$AUTONOMY_API_URL/api/artifacts/upload-url" \
-H "Authorization: Bearer $AUTONOMY_API_KEY" \
-H "Content-Type: application/json" | jq -r '.uploadUrl')
STORAGE_ID=$(curl -fsS -X POST "$UPLOAD_URL" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$APK_PATH" | jq -r '.storageId')
curl -fsS -X POST "$AUTONOMY_API_URL/api/artifacts/scan" \
-H "Authorization: Bearer $AUTONOMY_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg storageId "$STORAGE_ID" --arg fileName "$FILE_NAME" '{ storageId: $storageId, platform: "android", fileName: $fileName, contentType: "application/octet-stream" }')"
curl -fsS -X POST "$AUTONOMY_API_URL/api/trigger" \
-H "Authorization: Bearer $AUTONOMY_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg testPlanId "$AUTONOMY_TEST_PLAN_ID" \
--arg storageId "$STORAGE_ID" \
--arg packageName "$AUTONOMY_ANDROID_PACKAGE_NAME" \
--arg fileName "$FILE_NAME" \
--arg commitSha "$COMMIT_SHA" \
'{ testPlanId: $testPlanId, platforms: ["android"], targets: { android: { storageId: $storageId, sourceMode: "upload", packageName: $packageName, fileName: $fileName } }, deployment: { provider: "expo-eas", commitSha: $commitSha } }')"GitHub comments and checks
Pass the commit SHA whenever the EAS or GitHub context provides one so Autonomy can post a check run on the commit. Pass the PR number only when the workflow is running for a pull request. The workflow above guards both so it stays valid on push, PR, and manual dispatch.
Troubleshooting
Autonomy rejects the iOS build as a device build
The EAS profile is producing a signed .ipa instead of a Simulator .app. Confirm ios.simulator is true on the profile the workflow runs.
EAS emits an AAB instead of an APK
For the simplest install path, the Android profile should set android.buildType to apk. Autonomy also accepts .aab, but APK is faster to install on the emulator and avoids signing surprises. If you must ship AAB, make sure the artifact upload step selects the .aab file.
Autonomy cannot post a PR comment
The Autonomy GitHub App must be installed on the repository and the workflow must pass the commit SHA. Without those, Autonomy has no anchor for the comment.
Uploads accumulate in the dashboard
Use explicit run targets for short-lived EAS artifacts and saved environments for builds that should be reused. Reserve dashboard uploads for builds a release manager picks for production runs.