> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tester.army/llms.txt
> Use this file to discover all available pages before exploring further.

# GitHub Actions

> Automate mobile testing from GitHub Actions: upload your iOS Simulator or Android build and run a TesterArmy test group on every change.

GitHub Actions is one way to run mobile tests automatically from CI. For Expo apps, we recommend using [Expo EAS](/mobile/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](/mobile/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](https://github.com/tester-army/mobile-github-action) and the [mobile example app](https://github.com/tester-army/mobile-example).

## What you need

Add these GitHub secrets to your repository:

| Secret | Description |
| - | - |
| `TESTERARMY_API_KEY` | API key from **Settings → API Keys** |
| `TESTERARMY_PROJECT_ID` | Your TesterArmy project ID from **Project Settings** |
| `TESTERARMY_GROUP_ID` | Test group ID for the mobile tests you want to run |

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

## Recommended workflow

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.

<Tabs>
  <Tab title="iOS">
    ```yaml theme={"theme":"vesper"}
    name: iOS Mobile Tests

    on:
      push:
        branches:
          - main
      pull_request:
      workflow_dispatch:

    permissions:
      contents: read

    jobs:
      build_ios:
        name: Build iOS Simulator app
        runs-on: macos-latest

        steps:
          - name: Check out repository
            uses: actions/checkout@v5

          - name: Build iOS Simulator app
            shell: bash
            run: |
              set -euo pipefail
              xcodebuild \
                -workspace ios/MyApp.xcworkspace \
                -scheme MyApp \
                -configuration Release \
                -sdk iphonesimulator \
                -destination 'generic/platform=iOS Simulator' \
                -derivedDataPath ios/build \
                build

          - name: Copy .app to artifact directory
            shell: bash
            run: |
              set -euo pipefail
              mkdir -p .build
              cp -R ios/build/Build/Products/Release-iphonesimulator/MyApp.app .build/MyApp.app

          - name: Upload .app artifact
            uses: actions/upload-artifact@v5
            with:
              name: ios-simulator-app
              path: .build/MyApp.app
              retention-days: 1

      test_ios:
        name: Run iOS TesterArmy tests
        needs: build_ios
        runs-on: ubuntu-latest

        steps:
          - name: Download .app artifact
            uses: actions/download-artifact@v5
            with:
              name: ios-simulator-app
              path: .build/MyApp.app

          - name: Upload app and run TesterArmy tests
            id: mobile
            uses: tester-army/mobile-github-action@v1
            with:
              app_path: .build/MyApp.app
              platform: ios
              api_key: ${{ secrets.TESTERARMY_API_KEY }}
              project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
              group_id: ${{ secrets.TESTERARMY_GROUP_ID }}
              delete_app_after_run: "true"
              remove_after: "3600"

          - name: Print overall status
            run: echo "Overall status: ${{ steps.mobile.outputs.overall_status }}"
    ```
  </Tab>

  <Tab title="Android">
    ```yaml theme={"theme":"vesper"}
    name: Android Mobile Tests

    on:
      push:
        branches:
          - main
      pull_request:
      workflow_dispatch:

    permissions:
      contents: read

    jobs:
      build_android:
        name: Build Android app
        runs-on: ubuntu-latest

        steps:
          - name: Check out repository
            uses: actions/checkout@v5

          - name: Set up Node.js
            uses: actions/setup-node@v5
            with:
              node-version: "24"
              cache: npm

          - name: Set up Java
            uses: actions/setup-java@v5
            with:
              distribution: temurin
              java-version: "17"

          - name: Set up Android SDK
            uses: android-actions/setup-android@v3

          - name: Set up Gradle
            uses: gradle/actions/setup-gradle@v4

          - name: Install dependencies
            run: npm ci

          - name: Expo prebuild (Android)
            env:
              CI: "true"
            run: npx expo prebuild --platform android --no-install

          - name: Build Android APK
            working-directory: android
            run: ./gradlew :app:assembleRelease

          - name: Copy APK to artifact directory
            shell: bash
            run: |
              set -euo pipefail
              APK_PATH="$(find android/app/build/outputs/apk/release -type f -name '*.apk' | head -1)"
              if [[ -z "${APK_PATH:-}" || ! -f "$APK_PATH" ]]; then
                echo "Could not find a release APK under android/app/build/outputs/apk/release"
                exit 1
              fi
              mkdir -p .build
              cp "$APK_PATH" .build/MyApp.apk

          - name: Upload APK artifact
            uses: actions/upload-artifact@v5
            with:
              name: android-apk
              path: .build/MyApp.apk
              retention-days: 1

      test_android:
        name: Run Android TesterArmy tests
        needs: build_android
        runs-on: ubuntu-latest

        steps:
          - name: Download APK artifact
            uses: actions/download-artifact@v5
            with:
              name: android-apk
              path: .build

          - name: Upload app and run TesterArmy tests
            id: mobile
            uses: tester-army/mobile-github-action@v1
            with:
              app_path: .build/MyApp.apk
              platform: android
              api_key: ${{ secrets.TESTERARMY_API_KEY }}
              project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
              group_id: ${{ secrets.TESTERARMY_GROUP_ID }}
              delete_app_after_run: "true"
              remove_after: "3600"

          - name: Print overall status
            run: echo "Overall status: ${{ steps.mobile.outputs.overall_status }}"
    ```
  </Tab>
</Tabs>

<Info>
  **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.
</Info>

## 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:

| Mode | What it does |
| - | - |
| `all` (default) | Uploads the app and runs the dashboard group in one job. |
| `upload` | Uploads the app and exposes the `app_id` output for downstream jobs. |
| `test` | Runs the dashboard group against an existing `app_id`. |
| `dynamic_agent` | Runs the [exploration agent](#exploration-agent) against an existing `app_id` on pull request events. |

## Run defined tests and the exploration agent in parallel

The [exploration agent](#exploration-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 exploration agent jobs.

```yaml theme={"theme":"vesper"}
jobs:
  upload:
    runs-on: ubuntu-latest
    outputs:
      app_id: ${{ steps.upload.outputs.app_id }}
    steps:
      - uses: actions/download-artifact@v5
        with:
          name: ios-simulator-app
          path: .build/MyApp.app

      - uses: tester-army/mobile-github-action@v1
        id: upload
        with:
          mode: upload
          app_path: .build/MyApp.app
          api_key: ${{ secrets.TESTERARMY_API_KEY }}
          project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
          remove_after: "86400"

  defined_tests:
    runs-on: ubuntu-latest
    needs: upload
    steps:
      - uses: tester-army/mobile-github-action@v1
        id: defined
        with:
          mode: test
          app_id: ${{ needs.upload.outputs.app_id }}
          platform: ios
          api_key: ${{ secrets.TESTERARMY_API_KEY }}
          project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}
          group_id: ${{ secrets.TESTERARMY_GROUP_ID }}

      - run: echo "Defined tests: ${{ steps.defined.outputs.overall_status }}"

  dynamic_agent:
    runs-on: ubuntu-latest
    needs: upload
    if: ${{ github.event_name == 'pull_request' }}
    steps:
      - uses: tester-army/mobile-github-action@v1
        id: dynamic
        with:
          mode: dynamic_agent
          app_id: ${{ needs.upload.outputs.app_id }}
          platform: ios
          api_key: ${{ secrets.TESTERARMY_API_KEY }}
          project_id: ${{ secrets.TESTERARMY_PROJECT_ID }}

      - run: echo "Exploration 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, [Bitrise](/mobile/bitrise), Fastlane,
Buildkite, or GitLab CI, upload the app with `testerarmy upload-app`, then use
the returned app ID in the provider-agnostic CLI flow:

```bash theme={"theme":"vesper"}
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. It exits with code `0` only for a passed summary. Failed, blocked,
cancelled, and timed-out summaries exit with code `1`; blocked remains a
separate result in JSON because it means no product verdict was produced.

## Exploration agent

The [exploration agent](/run/pr-exploration-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 exploration agent in parallel](#run-defined-tests-and-the-exploration-agent-in-parallel)). On other CI providers, call the CLI directly with `testerarmy pr run-dynamic`:

```bash theme={"theme":"vesper"}
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 one binary serves many PRs and your app supports a configuration deep link (for example an Expo update-channel or backend-selection link), pass it with `--deeplink` (or `deeplink` on the API). TesterArmy opens the link on the device after launching the app and before testing starts, so the per-PR configuration is applied deterministically - the agent is told the setup is already done and must not re-run it. The link is visible in run artifacts and PR comments, so use per-PR parameters (channel names, update IDs, backend hosts) rather than long-lived secrets.

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: the PR check shows "Tests skipped" with the reason, the run is recorded as cancelled with that reason, and the CLI and action report it as skipped and exit 0, 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:

```bash theme={"theme":"vesper"}
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

| Input | Required | Description |
| - | - | - |
| `app_path` | For `all`, `upload` | Path to the iOS Simulator `.app`/archive or Android `.apk` to upload |
| `api_key` | Yes | TesterArmy API key |
| `project_id` | Yes | TesterArmy project ID |
| `group_id` | For `all`, `test` | TesterArmy test group ID |
| `app_id` | For `test`, PR `dynamic_agent` | Existing TesterArmy uploaded app ID to reuse |
| `mode` | No | Action mode: `all`, `upload`, `test`, or `dynamic_agent`. Defaults to `all` |
| `platform` | No | Mobile runtime platform: `ios` or `android`. Defaults to `ios` |
| `delete_app_after_run` | No | Delete the uploaded app after terminal runs, only when the same invocation owns the upload. Defaults to `true` |
| `remove_after` | No | Seconds 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_status` - `passed`, `failed`, or `timed_out`
* `dynamic_agent_status` - `passed`, `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:

```bash theme={"theme":"vesper"}
ios/build/Build/Products/Release-iphonesimulator/MyApp.app
```

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

```bash theme={"theme":"vesper"}
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.

## Related docs

* [App Uploads](/mobile/app-uploads)
* [Expo EAS](/mobile/expo-eas)
* [Bitrise](/mobile/bitrise)
* [API reference](/api-reference/projects/upload-a-project-mobile-app)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.