GitHub Actions

View as Markdown

GitHub Actions is one way to run mobile tests automatically from CI. For Expo apps, we recommend using Expo EAS instead.

You only need to provide an iOS Simulator build or Android APK and your TesterArmy credentials. TesterArmy handles the cloud simulator/emulator setup, app installation, test execution, and run orchestration for you, so you do not need to manage devices in your CI pipeline.

For Expo EAS workflows, see Expo EAS.

Before you set it up, make sure you have already:

  • built and uploaded a mobile app artifact,
  • created at least one mobile test in TesterArmy,
  • run that test manually once,
  • added the test to a group.

If you want a working reference, see the mobile GitHub Action and the mobile example app.

What you need

Add these GitHub secrets to your repository:

SecretDescription
TESTERARMY_API_KEYAPI key from Profile → API Keys
TESTERARMY_PROJECT_IDYour TesterArmy project ID from Project Settings
TESTERARMY_GROUP_IDTest group ID for the mobile tests you want to run

You can copy the group ID from the test group menu in the dashboard.

The most reliable setup is:

  1. Build the app artifact in a dedicated job.
  2. Upload the .app bundle or .apk as a GitHub artifact.
  3. Download that artifact on Linux.
  4. Run tester-army/mobile-github-action with the matching platform to upload the app, run the group, wait for results, and clean up.
1name: iOS Mobile Tests
2
3on:
4 push:
5 branches:
6 - main
7 pull_request:
8 workflow_dispatch:
9
10permissions:
11 contents: read
12
13jobs:
14 build_ios:
15 name: Build iOS Simulator app
16 runs-on: macos-latest
17
18 steps:
19 - name: Check out repository
20 uses: actions/checkout@v5
21
22 - name: Build iOS Simulator app
23 shell: bash
24 run: |
25 set -euo pipefail
26 xcodebuild \
27 -workspace ios/MyApp.xcworkspace \
28 -scheme MyApp \
29 -configuration Release \
30 -sdk iphonesimulator \
31 -destination 'generic/platform=iOS Simulator' \
32 -derivedDataPath ios/build \
33 build
34
35 - name: Copy .app to artifact directory
36 shell: bash
37 run: |
38 set -euo pipefail
39 mkdir -p .build
40 cp -R ios/build/Build/Products/Release-iphonesimulator/MyApp.app .build/MyApp.app
41
42 - name: Upload .app artifact
43 uses: actions/upload-artifact@v5
44 with:
45 name: ios-simulator-app
46 path: .build/MyApp.app
47 retention-days: 1
48
49 test_ios:
50 name: Run iOS TesterArmy tests
51 needs: build_ios
52 runs-on: ubuntu-latest
53
54 steps:
55 - name: Download .app artifact
56 uses: actions/download-artifact@v5
57 with:
58 name: ios-simulator-app
59 path: .build/MyApp.app
60
61 - name: Upload app and run TesterArmy tests
62 id: mobile
63 uses: tester-army/mobile-github-action@v1
64 with:
65 app_path: .build/MyApp.app
66 platform: ios
67 api_key: ${{ secrets.TESTERARMY_API_KEY }}
68 project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
69 group_id: ${{ secrets.TESTERARMY_GROUP_ID }}
70 delete_app_after_run: "true"
71 remove_after: "3600"
72
73 - name: Print overall status
74 run: echo "Overall status: ${{ steps.mobile.outputs.overall_status }}"

No manual upload step in CI

The GitHub Action accepts either a .app directory, an archived iOS app file, or an Android .apk. If you pass a .app directory, the action zips and uploads it for you.

What the action does

By default (mode: all) tester-army/mobile-github-action handles the full CI flow for you in a single job:

  1. Uploads the app to TesterArmy.
  2. Provisions the cloud simulator or emulator environment and installs the app for the run.
  3. Starts your mobile test group and orchestrates the run lifecycle.
  4. Polls until the runs finish.
  5. Deletes the uploaded app after the run if cleanup is enabled.

Results are written to the GitHub step summary so you can quickly see pass/fail status, duration, issues, and screenshots.

Action modes

The mode input selects what a single invocation does, so you can keep the simple one-job setup or split the work across jobs:

ModeWhat it does
all (default)Uploads the app and runs the dashboard group in one job.
uploadUploads the app and exposes the app_id output for downstream jobs.
testRuns the dashboard group against an existing app_id.
dynamic_agentRuns the dynamic PR agent against an existing app_id on pull request events.

Run defined tests and the dynamic PR agent in parallel

The dynamic PR agent explores what a pull request changes instead of running a fixed group. To run it alongside your defined tests, split the flow into separate jobs: upload the app once, then run the test and dynamic_agent jobs in parallel against the uploaded app_id. Composite actions cannot create jobs, so the split lives in your workflow.

This example covers one platform. To test both iOS and Android, duplicate the upload, defined_tests, and dynamic_agent jobs for the second platform, point the upload job at the Android artifact, and set platform: android in its test and dynamic agent jobs.

