Guides:our thoughts

Building a CI/CD pipeline for your Expo app using EAS

Learn how to automate the build and release process for your Expo mobile app.

Chris Rossi
Chris Rossi

Senior Developer

Checking read time...

Expo Application Services (EAS) is a cloud-based service for building and publishing React Native mobile apps, opinionated towards Expo apps.

Before migrating to EAS, we were required to build our mobile apps locally on Apple machines using Xcode and Android Studio, then manually upload the compiled binaries into the Apple App Store and Google Play Store respectively (where we could begin internal testing). This caused numerous issues, as software updates and outdated dependencies would constantly break things in our release process.

Publishing a new mobile app version could take anywhere from an hour to multiple days, because there was no way to predict what might go wrong and how long it would take to troubleshoot, especially in the case of cryptic CocoaPods/Gradle errors. This had a direct impact on how many times per year we could publish new app releases.

EAS takes care of everything. Once we have the app running on our simulator in the Expo Go sandbox, it becomes trivial to then have EAS handle the entire process for building and releasing the native app.

Having a painless and consistent mobile workflow allows us to feel more confident when releasing more regular improvements to our apps.

Building on EAS

Getting started

To get started with EAS, you will need to:

  1. Sign up for a free Expo account.
  2. Install the Expo CLI using npm install -g eas-cli.
  3. Log in to your Expo account using eas login.
  4. Configure the project and follow the steps using eas build:configure.

This final step will create a new eas.json file within your application.

You can find more information in the official EAS Build documentation.

Configuration

All the configuration for EAS is defined within a single eas.json file.

After running the eas build:configure command above to generate this, you should have this file already. However, the default file can still use some additional improvements.

For example, you may want to create new EAS profiles which will allow you to build with different configurations and for different environments. Here are the profiles we currently use:

  • development: Includes the Expo Development Client (expo-dev-client) and is built to run inside mobile simulators, which points to our development API endpoints on our local machines.
  • preview: Effectively the same as a production build, but specifically built for internal testing/QA purposes (can be installed on real test devices).
  • preview-simulator: Identical to preview except that it can be installed and tested on mobile simulators if needed (such as when troubleshooting a bug in the Production build).
  • production: Built for distribution to the Apple/Google Stores, and automatically increments the build number for us.

Our current eas.json file looks like this:

1{
2 ...
3 "cli": {
4 "version": ">= 5.9.3",
5 "appVersionSource": "remote"
6 },
7
8 "build": {
9 "base": {
10 "android": {
11 "image": "latest"
12 },
13 "ios": {
14 "image": "latest"
15 }
16 },
17 "development": {
18 "extends": "base",
19 "env": {
20 "EXPO_PUBLIC_API_BASEURL": "https://laravel.test:8080/api/v2"
21 },
22 "channel": "development",
23 "developmentClient": true,
24 "distribution": "internal",
25 "ios": {
26 "simulator": true
27 },
28 "android": {
29 "buildType": "apk"
30 }
31 },
32 "preview": {
33 "extends": "base",
34 "env": {
35 "EXPO_PUBLIC_API_BASEURL": "https://our-app.example.com/api/v2"
36 },
37 "channel": "preview",
38 "distribution": "internal"
39 },
40 "preview-simulator": {
41 "extends": "base",
42 "env": {
43 "EXPO_PUBLIC_API_BASEURL": "https://our-app.example.com/api/v2"
44 },
45 "channel": "preview",
46 "developmentClient": false,
47 "distribution": "internal",
48 "ios": {
49 "simulator": true
50 },
51 "android": {
52 "buildType": "apk"
53 }
54 },
55 "production": {
56 "extends": "base",
57 "env": {
58 "EXPO_PUBLIC_API_BASEURL": "https://our-app.example.com/api/v2"
59 },
60 "channel": "production",
61 "distribution": "store",
62 "autoIncrement": true
63 }
64 }
65}

As you can see, each of our profiles extends the base profile (allowing us to specify which EAS build environment we use), and can point to different API endpoints.

It's also possible to override this at runtime by using .env files if needed, such as temporarily pointing your development build to the Production API for testing.

Here are some helpful links for EAS Build:

