- Published on
Expo Release Cookbooks: TestFlight, Play Internal, and Git Tag Production
- Authors

- Name
- Motions Technologies
Expo Release Cookbooks: TestFlight, Play Internal, and Git Tag Production
Shipping Expo apps is less about eas build once and more about a repeatable path from commit → testers → store. Across customer, driver, and multi-app fleets, we standardized on a two-track model that keeps "what is in 1.2.0?" answerable from git.
Two paths, deliberately separate
| Path | Trigger | Destination |
|---|---|---|
| Beta | Push to main | TestFlight / Play internal testers |
| Production | Git tag v* | App Store / Play review → phased release |
main ships continuously to testers. Only a tag cuts a release candidate. That separation prevents "we merged Friday and accidentally submitted to the store."
iOS / TestFlight one-time setup
Per app you need:
- Apple Developer Program membership
- App Store Connect app record (numeric ASC App ID)
- App Store Connect API key (
.p8) stored securely for CI - EAS project linked (
eas login,eas initas needed)
Store secrets in EAS env / GitHub secrets, not in the repo. Classic footgun: putting EXPO_PUBLIC_* only in eas.json env blocks — those apply to builds, not necessarily to eas update. Prefer EAS environment variables so builds and OTA updates see the same API URLs and Sentry DSNs.
Android / Play symmetry
Treat Play internal testing as TestFlight's cousin. While org verification (for example DUNS) blocks production Play access, keep an EAS internal APK / preview workflow so Android QA does not stall.
Workflows we keep in each mobile repo:
eas-testflight.yml— iOS beta onmaineas-play-internal.ymlor preview APK — Android betarelease.yml— tagv*production path
Versioning discipline
- App version / build numbers bump in CI from the tag or a single source of truth
- Changelog comes from git history between tags
- Never hand-bump store build numbers in a random local commit that never lands on
main
Sentry and runtime config
Create the Sentry project first, then:
eas env:create --name EXPO_PUBLIC_SENTRY_DSN --value 'https://…' \
--type string --visibility plaintext --scope project \
--environment production --environment preview
Also provide SENTRY_AUTH_TOKEN (and org/project) to CI for source maps. Crash-free releases are a store requirement in practice even when Apple does not say so out loud.
Checklist before you tag
- Smoke the API environments the binary will call
- Verify push notifications / Sign in with Apple entitlements if used
- Confirm privacy nutrition labels and permission strings match actual APIs
- Run a TestFlight build that matches the tag commit
- Only then
git tag vX.Y.Z && git push --tags
Multi-app fleets
Customer and driver apps (or admin/tenant/handyman variants in other products) should copy the cookbook, not invent new release philosophy. Same workflow names, same secret shapes, different bundle IDs and ASC/Play app records.
Closing
Expo makes builds easy; release engineering makes nights quiet. Separate beta from production with main vs tags, put runtime config in EAS environments, and keep iOS/Android workflows symmetric. Your future self — tagging a hotfix at 11pm — will thank you.