1jobs:
2 upload:
3 runs-on: ubuntu-latest
4 outputs:
5 app_id: ${{ steps.upload.outputs.app_id }}
6 steps:
7 - uses: actions/download-artifact@v5
8 with:
9 name: ios-simulator-app
10 path: .build/MyApp.app
11
12 - uses: tester-army/mobile-github-action@v1
13 id: upload
14 with:
15 mode: upload
16 app_path: .build/MyApp.app
17 api_key: ${{ secrets.TESTERARMY_API_KEY }}
18 project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
19 remove_after: "86400"
20
21 defined_tests:
22 runs-on: ubuntu-latest
23 needs: upload
24 steps:
25 - uses: tester-army/mobile-github-action@v1
26 id: defined
27 with:
28 mode: test
29 app_id: ${{ needs.upload.outputs.app_id }}
30 platform: ios
31 api_key: ${{ secrets.TESTERARMY_API_KEY }}
32 project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
33 group_id: ${{ secrets.TESTERARMY_GROUP_ID }}
34
35 - run: echo "Defined tests: ${{ steps.defined.outputs.overall_status }}"
36
37 dynamic_agent:
38 runs-on: ubuntu-latest
39 needs: upload
40 if: ${{ github.event_name == 'pull_request' }}
41 steps:
42 - uses: tester-army/mobile-github-action@v1
43 id: dynamic
44 with:
45 mode: dynamic_agent
46 app_id: ${{ needs.upload.outputs.app_id }}
47 platform: ios
48 api_key: ${{ secrets.TESTERARMY_API_KEY }}
49 project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
50
51 - run: echo "Dynamic agent: ${{ steps.dynamic.outputs.dynamic_agent_status }}"

The upload job sets remove_after so TesterArmy removes the shared app on its own once both jobs are done.

The dynamic_agent job only runs on pull requests, so gate it with if: ${{ github.event_name == 'pull_request' }}. To add an on/off switch, you can also check a repository variable such as vars.TESTERARMY_DYNAMIC_AGENT_ENABLED == 'true'.

Direct CLI usage

For CI providers such as Expo EAS, Fastlane, Buildkite, or GitLab CI, upload the app with testerarmy upload-app, then use the returned app ID in the provider-agnostic CLI flow:

$testerarmy ci \
> --group "$GROUP_ID" \
> --project "$PROJECT_ID" \
> --platform ios \
> --app-id "$APP_ID" \
> --commit-sha "$COMMIT_SHA" \
> --pr-number "$PR_NUMBER"

Use --platform android for Android app runs. ci calls POST /api/v1/groups/{groupId}/runs, waits for every returned run to finish, and deletes only the explicit --app-id when cleanup is enabled and the run did not time out.

Dynamic PR agent

The dynamic PR agent explores what a pull request changes against a mobile build instead of running a predefined test group. In the GitHub Action, run it with mode: dynamic_agent (see Run defined tests and the dynamic PR agent in parallel). On other CI providers, call the CLI directly with testerarmy pr run-dynamic:

$testerarmy pr run-dynamic \
> --project "$PROJECT_ID" \
> --platform android \
> --app-id "$TESTERARMY_APP_ID" \
> --pr-number "$PR_NUMBER" \
> --pr-title "$PR_TITLE" \
> --commit-sha "$GITHUB_SHA"

Use --platform ios with an iOS Simulator .app build or --platform android with an Android .apk. You can use --app-id, --bundle-id, or --artifact-url with --artifact-filename, and pass optional PR context with --pr-description, --base-branch, and --head-branch. The command waits for the run to finish and uses the same result output as group CI runs.

If your project has a supported GitHub connection, TesterArmy looks at what a pull request actually changes before running. If nothing in the change is relevant to the selected platform, TesterArmy can skip the run instead of spinning up a simulator or emulator. A skipped run is neutral — it’s reported as cancelled with a short reason explaining why, not as a failed test — so it never blocks your PR. When a change can’t be clearly assessed, TesterArmy runs the tests rather than skipping.

If you do not want to use the CLI, call the pull request run API directly with platform: "ios" or "android" and a mobile app selector:

$curl -X POST "https://tester.army/api/v1/projects/$PROJECT_ID/pull-request-runs" \
> -H "Authorization: Bearer $TESTERARMY_API_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "platform": "android",
> "pullRequest": {
> "number": 123,
> "title": "Improve onboarding",
> "description": "PR body or CI summary",
> "commitSha": "'$GITHUB_SHA'"
> },
> "mobile": {
> "appId": "'$TESTERARMY_APP_ID'"
> }
> }'

The endpoint does not require changed files; if a supported GitHub connection exists, TesterArmy enriches the plan server-side and may skip execution for irrelevant changes as described above.

Important options

InputRequiredDescription
app_pathFor all, uploadPath to the iOS Simulator .app/archive or Android .apk to upload
api_keyYesTesterArmy API key
project_idYesTesterArmy project ID
group_idFor all, testTesterArmy test group ID
app_idFor test, PR dynamic_agentExisting TesterArmy uploaded app ID to reuse
modeNoAction mode: all, upload, test, or dynamic_agent. Defaults to all
platformNoMobile runtime platform: ios or android. Defaults to ios
delete_app_after_runNoDelete the uploaded app after terminal runs, only when the same invocation owns the upload. Defaults to true
remove_afterNoSeconds before TesterArmy auto-removes the upload. 0 disables it. Defaults to 3600

Outputs

The action exposes these outputs:

  • app_id — the uploaded or supplied TesterArmy app ID
  • overall_statuspassed, failed, or timed_out
  • dynamic_agent_statuspassed, failed, timed_out, or skipped

You can use overall_status or dynamic_agent_status in later workflow steps if you want custom reporting or notifications.

Troubleshooting

The app upload fails immediately

Make sure you are passing an iOS Simulator .app / .app.zip or an Android .apk. An .ipa, .aab, or .xapk will not work.

The action cannot find your build

Double-check the app path inside the build job. The most common iOS output path is:

$ios/build/Build/Products/Release-iphonesimulator/MyApp.app

For Android release .apk files, the output is usually under:

$android/app/build/outputs/apk/release/

The workflow starts but no tests run

Make sure your TESTERARMY_GROUP_ID points to a group that already contains mobile tests.