This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

Automate development builds with EAS Workflows

Edit page

Learn how to automate Android and iOS development builds with EAS Workflows and use fingerprints to skip rebuilds when only JavaScript changes.


Manually rebuilding a development client on every push to a GitHub repository can be slow. Team members or new contributors who pull native changes (a new module, a new permission, an SDK upgrade) wait for a fresh build before they can run the project.

Automating development builds on the main branch keeps a current build ready to install. We can run eas build:dev to install the latest compatible build. If the project fingerprint matches an existing build, EAS downloads that build instead of creating a new one.

Learning outcomes

  • Automate development builds for Android and iOS with the build job type
  • Use fingerprint and get-build jobs to skip rebuilding when native code hasn't changed
  • Add a custom unit testing job that gates the workflow on passing tests

Prerequisites

1 requirement

EAS Build configured for development

EAS CLI prompts to generate credentials the first time we trigger a build for a profile. Trigger the development build manually for both Android and iOS so the credentials exist before the workflow runs:

Terminal
eas build --profile development --platform all

The build job type for development builds

The build job type is one of the pre-packaged jobs from EAS Workflows. Instead of defining each step of the build process, we use the build job type to run an EAS Build for Android and iOS.

For this chapter, we use the development build profile from our eas.json. We use this profile during Expo app development.

1

Add a build.yml

Inside .eas/workflows/, add a new file called build.yml. It uses the build job type with profile and platform parameters to create development builds for Android and iOS.

Add the following code to the build.yml file:

.eas/workflows/build.yml
name: Development builds jobs: build_android: name: Build Android type: build params: platform: android profile: development build_ios: name: Build iOS type: build params: platform: ios profile: development

We add two jobs: build_android and build_ios. Both are of type build with different parameters for platform and build profile.

The params key for each job includes:

  • platform sets the target OS for the build (android or ios)
  • profile specifies the build profile from our eas.json

2

Run the workflow manually

Let's run the workflow manually using the command:

Terminal
eas workflow:run .eas/workflows/build.yml

3

Verify on the EAS dashboard

On the EAS dashboard, notice that it says "Triggered manually" under the Workflow graph tab. Any time the Trigger shows Manual, that means the workflow was started with the eas workflow:run command.

The logs for each build appear inside the workflow interface. Click a build's ID to open its EAS Build page.

Jobs without dependencies run in parallel by default in EAS Workflows. In the above workflow, since both build_android and build_ios jobs don't have any dependencies, they start at the same time.

Wait for both builds to complete before moving to the next step. Once they complete, we can use the Install or Open with Orbit buttons, or download from Artifacts to install on our device/emulator/simulator.

Skip unnecessary builds with fingerprints

When our project has only TypeScript/JavaScript changes, the existing development build is still compatible, so we do not need a new one.

Expo Fingerprint automates this decision. It hashes the project's native characteristics (dependencies, native project files, configuration). If the hash matches an existing build, native code has not changed, and EAS skips the rebuild. If the hash is different, native code changed, and EAS creates a new build.

Let's update build.yml step by step to detect native changes and build only when needed.

1

Add a fingerprint job

The fingerprint pre-packaged job hashes the project's native characteristics and outputs a fingerprint hash for each platform. The environment field tells EAS which environment variable configuration to use. Note that environment and profile can share the same name but refer to different things. profile points to a build profile in eas.json (how to build). environment points to environment variables configured on EAS (which values to inject at build time).

Update build.yml to add the fingerprint job before the build jobs:

.eas/workflows/build.yml
name: Development builds jobs: fingerprint: name: Fingerprint type: fingerprint environment: development build_android: # ... build_ios: # ...

The fingerprint job produces two outputs: android_fingerprint_hash and ios_fingerprint_hash. We use these hashes in the next step to check if a compatible build already exists.

2

Add get-build jobs

The get-build pre-packaged job checks if a build already exists for a given fingerprint hash and profile. If a matching build exists, it outputs a build_id. If not, build_id is empty.

In our workflow file, let's add get_android_build and get_ios_build jobs between fingerprint and the build jobs. Each one uses needs: [fingerprint] to wait for the fingerprint job to complete and access its output:

.eas/workflows/build.yml
name: Development builds jobs: fingerprint: # ... get_android_build: name: Check for existing Android build needs: [fingerprint] type: get-build params: fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }} profile: development get_ios_build: name: Check for existing iOS build needs: [fingerprint] type: get-build params: fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }} profile: development build_android: # ... build_ios: # ...

