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

Automate production deployments with EAS Workflows

Edit page

Learn how to automate production builds and over-the-air updates with EAS Workflows from a release branch for Android and iOS.


Rebuilding the app for every production change is wasteful when most changes are TypeScript/JavaScript only.

Learning outcomes

  • Trigger production deployments from a release/* branch instead of every push to main
  • Branch between a native build and an OTA update based on the project fingerprint
  • Automate app store submissions as a follow-up step in the production workflow

Prerequisites

1 requirement

EAS Build configured for production

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

Terminal
eas build --profile production --platform all

Using a release branch

Previously, with development builds, we have seen that using the on.push.branches trigger runs our workflow on every push to the specified main branch. For production deployments, triggering a workflow on every push to the main branch means every change merged is a new release. Teams usually want a deliberate release process, so they use a release branch or tag instead.

In a release branch strategy, we usually specify the branch with a pattern such as release/*, where * is the feature or version name. This is what separates Continuous Integration (CI) from Continuous Delivery (CD). CI runs on every push to the main branch, while CD runs on a release branch when the team is ready to deploy to production.

The table below shows how each workflow's trigger defines its purpose:

Workflow fileTriggerBuild profilePurpose
.eas/workflows/build.ymlon.push.branches: ['main']developmentCI workflow that runs on every push to the main branch. It can run unit tests and generate development builds.
.eas/workflows/preview.ymlon.push.branches: ['main']previewPreview workflow that runs on every push to the main branch. It can generate preview builds for internal testing and sharing with stakeholders.
.eas/workflows/production.ymlon.push.branches: ['release/*']productionCD workflow that runs on every push to a release branch.

The above files coexist inside a project's GitHub repository without any conflict. Each uses a different build profile and a trigger.

The build job type for production builds

Let's start by creating a production workflow. It uses the pre-packaged build job type from EAS Workflows and runs for both Android and iOS.

1

Add a production.yml

Inside .eas/workflows/, create a new file called production.yml that uses the production build profile from eas.json:

.eas/workflows/production.yml
name: Deploy to production on: push: branches: ['release/*'] jobs: build_android: name: Build Android type: build params: platform: android profile: production build_ios: name: Build iOS type: build params: platform: ios profile: production

This creates a .aab artifact for Android and an .ipa artifact for iOS on a push to any branch that matches the pattern release/*.

2

Add fingerprints to the workflow

Let's add a fingerprint job to the workflow to check if there are changes to native code and update existing build_android and build_ios jobs based on that. We are also adding get-build jobs to look up existing builds for Android and iOS.

Update the production.yml file with the following code:

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

In the above workflow, the fingerprint job runs first and generates a hash based on the changes in the codebase. The get-build jobs then check if there are existing builds for Android and iOS with the same fingerprint hash. If there are no existing builds, the build jobs run to create new builds for production.

The environment field on the fingerprint job tells EAS which environment variables to load when computing the fingerprint. Our production environment may have different values for variables such as API_URL or feature flags. Those values can affect the native layer when EAS bakes them into the build. Setting environment: production ensures the fingerprint reflects what ships.

The if field used on the build_android and build_ios jobs is a boolean expression. When it evaluates to false, the build jobs are skipped. The ! operator means the build job runs only when get-build did not find a matching build.

3

Add a job for updates

Currently, our workflow only creates production builds when there are changes to native code. If there are only changes to TypeScript/JavaScript files, we want to trigger an OTA update instead of a native build.

Let's add two jobs update_android and update_ios that run when there are no changes to native code and use update job type to trigger an OTA update:

.eas/workflows/production.yml
name: Deploy to production on: push: branches: ['release/*'] jobs: fingerprint: # ... get_android_build: # ... get_ios_build: # ... build_android: # ... build_ios: # ... update_android: name: Publish Android update needs: [get_android_build] if: ${{ needs.get_android_build.outputs.build_id }} type: update params: branch: production platform: android update_ios: name: Publish iOS update needs: [get_ios_build] if: ${{ needs.get_ios_build.outputs.build_id }} type: update params: branch: production platform: ios

Our workflow only builds production-ready app binaries or publishes OTA updates. Automating store submission is a common next step discussed below in the Automate app store submissions section.

4

Create a release branch and push changes

Note: Ensure that the workflow file is already part of our GitHub repository and available on the main branch at this point.

To test our workflow, let's create a release branch and push it to our GitHub repository from a terminal window:

Terminal
git checkout -b release/1.0.0

git push origin release/1.0.0

Open the EAS dashboard and find the workflow run. Since no build exists yet for this fingerprint, the build jobs should run, and the update jobs should be grayed out.

Both Android and iOS builds are running in parallel. Once each job completes, we receive a new artifact for Android and iOS.

Now, let's make a TypeScript/JavaScript only change in our example project on the same release branch and push again using the commands below:

Terminal
git add .
git commit -m "Update welcome text"
git push origin release/1.0.0

After the workflow runs, notice in the EAS dashboard that the fingerprint matches the build from the last push. The build jobs are skipped entirely, and the update jobs publish an OTA update.

Automate app store submissions

The workflow we have built in this chapter only builds production-ready app binaries or publishes OTA updates to an existing production app. Automating store submission is a common next step in a production release workflow.

EAS Workflows provides a submit pre-packaged job for automating app store submissions. It requires us to manage our app store credentials with EAS CLI. It also requires us to fulfill app store requirements, such as manually uploading the first Android release (.aab) to the Google Play Store. iOS has its own Apple Developer Program enrollment and signing credentials to set up.

Once the app store requirements are in place, we can add two new jobs submit_android and submit_ios to our workflow that run after the build jobs:

.eas/workflows/production.yml
# rest of the workflow file submit_android: name: Submit Android needs: [build_android] type: submit params: build_id: ${{ needs.build_android.outputs.build_id }} submit_ios: name: Submit iOS needs: [build_ios] type: submit params: build_id: ${{ needs.build_ios.outputs.build_id }}

The above submit_android and submit_ios jobs require a build_id parameter to know which build to submit to the app stores. They run only when the build_android and build_ios jobs run, which means a new production build is created. If there are only changes to TypeScript/JavaScript files, the submit jobs are skipped as well since no new build is created.

Summary

Chapter 5: Production deployments

We created a production workflow for Android and iOS triggered by pushes to a release branch, used fingerprinting to choose between a native build and an OTA update, and saw how EAS Submit automates app store submissions.

In the next chapter, learn how to switch production deployments from release branches to version tags.

Next: Chapter 6: Tag-based releases