How to integrate EAS Workflows with GitHub Actions for faster mobile CI
Product•Development•React Native••10 minutes read
Keith Kurak
Engineering
See how EAS Workflows enhances GitHub Actions by handling the hardest mobile tasks so React Native teams can build faster and maintain a cleaner CI setup.

When I talk to developers about EAS Workflows, other CI/CD solutions inevitably come up. As they should!
There are a lot of powerful continuous integration and delivery services out there. Many developers have already invested in those services, and it's a big deal to make a change to a critical process. Developers want to know two things:
- What sets EAS Workflows apart from other solutions?
- How can I integrate EAS Workflows into my existing CI runner?
Github Actions is naturally a frequent comparison. EAS Workflows itself has a Github app integration, that allows workflows to be run from the same Github events (e.g., pull request updates, branch merges) that Github Actions support. It’s quite likely that you’ve already tried Github Actions or have many actions running in production if you’re looking into EAS Workflows.
EAS Workflows is a CI/CD service purpose-built with React Native apps in mind. It includes built-in jobs that work great with EAS Build, Update, and Submit, with minimal configuration. It can be used instead of other CI solutions for many mobile-focused processes, but it can also be used alongside them. You can mix and match your current CI/CD with Workflows to achieve your optimal process. In doing so, you'll simplify signing, accelerate builds, and run only the jobs that matter, ultimately creating a pipeline that is faster, more flexible, and easier to maintain.
Let’s take a journey to understand what EAS Workflows offers and how you might integrate it by comparing/contrasting and mixing/matching with Github Actions. We’ll start from some simple Github Actions for a React Native app, see what the equivalent would be in EAS Workflows, and then we’ll start to blur the lines, using Workflows where it is most useful, while continuing to run in Github Actions the things that are already working well there.
A simple build workflow
Let’s suppose we want to make an Android and iOS preview build every time we merge a PR to the main branch, so QA can test approved changes. We need to trigger this based on a Github event, and we already have access to Github Actions, so it’s sensible at first look to write a simple Github Action to do this.
name: Build iOS and Android on merge to mainon:push:branches:- mainjobs:build-android:name: Android Release Buildruns-on: ubuntu-latestenv:# Signing secrets (configure in repo Settings > Secrets and variables > Actions)ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}steps:- name: Checkout repouses: actions/checkout@v4- name: Setup Bunuses: oven-sh/setup-bun@v1with:bun-version: latest- name: Decode Android keystorerun: |echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/release.keystore- name: Write signing config gradle.propertiesrun: |cat >> android/gradle.properties <<EOFMYAPP_UPLOAD_STORE_FILE=release.keystoreMYAPP_UPLOAD_KEY_ALIAS=${ANDROID_KEY_ALIAS}MYAPP_UPLOAD_STORE_PASSWORD=${ANDROID_KEYSTORE_PASSWORD}MYAPP_UPLOAD_KEY_PASSWORD=${ANDROID_KEY_PASSWORD}EOF- name: Set up JDKuses: actions/setup-java@v4with:distribution: temurinjava-version: "17"- name: Set up Android SDKuses: android-actions/setup-android@v3- name: Assemble Release APK/AABworking-directory: androidrun: |android/gradlew :app:bundleRelease --stacktrace- name: Upload Android artifactuses: actions/upload-artifact@v4with:name: android-release-aabpath: android/app/build/outputs/bundle/release/*.aabbuild-ios:name: iOS Release Buildruns-on: macos-latestneeds: build-androidenv:# You can set this if your Fastlane expects env varsMATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}APPLE_ID: ${{ secrets.APPLE_ID }}APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}steps:- name: Checkout repouses: actions/checkout@v4- name: Setup Bunuses: oven-sh/setup-bun@v1with:bun-version: latest- name: setup-cocoapodsuses: maxim-lobanov/setup-cocoapods@v1with:podfile-path: myApp/Podfile.lock# Pull signing certs / profiles with match- name: Fetch signing assets with fastlane matchworking-directory: iosenv:MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}APPLE_ID: ${{ secrets.APPLE_ID }}run: |bundle exec fastlane match appstore --readonly# Build the ipa (you can wrap this in a lane like `fastlane ios build`)- name: Build iOS archive / ipaworking-directory: iosenv:# If your Fastlane lane needs these:APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}run: |bundle exec fastlane ios build- name: Upload iOS artifactuses: actions/upload-artifact@v4with:name: ios-release-ipapath: ios/build/*.ipa
In terms of what we’re trying to do, this is not that complicated (right?). We’re just trying to make one Android build and one iOS build. But, things escalate quickly! Perhaps unsurprisingly, a lot of the complexity centers around signing credentials. We need to set a handful of environment variables for the Android keystore. Even though we’ve simplified the Apple signing step via Fastlane Match, that also means we need to setup an entirely separate Fastlane Match repository.
Let’s add a little EAS Build
For a long time, development teams have invoked EAS Build from Github Actions (even the Expo team has done it). Let’s try replacing the Gradle and Fastlane steps with calls to EAS Build:
name: EAS Build on merge to mainon:push:branches: [main]jobs:trigger-eas-builds:runs-on: ubuntu-latestenv:EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}steps:- uses: actions/checkout@v4- name: Setup Node & Bunuses: actions/setup-node@v4with:node-version: "20"- run: |bun install# Fire-and-forget Android and iOS build- name: Kick off Android and iOS buildsrun: |npx eas build \--platform all \--profile preview \--non-interactive \--no-wait
This is a lot shorter. EAS Build is already setup with all the appropriate prebuild, Gradle, Fastlane, etc. commands needed for a typical Expo project. However, the big savings in complexity is on the build credentials management side. We’ve swapped out several environment variables and Fastlane Match config for a single EXPO_TOKEN for invoking EAS Build. Your build credentials are already stored in EAS and can be either uploaded via eas credentials or automatically generated if you invoke a build manually for the first time.
The simple workflow above fires off EAS Build and then completes without waiting for the builds to finish. This is the most efficient route in terms of CI minutes used. However, you can also wait for the builds to complete and then run a custom script to process the results inside of your Github Action:
npx eas build --platform android --profile preview --json > build.jsonBUILD_URL=$(jq -r '.[0].artifacts.buildUrl' build.json)echo "Android build URL: $BUILD_URL"
This will use Github Action CI minutes while you are also using EAS Build, but, in terms of less configuration and maintenance, and the fact that EAS Build is optimized for React Native build speed with fast M4 Pro workers and more, this can often be a worthwhile tradeoff.
Full migration to EAS Workflows
At this point, all our Github Action is providing is the ability to trigger the build on a push to the main branch. Once your repo is connected to the Expo Github app, you can add the same triggers to an EAS Workflow, eliminating the need to use Github Actions as a relay:
name: Build preview on main pushon:push:branches: [main]jobs:build_ios:type: buildparams:platform: iosprofile: previewbuild_android:type: buildparams:platform: androidprofile: preview
Now we don’t need to create a bot user and provide an EXPO_TOKEN or use extra CI minutes on a separate service. You also don’t need to use Mac runners on Github Actions, as the iOS build itself will occur on EAS.
Powerful Expo-optimized workflows
That EAS Workflow YAML for making a build is short and sweet, but invoking a build from Github Actions isn’t dramatically more complex. Where EAS Workflows really shines is when you need to perform jobs conditionally based on the unique characteristics of Expo apps. Besides the basic build and update jobs, EAS Workflows has built-in jobs that can do things like generating a hash uniquely identifying your native configuration (fingerprint), apply new JavaScript to an existing build (repack) and run Maestro end-to-end tests on a simulator (maestro). It can take just a few lines of YAML to build powerful workflows speed up testing and deployment.
Build or update
Let’s suppose our app is like a lot of other React Native apps, where the average pull request is much more likely to only make changes to JavaScript rather than to native code. Our current workflow is set to do a full build on every commit to main, even though it’s quite likely that only a few commits actually change native code. Let’s fix that by using native fingerprints to determine when native code has changed and only build when that is true:
name: Build or update preview versionon:push:branches: [main]jobs:fingerprint: #calculate fingerprintenvironment: previewtype: fingerprintandroid_get_build: # if there's a matching build...needs: [fingerprint]type: get-buildparams:fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}platform: androidprofile: previewandroid_update: # publish an update to that buildneeds: [android_get_build]if: ${{ needs.android_get_build.outputs.build_id }}type: updateparams:channel: previewplatform: androidandroid_build: # otherwise make a new buildneeds: [android_get_build]if: ${{ !needs.android_get_build.outputs.build_id }}type: buildparams:platform: androidprofile: preview## repeat the get build -> update / build steps for iOS
Check out the full Fingerprint workflow here.
EAS already has your recent previous builds and fingerprints (which are calculated on every build), so this data is already available within an EAS Workflow- you just need to connect the dots in whatever way you’d like.
Fast E2E testing with fingerprint and repack
We can use the same principle (conditionally building based on a native fingerprint) for speeding up end-to-end tests.
You may have previously chosen to limit when and where end-to-end tests are run, because they require a full build in addition to the test run. Ideally, you would run them every time you create or update a pull request, just like you likely do with your unit tests, linting, and other checks.
With fingerprint, you can speed up the build phase of Maestro end-to-end tests by running a Repack instead of a build when there is already a matching build. You can think of a repack as almost like a “build time update”. The repack job takes an existing build, re-bundles the JavaScript, replaces the JavaScript bundle in the old build with the new, and resigns it. That way, you can get a new build for running tests on in just a minute or two.
name: Build or repack, then Maestro testsjobs:fingerprint:environment: previewtype: fingerprintandroid_get_build:needs: [fingerprint]type: get-buildparams:fingerprint_hash: ${{ needs.fingerprint.outputs.android_fingerprint_hash }}platform: androidprofile: previewandroid_repack:needs: [android_get_build]if: ${{ needs.android_get_build.outputs.build_id }}type: repackparams:build_id: ${{ needs.android_get_build.outputs.build_id }}android_build:needs: [android_get_build]if: ${{ !needs.android_get_build.outputs.build_id }}type: buildparams:platform: androidprofile: previewandroid_maestro:after: [android_repack, android_build]type: maestroimage: latestparams:build_id: ${{ needs.android_repack.outputs.build_id || needs.android_build.outputs.build_id }}flow_path: ['.maestro/preview-smoke.yaml']# repeat the same get build -> build/repack -> maestro flow for iOS
@expo/fingerprint and @expo/repack-app are also available as standalone CLI commands, so it is also possible to integrate them into a Github Action. Depending on your level of EAS integration, you may need to save your fingerprint hashes somewhere. A common pattern is to commit the fingerprint to source code, and then compare the current fingerprint with the committed fingerprint.
Combining Github Actions with EAS Workflows
Let’s suppose you have a lot of jobs already working well in Github Actions, perhaps your linting, unit tests, or other jobs customized to your environment. You may need to keep a certain degree of continuity with web frontend and service developers in your organization, who will continue using Github Actions. Or, maybe you would consider moving all of your mobile-specific tasks to EAS Workflows, but would rather do so gradually to avoid any potential interruption of critical processes. That makes perfect sense. We feel the same way as we adopt new technology.
Just like with standalone EAS Build and EAS Update commands that you can run within Github Actions, you can run workflows the same way, splitting off the parts of your pipeline where you have the most to gain by moving to EAS. You can kick off a workflow via the CLI, wait for it to finish, and obtain the results in machine-readable JSON format like this:
eas workflow:run workflow-file.yaml —wait —json
Let’s combine the two approaches, calling our build or update EAS Workflow from Github Actions, and outputting links to the build for downloading:
name: Lint, test, then run EAS Workflow to build or updateon:push:branches: [main]jobs:run-eas-workflow:runs-on: ubuntu-latestenv:EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}steps:- uses: actions/checkout@v4- name: Setup Bunuses: oven-sh/setup-bun@v1with:bun-version: latest- name: Lintrun: npx expo lint- name: Testrun: bun test- name: Setup EASuses: expo/expo-github-action@v8with:eas-version: latesttoken: ${{ secrets.EXPO_TOKEN }}- name: Run EAS Workflow (wait + json → temp file)run: |npx eas workflow:run "$WORKFLOW_FILE" --wait --json > "$RUNNER_TEMP/result.json"- name: Read results (build URL's + success)id: parserun: |FILE="$RUNNER_TEMP/workflow.json"echo "Full JSON saved to: $FILE"# Overall success: true/falseSUCCESS=$(jq -r '.status == "SUCCESS"' "$FILE")echo "Workflow success: $SUCCESS"# Collect any build URLs from GET_BUILD jobs (Android/iOS)BUILD_URLS=$(jq -r '.jobs[]? | select(.type=="GET_BUILD") | .turtleBuild.artifacts.buildUrl' "$FILE" || true)# Do something with the URL'sif [ -n "$BUILD_URLS" ]; thenecho "Build URLs:"echo "$BUILD_URLS"elseecho "No build URLs found."fi
In this sense, you can almost think of EAS Workflows as “EAS Build+ (whatever else you need)”. Sure, it is a standalone CI/CD service that can integrate directly with your Github source control, but it’s also a way to chain together whatever EAS functionality you want, in any combination you can imagine, and stick it wherever you need it. This works basically the same even if you’re using a CI/CD system other than Github Actions.
Conclusion
EAS Workflows doesn’t have to replace your entire CI/CD setup. It can enhance it. By offloading mobile-specific complexity to a service purpose-built for React Native, you can simplify signing, speed up builds, and conditionally run jobs that make sense for your app. Whether you fully migrate to Workflows or mix it into your existing Github Actions, you’ll gain a more efficient, flexible, and maintainable build pipeline.