In the above workflow, get-build jobs receive the fingerprint hash from the previous job using the ${{ needs.fingerprint.outputs.* }} expression syntax.

3

Add conditional builds

The if field controls whether a job runs. The expression ${{ !needs.get_android_build.outputs.build_id }} means: run a job only if get-build did not find a matching build. The ! is a negation operator. If a compatible build already exists for this fingerprint, the build job is skipped.

Let's update each build job to depend on its get-build job and add the if condition:

.eas/workflows/build.yml
name: Development builds jobs: fingerprint: # ... get_android_build: # ... get_ios_build: # ... build_android: name: Build Android needs: [get_android_build] if: ${{ !needs.get_android_build.outputs.build_id }} type: build params: platform: android profile: development build_ios: name: Build iOS needs: [get_ios_build] if: ${{ !needs.get_ios_build.outputs.build_id }} type: build params: platform: ios profile: development

4

Add a trigger and run the workflow

Let's add the on.push trigger to the workflow so it runs automatically on every push to main:

.eas/workflows/build.yml
name: Development builds on: push: branches: ['main'] jobs: fingerprint: name: Fingerprint type: fingerprint environment: development get_android_build: name: Check for existing Android build needs: [fingerprint] type: get-build params: fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }} profile: development get_ios_build: name: Check for existing iOS build needs: [fingerprint] type: get-build params: fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }} profile: development build_android: name: Build Android needs: [get_android_build] if: ${{ !needs.get_android_build.outputs.build_id }} type: build params: platform: android profile: development build_ios: name: Build iOS needs: [get_ios_build] if: ${{ !needs.get_ios_build.outputs.build_id }} type: build params: platform: ios profile: development

Let's assume we made some TypeScript/JavaScript changes to our Expo project, like changing a button's color or updating the text on a screen. After doing so, we can push a commit to the main branch to trigger the workflow from our GitHub repository:

Terminal
git add .
git commit -m "Update button label and tweak styling"
git push origin main

This triggers a new workflow that completes in seconds. On the EAS dashboard, both Android and iOS builds are skipped because a matching build already exists from the previous section.

We pull the latest from the main branch and run npx expo start. The existing development build picks up the JS changes.

Custom jobs for unit testing

This section assumes Jest is set up in the project. For setup instructions, see Unit testing with Jest.

When working in a team, we want every change pushed to the main branch to pass tests before triggering a new build.

Without automation, the team has to remember to run tests locally before pushing to main. If they forget and the code has a failing test, the next workflow can create a build from broken code.

With EAS Workflows, we can automate this process by mixing a custom job with our existing pre-packaged jobs. This custom job runs before fingerprint and build jobs. If a test fails, EAS stops the workflow and creates no build.

1

Add a custom job for unit tests

In our build.yml workflow file, let's add a custom job that runs unit tests before the builds start:

.eas/workflows/build.yml
name: Development builds on: push: branches: ['main'] jobs: run_tests: name: Run unit tests steps: - uses: eas/checkout # Check out the repo - uses: eas/install_node_modules - run: npx jest --ci fingerprint: name: Fingerprint needs: [run_tests] type: fingerprint environment: development get_android_build: # ... get_ios_build: # ... build_android: # ... build_ios: # ...

The custom run_tests job does the following:

  • uses: eas/checkout checks out the project source files from the GitHub repository. Custom jobs run on a new virtual machine, so the code is not available by default. This step is required before accessing any project files.
  • uses: eas/install_node_modules installs dependencies using the package manager detected in the project (bun, npm, pnpm, or Yarn). This is needed before running any commands that depend on packages from node_modules, like Jest.
  • run: npx jest --ci executes the test suite. The run key runs any shell command, similar to the echo command. The --ci flag tells Jest to run in CI mode, which skips watching for file changes and exits after running all tests.

Now, the fingerprint job has needs: [run_tests], so it only runs after the tests pass. If tests fail, EAS skips the rest of the workflow.

2

Trigger the workflow

Commit and push the updated workflow along with the test file to main:

Terminal
git add .
git commit -m "Add unit tests to development builds workflow"
git push origin main

After triggering the workflow, open the EAS dashboard and notice the flow of the jobs inside our workflow.

Summary

Chapter 2: Development builds

We automated development builds using pre-packaged jobs, added fingerprints to skip unnecessary rebuilds, and integrated unit tests as a custom job before the build pipeline.

In the next chapter, learn how to create preview builds with Slack notifications and PR preview updates.

Next: Chapter 3: Preview builds