Expo EAS
Run TesterArmy mobile tests from Expo EAS workflows by uploading an EAS iOS Simulator build or Android APK and triggering a remote test group.
Prerequisites
Before you set it up, make sure you have already:
- Created mobile project (can be without uploaded app)
- Created at least one mobile test
Also make sure that you have configured Expo EAS with Github
Required environment variables
These values can be found in the TesterArmy Dashboard:
- API key - Profile → API Keys
- Project ID - Project Settings
- Group ID - Test Tab, then click on three dots on the group you want to run tests on and copy the ID
Set TESTERARMY_API_KEY as an EAS secret or sensitive environment variable. Do not commit it, and
do not expose it through an EXPO_PUBLIC_* environment variable.
Instructions for AI agents
Below you will find instructions for AI agent to set up EAS integration with TesterArmy for you.
Setup Expo EAS integration with TesterArmy
You are working in an Expo app repository. Add TesterArmy mobile testing through Expo EAS workflows.
Before editing files:
* Inspect the existing `eas.json`, `.eas/workflows`, `package.json`, and package manager lockfile.
* Do not overwrite an existing EAS workflow. Create a new workflow file or make the smallest safe edit.
* Do not hard-code secrets, API keys, project IDs, or group IDs in committed files.
* Use the repository’s existing package manager style when adding commands.
Implement this setup:
1. Detect which platforms are already configured in the app (`ios`, `android`, or both). If it is not clear, ask the user whether to set up iOS, Android, or both before editing workflow files.
2. For iOS, ensure `eas.json` has a TesterArmy iOS Simulator build profile. Prefer `testerarmy-ios-simulator`. If the project already has a compatible simulator profile, reuse it and keep the workflow profile name in sync.
3. For Android, ensure `eas.json` has a TesterArmy APK build profile. Prefer `testerarmy-android-apk`. If the project already has a compatible APK profile, reuse it and keep the workflow profile name in sync.
4. The iOS profile must set `ios.simulator` to `true`. TesterArmy needs a `.app` simulator build, not an `.ipa` device build.
5. The Android profile must produce an APK. Prefer `android.buildType: "apk"`. TesterArmy needs a `.apk`; `.aab` and `.xapk` are not supported.
6. Create `.eas/workflows/testerarmy-mobile-tests.yml` if it does not already exist.
7. The workflow should support `push` to `main`, `pull_request` to `main`, and `workflow_dispatch`.
8. The workflow should read these values from EAS environment variables:
* `TESTERARMY_API_KEY`
* `TESTERARMY_PROJECT_ID`
* `TESTERARMY_GROUP_ID`
* `TESTERARMY_DYNAMIC_AGENT_ENABLED` (optional; defaults to `true`, set to `false` to skip the dynamic PR agent)
9. The workflow should build only the selected platform artifacts: an iOS Simulator app, an Android APK, or both.
10. The workflow should download the selected build artifacts with `eas/download_build`.
11. The workflow should upload each downloaded artifact with `npx --yes testerarmy@latest upload-app`.
12. The workflow should run the saved TesterArmy group for each selected platform, using `--platform ios` for iOS and `--platform android` for Android, plus the uploaded app ID for that platform.
13. The workflow should pass commit SHA for GitHub check reporting when the EAS/GitHub context provides it.
14. The workflow should pass PR number only when the workflow is running for a pull request.
15. The workflow should clean up each uploaded app. When you add the dynamic PR agent (step 16), the test and agent jobs share one upload, so set `--remove-after 86400` on the upload step and let TesterArmy remove it. Otherwise, you can delete each upload after its test run.
16. Optionally add the dynamic PR agent: for each selected platform add a job that `needs` the matching upload job, runs only when `github.event_name == 'pull_request'`, respects `TESTERARMY_DYNAMIC_AGENT_ENABLED` (default `true`), and runs `npx --yes testerarmy@latest pr run-dynamic` with `--project`, `--platform`, `--app-id` (from the upload job output), `--pr-number`, `--pr-title`, and `--commit-sha`. Pass `--pr-description`, `--base-branch`, and `--head-branch` when available.
Use this workflow as the baseline and adapt it to the repository if needed:
```yaml
name: TesterArmy Mobile Tests
on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch: {}
jobs:
build_ios:
name: Build iOS Simulator app
type: build
environment: preview
params:
platform: ios
profile: testerarmy-ios-simulator
build_android:
name: Build Android app
type: build
environment: preview
params:
platform: android
profile: testerarmy-android-apk
upload_ios_app:
name: Upload iOS app to TesterArmy
needs: [build_ios]
environment: preview
outputs:
app_id: ${{ fromJSON(steps.upload_app.outputs.upload_result).uploadedAppId }}
steps:
- uses: eas/checkout
- uses: eas/download_build
id: download_build
with:
build_id: ${{ needs.build_ios.outputs.build_id }}
extensions:
- app
- name: Upload app
id: upload_app
run: |
set -euo pipefail
APP_PATH="${{ steps.download_build.outputs.artifact_path }}"
mkdir -p .testerarmy
npx --yes testerarmy@latest upload-app \
--app-path "$APP_PATH" \
--project "$TESTERARMY_PROJECT_ID" \
--remove-after 86400 \
--output .testerarmy/upload.json
set-output upload_result "$(tr -d '\n' < .testerarmy/upload.json)"
upload_android_app:
name: Upload Android app to TesterArmy
needs: [build_android]
environment: preview
outputs:
app_id: ${{ fromJSON(steps.upload_app.outputs.upload_result).uploadedAppId }}
steps:
- uses: eas/checkout
- uses: eas/download_build
id: download_build
with:
build_id: ${{ needs.build_android.outputs.build_id }}
extensions:
- apk
- name: Upload app
id: upload_app
run: |
set -euo pipefail
APP_PATH="${{ steps.download_build.outputs.artifact_path }}"
mkdir -p .testerarmy
npx --yes testerarmy@latest upload-app \
--app-path "$APP_PATH" \
--project "$TESTERARMY_PROJECT_ID" \
--remove-after 86400 \
--output .testerarmy/upload.json
set-output upload_result "$(tr -d '\n' < .testerarmy/upload.json)"
run_ios_tests:
name: Run iOS TesterArmy tests
needs: [upload_ios_app]
environment: preview
steps:
- uses: eas/checkout
- name: Run tests
run: |
set -euo pipefail
APP_ID="${{ needs.upload_ios_app.outputs.app_id }}"
COMMIT_SHA="${{ github.sha }}"
EVENT_NAME="${{ github.event_name }}"
PR_NUMBER="${{ github.event.pull_request.number || '' }}"
TIMEOUT_MS="1800000"
POLL_INTERVAL_SECONDS="10"
mkdir -p .testerarmy
args=(
ci
--group "$TESTERARMY_GROUP_ID"
--project "$TESTERARMY_PROJECT_ID"
--platform ios
--app-id "$APP_ID"
--commit-sha "$COMMIT_SHA"
--timeout "$TIMEOUT_MS"
--poll-interval-seconds "$POLL_INTERVAL_SECONDS"
--output .testerarmy/ci-result.json
)
if [ "$EVENT_NAME" = "pull_request" ] && [ -n "$PR_NUMBER" ]; then
args+=(--pr-number "$PR_NUMBER")
fi
npx --yes testerarmy@latest "${args[@]}"
run_android_tests:
name: Run Android TesterArmy tests
needs: [upload_android_app]
environment: preview
steps:
- uses: eas/checkout
- name: Run tests
run: |
set -euo pipefail
APP_ID="${{ needs.upload_android_app.outputs.app_id }}"
COMMIT_SHA="${{ github.sha }}"
EVENT_NAME="${{ github.event_name }}"
PR_NUMBER="${{ github.event.pull_request.number || '' }}"
TIMEOUT_MS="1800000"
POLL_INTERVAL_SECONDS="10"
mkdir -p .testerarmy
args=(
ci
--group "$TESTERARMY_GROUP_ID"
--project "$TESTERARMY_PROJECT_ID"
--platform android
--app-id "$APP_ID"
--commit-sha "$COMMIT_SHA"
--timeout "$TIMEOUT_MS"
--poll-interval-seconds "$POLL_INTERVAL_SECONDS"
--output .testerarmy/ci-result.json
)
if [ "$EVENT_NAME" = "pull_request" ] && [ -n "$PR_NUMBER" ]; then
args+=(--pr-number "$PR_NUMBER")
fi
npx --yes testerarmy@latest "${args[@]}"
run_ios_dynamic_agent:
name: Run iOS TesterArmy dynamic agent
needs: [upload_ios_app]
if: ${{ github.event_name == 'pull_request' }}
environment: preview
env:
APP_ID: ${{ needs.upload_ios_app.outputs.app_id }}
COMMIT_SHA: ${{ github.sha }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
PR_TITLE: ${{ github.event.pull_request.title || '' }}
PR_DESCRIPTION: ${{ github.event.pull_request.body || '' }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref || '' }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref || '' }}
steps:
- uses: eas/checkout
- name: Run dynamic PR agent
run: |
set -euo pipefail
if [ "${TESTERARMY_DYNAMIC_AGENT_ENABLED:-true}" = "false" ]; then
echo "TesterArmy dynamic agent is disabled because TESTERARMY_DYNAMIC_AGENT_ENABLED=false."
exit 0
fi
if [ -z "$PR_NUMBER" ]; then
echo "No pull request number found; skipping TesterArmy dynamic agent."
exit 0
fi
mkdir -p .testerarmy
dynamic_pr_title="$PR_TITLE"
if [ -z "$dynamic_pr_title" ]; then
dynamic_pr_title="Pull request #$PR_NUMBER"
fi
args=(
pr
run-dynamic
--project "$TESTERARMY_PROJECT_ID"
--platform ios
--app-id "$APP_ID"
--pr-number "$PR_NUMBER"
--pr-title "$dynamic_pr_title"
--commit-sha "$COMMIT_SHA"
--output .testerarmy/dynamic-result.json
)
if [ -n "$PR_DESCRIPTION" ]; then
args+=(--pr-description "$PR_DESCRIPTION")
fi
if [ -n "$BASE_BRANCH" ]; then
args+=(--base-branch "$BASE_BRANCH")
fi
if [ -n "$HEAD_BRANCH" ]; then
args+=(--head-branch "$HEAD_BRANCH")
fi
npx --yes testerarmy@latest "${args[@]}"
run_android_dynamic_agent:
name: Run Android TesterArmy dynamic agent
needs: [upload_android_app]
if: ${{ github.event_name == 'pull_request' }}
environment: preview
env:
APP_ID: ${{ needs.upload_android_app.outputs.app_id }}
COMMIT_SHA: ${{ github.sha }}
PR_NUMBER: ${{ github.event.pull_request.number || '' }}
PR_TITLE: ${{ github.event.pull_request.title || '' }}
PR_DESCRIPTION: ${{ github.event.pull_request.body || '' }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref || '' }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref || '' }}
steps:
- uses: eas/checkout
- name: Run dynamic PR agent
run: |
set -euo pipefail
if [ "${TESTERARMY_DYNAMIC_AGENT_ENABLED:-true}" = "false" ]; then
echo "TesterArmy dynamic agent is disabled because TESTERARMY_DYNAMIC_AGENT_ENABLED=false."
exit 0
fi
if [ -z "$PR_NUMBER" ]; then
echo "No pull request number found; skipping TesterArmy dynamic agent."
exit 0
fi
mkdir -p .testerarmy
dynamic_pr_title="$PR_TITLE"
if [ -z "$dynamic_pr_title" ]; then
dynamic_pr_title="Pull request #$PR_NUMBER"
fi
args=(
pr
run-dynamic
--project "$TESTERARMY_PROJECT_ID"
--platform android
--app-id "$APP_ID"
--pr-number "$PR_NUMBER"
--pr-title "$dynamic_pr_title"
--commit-sha "$COMMIT_SHA"
--output .testerarmy/dynamic-result.json
)
if [ -n "$PR_DESCRIPTION" ]; then
args+=(--pr-description "$PR_DESCRIPTION")
fi
if [ -n "$BASE_BRANCH" ]; then
args+=(--base-branch "$BASE_BRANCH")
fi
if [ -n "$HEAD_BRANCH" ]; then
args+=(--head-branch "$HEAD_BRANCH")
fi
npx --yes testerarmy@latest "${args[@]}"
```
Because the test and dynamic agent jobs share one upload, the upload steps set `--remove-after 86400` and let TesterArmy clean up the app on its own.
Use these command shapes:
```bash
npx --yes testerarmy@latest upload-app \
--app-path "$APP_PATH" \
--project "$PROJECT_ID" \
--output .testerarmy/upload.json
```
```bash
npx --yes testerarmy@latest ci \
--group "$GROUP_ID" \
--project "$PROJECT_ID" \
--platform ios \
--app-id "$APP_ID" \
--commit-sha "$COMMIT_SHA" \
--timeout "$TIMEOUT_MS" \
--poll-interval-seconds "$POLL_INTERVAL_SECONDS" \
--output .testerarmy/ci-result.json
```
For the dynamic PR agent (pull requests only):
```bash
npx --yes testerarmy@latest pr run-dynamic \
--project "$PROJECT_ID" \
--platform ios \
--app-id "$APP_ID" \
--pr-number "$PR_NUMBER" \
--pr-title "$PR_TITLE" \
--commit-sha "$COMMIT_SHA" \
--output .testerarmy/dynamic-result.json
```
Use `--platform android` for Android runs. Pass `--pr-description`, `--base-branch`, and `--head-branch` when available.
If a PR number is available, append to the `ci` command:
```bash
--pr-number "$PR_NUMBER"
```
For cleanup, set `--remove-after 86400` on the `upload-app` step so TesterArmy removes the shared upload automatically:
```bash
--remove-after 86400
```
After editing:
* Show the user which files changed.
* Tell the user to configure `TESTERARMY_API_KEY`, `TESTERARMY_PROJECT_ID`, and `TESTERARMY_GROUP_ID` in their EAS environment. If you added the dynamic agent, mention the optional `TESTERARMY_DYNAMIC_AGENT_ENABLED` toggle.
* Tell the user which EAS workflow command to run.
* Do not run the real TesterArmy CLI or EAS build unless the user explicitly asks you to.
Finish by summarizing the files changed and the manual test command.Configure mobile build profiles
TesterArmy requires an iOS Simulator build profile and an Android APK build profile in EAS. Add the following snippet to your eas.json file:
For Android builds, make sure your Expo config has an Android package name, for example
android.package in app.json.
Add the EAS workflow
Basic
Fingerprint + Repack
With dynamic PR agent
Here is an example workflow file that:
- Builds the iOS Simulator app and Android APK
- Uploads each app to TesterArmy
- Runs the saved test group once for iOS and once for Android
- Deletes each uploaded app from TesterArmy after the tests complete
Dynamic PR agent
On pull requests you can also run the dynamic PR agent, which explores what a pull request changes instead of running a fixed test group. See the With dynamic PR agent tab above for a complete workflow.
The agent runs on the same app you already uploaded and only on pull requests. To turn it off, set TESTERARMY_DYNAMIC_AGENT_ENABLED=false.
If a pull request doesn’t change anything users can see in the app, the dynamic run is reported as skipped and still passes, so it never blocks the PR. Real failures and cancellations still fail the job.
GitHub PR comments and checks
When your workflow is triggered by a pull request, TesterArmy will create a GitHub check and comment on the PR.
When workflow is triggered by a commit (eg. on push to main), TesterArmy will create a GitHub check on the commit.
To enable this, you need to configure your TesterArmy project with your GitHub repository. You can do it in Integrations Tab in project.
Example repository
You can find a working example repository here.
Troubleshooting
The build has an unsupported platform or format
TesterArmy requires an iOS Simulator .app build or Android .apk build. If you are using an iOS physical-device build, build a simulator app first. If you are using Android, make sure the EAS profile produces an APK, not an .aab.
Prefer release builds for CI, especially for Android runs that execute without a Metro dev server.
TesterArmy warns that GitHub integration is not configured / TesterArmy cannot create a PR comment
If you are seeing this error, it means that your TesterArmy project has no GitHub integration configured. You can configure it in Integrations Tab in project.
