---
title: Agentic CI for Expo apps with TesterArmy
authors: Oskar Kwaśniewski
published: September 29, 2026
categories: AI, Development
tags: EAS Workflows, CI/CD, agentic development, AI development, testing
---

_This is a guest post from Oskar Kwaśniewski, CTO and co-founder of TesterArmy, where agents test mobile apps, web apps and websites like real users._

Leverage EAS workflows to build and run agentic tests on your Expo app in minutes with TesterArmy.

Last week Expo showed [what happens when you put an agent inside EAS Workflows](https://expo.dev/blog/agent-fixes-bugs-on-eas-simulator): a GitHub issue labelled repro becomes a pull request with a fix and recordings that prove it works. That is the repair half of the story. This post is about the other half, the pull request that has not merged yet, and about what I have started calling agentic CI: a pipeline that decides what to build and what to test for each change, instead of running the same list every time.

When I talk with devs about their development process, I often hear that the problem shifts from writing the code to actually reviewing it and clicking the merge button. Agents can create tons of pull requests, but they are still bottlenecked by the review process and the missing confidence to merge the PR.

Pull requests wait in the queue because nobody has the time to sit down, open the app, and tap through the screen that changed… So the change merges, and the first person to tap through it is a user.

The solution is to use what we call “agentic CI”: imagine an agent that, on every pull request, analyzes the description and diff to deeply understand the changes, then opens the app and clicks through the change like a real user would, leaving you with a recording of how it works and what it does to the app.

## What agentic CI means here

CI has always been a fixed list. Every pull request runs the same jobs in the same order, whether it renamed a variable or rewrote onboarding. Agentic CI means the list adapts to each pull request. EAS Workflows builds the app, and reuses the last native build when only JavaScript changed. TesterArmy agent reads the pull request and decides what to tap through on the simulator.

Currently, TesterArmy runs on cloud iOS Simulators and Android emulators, not physical devices (although we are working on adding support for real devices). You can use our agent alongside your existing e2e tests and frameworks.

## Part one: an agent writes the workflow

The [TesterArmy docs page for Expo EAS](https://tester.army/docs/mobile/expo-eas?utm_source=expo&utm_medium=partner&utm_campaign=expo-guest-post-202610) has a section called "Instructions for AI agents". It is a prompt. Paste it into the coding agent you already use in the repository, Claude Code, Codex or Cursor, and it does the setup for you: it inspects `eas.json` and `.eas/workflows`, adds two build profiles, writes a new workflow file without touching an existing one, and never commits a secret.

The two profiles are the whole requirement on the Expo side. TesterArmy needs an iOS Simulator `.app`, not an `.ipa`, and a single Android `.apk`, not an `.aab`.

<!-- eas.json -->
```json
{
  "build": {
    "testerarmy-ios-simulator": {
      "ios": {
        "simulator": true
      }
    },
    "testerarmy-android-apk": {
      "android": {
        "buildType": "apk"
      }
    }
  }
}
```

The workflow reads three values from your EAS environment: `TESTERARMY_API_KEY`, `TESTERARMY_PROJECT_ID` and `TESTERARMY_GROUP_ID`. Store the API key as an EAS secret, never as an `EXPO_PUBLIC` variable. A fourth, `TESTERARMY_DYNAMIC_AGENT_ENABLED`, is optional and defaults to `true`.

Before you run it, you need a mobile project in TesterArmy with at least one saved test. Run that test once from the dashboard on an uploaded build, so you know the upload and the flow work before CI depends on them. You can get more information about this in the [docs](https://tester.army/docs).

## Part two: EAS builds the app and reuses what it can

The basic workflow triggers on push to main, on pull requests to main, and on `workflow_dispatch`. It builds the iOS Simulator app and the Android APK, downloads each artifact with `eas/download_build`, uploads it to TesterArmy, and runs your saved test group once per platform. Start there. A full native build on every run is slower, but it removes variables while you are wiring things up.

Once it is stable, let EAS skip the native build when it can. Expo's fingerprint job calculates a hash of the native runtime. The get-build job looks for an existing build with that hash. The repack job repackages the app's metadata and JavaScript bundle on top of it without a full native rebuild. When no matching build exists, the workflow falls back to a normal build.

```yaml
jobs:
  fingerprint:
    name: Calculate app fingerprints
    type: fingerprint
    environment: preview

  get_ios_build:
    name: Find matching iOS Simulator build
    needs: [fingerprint]
    type: get-build
    params:
      platform: ios
      profile: testerarmy-ios-simulator
      simulator: true
      fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
      wait_for_in_progress: true

  repack_ios:
    name: Repack iOS app
    needs: [get_ios_build]
    if: ${{ needs.get_ios_build.outputs.build_id }}
    type: repack
    params:
      build_id: ${{ needs.get_ios_build.outputs.build_id }}

  build_ios:
    name: Build iOS Simulator app
    needs: [get_ios_build]
    if: ${{ !needs.get_ios_build.outputs.build_id }}
    type: build
    environment: preview
    params:
      platform: ios
      profile: testerarmy-ios-simulator
```

The Android half mirrors this with the APK profile. The upload job runs after whichever of repack or build produced a `build_id`. One caveat from Expo's own docs: the fingerprint job is for CNG projects. If you commit your android or ios directories, it will not work, so stay on the basic workflow.

This is the part that keeps the loop short. The native build is the slow step in any EAS workflow, and most pull requests on a mature Expo app touch JavaScript only. With repack, the native build happens once per runtime change, and every other pull request goes straight from repack to upload and test.

## Part three: an agent tests what changed

Running regression tests on each PR makes sure that you didn’t break anything that worked before. The new part is the job that runs only on pull requests, against the same uploaded build, and works out its own test plan.

It gathers a deep understanding of the intention behind the pull request by reading files and diff. From that, it writes an ordered list of natural language steps for this change, runs them on the simulator, and posts a GitHub check and a pull request comment. The comment first shows the planned steps as a table, then updates in place with per-step results. Every run has a video.

```yaml
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 || '' }}
  steps:
    - uses: eas/checkout
    - name: Run dynamic PR agent
      run: |
        npx --yes testerarmy@latest pr run-dynamic \
          --project "$TESTERARMY_PROJECT_ID" \
          --platform ios \
          --app-id "$APP_ID" \
          --pr-number "$PR_NUMBER" \
          --pr-title "$PR_TITLE" \
          --pr-description "$PR_DESCRIPTION" \
          --commit-sha "$COMMIT_SHA" \
          --output .testerarmy/dynamic-result.json
```

Two behaviors are worth knowing before you turn this on.

First, a skip judge reads the diffs before anything runs. If the pull request changes nothing a user can see, a docs edit or a config bump, the run is reported as skipped, the check shows "Tests skipped" with the reason, and the job passes. On mobile, the judge decides per platform, so an iOS-only change still tests on iOS while the Android run is skipped. Once the judge decides to test, the run always executes.

Second, a real failure fails the EAS job, but the GitHub check is advisory by default. Failed runs conclude neutral, and GitHub counts neutral as passing. If you want a failed test to block the merge, turn on "Block merges on failed tests" in the project's PR Testing tab and mark the "TesterArmy / Exploration Test" check as required in your branch protection.

## Close the loop with your coding agent

If Claude Code, Cursor or Codex writes your pull requests, you can make every one of them arrive planner-ready. Add the excerpt from the docs to `AGENTS.md` or `CLAUDE.md` once. From then on each pull request description ends with a testing-instructions section the agent can execute.

```markdown
## TesterArmy testing instructions

Every pull request description must end with a `## TesterArmy testing
instructions` section. A QA agent reads the full PR description and
writes a test plan from it, so write instructions the agent can execute:

- State the entry point: the exact route, path, or deep link where the
  change is visible.
- Give numbered steps to reach and exercise the feature, in the order a
  user would take them. Name buttons, tabs, and fields by their visible
  labels.
- State the expected behavior after each meaningful action.
- If the flow needs an account, reference the test account by its label.
  Never paste credentials or secrets.
- List required setup the planner cannot infer: feature flags, mock or
  sandbox mode, seed data, or a configuration link that must be opened
  first.
- Add an "Out of scope" line for adjacent surfaces the plan should not
  test.
```

That is the loop. A coding agent writes the change and the instructions. EAS builds or repacks. A testing agent plans from the instructions and the diff, taps through the app, and leaves a video on the pull request. No need to write a test script.

## What this looks like at fifty pull requests a day

[Juno](https://tester.army/customers/juno?utm_source=expo&utm_medium=partner&utm_campaign=expo-guest-post-202610) builds a health assistant for people living with chronic illness, on iOS and Android, in React Native with Expo. The team is seven people shipping one to two updates a week, which on their side means fifty to a hundred pull requests a day.

Every pull request gets about six TesterArmy runs at roughly five minutes each, on both platforms, covering onboarding, symptom logging, medication, notifications, paywall and account flows. The flows are written as plain-language user journeys, not selectors. In July 2026 the team merged 501 pull requests. In August they merged 791. Marshall Gould, Juno's co-founder and CEO, puts it more bluntly than I would: "We are shipping PRs ten times faster."

## Where to start

Start with the [example repository](https://github.com/tester-army/mobile-example). It has the workflow file, both profiles and the dynamic agent job, and it runs as is. When a saved test passes from the dashboard, turn on the dynamic agent, then add fingerprint and repack. Everything else, including the agent prompt and the troubleshooting section, is in the Expo EAS docs.

If it snags on your app, open an issue on the example repository and tell us where.

[Video: TesterArmy × Expo livestream](https://www.youtube.com/watch?v=yrtWWbhTsHE)