Fully local Expo EAS build for CI
Expo EAS CLI has a --local build mode but it's not completely local out of the box. This article describes how to make it less reliant on the expo.dev cloud in order to build your app on a CI like GitHub Actions.
While Expo's documentation is quite comprehensive, their documentation for the --local mode is understandably not that good (as it drives customers away from their cloud build service).
Avoiding expo.dev login and interactivity​
Create a token on the expo.dev site and use it as an env variable (EXPO_TOKEN). Then run the build command with --non-interactive.
Ref:
Local signing credentials.json​
Normally credentials have to be set up in expo.dev and eas build will pull them from there on every build.
Solution: Set up a local credentials.json file.
{
"ios": {
"distributionCertificate": {
"path": "credentials/ios/dist-cert.p12",
"password": "..."
},
"provisioningProfilePath": "credentials/ios/profile.mobileprovision"
}
}
Generate signing credentials:
- Certificates, Identifiers & Profiles
- New certificate -> Apple Distribution
- On macOS open Keychain Access -> Request certificate from authority -> Save CSR locally
- Upload CSR file
- Download resulting
distribution.cer - Open
distribution.cerin Keychain Access - Right click new cert in Keychain Access and export as
p12with a password, save it tocredentials/ios/dist-cert.p12 - Identifiers -> Create new app ID
- Create new App Store Connect provisioning profile, download
.mobileprovisionfile and move it tocredentials/ios/profile.mobileprovision
Remember to add /credentials and /credentials.json these to .gitignore (and .easignore).
For CI you will have to generate this credentials.json dynamically from secrets.
Ref
- https://docs.expo.dev/app-signing/local-credentials/#credentialsjson
- https://docs.expo.dev/app-signing/syncing-credentials/
Local versioning​
By default expo stores version in expo.dev and auto increments it there on every build. To instead use the local version, update eas.json:
- Add:
cli.appVersionSource: local - Remove:
build.*.autoIncrement
Ref
Local .env not picked up​
EAS will only use local .env files that are not .gitignored. But it's common practice to ignore such files.
Solution: create a .easignore which contains the same content as .gitignore, except .env entries.
Ref:
- https://www.reddit.com/r/expo/comments/1b7wn4y/local_eas_build_env_variables/
- https://docs.expo.dev/build-reference/easignore/
- https://docs.expo.dev/guides/environment-variables/
NODE_ENVor not? https://github.com/expo/expo/issues/39842
Building​
Install fastlane:
brew install fastlane
Complete local build command:
EXPO_TOKEN='...' \
NODE_ENV=production \
EAS_LOCAL_BUILD_WORKINGDIR=./release \
EAS_LOCAL_BUILD_SKIP_CLEANUP=1 \
EAS_BUILD_DISABLE_EXPO_DOCTOR_STEP=1 \
EAS_LOCAL_BUILD_PLUGIN_PATH="$(yarn bin eas-cli-local-build-plugin)" \
yarn eas build --local --non-interactive --platform ios --profile production --output=./production.ipa
Explanation:
EAS_LOCAL_BUILD_WORKINGDIRandEAS_LOCAL_BUILD_SKIP_CLEANUPis good for debugging failed builds.EAS_LOCAL_BUILD_PLUGIN_PATHis only needed if you don't want it to run a (slow)npx eas-cli-local-build-plugin, but instead use a local package withyarn.
Uploading the build to App Store Connect​
Initial setup:
- Users and Access
- Integrations
- Create Team key
- Role: Developer
- Download key p8 and save it to
~/.appstoreconnect/private_keys/AuthKey_*.p8 - Copy Issuer ID, Key ID
This command can be run for every new .ipa to upload:
xcrun altool --upload-app --type ios --file app.ipa --apiKey KEY_ID --apiIssuer ISSUER_ID
This can also be automated in CI with secrets.
Limitations​
Some operations are still hitting the expo.dev API, although I don't think that should be necessary. Hopefully this gets improved by the Expo team in the future.