Looking for help with your Expo app?

Your first build

In this step we will use EAS to actually build a working development version of our app which can be installed into a mobile simulator.

From within the application directory, run:

1eas build -p ios --profile development

This will trigger an iOS build using the development profile, which you can also monitor via the Expo web UI:

Creating the GitHub repository via Laravel Vapor

If the build happens to fail for any reason, you can review the full XCode or Gradle logs from this screen.

When this build completes successfully, you should be prompted by EAS CLI to install it within the iOS simulator, which you should do to verify that it was built correctly. You can also download the compiled build artifact from the Expo web UI.

Alternatively, if you are developing on a macOS device, you can install Expo Orbit, which is a GUI to deploy builds to simulators running on your local machine, allowing rapid testing across multiple iOS and Android devices.

Once Orbit is installed, click the "Open with Orbit" button on any successful build on the Expo web UI.

Expo Web - Open with Orbit for MacOS

Expo Orbit for MacOS

You can also now repeat the process for an Android build:

1eas build -p android --profile development

Automating your build workflow using GitHub Actions

We have now successfully built and tested a working development version of our mobile app manually using Expo CLI.

We could repeat the steps for a production build, however a better approach is to trigger this automatically whenever we merge changes into our production (or main) GitHub branch.

Not using GitHub?

Refer to the official documentation for other CI systems including Bitbucket Pipelines, GitLab CI, and Travis.

First, you will need to generate a Personal Access Token for Expo. This will allow GitHub Actions to authenticate to the Expo API and trigger builds automatically. Once you have this token, you can add it to your GitHub Actions secrets:

GitHub Secrets - Expo Token

You can then create a new GitHub Action YAML file, e.g. .github/workflows/eas-build.yml

1name: EAS Build
2on:
3 workflow_dispatch:
4 inputs:
5 platform:
6 description: 'Which platform to build for?'
7 required: true
8 type: choice
9 default: all
10 options:
11 - ios
12 - android
13 - all
14 push:
15 branches:
16 - production # or `main`, or whatever you use
17jobs:
18 build:
19 name: Install and build
20 runs-on: ubuntu-latest
21 steps:
22 - uses: actions/checkout@v3
23 - uses: actions/setup-node@v3
24 with:
25 node-version: 18.x
26 cache: npm
27 - name: Setup Expo and EAS
28 uses: expo/expo-github-action@v8
29 with:
30 eas-version: latest
31 token: ${{ secrets.EXPO_TOKEN }}
32 - name: Install dependencies
33 run: npm ci
34 - name: Set the variables
35 run: echo "DEPLOY_PLATFORM=${{ inputs.platform || 'all' }}" >> $GITHUB_ENV
36 - name: Build on EAS
37 run: eas build --platform "$DEPLOY_PLATFORM" --non-interactive --no-wait --profile production

Whenever we merge into our production branch, GitHub will trigger the Install and build job. The final step of the job is to trigger an EAS build of the production profile on all platforms (iOS and Android).

Check the --no-wait flag

EAS builds can take anywhere from 5 - 20 minutes to run, so our workflow runs with the --no-wait flag which allows us to immediately finish the job and save CI minutes.

If you prefer that the job continues running until the EAS build has completed, you can omit this flag.

This particular workflow can also be triggered manually via the GitHub UI if needed (across one or both platforms):

GitHub Actions - Manually run workflow

Once this Action has completed you should successfully see a new production build within the Expo web UI.

If you have not yet run a production build on EAS before, you may have to upload your Apple/Google credentials for the app to be built correctly.

Releasing on EAS

Releasing to internal testers

Expo also has the ability to automatically release builds of your app to App Store Connect and Google Play Console, where they can be distributed to internal testers via TestFlight and Test Tracks respectively.

This means that within minutes of merging changes into the production branch, your QA team could upgrade to the latest version of your app, making it a completely hands-off workflow.

To do this, you first need to configure Expo's credentials for submitting to Apple/Google as per Expo's documentation:

You may need to include additional settings in your eas.json file, such as your iOS app/team identifiers:

1{
2 ...
3 "cli": {
4 "version": ">= 5.9.3",
5 "appVersionSource": "remote"
6 },
7 "build": {
8 "base": {
9 "android": {
10 "image": "latest"
11 },
12 "ios": {
13 "image": "latest"
14 }
15 },
16 "development": {
17 "extends": "base",
18 "env": {
19 "EXPO_PUBLIC_API_BASEURL": "https://laravel.test:8080/api/v2"
20 },
21 "channel": "development",
22 "developmentClient": true,
23 "distribution": "internal",
24 "ios": {
25 "simulator": true
26 },
27 "android": {
28 "buildType": "apk"
29 }
30 },
31 "preview": {
32 "extends": "base",
33 "env": {
34 "EXPO_PUBLIC_API_BASEURL": "https://our-app.example.com/api/v2"
35 },
36 "channel": "preview",
37 "distribution": "internal"
38 },
39 "preview-simulator": {
40 "extends": "base",
41 "env": {
42 "EXPO_PUBLIC_API_BASEURL": "https://our-app.example.com/api/v2"
43 },
44 "channel": "preview",
45 "developmentClient": false,
46 "distribution": "internal",
47 "ios": {
48 "simulator": true
49 },
50 "android": {
51 "buildType": "apk"
52 }
53 },
54 "production": {
55 "extends": "base",
56 "env": {
57 "EXPO_PUBLIC_API_BASEURL": "https://our-app.example.com/api/v2"
58 },
59 "channel": "production",
60 "distribution": "store",
61 "autoIncrement": true
62 }
63 },
64
65 "submit": {
66 "production": {
67 "ios": {
68 "ascAppId": "9876543210",
69 "appleTeamId": "ABC12345"
70 }
71 }
72 }
73}

You can then modify your GitHub Action to include the --auto-submit parameter in the eas build command:

1name: EAS Build
2on: ...
3 workflow_dispatch:
4 inputs:
5 platform:
6 description: 'Which platform to build for?'
7 required: true
8 type: choice
9 default: all
10 options:
11 - ios
12 - android
13 - all
14 push:
15 branches:
16 - production
17 
18jobs:
19 build:
20 name: Install and build
21 runs-on: ubuntu-latest
22 steps:
23 ...
24 - uses: actions/checkout@v3
25 - uses: actions/setup-node@v3
26 with:
27 node-version: 18.x
28 cache: npm
29 - name: Setup Expo and EAS
30 uses: expo/expo-github-action@v8
31 with:
32 eas-version: latest
33 token: ${{ secrets.EXPO_TOKEN }}
34 - name: Install dependencies
35 run: npm ci
36 - name: Set the variables
37 run: echo "DEPLOY_PLATFORM=${{ inputs.platform || 'all' }}" >> $GITHUB_ENV
38 
39 - name: Build on EAS and Auto-Submit to App/Play Stores if successful
40 run: eas build --platform "$DEPLOY_PLATFORM" --non-interactive --no-wait --profile production --auto-submit

This will automatically create pending iOS and Android submissions, which will roll out as soon as the linked builds complete successfully. You can find these in the Submissions section of the Expo web UI.

When the submission is completed, your internal testers will receive an email from Apple/Google notifying them that there is a new release of the app to test. They can install this via TestFlight or the Google Play Store.

New App Version Notification Email - Apple TestFlight

New App Version Notification Email - Google Play Console

Releasing to production

Once internal testing/QA is completed, you can use App Store Connect and the Google Play Console to promote each new version to the public.

Managing app versions

You can use eas.json to automatically manage your app versions instead of manually incrementing this via app.json. Our example above is configured to only increment the current version when a production build is run, so that development/preview builds don't create unnecessary versions.

If you already have an existing application, you can use the following CLI command to set Expo to your current app versions, so that it can begin incrementing from there as of the next release:

1eas build:version:set

More information can be found here.

Next steps

Expo includes many other features which you may find useful for improving your mobile app workflow, such as:

  • EAS Updates: Hosted service that serves updates for projects using the expo-updates library.
  • EAS Metadata: Automate and maintain your app store presence from the command line.

Want to receive updates from us?

Sign up for our newsletter to stay up to date with Laravel, Inertia, and Expo development.