EAS Build and Submit
Purpose
Guide the configuration and execution of Expo Application Services (EAS) Build and Submit for both Google Play and the App Store — covering credential setup (GCP service account for Android, App Store Connect API key for iOS), eas.json submit profile configuration, EAS environment variables, CI/CD integration via GitHub Actions, and the EAS CLI submission workflow.
When to use
After an Expo React Native app builds successfully via EAS Build. Apply when setting up Google Play or App Store submission for the first time, integrating EAS into a GitHub Actions pipeline, or diagnosing credential, environment, or build-reuse errors during build or submission. Use alongside expo-react-native-app for app development setup, google-play-listing for Play Console listing content, and app-store-listing for App Store Connect listing content.
Inputs expected
- EAS CLI installed (
npm install -g eas-cli) - Expo project with
app.json/app.config.jsandeas.jsonpresent or to be created - Google Play Developer account with the app created in Play Console
- Access to Google Play Console → Setup → API access
Guiding principles
- EAS Submit requires a GCP service account JSON key — not your personal Google account. The credential for
eas submitis a JSON key file downloaded from a GCP service account, not your Play Console login. Generate it in Play Console → Setup → API access → Create new service account → follow the GCP link → download JSON key. - The Release Manager role must be granted in Play Console, not just in GCP. Creating the GCP service account and assigning GCP roles is not sufficient. The account must also be added and granted Release Manager in Play Console → Users and permissions. Both steps are required.
- Reference the key file via
serviceAccountKeyPathineas.json— never inline credentials. Store the JSON file outside version control (gitignored) and reference it as"serviceAccountKeyPath": "./google-service-account.json"in the submit profile. Commit the path, not the file. - Configure separate
eas.jsonsubmit profiles for each track. Use named profiles (production,internal) to target different Play Console tracks. Each profile sets its owntrackandreleaseStatusso internal test releases and production releases don't share configuration. - Run
eas submitfrom the directory containingeas.json. The CLI reads credentials and configuration fromeas.jsonin the working directory. If the file is missing or the profile name doesn't match, submission will fail or prompt for manual input. - Check existing GitHub Secrets before creating a new Expo access token.
EXPO_TOKENmay already exist in the repo from a prior setup run. Expo access tokens are only shown once at creation — if the value is lost, delete the token in the Expo dashboard and create a new one with a descriptive name (e.g.<ProjectName>_GH_Actions_CI). - iOS
eas submit --non-interactiverequires a pre-registered App Store Connect API key. Without it the command fails with "App Store Connect API Keys cannot be set up in --non-interactive mode". Create the key in App Store Connect → Users and Access → Integrations (request access first; approved by email), then runeas credentials --platform iosand select the API key option. EAS stores the key on its servers; the p8 file can then be discarded. - GitHub Actions secrets cannot reach EAS cloud builds. GitHub secrets exist only on the runner, not on EAS servers where
app.config.jsis evaluated. Values needed at build time must be stored as EAS environment variables (eas env:create --environment production) and linked viaenvironment: "production"in the eas.json build profile. Reserve GitHub secrets forEXPO_TOKENand deployment credentials only — never for app config values. - Do not double-specify the environment. Passing
--environment productionon theeas buildCLI and declaringenvironment: "production"in the eas.json build profile causes an immediate failure (exit 1, ~1s). Remove the CLI flag and rely solely on the eas.json profile field. expo/expo-github-action@8.2.1is hardcoded to Node 20. No Node 24 version existed as of May 2026. AddACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: trueat the job level (not just the build step env) in every workflow that uses the action. Node 20 is not removed from runners until September 2026.npm cifails in CI whenpackage.jsonandpackage-lock.jsonare out of sync. This happens when packages are added or removed locally without runningnpm installbefore committing. Runnpm installlocally and commit the updated lock file.- When a build succeeds but the deploy fails, re-deploy — don't rebuild. For a build-number conflict or network error after a successful build, trigger
deploy-onlymode passing the EASbuild_idfrom the successful build. This skips the 20–25 minute build and goes straight to submission — critical for iterating on submit failures. - EAS caches the native build layer. When only JavaScript changes (no native package added or removed), subsequent builds reuse the cached native output and only re-bundle JS, cutting build time from ~25 min to ~6–7 min. Adding or removing a native package invalidates the cache and forces a full rebuild.
- If Sentry source-map upload fails an EAS production build, disable it to unblock. A missing
SENTRY_ORG/auth token fails the@sentry/react-native/expoupload step ("An organization ID or slug is required"); setSENTRY_DISABLE_AUTO_UPLOAD: "true"+SENTRY_ALLOW_FAILURE: "true"in the profile'senvso the build succeeds. Runtime crash reporting still works — crashes are just unsymbolicated until you wireSENTRY_ORG/SENTRY_AUTH_TOKENand re-enable upload in a later build. - When enabling Sentry upload, set
SENTRY_ORG+SENTRY_PROJECTin EVERY build profile whose env carries the Sentry DSN. A preview profile inheriting the production environment fails its Gradle source-map upload step without the slugs — the same failure class as the production profile. Auth via aSENTRY_AUTH_TOKENEAS secret (org token with source-map-upload scope). - An .aab cannot be sideloaded onto a device (it's a Play-upload format; the device just offers "Open with") — for direct-install testing build an APK via a separate profile (e.g.
preview) pointed at the same production env. Keep both: AAB for the store track, APK for handing testers a direct link. - EAS free plan is 15 Android + 15 iOS builds/month (not 30 as older docs state). The Starter plan ($19/month) provides $45 in build credits — enough for active CI/CD. Cancel once the pipeline is stable to avoid ongoing cost.
- Treat a build as a scarce resource, not a per-change action. The free tier's ~30 builds/month is exhausted fast by iterative per-tweak app rebuilds — batch app-code changes into ONE build and iterate anything web/portal (deployed separately) for free.
eas build:viewshows "Build Artifacts URL: null" for a finished app build — the installable APK/IPA is under the separate Application Archive URL field. SetEAS_SKIP_AUTO_FINGERPRINT=1beforeeas build ... --no-waitto skip a slow project-fingerprint step that can otherwise make the kickoff exceed a short shell timeout before it prints the build URL.- Auto-submit an iOS build to internal TestFlight from the build command. Put the App Store Connect API key config (
ascAppId,appleTeamId,ascApiKeyPathto a local.p8,ascApiKeyId,ascApiKeyIssuerId) in asubmit.<profile>block ineas.json, theneas build --profile <p> --platform ios --auto-submit; the build number auto-increments and the submission is scheduled after the build, with Apple's TestFlight processing adding roughly 10–30 min before it is installable.
Process
- Create the service account. In Play Console → Setup → API access, link or create a GCP project. Click Create new service account → follow the GCP console link → create the account → download the JSON key → save as
google-service-account.jsonin the project root. - Grant the Play Console role. In Play Console → Users and permissions → Invite new users, enter the service account email and grant Release Manager under Account permissions.
- Gitignore the key file. Add
google-service-account.jsonto.gitignoreimmediately. Never commit it. - Configure the
eas.jsonsubmit profile. Add asubmitsection withserviceAccountKeyPathandtrackset for the target Play Console track. - For iOS, register an App Store Connect API key. Create the key in App Store Connect → Users and Access → Integrations, then run
eas credentials --platform iosand select the API key option so EAS can submit non-interactively. - Store build-time config as EAS environment variables. Use
eas env:create --environment productionfor any valueapp.config.jsneeds at build time — GitHub secrets do not reach EAS servers. Link the environment viaenvironment: "production"in the eas.json build profile (do not also pass--environmenton the CLI). - Build via EAS. Run
eas build --platform android|ios --profile <profile>(withEAS_SKIP_AUTO_FINGERPRINT=1and--no-waitfor a fast kickoff) or reuse an existing build ID; batch changes — builds are rationed. For iOS internal TestFlight, add--auto-submitwith asubmit.<profile>ASC key block. - Submit. Run
eas submit --platform android|ios --profile <profile>. Pass--id <build-id>to target a specific build. If a build succeeded but the deploy failed, re-run in deploy-only mode with the existingbuild_idrather than rebuilding.
Output format
- Credential setup summary — service account created, JSON key downloaded, Play Console role assigned
eas.jsonsubmit profile — the relevant JSON block configured for the target track- Submission result — build submitted, track confirmed, any errors surfaced with resolution
Quality checklist
- GCP service account JSON key downloaded from Play Console → Setup → API access
- Service account email granted Release Manager in Play Console → Users and permissions
-
google-service-account.jsonadded to.gitignore— not committed -
serviceAccountKeyPathandtrackset ineas.jsonsubmit profile -
eas submit --platform android|ioscompletes and build appears in the target store track - For iOS: App Store Connect API key registered with EAS via
eas credentials --platform ios - Build-time config stored as EAS environment variables — not GitHub secrets
- No double-specification of
--environmentflag and eas.jsonenvironmentfield -
ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION: trueset at job level if usingexpo/expo-github-action -
package-lock.jsoncommitted and in sync before relying onnpm ci - App-code changes batched into one build; artefact fetched from Application Archive URL, not Build Artifacts URL
- iOS
submit.<profile>block holdsascAppId/appleTeamId/ascApiKeyPath/ascApiKeyId/ascApiKeyIssuerIdwhen using--auto-submit
Avoid
- Using a personal Google account credential for
eas submit— it requires a GCP service account JSON key - Committing the service account JSON key to the repository
- Granting the Release Manager role only in GCP without also granting it in Play Console — both are required
- Running
eas submitfrom a directory that does not containeas.json - Creating a new Expo access token without first checking whether
EXPO_TOKENalready exists in GitHub Secrets — tokens are only shown once at creation; if lost, delete and recreate with a descriptive name - Storing app config values (e.g. Supabase URL/anon key) as GitHub secrets and expecting them at EAS build time — they never reach EAS servers; use EAS environment variables
- Passing
--environmenton the CLI while also settingenvironmentin eas.json — the double-specification fails immediately - Rebuilding from scratch when a build succeeded but only the deploy failed — re-run deploy-only with the existing
build_id - Running
eas submit --non-interactivefor iOS without a pre-registered App Store Connect API key — it cannot prompt for one in non-interactive mode - Rebuilding the app for every small tweak — the free tier's ~30 builds/month runs out; batch, and iterate web/portal separately
- Reading "Build Artifacts URL: null" as a failed build — the APK/IPA is under Application Archive URL
Example usage
Expo app builds via EAS successfully. Need to submit the AAB to Google Play internal testing. No service account set up yet.
Source: This skill is sourced from the Matrix Skills library. Learn more at the AI Agent Skills Library.